resource_policy 1.0.0

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.
Files changed (42) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/ruby.yml +20 -0
  3. data/.gitignore +15 -0
  4. data/.hound.yml +3 -0
  5. data/.rspec +3 -0
  6. data/.rubocop.yml +43 -0
  7. data/.ruby-version +1 -0
  8. data/.travis.yml +7 -0
  9. data/CHANGELOG.md +19 -0
  10. data/CODE_OF_CONDUCT.md +74 -0
  11. data/Gemfile +11 -0
  12. data/Gemfile.lock +232 -0
  13. data/LICENSE.txt +21 -0
  14. data/Rakefile +6 -0
  15. data/bin/console +14 -0
  16. data/bin/setup +8 -0
  17. data/docs/.nojekyll +0 -0
  18. data/docs/README.md +163 -0
  19. data/docs/_sidebar.md +6 -0
  20. data/docs/components/action_validator.md +34 -0
  21. data/docs/components/actions_policy.md +68 -0
  22. data/docs/components/attributes_policy.md +68 -0
  23. data/docs/components/policy.md +202 -0
  24. data/docs/index.html +70 -0
  25. data/lib/resource_policy/policy/action_policy_configuration.rb +37 -0
  26. data/lib/resource_policy/policy/actions_policy/action_policy.rb +32 -0
  27. data/lib/resource_policy/policy/actions_policy/actions_policy_model.rb +39 -0
  28. data/lib/resource_policy/policy/actions_policy.rb +35 -0
  29. data/lib/resource_policy/policy/attributes_policy/attribute_configuration.rb +72 -0
  30. data/lib/resource_policy/policy/attributes_policy/attribute_policy.rb +49 -0
  31. data/lib/resource_policy/policy/attributes_policy/attributes_policy_model.rb +52 -0
  32. data/lib/resource_policy/policy/attributes_policy.rb +58 -0
  33. data/lib/resource_policy/policy/merge_policies.rb +44 -0
  34. data/lib/resource_policy/policy/policy_configuration.rb +87 -0
  35. data/lib/resource_policy/policy.rb +31 -0
  36. data/lib/resource_policy/protected_resource.rb +43 -0
  37. data/lib/resource_policy/rails.rb +5 -0
  38. data/lib/resource_policy/validators/action_policy_validator.rb +54 -0
  39. data/lib/resource_policy/version.rb +5 -0
  40. data/lib/resource_policy.rb +11 -0
  41. data/resource_policy.gemspec +47 -0
  42. metadata +212 -0
