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.
@@ -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