action_policy 0.4.4 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (97) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +203 -174
  3. data/README.md +5 -4
  4. data/lib/action_policy.rb +7 -1
  5. data/lib/action_policy/behaviour.rb +22 -16
  6. data/lib/action_policy/behaviours/policy_for.rb +10 -3
  7. data/lib/action_policy/behaviours/scoping.rb +2 -1
  8. data/lib/action_policy/behaviours/thread_memoized.rb +1 -3
  9. data/lib/action_policy/ext/module_namespace.rb +1 -6
  10. data/lib/action_policy/ext/policy_cache_key.rb +10 -30
  11. data/lib/action_policy/i18n.rb +1 -1
  12. data/lib/action_policy/lookup_chain.rb +29 -15
  13. data/lib/action_policy/policy/aliases.rb +7 -12
  14. data/lib/action_policy/policy/authorization.rb +8 -7
  15. data/lib/action_policy/policy/cache.rb +11 -17
  16. data/lib/action_policy/policy/core.rb +25 -12
  17. data/lib/action_policy/policy/defaults.rb +3 -9
  18. data/lib/action_policy/policy/execution_result.rb +3 -9
  19. data/lib/action_policy/policy/pre_check.rb +19 -58
  20. data/lib/action_policy/policy/reasons.rb +29 -19
  21. data/lib/action_policy/policy/scoping.rb +5 -6
  22. data/lib/action_policy/rails/controller.rb +6 -1
  23. data/lib/action_policy/rails/policy/instrumentation.rb +1 -1
  24. data/lib/action_policy/rspec/be_authorized_to.rb +5 -9
  25. data/lib/action_policy/rspec/dsl.rb +1 -1
  26. data/lib/action_policy/rspec/have_authorized_scope.rb +5 -7
  27. data/lib/action_policy/utils/pretty_print.rb +21 -24
  28. data/lib/action_policy/utils/suggest_message.rb +1 -3
  29. data/lib/action_policy/version.rb +1 -1
  30. data/lib/generators/action_policy/install/templates/{application_policy.rb → application_policy.rb.tt} +0 -0
  31. data/lib/generators/action_policy/policy/policy_generator.rb +4 -1
  32. data/lib/generators/action_policy/policy/templates/{policy.rb → policy.rb.tt} +0 -0
  33. data/lib/generators/rspec/templates/{policy_spec.rb → policy_spec.rb.tt} +0 -0
  34. data/lib/generators/test_unit/templates/{policy_test.rb → policy_test.rb.tt} +0 -0
  35. metadata +29 -119
  36. data/.gitattributes +0 -2
  37. data/.github/ISSUE_TEMPLATE.md +0 -21
  38. data/.github/PULL_REQUEST_TEMPLATE.md +0 -29
  39. data/.github/bug_report_template.rb +0 -175
  40. data/.gitignore +0 -15
  41. data/.rubocop.yml +0 -54
  42. data/.tidelift.yml +0 -6
  43. data/.travis.yml +0 -31
  44. data/Gemfile +0 -22
  45. data/Rakefile +0 -27
  46. data/action_policy.gemspec +0 -44
  47. data/benchmarks/namespaced_lookup_cache.rb +0 -74
  48. data/benchmarks/pre_checks.rb +0 -73
  49. data/bin/console +0 -14
  50. data/bin/setup +0 -8
  51. data/docs/.nojekyll +0 -0
  52. data/docs/CNAME +0 -1
  53. data/docs/README.md +0 -79
  54. data/docs/_sidebar.md +0 -27
  55. data/docs/aliases.md +0 -122
  56. data/docs/assets/docsify-search.js +0 -364
  57. data/docs/assets/docsify.min.js +0 -3
  58. data/docs/assets/fonts/FiraCode-Medium.woff +0 -0
  59. data/docs/assets/fonts/FiraCode-Regular.woff +0 -0
  60. data/docs/assets/images/banner.png +0 -0
  61. data/docs/assets/images/cache.png +0 -0
  62. data/docs/assets/images/cache.svg +0 -70
  63. data/docs/assets/images/layer.png +0 -0
  64. data/docs/assets/images/layer.svg +0 -35
  65. data/docs/assets/prism-ruby.min.js +0 -1
  66. data/docs/assets/styles.css +0 -347
  67. data/docs/assets/vue.min.css +0 -1
  68. data/docs/authorization_context.md +0 -92
  69. data/docs/behaviour.md +0 -113
  70. data/docs/caching.md +0 -291
  71. data/docs/controller_action_aliases.md +0 -109
  72. data/docs/custom_lookup_chain.md +0 -48
  73. data/docs/custom_policy.md +0 -53
  74. data/docs/debugging.md +0 -55
  75. data/docs/decorators.md +0 -27
  76. data/docs/favicon.ico +0 -0
  77. data/docs/graphql.md +0 -302
  78. data/docs/i18n.md +0 -44
  79. data/docs/index.html +0 -43
  80. data/docs/instrumentation.md +0 -84
  81. data/docs/lookup_chain.md +0 -22
  82. data/docs/namespaces.md +0 -77
  83. data/docs/non_rails.md +0 -28
  84. data/docs/pre_checks.md +0 -57
  85. data/docs/pundit_migration.md +0 -80
  86. data/docs/quick_start.md +0 -118
  87. data/docs/rails.md +0 -120
  88. data/docs/reasons.md +0 -120
  89. data/docs/scoping.md +0 -255
  90. data/docs/testing.md +0 -390
  91. data/docs/writing_policies.md +0 -107
  92. data/gemfiles/jruby.gemfile +0 -8
  93. data/gemfiles/rails42.gemfile +0 -9
  94. data/gemfiles/rails6.gemfile +0 -8
  95. data/gemfiles/railsmaster.gemfile +0 -6
  96. data/lib/action_policy/ext/string_match.rb +0 -14
  97. data/lib/action_policy/ext/yield_self_then.rb +0 -25