@@ -0,0 +1,34 @@
1
+ # ResourcePolicy::ActionValidator
2
+
3
+ The `ResourcePolicy::ActionValidator` is a validator used to check the policy of a resource before performing a certain action. It helps to ensure that a user is only allowed to perform actions on a resource that they have permission to do so.
4
+
5
+ ## Options
6
+
7
+ The `ResourcePolicy::ActionValidator` accepts two options:
8
+
9
+ - `:allowed_to` (required) - Specifies the action type that needs to be checked. This can be a symbol or a string.
10
+ - `:as` (optional) - Specifies the key that will be used to display errors. This is useful if you want to rename the attribute being validated.
11
+
12
+ ## Usage Example
13
+
14
+ ```ruby
15
+ require 'resource_policy/action_validator'
16
+
17
+ class SomeClass
18
+ validates :some_policy, 'resource_policy/action': { allowed_to: :create, as: :some_item }
19
+
20
+ def some_policy
21
+ SomePolicy.new
22
+ end
23
+ end
24
+
25
+ some_object = SomeClass.new
26
+
27
+ if some_object.valid?
28
+ # No validation errors, continue with the process
29
+ else
30
+ some_object.errors.messages # => { some_item: ['action "create" is not allowed'] }
31
+ end
32
+ ```
33
+
34
+ In this example, the `SomeClass` has an attribute named `some_policy` which is being validated using the `ResourcePolicy::ActionValidator`. The validator checks if the create action is allowed using the SomePolicy object. If the action is not allowed, an error message will be added to the `record.errors` object with the key `:some_item`. If the `:as` option is not provided, the key used to display the error will be the name of the attribute being validated.
@@ -0,0 +1,68 @@
1
+ # ResourcePolicy::ActionsPolicy
2
+
3
+ ## policy#action
4
+
5
+ Actions policy allows you to define config for each action.
6
+
7
+ Using `action` and `allowed` methods chain you can define conditions for each action:
8
+
9
+ ```ruby
10
+ class UserPolicy
11
+ include ResourcePolicy::Policy
12
+
13
+ policy do |c|
14
+ c.action(:read).allowed
15
+ c.action(:write).allowed(if: %i[admin? admin?])
16
+ end
17
+
18
+ def initialize(user, current_user)
19
+ @user, @current_user = user, current_user
20
+ end
21
+
22
+ private
23
+
24
+ def user_itself?
25
+ @user == @current_user
26
+ end
27
+
28
+ def admin?
29
+ @current_user.admin?
30
+ end
31
+ end
32
+ ```
33
+
34
+ If no condition is given then action will be always allowed.
35
+
36
+ This config means:
37
+
38
+ * `read` action is always allowed;
39
+ * `write` action is allowed only if both `admin?` and `writable?` methods returns `true`.
40
+
41
+
42
+ ### policy#group
43
+
44
+ Sometimes you might have action groups which share same conditions. In this case you can group then using `#group` method:
45
+
46
+ ```ruby
47
+ class UserPolicy
48
+ include ResourcePolicy::Policy
49
+
50
+ policy do |c|
51
+ c.group(:user_itself?) do |g|
52
+ g.allowed_to(:change_password)
53
+ g.allowed_to(:destroy, if: :admin?)
54
+ end
55
+ end
56
+
57
+ private
58
+
59
+ def admin?
60
+ ...
61
+ end
62
+ end
63
+ ```
64
+
65
+ In this case:
66
+
67
+ * `change_password` will be allowed if `user_itself?` returns `true`;
68
+ * `destroy` will be allowed if both `user_itself?` and ``admin?` returns `true`.
@@ -0,0 +1,68 @@
1
+ # ResourcePolicy::AttributesPolicy
2
+
3
+
4
+ ### policy#attribute
5
+
6
+ Attributes policy allows you to define separate attributes.
7
+
8
+ Using `attribute#allowed` method you can define conditions for each attribute and for each action type like `read`, `write` and etc:
9
+
10
+ ```ruby
11
+ class UserPolicy
12
+ include ResourcePolicy::AttributesPolicy
13
+
14
+ policy do |c|
15
+ c.attribute(:first_name)
16
+ .allowed(:read)
17
+ .allowed(:write, if: %i[admin? writable?])
18
+ end
19
+
20
+ private
21
+
22
+ def admin?
23
+ ...
24
+ end
25
+
26
+ def writable?
27
+ ...
28
+ end
29
+ end
30
+ ```
31
+
32
+ If no condition is given then action will be always allowed for given attribute.
33
+
34
+ ### attributes_policy#group
35
+
36
+ Sometimes you might have attribute groups which share same conditions. In this case you can group then using `#group` method:
37
+
38
+ ```ruby
39
+ class UserPolicy
40
+ include ResourcePolicy::AttributesPolicy
41
+
42
+ group(:user_itself?) do |c|
43
+ c.attribute(:password).allowed(:write)
44
+ c.attribute(:email).allowed(:read, :write)
45
+ c.attribute(:children).allowed(:read, if: :parent?)
46
+ end
47
+
48
+ def initialize(user, current_user)
49
+ @user, @current_user = user, current_user
50
+ end
51
+
52
+ private
53
+
54
+ def user_itself?
55
+ @user == @current_user
56
+ end
57
+
58
+ def parent?
59
+ @user.parent?
60
+ end
61
+ end
62
+ ```
63
+
64
+ In this case:
65
+
66
+ * `password` will be allowed to `write` only if `user_itself?` returns `true`.
67
+ * `email` will be allowed to `read` and `write` only if `user_itself?` returns `true`.
68
+ * `children` will be allowed to `read` if both `user_itself?` **and** `parent?` returns `true`.
@@ -0,0 +1,202 @@
1
+ # ResourcePolicy::Policy
2
+
3
+ `Policy` includes both `AttributesPolicy` and `ActionsPolicy` modules. Their features are described separately, so read more there if you need more info.
4
+
5
+ ## Policy Configuration
6
+
7
+ Here is and example how policy looks like:
8
+
9
+ ```ruby
10
+ class UserPolicy
11
+ include ResourcePolicy::Policy
12
+
13
+ policy do |c|
14
+ c.policy_target :user
15
+
16
+ c.action(:read).allowed(if: :readable?)
17
+
18
+ c.attribute(:first_name)
19
+ .allowed(:read, if: :readable?)
20
+ .allowed(:write, if: :writable?)
21
+ end
22
+
23
+ def initialize(user, current_user:)
24
+ @user = user
25
+ @current_user = current_user
26
+ end
27
+
28
+ private
29
+
30
+ def readable?
31
+ true
32
+ end
33
+
34
+ def writable?
35
+ true
36
+ end
37
+ end
38
+ ```
39
+
40
+ ### policy#group
41
+
42
+ Sometimes you might have action groups which share same conditions. In this case you can group then using `#group` method:
43
+
44
+ ```ruby
45
+ class UserPolicy
46
+ include ResourcePolicy::Policy
47
+
48
+ policy do |c|
49
+ c.group(:user_itself?) do |g|
50
+ g.action(:change_password).allowed
51
+ g.action(:destroy).allowed(if: :admin?)
52
+ end
53
+ end
54
+
55
+ private
56
+
57
+ def admin?
58
+ ...
59
+ end
60
+ end
61
+ ```
62
+
63
+ In this case:
64
+
65
+ * `change_password` will be allowed if `user_itself?` returns `true`;
66
+ * `destroy` will be allowed if both `user_itself?` and ``admin?` returns `true`.
67
+
68
+
69
+ ## Usage of Policy#action
70
+
71
+ Suppose we have policy like this:
72
+
73
+ ```ruby
74
+ class UserPolicy
75
+ include ResourcePolicy::Policy
76
+
77
+ policy do |c|
78
+ c.action(:read).allowed # current_user can always see user
79
+ c.action(:write).allowed(if: :admin?) # only admin current_user can update user
80
+ end
81
+
82
+ def initialize(user, current_user:)
83
+ @user = user
84
+ @current_user = current_user
85
+ end
86
+
87
+ private
88
+
89
+ def admin?
90
+ @current_user.admin?
91
+ end
92
+ end
93
+ ```
94
+
95
+ then we can check each action like this:
96
+
97
+ ```ruby
98
+ policy = UserPolicy.new(user, current_user: current_user)
99
+ policy.action(:read).allowed? # => true
100
+ policy.action(:write).allowed? # ... depends on `admin?` result
101
+ ```
102
+
103
+ ## Policy#actions_policy
104
+
105
+ Another way to check each action is to use `actions_policy` object like this:
106
+
107
+ ```ruby
108
+ policy = UserPolicy.new(user, current_user: current_user)
109
+ actions_policy = policy.actions_policy
110
+ actions_policy.read.allowed? # => true
111
+ actions_policy.write.allowed? # ... depends on `admin?` result
112
+ ```
113
+
114
+ ## Usage of Policy#attribute
115
+
116
+ Suppose we have policy like this:
117
+
118
+ ```ruby
119
+ class UserPolicy
120
+ include ResourcePolicy::Policy
121
+
122
+ policy do |c|
123
+ c.attribute(:email)
124
+ .allowed(:read) # current_user can always view user.email
125
+ .allowed(:write, if: :admin?) # only admin current_user can change email
126
+ end
127
+
128
+ def initialize(user, current_user:)
129
+ @user = user
130
+ @current_user = current_user
131
+ end
132
+
133
+ private
134
+
135
+ def admin?
136
+ @current_user.admin?
137
+ end
138
+ end
139
+ ```
140
+
141
+ then we can check each attribute like this:
142
+
143
+ ```ruby
144
+ policy = UserPolicy.new(user, current_user: current_user)
145
+ policy.attribute(:email).allowed_to?(:change) # => false - no such rule
146
+ policy.attribute(:email).readable? # => true
147
+ policy.attribute(:email).writable? # ... depends on `admin?` result
148
+ ```
149
+
150
+ ## Usage of Policy#attributes_policy
151
+
152
+ Another way to check each action is to use `attributes_policy` object like this:
153
+
154
+ ```ruby
155
+ policy = UserPolicy.new(user, current_user: current_user)
156
+ attributes_policy = policy.attributes_policy
157
+
158
+ attributes_policy.email.allowed_to?(:change) # => false - no such rule
159
+ attributes_policy.email.readable? # same as `allowed_to?(:read)`
160
+ attributes_policy.email.writable? # same as `allowed_to?(:write)`
161
+ ```
162
+
163
+ ## Usage of Policy#protected_resource
164
+
165
+ Policy provides `#protected_resource` method which returns wrapped model instance and does not allow to view fields which current_user does not have access to. You must define `policy_target` in order to be able to use `protected_resource` feature
166
+
167
+ Usage example:
168
+
169
+ ```ruby
170
+ class UserPolicy
171
+ include ResourcePolicy::Policy
172
+
173
+ policy do |c|
174
+ c.policy_target(:user) # method name which returns target
175
+ c.attribute(:id).allowed(:read) # visible to all
176
+ c.attribute(:salary).allowed(:read, if: :admin?) # only visible to admin
177
+ end
178
+
179
+ def initialize(user, current_user:)
180
+ @user = user
181
+ @current_user = current_user
182
+ end
183
+
184
+ def admin?
185
+ @current_user.admin?
186
+ end
187
+ end
188
+ ```
189
+
190
+ Now you can protect `user` like this:
191
+
192
+ ```ruby
193
+ current_user.admin? #=> false
194
+
195
+ user = User.find(1337)
196
+ user.id #=> 1337
197
+ user.email #=> "john.doe@example.com"
198
+
199
+ policy = UserPolicy.new(user, current_user: current_user)
200
+ policy.protected_resource.id #=> 1337
201
+ policy.protected_resource.email # nil
202
+ ```
data/docs/index.html ADDED
@@ -0,0 +1,70 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <title>Document</title>
6
+ <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
7
+ <meta name="description" content="Description">
8
+ <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
9
+ <link rel="stylesheet" href="https://unpkg.com/docsify/lib/themes/vue.css">
10
+ </head>
11
+ <body>
12
+ <div id="app"></div>
13
+ <script>
14
+ function parseQueryString (queryString) {
15
+ var params = {};
16
+ var temp;
17
+ // Split into key/value pairs
18
+ queries = queryString.split("&");
19
+ // Convert the array of strings into an object
20
+ for (var i = 0, l = queries.length; i < l; i++ ) {
21
+ temp = queries[i].split('=');
22
+ params[temp[0]] = temp[1];
23
+ }
24
+ return params;
25
+ };
26
+
27
+ function getJsonFromUrl() {
28
+ return parseQueryString(location.search.substr(1));
29
+ }
30
+
31
+ window.$docsify = {
32
+ auto2top: true,
33
+ name: 'ResourcePolicy',
34
+ repo: 'https://github.com/samesystem/resource_policy',
35
+ subMaxLevel: 3,
36
+ loadSidebar: true,
37
+ formatUpdated: '{MM}/{DD} {HH}:{mm}',
38
+ branchBasePath: 'https://raw.githubusercontent.com/samesystem/resource_policy/',
39
+ plugins: [
40
+ function (hook, vm) { // reasign any config value by param attribute
41
+ Object.assign(window.$docsify, getJsonFromUrl());
42
+ },
43
+
44
+ function (hook, vm) { // allow to change branch
45
+ if (!window.$docsify.branchBasePath || !window.$docsify.branch) {
46
+ return;
47
+ }
48
+
49
+ var branch = window.$docsify.branch;
50
+ var basePath = window.$docsify.branchBasePath + branch;
51
+ window.$docsify.basePath = basePath;
52
+ },
53
+
54
+ function (hook, vm) { // add edit page link
55
+ hook.beforeEach(function (html) {
56
+ var branch = window.$docsify.branch || 'master'
57
+ var url = 'https://github.com/samesystem/resource_policy/edit/' + branch + '/docs/' + vm.route.file
58
+ var editHtml = '[:memo: Edit Document](' + url + ')\n'
59
+ return html
60
+ + '\n\n----\n\n'
61
+ + editHtml
62
+ })
63
+ }
64
+ ]
65
+ }
66
+ </script>
67
+ <script src="https://unpkg.com/docsify/lib/docsify.js"></script>
68
+ <script src="https://unpkg.com/docsify/lib/plugins/search.min.js"></script>
69
+ </body>
70
+ </html>
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ResourcePolicy
4
+ module Policy
5
+ # @private
6
+ #
7
+ # Stores configuration for action policy.
8
+ class ActionPolicyConfiguration
9
+ attr_reader :name
10
+
11
+ def initialize(name, policy_configuration:)
12
+ @name = name.to_sym
13
+ @policy_configuration = policy_configuration
14
+ @extra_conditions = []
15
+ @configured = false
16
+ end
17
+
18
+ def allowed(options = {})
19
+ @extra_conditions = (@extra_conditions + Array(options[:if])).uniq
20
+ @configured = true
21
+ self
22
+ end
23
+
24
+ def conditions
25
+ policy_configuration.group_conditions + @extra_conditions
26
+ end
27
+
28
+ def configured?
29
+ @configured
30
+ end
31
+
32
+ private
33
+
34
+ attr_reader :policy_configuration
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ResourcePolicy
4
+ module Policy
5
+ module ActionsPolicy
6
+ # Contains information about single action
7
+ class ActionPolicy
8
+ attr_reader :name
9
+
10
+ def initialize(name, policy:)
11
+ @name = name.to_sym
12
+ @policy = policy
13
+ end
14
+
15
+ def allowed?
16
+ return @allowed if defined?(@allowed)
17
+
18
+ conditions = policy_config.action(name).conditions
19
+ @allowed = conditions.all? { |condition| policy.send(condition) }
20
+ end
21
+
22
+ private
23
+
24
+ attr_reader :policy
25
+
26
+ def policy_config
27
+ policy.class.policy
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ResourcePolicy
4
+ module Policy
5
+ module ActionsPolicy
6
+ # Class which isolates methods defined via actions_policy config
7
+ class ActionsPolicyModel
8
+ require 'resource_policy/policy/actions_policy/action_policy'
9
+
10
+ def initialize(policy)
11
+ @policy = policy
12
+ @policy_item_by_name ||= {}
13
+ end
14
+
15
+ def method_missing(method_name)
16
+ return super unless config.actions.key?(method_name.to_sym)
17
+
18
+ policy_item(method_name.to_sym)
19
+ end
20
+
21
+ def respond_to_missing?(method_name, *args)
22
+ config.actions.key?(method_name.to_sym) || super
23
+ end
24
+
25
+ private
26
+
27
+ attr_reader :policy
28
+
29
+ def config
30
+ policy.class.policy
31
+ end
32
+
33
+ def policy_item(name)
34
+ @policy_item_by_name[name] ||= ActionPolicy.new(name, policy: policy)
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ResourcePolicy
4
+ module Policy
5
+ # Allows to define actions policy using configuration block.
6
+ #
7
+ # Usage example:
8
+ #
9
+ # class SomeModelPolicy
10
+ # include Policy::ActionsPolicy
11
+ #
12
+ # policy do |c|
13
+ # c.action(:create).allowed(if: :current_user_is_admin?)
14
+ # end
15
+ #
16
+ # private
17
+ #
18
+ # def current_user_is_admin?
19
+ # current_user.admin?
20
+ # end
21
+ # ...
22
+ # end
23
+ module ActionsPolicy
24
+ require 'resource_policy/policy/actions_policy/actions_policy_model'
25
+
26
+ def actions_policy
27
+ @actions_policy ||= ActionsPolicyModel.new(self)
28
+ end
29
+
30
+ def action(name)
31
+ actions_policy.public_send(name) if actions_policy.respond_to?(name)
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ResourcePolicy
4
+ module Policy
5
+ module AttributesPolicy
6
+ # @private
7
+ #
8
+ # Allows to define policy for single attribute
9
+ class AttributeConfiguration
10
+ DEFAULT_OPTIONS = { if: [] }.freeze
11
+ ALLOWED_ACTIONS = %i[read write].freeze
12
+
13
+ attr_reader :name
14
+
15
+ def initialize(name, policy_configuration:)
16
+ @name = name
17
+ @allowed_actions = {}
18
+ @policy_configuration = policy_configuration
19
+ end
20
+
21
+ def initialize_copy(other)
22
+ super
23
+ @allowed_actions = @allowed_actions.dup.transform_values(&:dup)
24
+ end
25
+
26
+ def allowed(*action_types, **options)
27
+ action_types.map(&:to_sym).each do |action|
28
+ allowed_actions[action] = merged_action_options(action, options)
29
+ end
30
+ self
31
+ end
32
+
33
+ def conditions_for(action)
34
+ action_conditions = allowed_actions.fetch(action, {}).fetch(:if, [])
35
+ (action_conditions + policy_configuration.group_conditions).uniq
36
+ end
37
+
38
+ def configured?
39
+ !defined_actions.empty?
40
+ end
41
+
42
+ def defined_actions
43
+ allowed_actions.keys
44
+ end
45
+
46
+ def defined_action?(action_name)
47
+ defined_actions.include?(action_name.to_sym)
48
+ end
49
+
50
+ def merge(other)
51
+ dup.tap do |new_attribute|
52
+ other.defined_actions.each do |action|
53
+ new_attribute.allowed(action, if: other.conditions_for(action))
54
+ end
55
+ end
56
+ end
57
+
58
+ private
59
+
60
+ attr_reader :allowed_actions, :policy_configuration
61
+
62
+ def merged_action_options(action, new_options)
63
+ previous_options = allowed_actions[action]
64
+ options = previous_options || DEFAULT_OPTIONS.dup
65
+ options[:if] += Array(new_options[:if])
66
+ options[:if].uniq!
67
+ options
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ResourcePolicy
4
+ module Policy
5
+ module AttributesPolicy
6
+ # @private
7
+ #
8
+ # Stores information about access level of single attribute.
9
+ class AttributePolicy
10
+ def initialize(attribute_config, policy:)
11
+ @policy = policy
12
+ @attribute_config = attribute_config
13
+ end
14
+
15
+ def name
16
+ attribute_config.name
17
+ end
18
+
19
+ def readable?
20
+ allowed_to?(:read)
21
+ end
22
+
23
+ def writable?
24
+ allowed_to?(:write)
25
+ end
26
+
27
+ def allowed_to?(access_level)
28
+ @allowed_to ||= {}
29
+ level_name = access_level.to_sym
30
+
31
+ return @allowed_to[level_name] if @allowed_to.key?(level_name)
32
+
33
+ @allowed_to[level_name] = fetch_allowed_to(level_name)
34
+ end
35
+
36
+ private
37
+
38
+ attr_reader :policy, :attribute_config
39
+
40
+ def fetch_allowed_to(access_level)
41
+ return false unless attribute_config.defined_actions.include?(access_level)
42
+
43
+ conditions = attribute_config.conditions_for(access_level)
44
+ conditions.all? { |condition| policy.send(condition) }
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end