dry-ability 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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