dry-ability 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitattributes +2 -0
- data/.gitignore +50 -0
- data/.ruby-version +1 -0
- data/Gemfile +3 -0
- data/LICENSE +21 -0
- data/README.md +20 -0
- data/Rakefile +13 -0
- data/dry-ability.gemspec +36 -0
- data/init.rb +1 -0
- data/lib/dry-ability.rb +1 -0
- data/lib/dry/ability.rb +120 -0
- data/lib/dry/ability/container.rb +43 -0
- data/lib/dry/ability/controller.rb +21 -0
- data/lib/dry/ability/controller/dsl.rb +161 -0
- data/lib/dry/ability/controller/mixin.rb +80 -0
- data/lib/dry/ability/controller/resource.rb +218 -0
- data/lib/dry/ability/controller_resource.rb +62 -0
- data/lib/dry/ability/exceptions.rb +36 -0
- data/lib/dry/ability/f.rb +57 -0
- data/lib/dry/ability/inherited_resource.rb +26 -0
- data/lib/dry/ability/key.rb +19 -0
- data/lib/dry/ability/resource_mediator.rb +94 -0
- data/lib/dry/ability/rule.rb +141 -0
- data/lib/dry/ability/rule_interface.rb +27 -0
- data/lib/dry/ability/rules_builder.rb +92 -0
- data/lib/dry/ability/t.rb +45 -0
- data/lib/dry/ability/version.rb +7 -0
- metadata +214 -0
@@ -0,0 +1,80 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Dry
|
4
|
+
module Ability
|
5
|
+
module Controller
|
6
|
+
module Mixin
|
7
|
+
extend ActiveSupport::Concern
|
8
|
+
|
9
|
+
included do
|
10
|
+
class_attribute :ability_class, instance_accessor: false
|
11
|
+
helper_method :can?, :cannot?, :current_ability if respond_to? :helper_method
|
12
|
+
end
|
13
|
+
|
14
|
+
# Raises a Dry::Ability::AccessDenied exception if the current_ability cannot
|
15
|
+
# perform the given action. This is usually called in a controller action or
|
16
|
+
# before filter to perform the authorization.
|
17
|
+
#
|
18
|
+
# def show
|
19
|
+
# @article = Article.find(params[:id])
|
20
|
+
# authorize! :read, @article
|
21
|
+
# end
|
22
|
+
#
|
23
|
+
# A :message option can be passed to specify a different message.
|
24
|
+
#
|
25
|
+
# authorize! :read, @article, :message => "Not authorized to read #{@article.name}"
|
26
|
+
#
|
27
|
+
# You can also use I18n to customize the message. Action aliases defined in Ability work here.
|
28
|
+
#
|
29
|
+
# en:
|
30
|
+
# unauthorized:
|
31
|
+
# manage:
|
32
|
+
# all: "Not authorized to %{action} %{subject}."
|
33
|
+
# user: "Not allowed to manage other user accounts."
|
34
|
+
# update:
|
35
|
+
# project: "Not allowed to update this project."
|
36
|
+
#
|
37
|
+
# You can rescue from the exception in the controller to customize how unauthorized
|
38
|
+
# access is displayed to the user.
|
39
|
+
#
|
40
|
+
# class ApplicationController < ActionController::Base
|
41
|
+
# rescue_from CanCan::AccessDenied do |exception|
|
42
|
+
# redirect_to root_url, :alert => exception.message
|
43
|
+
# end
|
44
|
+
# end
|
45
|
+
#
|
46
|
+
# See the CanCan::AccessDenied exception for more details on working with the exception.
|
47
|
+
#
|
48
|
+
# See the load_and_authorize_resource method to automatically add the authorize! behavior
|
49
|
+
# to the default RESTful actions.
|
50
|
+
def authorize!(*args)
|
51
|
+
@_authorized = true
|
52
|
+
current_ability.authorize!(*args)
|
53
|
+
end
|
54
|
+
|
55
|
+
# Creates and returns the current user's ability and caches it. If you
|
56
|
+
# want to override how the Ability is defined then this is the place.
|
57
|
+
# Just define the method in the controller to change behavior.
|
58
|
+
#
|
59
|
+
# def current_ability
|
60
|
+
# # instead of Ability.new(current_user)
|
61
|
+
# @current_ability ||= UserAbility.new(current_account)
|
62
|
+
# end
|
63
|
+
#
|
64
|
+
# Notice it is important to cache the ability object so it is not
|
65
|
+
# recreated every time.
|
66
|
+
def current_ability
|
67
|
+
@current_ability ||= ability_class.new(current_user)
|
68
|
+
end
|
69
|
+
|
70
|
+
# @!method can?(*args)
|
71
|
+
# @see Dry::Ability::Mixin#can?
|
72
|
+
delegate :can?, to: :current_ability
|
73
|
+
|
74
|
+
# @!method cannot?(*args)
|
75
|
+
# @see Dry::Ability::Mixin#cannot?
|
76
|
+
delegate :cannot?, to: :current_ability
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,218 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "dry/initializer"
|
4
|
+
require "dry/ability/t"
|
5
|
+
|
6
|
+
module Dry
|
7
|
+
module Ability
|
8
|
+
module Controller
|
9
|
+
# @private
|
10
|
+
class Resource
|
11
|
+
include Initializer[undefined: false].define -> do
|
12
|
+
param :mediator, T.Instance(ResourceMediator)
|
13
|
+
param :controller, T.Instance(Controller::Mixin)
|
14
|
+
|
15
|
+
option :action_name, T['params.symbol'], default: proc { @controller.action_name.to_sym }
|
16
|
+
option :controller_name, T['params.symbol'], default: proc { @controller.controller_name.to_sym }
|
17
|
+
option :is_member, T['bool'], default: proc { @mediator.member_action?(action_name, params) }
|
18
|
+
option :is_collection, T['bool'], default: proc { @mediator.collection_action?(action_name) }
|
19
|
+
end
|
20
|
+
|
21
|
+
alias_method :member_action?, :is_member
|
22
|
+
alias_method :collection_action?, :is_collection
|
23
|
+
|
24
|
+
delegate :params, to: :controller
|
25
|
+
delegate :name, to: :mediator
|
26
|
+
delegate_missing_to :mediator
|
27
|
+
|
28
|
+
def call
|
29
|
+
@controller.instance_variable_set(:@_ability_resource, self)
|
30
|
+
retval = nil
|
31
|
+
@mediator.sequence.each do |sym|
|
32
|
+
retval = public_send(sym)
|
33
|
+
end
|
34
|
+
retval
|
35
|
+
end
|
36
|
+
|
37
|
+
def load_and_authorize_resource
|
38
|
+
load_resource
|
39
|
+
authorize_resource
|
40
|
+
end
|
41
|
+
|
42
|
+
def load_resource
|
43
|
+
return if skip?(:load)
|
44
|
+
if load_instance?
|
45
|
+
self.resource_instance ||= load_resource_instance
|
46
|
+
elsif collection_action?
|
47
|
+
self.collection_instance ||= load_collection
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def authorize_resource
|
52
|
+
return if skip?(:authorize)
|
53
|
+
@controller.authorize!(authorization_action, resource_instance || resource_class_with_parent)
|
54
|
+
end
|
55
|
+
|
56
|
+
def parent?
|
57
|
+
@mediator.parent.nil? ? @mediator.collection_name != controller_name.to_sym : @mediator.parent?
|
58
|
+
end
|
59
|
+
|
60
|
+
def skip?(behavior)
|
61
|
+
options = @controller.class.cancan_skipper.dig(behavior, name)
|
62
|
+
return false if options.nil?
|
63
|
+
options.blank? &&
|
64
|
+
options[:except] && !action_exists_in?(options[:except]) ||
|
65
|
+
action_exists_in?(options[:only])
|
66
|
+
end
|
67
|
+
|
68
|
+
def load_resource_instance
|
69
|
+
raise NotImplementedError
|
70
|
+
end
|
71
|
+
|
72
|
+
def resource_base
|
73
|
+
raise NotImplementedError
|
74
|
+
end
|
75
|
+
|
76
|
+
def load_instance?
|
77
|
+
parent? || member_action?
|
78
|
+
end
|
79
|
+
|
80
|
+
# def load_collection?
|
81
|
+
# collection_action?
|
82
|
+
# # current_ability.has_scope?(authorization_action, resource_class) || resource_base.respond_to?(:accessible_by)
|
83
|
+
# end
|
84
|
+
|
85
|
+
def load_collection
|
86
|
+
current_ability.scope_for(authorization_action, resource_class) do
|
87
|
+
resource_base.accessible_by(current_ability, authorization_action)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def assign_attributes(resource)
|
92
|
+
resource.send(:"#{parent_name}=", parent_resource) if singleton? && parent_resource
|
93
|
+
initial_attributes.each do |attr_name, value|
|
94
|
+
resource.send(:"#{attr_name}=", value)
|
95
|
+
end
|
96
|
+
resource
|
97
|
+
end
|
98
|
+
|
99
|
+
def initial_attributes
|
100
|
+
current_ability.attributes_for(@action_name, resource_class).delete_if do |key, _|
|
101
|
+
resource_params && resource_params.include?(key)
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def authorization_action
|
106
|
+
parent? ? @mediator.parent_action : @action_name
|
107
|
+
end
|
108
|
+
|
109
|
+
def id_param
|
110
|
+
params[@mediator.id_param_key] if params.key?(@mediator.id_param_key)
|
111
|
+
end
|
112
|
+
|
113
|
+
# Returns the class used for this resource. This can be overriden by the :class option.
|
114
|
+
# If +false+ is passed in it will use the resource name as a symbol in which case it should
|
115
|
+
# only be used for authorization, not loading since there's no class to load through.
|
116
|
+
def resource_class
|
117
|
+
case class_name
|
118
|
+
when false then
|
119
|
+
name.to_sym
|
120
|
+
when String then
|
121
|
+
class_name.constantize
|
122
|
+
else
|
123
|
+
raise ArgumentError, "unexpected class_name: #{class_name}"
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
def resource_class_with_parent
|
128
|
+
parent_resource ? { parent_resource => resource_class } : resource_class
|
129
|
+
end
|
130
|
+
|
131
|
+
def resource_instance=(instance)
|
132
|
+
@controller.instance_variable_set(:"@#{instance_name}", instance)
|
133
|
+
end
|
134
|
+
|
135
|
+
def resource_instance
|
136
|
+
@controller.instance_variable_get(:"@#{instance_name}") if load_instance?
|
137
|
+
end
|
138
|
+
|
139
|
+
def collection_instance=(instance)
|
140
|
+
@controller.instance_variable_set(:"@#{collection_name}", instance)
|
141
|
+
end
|
142
|
+
|
143
|
+
def collection_instance
|
144
|
+
@controller.instance_variable_get(:"@#{collection_name}")
|
145
|
+
end
|
146
|
+
|
147
|
+
def parent_name
|
148
|
+
return @parent_name if defined?(@parent_name)
|
149
|
+
@parent_name = @mediator.through unless parent_resource.nil?
|
150
|
+
end
|
151
|
+
|
152
|
+
# The object to load this resource through.
|
153
|
+
def parent_resource
|
154
|
+
return @parent_resource if defined?(@parent_resource)
|
155
|
+
@parent_resource = if @mediator.through
|
156
|
+
if @controller.instance_variable_defined? :"@#{@mediator.through}"
|
157
|
+
@controller.instance_variable_get(:"@#{@mediator.through}")
|
158
|
+
elsif @controller.respond_to?(@mediator.through, true)
|
159
|
+
@controller.send(@mediator.through)
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
def current_ability
|
165
|
+
@controller.send(:current_ability)
|
166
|
+
end
|
167
|
+
|
168
|
+
def resource_params
|
169
|
+
if parameters_require_sanitizing? && params_method.present?
|
170
|
+
case params_method
|
171
|
+
when Symbol then
|
172
|
+
@controller.send(params_method)
|
173
|
+
when String then
|
174
|
+
@controller.instance_eval(params_method)
|
175
|
+
when Proc then
|
176
|
+
params_method.call(@controller)
|
177
|
+
end
|
178
|
+
else
|
179
|
+
resource_params_by_namespaced_name
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
def parameters_require_sanitizing?
|
184
|
+
@mediator.save_actions.include?(@action_name) || resource_params_by_namespaced_name.present?
|
185
|
+
end
|
186
|
+
|
187
|
+
def resource_params_by_namespaced_name
|
188
|
+
return @resource_params_by_namespaced_name if defined?(@resource_params_by_namespaced_name)
|
189
|
+
@resource_params_by_namespaced_name =
|
190
|
+
if params.key?(@mediator.instance_name)
|
191
|
+
params[@mediator.instance_name]
|
192
|
+
elsif params.key?(key = extract_key(@mediator.class_name))
|
193
|
+
params[key]
|
194
|
+
else
|
195
|
+
params[name]
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
def params_method
|
200
|
+
@params_method ||= @mediator.params_method || begin
|
201
|
+
[:"#{@action_name}_params", :"#{name}_params", :resource_params].
|
202
|
+
detect { |method| @controller.respond_to?(method, true) }
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
private
|
207
|
+
|
208
|
+
def action_exists_in?(options)
|
209
|
+
Array.wrap(options).include?(@controller.action_name.to_sym)
|
210
|
+
end
|
211
|
+
|
212
|
+
def extract_key(value)
|
213
|
+
value.to_s.underscore.tr(?/, ?_)
|
214
|
+
end
|
215
|
+
end
|
216
|
+
end
|
217
|
+
end
|
218
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "dry/ability/controller/resource"
|
4
|
+
|
5
|
+
module Dry
|
6
|
+
module Ability
|
7
|
+
# Handle the load and authorization controller logic
|
8
|
+
# so we don't clutter up all controllers with non-interface methods.
|
9
|
+
# This class is used internally, so you do not need to call methods directly on it.
|
10
|
+
class ControllerResource < Controller::Resource
|
11
|
+
def load_resource_instance
|
12
|
+
if !parent? && @mediator.new_actions.include?(@action_name)
|
13
|
+
build_resource
|
14
|
+
elsif @mediator.id_param_key || @mediator.singleton?
|
15
|
+
find_resource
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def build_resource
|
20
|
+
resource = resource_base.new(resource_params || {})
|
21
|
+
assign_attributes(resource)
|
22
|
+
end
|
23
|
+
|
24
|
+
def find_resource
|
25
|
+
if singleton? && parent_resource.respond_to?(name)
|
26
|
+
parent_resource.public_send(name)
|
27
|
+
elsif find_by.present?
|
28
|
+
if resource_base.respond_to? find_by
|
29
|
+
resource_base.public_send(find_by, id_param)
|
30
|
+
else
|
31
|
+
resource_base.find_by(find_by => id_param)
|
32
|
+
end
|
33
|
+
else
|
34
|
+
resource_base.find(id_param)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
# The object that methods (such as "find", "new" or "build") are called on.
|
39
|
+
# If the :through option is passed it will go through an association on that instance.
|
40
|
+
# If the :shallow option is passed it will use the resource_class if there's no parent
|
41
|
+
# If the :singleton option is passed it won't use the association because it needs to be handled later.
|
42
|
+
def resource_base
|
43
|
+
if @mediator.through
|
44
|
+
resource_base_through
|
45
|
+
else
|
46
|
+
resource_class
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def resource_base_through
|
51
|
+
if parent_resource
|
52
|
+
@mediator.singleton? ? resource_class : parent_resource.public_send(@mediator.through_association)
|
53
|
+
elsif @mediator.shallow?
|
54
|
+
resource_class
|
55
|
+
else
|
56
|
+
# maybe this should be a record not found error instead?
|
57
|
+
raise AccessDenied.new(nil, authorization_action, resource_class)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Dry
|
4
|
+
module Ability
|
5
|
+
# A general CanCan exception
|
6
|
+
class Error < StandardError; end
|
7
|
+
|
8
|
+
# Raised when using check_authorization without calling authorized!
|
9
|
+
class AuthorizationNotPerformed < Error; end
|
10
|
+
|
11
|
+
class RuleNotDefined < Error
|
12
|
+
DEFAULT_MESSAGE_TEMPLATE = "Rule for subject: %p, action: %p is not defined (candidates: %p)"
|
13
|
+
NONE = [].freeze
|
14
|
+
|
15
|
+
def initialize(message = nil, subject:, action:, candidates: NONE)
|
16
|
+
@action, @subject = action, subject
|
17
|
+
message ||= format(DEFAULT_MESSAGE_TEMPLATE, subject, action, candidates)
|
18
|
+
super(message)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
class ScopeNotDefault < Error; end
|
23
|
+
|
24
|
+
class AccessDenied < Error
|
25
|
+
DEFAULT_MESSAGE_TEMPLATE = "The requester is not authorized to %s %p."
|
26
|
+
|
27
|
+
attr_reader :action, :subject
|
28
|
+
|
29
|
+
def initialize(message = nil, action, subject)
|
30
|
+
@action, @subject = action, subject
|
31
|
+
message ||= format(DEFAULT_MESSAGE_TEMPLATE, action, subject)
|
32
|
+
super(message)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "dry/transformer/recursion"
|
4
|
+
require "dry/transformer/hash"
|
5
|
+
|
6
|
+
module Dry
|
7
|
+
module Ability
|
8
|
+
module F
|
9
|
+
extend Transformer::Registry
|
10
|
+
|
11
|
+
import :array_recursion, from: Transformer::Recursion
|
12
|
+
import :eval_values, from: Transformer::HashTransformations
|
13
|
+
|
14
|
+
key_or_to_s = T::CoercKey | T['coercible.string']
|
15
|
+
register :coerc_key, Transformer::Function.new(key_or_to_s.to_proc)
|
16
|
+
|
17
|
+
# def self.to_mapping_key(key, *namespaces)
|
18
|
+
# *namespaces, key = key if key.is_a?(Array)
|
19
|
+
# coerced = coerc_key(key)
|
20
|
+
# namespaces.blank ? coerced : "#{namespaces * ?.}.#{coerced}"
|
21
|
+
# end
|
22
|
+
#
|
23
|
+
# def self.ns_path(*namespaces)
|
24
|
+
# namespaces.flatten!
|
25
|
+
# namespaces.blank? ? nil : namespaces.join(?.)
|
26
|
+
# end
|
27
|
+
|
28
|
+
def self.get_mapping(key, container, nsfn, &block)
|
29
|
+
key = Key.new(key, nsfn)
|
30
|
+
yield(key) if block_given?
|
31
|
+
if container.key?(key.nsed)
|
32
|
+
Array.wrap(container[key.nsed]).flat_map do |mapped|
|
33
|
+
get_mapping(mapped, container, nsfn, &block)
|
34
|
+
end
|
35
|
+
else
|
36
|
+
key.to_s
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def self.collect_mappings(key, container, nsfn)
|
41
|
+
list = Set.new
|
42
|
+
get_mapping(key, container, nsfn) do |key|
|
43
|
+
key = yield(key) if block_given?
|
44
|
+
list << key
|
45
|
+
end
|
46
|
+
list.to_a
|
47
|
+
end
|
48
|
+
|
49
|
+
def self.string_tpl(*args, pattern)
|
50
|
+
args = args[0] if args.size == 1 && args[0].is_a?(Array)
|
51
|
+
format(pattern, *args)
|
52
|
+
end
|
53
|
+
|
54
|
+
# register :recursively_apply_mappings, t(:array_recursion, t)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|