@@ -1,120 +0,0 @@
1
- # Using with Rails
2
-
3
- Action Policy seamlessly integrates with Ruby on Rails applications.
4
-
5
- In most cases, you do not have to do anything except writing policy files and adding `authorize!` calls.
6
-
7
- **NOTE:** both controllers and channels extensions are built on top of the Action Policy [behaviour](./behaviour.md) mixin.
8
-
9
- ## Generators
10
-
11
- Action Policy provides a couple of useful Rails generators:
12
-
13
- - `rails g action_policy:install` — adds `app/policies/application_policy.rb` file
14
- - `rails g action_policy:policy MODEL_NAME` — adds a policy file and a policy test file for a given model (also creates an `application_policy.rb` if it's missing)
15
-
16
- ## Controllers integration
17
-
18
- Action Policy assumes that you have a `current_user` method which specifies the current authenticated subject (`user`).
19
-
20
- You can turn off this behaviour by setting `config.action_policy.controller_authorize_current_user = false` in `application.rb`, or override it:
21
-
22
- ```ruby
23
- class ApplicationController < ActionController::Base
24
- authorize :user, through: :my_current_user
25
- end
26
- ```
27
-
28
- > Read more about [authorization context](authorization_context.md).
29
-
30
- In case you don't want to include Action Policy to controllers at all,
31
- you can turn disable the integration by setting `config.action_policy.auto_inject_into_controller = false` in `application.rb`.
32
-
33
- ### `verify_authorized` hooks
34
-
35
- Usually, you need all of your actions to be authorized. Action Policy provides a controller hook which ensures that an `authorize!` call has been made during the action:
36
-
37
- ```ruby
38
- class ApplicationController < ActionController::Base
39
- # adds an after_action callback to verify
40
- # that `authorize!` has been called.
41
- verify_authorized
42
-
43
- # you can also pass additional options,
44
- # like with a usual callback
45
- verify_authorized except: :index
46
- end
47
- ```
48
-
49
- You can skip this check when necessary:
50
-
51
- ```ruby
52
- class PostsController < ApplicationController
53
- skip_verify_authorized only: :show
54
- end
55
- ```
56
-
57
- When an unauthorized action is encountered, the `ActionPolicy::UnauthorizedAction` error is raised.
58
-
59
- ### Resource-less `authorize!`
60
-
61
- You can also call `authorize!` without a resource specified.
62
- In that case, Action Policy tries to infer the resource class from the controller name:
63
-
64
- ```ruby
65
- class PostsController < ApplicationPolicy
66
- def index
67
- # Uses Post class as a resource implicitly.
68
- # NOTE: it just calls `controller_name.classify.safe_constantize`,
69
- # you can override this by defining `implicit_authorization_target` method.
70
- authorize!
71
- end
72
- end
73
- ```
74
-
75
- ### Usage with `API` and `Metal` controllers
76
-
77
- Action Policy is only included into `ActionController::Base`. If you want to use it with other base Rails controllers, you have to include it manually:
78
-
79
- ```ruby
80
- class ApiController < ApplicationController::API
81
- include ActionPolicy::Controller
82
-
83
- # NOTE: you have to provide authorization context manually as well
84
- authorize :user, through: :current_user
85
- end
86
- ```
87
-
88
- ## Channels integration
89
-
90
- Action Policy also integrates with Action Cable to help you authorize your channels actions:
91
-
92
- ```ruby
93
- class ChatChannel < ApplicationCable::Channel
94
- def follow(data)
95
- chat = Chat.find(data["chat_id"])
96
-
97
- # Verify against ChatPolicy#show? rule
98
- authorize! chat, to: :show?
99
- stream_from chat
100
- end
101
- end
102
- ```
103
-
104
- Action Policy assumes that you have `current_user` as a connection identifier.
105
-
106
- You can turn off this behaviour by setting `config.action_policy.channel_authorize_current_user = false` in `application.rb`, or override it:
107
-
108
- ```ruby
109
- module ApplicationCable
110
- class Channel < ActionCable::Channel::Base
111
- # assuming that identifier is called `user`
112
- authorize :user
113
- end
114
- end
115
- ```
116
-
117
- > Read more about [authorization context](authorization_context.md).
118
-
119
- In case you do not want to include Action Policy to channels at all,
120
- you can disable the integration by setting `config.action_policy.auto_inject_into_channel = false` in `application.rb`.
@@ -1,120 +0,0 @@
1
- # Failure Reasons
2
-
3
- When you have complex policy rules, it could be helpful to have an ability to define an exact reason for why a specific authorization was rejected.
4
-
5
- It is especially helpful when you compose policies (i.e., use one policy within another) or want
6
- to expose permissions to client applications (see [GraphQL](./graphql)).
7
-
8
- Action Policy allows you to track failed `allowed_to?` checks in your rules.
9
-
10
- Consider an example:
11
-
12
- ```ruby
13
- class ApplicantPolicy < ApplicationPolicy
14
- def show?
15
- user.has_permission?(:view_applicants) &&
16
- allowed_to?(:show?, object.stage)
17
- end
18
- end
19
- ```
20
-
21
- When `ApplicantPolicy#show?` check fails, the exception has the `result` object, which in its turn contains additional information about the failure (`reasons`):
22
-
23
- ```ruby
24
- class ApplicationController < ActionController::Base
25
- rescue_from ActionPolicy::Unauthorized do |ex|
26
- p ex.result.reasons.details #=> { stage: [:show?] }
27
-
28
- # or with i18n support
29
- p ex.result.reasons.full_messages #=> ["You do not have access to the stage"]
30
- end
31
- end
32
- ```
33
-
34
- The reason key is the corresponding policy [identifier](writing_policies.md#identifiers).
35
-
36
- You can also wrap _local_ rules into `allowed_to?` to populate reasons:
37
-
38
- ```ruby
39
- class ApplicantPolicy < ApplicationPolicy
40
- def show?
41
- allowed_to?(:view_applicants?) &&
42
- allowed_to?(:show?, object.stage)
43
- end
44
-
45
- def view_applicants?
46
- user.has_permission?(:view_applicants)
47
- end
48
- end
49
-
50
- # then the reasons object could be
51
- p ex.result.reasons.details #=> { applicant: [:view_applicants?] }
52
-
53
- # or
54
- p ex.result.reasons.details #=> { stage: [:show?] }
55
- ```
56
-
57
- ## Detailed Reasons
58
-
59
- You can provide additional details to your failure reasons by using a `details: { ... }` option:
60
-
61
- ```ruby
62
- class ApplicantPolicy < ApplicationPolicy
63
- def show?
64
- allowed_to?(:show?, object.stage)
65
- end
66
- end
67
-
68
- class StagePolicy < ApplicationPolicy
69
- def show?
70
- # Add stage title to the failure reason (if any)
71
- # (could be used by client to show more descriptive message)
72
- details[:title] = record.title
73
-
74
- # then perform the checks
75
- user.stages.where(id: record.id).exists?
76
- end
77
- end
78
-
79
- # when accessing the reasons
80
- p ex.result.reasons.details #=> { stage: [{show?: {title: "Onboarding"}] }
81
- ```
82
-
83
- **NOTE**: when using detailed reasons, the `details` array contains as the last element
84
- a hash with ALL details reasons for the policy (in a form of `<rule> => <details>`).
85
-
86
- The additional details are especially helpful when combined with localization, 'cause you can you them as interpolation data source for your translations. For example, for the above policy:
87
-
88
- ```yml
89
- en:
90
- action_policy:
91
- policy:
92
- stage:
93
- show?: "The %{title} stage is not accessible"
94
- ```
95
-
96
- And then when you call `full_messages`:
97
-
98
- ```ruby
99
- p ex.result.reasons.full_messages #=> The Onboarding stage is not accessible
100
- ```
101
-
102
- **P.S. What is the point of failure reasons?**
103
-
104
- Failure reasons helps you to write _actionable_ error messages, i.e. to provide a user with helpful feedback.
105
-
106
- For example, in the above scenario, when the reason is `ApplicantPolicy#view_applicants?`, you could show the following message:
107
-
108
- ```
109
- You don't have enough permissions to view applicants.
110
- Please, ask your manager to update your role.
111
- ```
112
-
113
- And when the reason is `StagePolicy#show?`:
114
-
115
- ```
116
- You don't have access to the stage XYZ.
117
- Please, ask your manager to grant access to this stage.
118
- ```
119
-
120
- Much more useful than just showing "You are not authorized to perform this action," isn't it?
@@ -1,255 +0,0 @@
1
- # Scoping
2
-
3
- By _scoping_ we mean an ability to use policies to _scope data_ (or _filter/modify/transform/choose-your-verb_).
4
-
5
- The most common situation is when you want to _scope_ ActiveRecord relations depending
6
- on the current user permissions. Without policies it could look like this:
7
-
8
- ```ruby
9
- class PostsController < ApplicationController
10
- def index
11
- @posts =
12
- if current_user.admin?
13
- Post.all
14
- else
15
- Post.where(user: current_user)
16
- end
17
- end
18
- end
19
- ```
20
-
21
- That's a very simplified example. In practice scoping rules might be more complex, and it's likely that we would use them in multiple places.
22
-
23
- Action Policy allows you to define scoping rules within a policy class and use them with the help of `authorized_scope` method (`authorized` alias is also available):
24
-
25
- ```ruby
26
- class PostsController < ApplicationController
27
- def index
28
- @posts = authorized_scope(Post.all)
29
- end
30
- end
31
-
32
- class PostPolicy < ApplicationPolicy
33
- relation_scope do |relation|
34
- next relation if user.admin?
35
- relation.where(user: user)
36
- end
37
- end
38
- ```
39
-
40
- ## Define scopes
41
-
42
- To define scope you should use either `scope_for` or `smth_scope` methods in your policy:
43
-
44
- ```ruby
45
- class PostPolicy < ApplicationPolicy
46
- # define a scope of a `relation` type
47
- scope_for :relation do |relation|
48
- relation.where(user: user)
49
- end
50
-
51
- # define a scope of `my_data` type,
52
- # which acts on hashes
53
- scope_for :my_data do |data|
54
- next data if user.admin?
55
- data.delete_if { |k, _| SENSITIVE_KEYS.include?(k) }
56
- end
57
- end
58
- ```
59
-
60
- Scopes have _types_: different types of scopes are meant to be applied to different data types.
61
-
62
- You can specify multiple scopes (_named scopes_) for the same type providing a scope name:
63
-
64
- ```ruby
65
- class EventPolicy < ApplictionPolicy
66
- scope_for :relation, :own do |relation|
67
- relation.where(owner: user)
68
- end
69
- end
70
- ```
71
-
72
- When the second argument is not specified, the `:default` is implied as the scope name.
73
-
74
- Also, there are cases where it might be easier to add options to existing scope than create a new one.
75
-
76
- For example, if you use soft-deletion and your logic inside a scope depends on if deleted records are included, you can add `with_deleted` option:
77
-
78
- ```ruby
79
- class PostPolicy < ApplicationPolicy
80
- scope_for :relation do |relation, with_deleted: false|
81
- rel = some_logic(relation)
82
- with_deleted ? rel.with_deleted : rel
83
- end
84
- end
85
- ```
86
-
87
- You can add as many options as you want:
88
-
89
- ```ruby
90
- class PostPolicy < ApplicationPolicy
91
- scope_for :relation do |relation, with_deleted: false, magic_number: 42, some_required_option:|
92
- # Your code
93
- end
94
- end
95
- ```
96
- ## Apply scopes
97
-
98
- Action Policy behaviour (`ActionPolicy::Behaviour`) provides an `authorized` method which allows you to use scoping:
99
-
100
- ```ruby
101
- class PostsController < ApplicationController
102
- def index
103
- # The first argument is the target,
104
- # which is passed to the scope block
105
- #
106
- # The second argument is the scope type
107
- @posts = authorized_scope(Post, type: :relation)
108
- #
109
- # For named scopes provide `as` option
110
- @events = authorized_scope(Event, type: :relation, as: :own)
111
- #
112
- # If you want to specify scope options provide `scope_options` option
113
- @events = authorized_scope(Event, type: :relation, scope_options: {with_deleted: true})
114
- end
115
- end
116
- ```
117
-
118
- You can also specify additional options for policy class inference (see [behaviour docs](behaviour)). For example, to explicitly specify the policy class use:
119
-
120
- ```ruby
121
- @posts = authorized_scope(Post, with: CustomPostPolicy)
122
- ```
123
-
124
- ## Using scopes within policy
125
-
126
- You can also use scopes within policy classes using the same `authorized_scope` method.
127
- For example:
128
-
129
- ```ruby
130
- relation_scope(:edit) do |scope|
131
- teachers = authorized_scope(Teacher.all, as: :edit)
132
- scope
133
- .joins(:teachers)
134
- .where(teacher_id: teachers)
135
- end
136
- ```
137
-
138
- ## Using scopes explicitly
139
-
140
- To use scopes without including Action Policy [behaviour](behaviour)
141
- do the following:
142
-
143
- ```ruby
144
- # initialize policy
145
- policy = ApplicantPolicy.new(user: user)
146
- # apply scope
147
- policy.apply_scope(User.all, type: :relation)
148
- ```
149
-
150
- ## Scope type inference
151
-
152
- Action Policy could look up a scope type if it's not specified and if _scope matchers_ were configured.
153
-
154
- Scope matcher is an object that implements `#===` (_case equality_) or a Proc. You can define it within a policy class:
155
-
156
- ```ruby
157
- class ApplicationPolicy < ActionPolicy::Base
158
- scope_matcher :relation, ActiveRecord::Relation
159
-
160
- # use Proc to handle AR models classes
161
- scope_matcher :relation, ->(target) { target < ActiveRecord::Base }
162
-
163
- scope_matcher :custom, MyCustomClass
164
- end
165
- ```
166
-
167
- Adding a scope matcher also adds a DSL to define scope rules (just a syntax sugar):
168
-
169
- ```ruby
170
- class ApplicationPolicy < ActionPolicy::Base
171
- scope_matcher :relation, ActiveRecord::Relation
172
-
173
- # now you can define scope rules like this
174
- relation_scope { |relation| relation }
175
- end
176
- ```
177
-
178
- When `authorized_scope` is called without the explicit scope type, Action Policy uses matchers (in the order they're defined) to infer the type.
179
-
180
- ## Rails integration
181
-
182
- Action Policy provides a couple of _scope matchers_ out-of-the-box for Active Record relations and Action Controller paramters.
183
-
184
- ### Active Record scopes
185
-
186
- Scope type `:relation` is automatically applied to the object of `ActiveRecord::Relation` type.
187
-
188
- To define Active Record scopes you can use `relation_scope` macro (which is just an alias for `scope :relation`) in your policy:
189
-
190
- ```ruby
191
- class PostPolicy < ApplicationPolicy
192
- # Equals `scope_for :active_record_relation do ...`
193
- relation_scope do |scope|
194
- if super_user? || admin?
195
- scope
196
- else
197
- scope.joins(:accesses).where(accesses: {user_id: user.id})
198
- end
199
- end
200
-
201
- # define named scope
202
- relation_scope(:own) do |scope|
203
- next scope.none if user.guest?
204
- scope.where(user: user)
205
- end
206
- end
207
- ```
208
-
209
- **NOTE:** the `:active_record_relation` scoping is used if and only if an `ActiveRecord::Relation` is passed to `authorized`:
210
-
211
- ```ruby
212
- def index
213
- # BAD: Post is not a relation; raises an exception
214
- @posts = authorized_scope(Post)
215
-
216
- # GOOD:
217
- @posts = authorized_scope(Post.all)
218
- end
219
- ```
220
-
221
- ### Action Controller parameters
222
-
223
- Use scopes of type `:params` if your strong parameters filterings depend on the current user:
224
-
225
- ```ruby
226
- class UserPolicy < ApplicationPolicy
227
- # Equals to `scope_for :action_controller_params do ...`
228
- params_filter do |params|
229
- if user.admin?
230
- params.permit(:name, :email, :role)
231
- else
232
- params.permit(:name)
233
- end
234
- end
235
-
236
- params_filter(:update) do |params|
237
- params.permit(:name)
238
- end
239
- end
240
-
241
- class UsersController < ApplicationController
242
- def create
243
- # Call `authorized_scope` on `params` object
244
- @user = User.create!(authorized_scope(params.require(:user)))
245
- # Or you can use `authorized` alias which fits this case better
246
- @user = User.create!(authorized(params.require(:user)))
247
- head :ok
248
- end
249
-
250
- def update
251
- @user.update!(authorized_scope(params.require(:user), as: :update))
252
- head :ok
253
- end
254
- end
255
- ```