resource_policy 1.0.0

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