accessly 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,8 @@
1
+ require "active_record"
2
+
3
+ module Accessly
4
+ class PermittedActionOnObject < ActiveRecord::Base
5
+ belongs_to :actor, polymorphic: true
6
+ belongs_to :object, polymorphic: true
7
+ end
8
+ end
@@ -0,0 +1,95 @@
1
+ module Accessly
2
+ module Permission
3
+ class Grant < Accessly::Base
4
+
5
+ # Create an instance of Accessly::Permission::Grant
6
+ # Pass in an ActiveRecord::Base for actor
7
+ #
8
+ # @param actor [ActiveRecord::Base] The actor to grant permission
9
+ def initialize(actor)
10
+ super(actor)
11
+ @actor = case actor
12
+ when ActiveRecord::Base
13
+ actor
14
+ else
15
+ raise Accessly::GrantError.new("Actor is not an ActiveRecord::Base object")
16
+ end
17
+ end
18
+
19
+ # Grant a permission to an actor.
20
+ # @return [nil]
21
+ # @overload grant!(action_id, object_type)
22
+ # Allow permission on a general action in the given namespace represented by object_type.
23
+ # A grant is universally unique and is enforced at the database level.
24
+ #
25
+ # @param action_id [Integer] The action to grant for the object
26
+ # @param object_type [String] The namespace of the given action_id.
27
+ # @raise [Accessly::GrantError] if the operation does not succeed
28
+ # @return [nil] Returns nil if successful
29
+ #
30
+ # @example
31
+ # # Allow the user access to posts for action id 3
32
+ # Accessly::Permission::Grant.new(user).grant!(3, "posts")
33
+ # @example
34
+ # # Allow the user access to posts for action id 3 on a segment
35
+ # Accessly::Permission::Grant.new(user).on_segment(1).grant!(3, "posts")
36
+ #
37
+ # @overload grant!(action_id, object_type, object_id)
38
+ # Allow permission on an ActiveRecord object.
39
+ # A grant is universally unique and is enforced at the database level.
40
+ #
41
+ # @param action_id [Integer] The action to grant for the object
42
+ # @param object_type [ActiveRecord::Base] The ActiveRecord model that receives a permission grant.
43
+ # @param object_id [Integer] The id of the ActiveRecord object which receives a permission grant
44
+ # @raise [Accessly::GrantError] if the operation does not succeed
45
+ # @return [nil] Returns nil if successful
46
+ #
47
+ # @example
48
+ # # Allow the user access to Post 7 for action id 3
49
+ # Accessly::Permission::Grant.new(user).grant!(3, Post, 7)
50
+ # @example
51
+ # # Allow the user access to Post 7 for action id 3 on a segment
52
+ # Accessly::Permission::Grant.new(user).on_segment(1).grant!(3, Post, 7)
53
+ def grant!(action_id, object_type, object_id = nil)
54
+ if object_id.nil?
55
+ general_action_grant(action_id, object_type)
56
+ else
57
+ object_action_grant(action_id, object_type, object_id)
58
+ end
59
+ end
60
+
61
+ private
62
+
63
+ def general_action_grant(action_id, object_type)
64
+ Accessly::PermittedAction.create!(
65
+ id: SecureRandom.uuid,
66
+ segment_id: @segment_id,
67
+ actor: @actor,
68
+ action: action_id,
69
+ object_type: String(object_type)
70
+ )
71
+ nil
72
+ rescue ActiveRecord::RecordNotUnique
73
+ nil
74
+ rescue => e
75
+ raise Accessly::GrantError.new("Could not grant action #{action_id} on object #{object_type} for actor #{@actor} because #{e}")
76
+ end
77
+
78
+ def object_action_grant(action_id, object_type, object_id)
79
+ Accessly::PermittedActionOnObject.create!(
80
+ id: SecureRandom.uuid,
81
+ segment_id: @segment_id,
82
+ actor: @actor,
83
+ action: action_id,
84
+ object_type: String(object_type),
85
+ object_id: object_id
86
+ )
87
+ nil
88
+ rescue ActiveRecord::RecordNotUnique
89
+ nil
90
+ rescue => e
91
+ raise Accessly::GrantError.new("Could not grant action #{action_id} on object #{object_type} with id #{object_id} for actor #{@actor} because #{e}")
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,87 @@
1
+ module Accessly
2
+ module Permission
3
+ class Revoke < Accessly::Base
4
+
5
+ # Create an instance of Accessly::Permission::Revoke
6
+ # Pass in an ActiveRecord::Base for actor
7
+ #
8
+ # @param actor [ActiveRecord::Base] The actor to revoke permission
9
+ def initialize(actor)
10
+ super(actor)
11
+ @actor = case actor
12
+ when ActiveRecord::Base
13
+ actor
14
+ else
15
+ raise Accessly::RevokeError.new("Actor is not an ActiveRecord::Base object")
16
+ end
17
+ end
18
+
19
+ # Revoke a permission for an actor.
20
+ # @return [nil]
21
+ # @overload revoke!(action_id, object_type)
22
+ # Revoke permission on a general action in the given namespace represented by object_type.
23
+ #
24
+ # @param action_id [Integer] The action to revoke
25
+ # @param object_type [String] The namespace of the given action_id.
26
+ # @raise [Accessly::RevokeError] if the operation does not succeed
27
+ # @return [nil] Returns nil if successful
28
+ #
29
+ # @example
30
+ # # Remove user access to posts for action id 3
31
+ # Accessly::Permission::Revoke.new(user).revoke!(3, Post)
32
+ # @example
33
+ # # Remove user access to posts for action id 3 on a segment
34
+ # Accessly::Permission::Revoke.new(user).on_segment(1).revoke!(3, Post)
35
+ #
36
+ # @overload revoke!(action_id, object_type, object_id)
37
+ # Revoke permission on an ActiveRecord object.
38
+ #
39
+ # @param action_id [Integer] The action to revoke
40
+ # @param object_type [ActiveRecord::Base] The ActiveRecord model that removes a permission.
41
+ # @param object_id [Integer] The id of the ActiveRecord object that removes a permission
42
+ # @raise [Accessly::RevokeError] if the operation does not succeed
43
+ # @return [nil] Returns nil if successful
44
+ #
45
+ # @example
46
+ # # Remove user access to Post 7 for action id 3
47
+ # Accessly::Permission::Revoke.new(user).revoke!(3, Post, 7)
48
+ # @example
49
+ # # Remove user access to Post 7 for action id 3 on a segment
50
+ # Accessly::Permission::Revoke.new(user).on_segment(1).revoke!(3, Post, 7)
51
+ def revoke!(action_id, object_type, object_id = nil)
52
+ if object_id.nil?
53
+ general_action_revoke(action_id, object_type)
54
+ else
55
+ object_action_revoke(action_id, object_type, object_id)
56
+ end
57
+ end
58
+
59
+ private
60
+
61
+ def general_action_revoke(action_id, object_type)
62
+ Accessly::PermittedAction.where(
63
+ segment_id: @segment_id,
64
+ actor: @actor,
65
+ action: action_id,
66
+ object_type: String(object_type)
67
+ ).delete_all
68
+ nil
69
+ rescue => e
70
+ raise Accessly::RevokeError.new("Could not revoke action #{action_id} on object #{object_type} for actor #{@actor} because #{e}")
71
+ end
72
+
73
+ def object_action_revoke(action_id, object_type, object_id)
74
+ Accessly::PermittedActionOnObject.where(
75
+ segment_id: @segment_id,
76
+ actor: @actor,
77
+ action: action_id,
78
+ object_type: String(object_type),
79
+ object_id: object_id
80
+ ).delete_all
81
+ nil
82
+ rescue => e
83
+ raise Accessly::RevokeError.new("Could not revoke #{action_id} on object #{object_type} with id #{object_id} for actor #{@actor} because #{e}")
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,40 @@
1
+ require "accessly/query_builder"
2
+
3
+ module Accessly
4
+ module PermittedActions
5
+ class Base
6
+
7
+ def initialize(actors, segment_id)
8
+ @actors = actors
9
+ @segment_id = segment_id
10
+ end
11
+
12
+ protected
13
+
14
+ def past_lookups
15
+ @_past_lookups ||= {}
16
+ end
17
+
18
+ def find_or_set_value(*keys, &query)
19
+ found_value = past_lookups.dig(*keys)
20
+
21
+ if found_value.nil?
22
+ found_value = query.call
23
+ set_value(*keys, value: found_value)
24
+ end
25
+
26
+ found_value
27
+ end
28
+
29
+ def set_value(*keys, value:)
30
+ lookup = past_lookups
31
+ keys[0..-2].each do |key|
32
+ lookup[key] ||= {}
33
+ lookup = lookup[key]
34
+ end
35
+
36
+ lookup[keys[-1]] = value
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,65 @@
1
+ module Accessly
2
+ module PermittedActions
3
+ class OnObjectQuery < Base
4
+
5
+ def initialize(actors, segment_id)
6
+ super(actors, segment_id)
7
+ end
8
+ # Ask whether the actor has permission to perform action_id
9
+ # on a given record.
10
+ #
11
+ # Lookups are cached in the object to prevent redundant database calls.
12
+ #
13
+ # @param action_id [Integer, Array<Integer>] The action or actions we're checking whether the actor has. If this is an array, then the check is ORed.
14
+ # @param object_type [ActiveRecord::Base] The ActiveRecord model which we're checking for permission on.
15
+ # @param object_id [Integer] The id of the ActiveRecord object which we're checking for permission on.
16
+ # @return [Boolean] Returns true if actor has been granted the permission on the specified record, false otherwise.
17
+ #
18
+ # @example
19
+ # # Can the user perform the action with id 5 for the Post with id 7?
20
+ # Accessly::Query.new(user).can?(5, Post, 7)
21
+ # @example
22
+ # # Can the user perform the action with id 5 for the Post with id 7 on segment 1?
23
+ # Accessly::Query.new(user).on_segment(1).can?(5, Post, 7)
24
+ def can?(action_id, object_type, object_id)
25
+ find_or_set_value(action_id, object_type, object_id) do
26
+ Accessly::QueryBuilder.with_actors(Accessly::PermittedActionOnObject, @actors)
27
+ .where(
28
+ segment_id: @segment_id,
29
+ action: action_id,
30
+ object_type: String(object_type),
31
+ object_id: object_id
32
+ ).exists?
33
+ end
34
+ end
35
+
36
+ # Returns an ActiveRecord::Relation of ids in the namespace for
37
+ # which the actor has permission to perform action_id.
38
+ #
39
+ # @param action_id [Integer] The action we're checking on the actor in the namespace.
40
+ # @param namespace [String] The namespace to check actor permissions.
41
+ # @return [ActiveRecord::Relation]
42
+ #
43
+ # @example
44
+ # # Give me the list of Post ids on which the user has permission to perform action_id 3
45
+ # Accessly::Query.new(user).list(3, Post)
46
+ # @example
47
+ # # Give me the list of Post ids on which the user has permission to perform action_id 3 on segment 1
48
+ # Accessly::Query.new(user).on_segment(1).list(3, Post)
49
+ # @example
50
+ # # Give me the list of Post ids on which the user and its groups has permission to perform action_id 3
51
+ # Accessly::Query.new(User => user.id, Group => [1,2]).list(3, Post)
52
+ # @example
53
+ # # Give me the list of Post ids on which the user and its groups has permission to perform action_id 3 on segment 1
54
+ # Accessly::Query.new(User => user.id, Group => [1,2]).on_segment(1).list(3, Post)
55
+ def list(action_id, namespace)
56
+ Accessly::QueryBuilder.with_actors(Accessly::PermittedActionOnObject, @actors)
57
+ .where(
58
+ segment_id: @segment_id,
59
+ action: Integer(action_id),
60
+ object_type: String(namespace),
61
+ ).select(:object_id)
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,42 @@
1
+ require "accessly/permitted_actions/base"
2
+
3
+ module Accessly
4
+ module PermittedActions
5
+ class Query < Base
6
+
7
+ def initialize(actors, segment_id)
8
+ super(actors, segment_id)
9
+ end
10
+
11
+ # Ask whether the actor has permission to perform action_id
12
+ # in the given namespace. Multiple actions can have the same id
13
+ # as long as their namespace is different. The namespace can be
14
+ # any String. We recommend using namespace to group a class of
15
+ # permissions, such as to group parts of a particular feature
16
+ # in your application.
17
+ #
18
+ # Lookups are cached in the object to prevent redundant database calls.
19
+ #
20
+ # @param action_id [Integer, Array<Integer>] The action or actions we're checking whether the actor has. If this is an array, then the check is ORed.
21
+ # @param object_type [String] The namespace of the given action_id.
22
+ # @return [Boolean] Returns true if actor has been granted the permission, false otherwise.
23
+ #
24
+ # @example
25
+ # # Can the user perform the action with id 3 for posts?
26
+ # Accessly::Query.new(user).can?(3, Post)
27
+ # @example
28
+ # # Can the user perform the action with id 3 for posts on segment 1?
29
+ # Accessly::Query.new(user).on_segment(1).can?(3, Post)
30
+ def can?(action_id, object_type)
31
+ find_or_set_value(action_id, object_type) do
32
+ Accessly::QueryBuilder.with_actors(Accessly::PermittedAction, @actors)
33
+ .where(
34
+ segment_id: @segment_id,
35
+ action: action_id,
36
+ object_type: String(object_type),
37
+ ).exists?
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,269 @@
1
+ module Accessly
2
+ module Policy
3
+ class Base
4
+
5
+ attr_reader :actor
6
+
7
+ def initialize(actor)
8
+ @actor = actor
9
+ end
10
+
11
+ def self.actions(actions)
12
+ _actions.merge!(actions)
13
+ actions.each do |action, action_id|
14
+ _define_action_methods(action, action_id)
15
+ end
16
+ end
17
+
18
+ def self.actions_on_objects(actions_on_objects)
19
+ _actions_on_objects.merge!(actions_on_objects)
20
+ actions_on_objects.each do |action, action_id|
21
+ _define_action_methods(action, action_id)
22
+ end
23
+ end
24
+
25
+ def self.namespace
26
+ String(self)
27
+ end
28
+
29
+ def namespace
30
+ self.class.namespace
31
+ end
32
+
33
+ def self.model_scope
34
+ raise ArgumentError.new("#model_scope is not defined on #{self.name}.")
35
+ end
36
+
37
+ def model_scope
38
+ self.class.model_scope
39
+ end
40
+
41
+ def unrestricted?
42
+ false
43
+ end
44
+
45
+ def segment_id
46
+ nil
47
+ end
48
+
49
+ def grant(action, object = nil)
50
+ object_id = _get_object_id(object)
51
+
52
+ action_id = if object_id.nil?
53
+ _get_general_action_id!(action)
54
+ else
55
+ _get_action_on_object_id!(action)
56
+ end
57
+
58
+ grant_object.grant!(action_id, namespace, object_id)
59
+ end
60
+
61
+ def revoke(action, object = nil)
62
+ object_id = _get_object_id(object)
63
+
64
+ action_id = if object_id.nil?
65
+ _get_general_action_id!(action)
66
+ else
67
+ _get_action_on_object_id!(action)
68
+ end
69
+
70
+ revoke_object.revoke!(action_id, namespace, object_id)
71
+ end
72
+
73
+ def accessly_query
74
+ @_accessly_query ||= begin
75
+ query = Accessly::Query.new(actor)
76
+ query.on_segment(segment_id) unless segment_id.nil?
77
+ query
78
+ end
79
+ end
80
+
81
+ def grant_object
82
+ grant_object = Accessly::Permission::Grant.new(actor)
83
+ grant_object.on_segment(segment_id) unless segment_id.nil?
84
+
85
+ grant_object
86
+ end
87
+
88
+ def revoke_object
89
+ revoke_object = Accessly::Permission::Revoke.new(actor)
90
+ revoke_object.on_segment(segment_id) unless segment_id.nil?
91
+
92
+ revoke_object
93
+ end
94
+
95
+ private
96
+
97
+ # Determines whether the caller is trying to call an action method
98
+ # in the format `action_name?`. If so, this calls that method with
99
+ # the given arguments.
100
+ def method_missing(method_name, *args)
101
+ action_method_name = _resolve_action_method_name(method_name)
102
+ if action_method_name.nil?
103
+ super
104
+ else
105
+ send(action_method_name, *args)
106
+ end
107
+ end
108
+
109
+ # Parses an action name from a given method name of the format
110
+ # `action_name?` or `action_name and returns the action method
111
+ # or the list method name. If the method name does not follow
112
+ # one of those formats, this assumes the caller is not calling
113
+ # an action or list method and returns nil.
114
+ def _resolve_action_method_name(method_name)
115
+ action_method_match = /\A(\w+)(\??)\z/.match(method_name)
116
+
117
+ return nil if action_method_match.nil? || action_method_match[1].nil?
118
+
119
+ action_name = action_method_match[1].to_sym
120
+ is_predicate = action_method_match[2] == "?"
121
+
122
+ if !_action_defined?(action_name)
123
+ nil
124
+ elsif is_predicate
125
+ _action_method_name(action_name)
126
+ else
127
+ _action_list_method_name(action_name)
128
+ end
129
+ end
130
+
131
+ # The implementation for action methods follow the naming format
132
+ # `_resolve_action_name`. This is to allow child Policies to override
133
+ # the action method and still be able to call `super` when they
134
+ # need to call the base implementation of the action method.
135
+ def self._action_method_name(action_name)
136
+ "_resolve_#{action_name}"
137
+ end
138
+
139
+ def _action_method_name(action_name)
140
+ self.class._action_method_name(action_name)
141
+ end
142
+
143
+ # The implementation for list methods follow the naming format
144
+ # `_list_action_name`. This is to allow child Policies to override
145
+ # the list method and still be able to call `super` when they
146
+ # need to call the base implementation of the lsit method.
147
+ def self._action_list_method_name(action_name)
148
+ "_list_#{action_name}"
149
+ end
150
+
151
+ def _action_list_method_name(action_name)
152
+ self.class._action_list_method_name(action_name)
153
+ end
154
+
155
+ # Defines the action method on the Policy class for the given
156
+ # action name.
157
+ def self._define_action_methods(action, action_id)
158
+ unless method_defined?(_action_method_name(action))
159
+ define_method(_action_method_name(action)) do |*args|
160
+ _can_do_action?(action, action_id, args.first)
161
+ end
162
+ end
163
+
164
+ unless method_defined?(_action_list_method_name(action))
165
+ define_method(_action_list_method_name(action)) do |*args|
166
+ _list_for_action(action, action_id)
167
+ end
168
+ end
169
+ end
170
+
171
+ # Determines whether the caller is calling an object action
172
+ # method or a non-object action method and calls the appropriate
173
+ # implementation.
174
+ def _can_do_action?(action, action_id, object)
175
+ if object.nil?
176
+ _can_do_action_without_object?(action, action_id)
177
+ else
178
+ _can_do_action_with_object?(action, action_id, object)
179
+ end
180
+ end
181
+
182
+ # Determines whether the actor has permission to do the action
183
+ # outside of an object context. If the actor should have unrestricted
184
+ # access, then this returns true without checking.
185
+ #
186
+ # @return [Boolean]
187
+ def _can_do_action_without_object?(action, action_id)
188
+ if _actions[action].nil?
189
+ _invalid_general_action!(action)
190
+ elsif unrestricted?
191
+ true
192
+ else
193
+ accessly_query.can?(action_id, namespace)
194
+ end
195
+ end
196
+
197
+ # Determines whether the actor has permission to do the action
198
+ # on an object. If the actor should have unrestricted access,
199
+ # then this returns true without checking.
200
+ #
201
+ # @return [Boolean]
202
+ def _can_do_action_with_object?(action, action_id, object)
203
+ object_id = _get_object_id(object)
204
+
205
+ if _actions_on_objects[action].nil?
206
+ _invalid_action_on_object!(action)
207
+ elsif unrestricted?
208
+ true
209
+ else
210
+ accessly_query.can?(action_id, namespace, object_id)
211
+ end
212
+ end
213
+
214
+ def _list_for_action(action, action_id)
215
+ if _actions_on_objects[action].nil?
216
+ _invalid_action_on_object!(action)
217
+ elsif unrestricted?
218
+ model_scope
219
+ else
220
+ model_scope.where(id: accessly_query.list(action_id, namespace))
221
+ end
222
+ end
223
+
224
+ def _get_general_action_id!(action)
225
+ _actions[action] || _invalid_general_action!(action)
226
+ end
227
+
228
+ def _get_action_on_object_id!(action)
229
+ _actions_on_objects[action] || _invalid_action_on_object!(action)
230
+ end
231
+
232
+ def _invalid_general_action!(action)
233
+ raise ArgumentError.new("#{action} is not defined as a general action for #{self.class.name}")
234
+ end
235
+
236
+ def _invalid_action_on_object!(action)
237
+ raise ArgumentError.new("#{action} is not defined as an action-on-object for #{self.class.name}")
238
+ end
239
+
240
+ def _get_object_id(object)
241
+ object.respond_to?(:id) ? object.id : object
242
+ end
243
+
244
+ def self._action_defined?(action_name)
245
+ _actions.include?(action_name) || _actions_on_objects.include?(action_name)
246
+ end
247
+
248
+ def _action_defined?(action_name)
249
+ self.class._action_defined?(action_name)
250
+ end
251
+
252
+ def self._actions
253
+ @@_actions ||= {}
254
+ end
255
+
256
+ def _actions
257
+ self.class._actions
258
+ end
259
+
260
+ def self._actions_on_objects
261
+ @@_actions_on_objects ||= {}
262
+ end
263
+
264
+ def _actions_on_objects
265
+ self.class._actions_on_objects
266
+ end
267
+ end
268
+ end
269
+ end