piko-pro-gem 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.
- checksums.yaml +7 -0
- data/piko-pro-gem.gemspec +12 -0
- data/pundit-2.5.2/CHANGELOG.md +196 -0
- data/pundit-2.5.2/CONTRIBUTING.md +31 -0
- data/pundit-2.5.2/LICENSE.txt +22 -0
- data/pundit-2.5.2/README.md +891 -0
- data/pundit-2.5.2/SECURITY.md +19 -0
- data/pundit-2.5.2/config/rubocop-rspec.yml +5 -0
- data/pundit-2.5.2/lib/generators/pundit/install/USAGE +2 -0
- data/pundit-2.5.2/lib/generators/pundit/install/install_generator.rb +15 -0
- data/pundit-2.5.2/lib/generators/pundit/install/templates/application_policy.rb.tt +53 -0
- data/pundit-2.5.2/lib/generators/pundit/policy/USAGE +8 -0
- data/pundit-2.5.2/lib/generators/pundit/policy/policy_generator.rb +17 -0
- data/pundit-2.5.2/lib/generators/pundit/policy/templates/policy.rb.tt +16 -0
- data/pundit-2.5.2/lib/generators/rspec/policy_generator.rb +16 -0
- data/pundit-2.5.2/lib/generators/rspec/templates/policy_spec.rb.tt +27 -0
- data/pundit-2.5.2/lib/generators/test_unit/policy_generator.rb +16 -0
- data/pundit-2.5.2/lib/generators/test_unit/templates/policy_test.rb.tt +18 -0
- data/pundit-2.5.2/lib/pundit/authorization.rb +269 -0
- data/pundit-2.5.2/lib/pundit/cache_store/legacy_store.rb +27 -0
- data/pundit-2.5.2/lib/pundit/cache_store/null_store.rb +30 -0
- data/pundit-2.5.2/lib/pundit/cache_store.rb +24 -0
- data/pundit-2.5.2/lib/pundit/context.rb +190 -0
- data/pundit-2.5.2/lib/pundit/error.rb +71 -0
- data/pundit-2.5.2/lib/pundit/helper.rb +16 -0
- data/pundit-2.5.2/lib/pundit/policy_finder.rb +135 -0
- data/pundit-2.5.2/lib/pundit/railtie.rb +20 -0
- data/pundit-2.5.2/lib/pundit/rspec.rb +165 -0
- data/pundit-2.5.2/lib/pundit/version.rb +6 -0
- data/pundit-2.5.2/lib/pundit.rb +77 -0
- metadata +70 -0
|
@@ -0,0 +1,891 @@
|
|
|
1
|
+
# Pundit
|
|
2
|
+
|
|
3
|
+
[](https://github.com/varvet/pundit/actions/workflows/main.yml)
|
|
4
|
+
[](https://qlty.sh/gh/varvet/projects/pundit)
|
|
5
|
+
[](https://inch-ci.org/github/varvet/pundit)
|
|
6
|
+
[](https://badge.fury.io/rb/pundit)
|
|
7
|
+
|
|
8
|
+
Pundit provides a set of helpers which guide you in leveraging regular Ruby
|
|
9
|
+
classes and object oriented design patterns to build a straightforward, robust, and
|
|
10
|
+
scalable authorization system.
|
|
11
|
+
|
|
12
|
+
## Links:
|
|
13
|
+
|
|
14
|
+
- [API documentation for the most recent version](https://www.rubydoc.info/gems/pundit)
|
|
15
|
+
- [Source Code](https://github.com/varvet/pundit)
|
|
16
|
+
- [Contributing](https://github.com/varvet/pundit/blob/main/CONTRIBUTING.md)
|
|
17
|
+
- [Code of Conduct](https://github.com/varvet/pundit/blob/main/CODE_OF_CONDUCT.md)
|
|
18
|
+
|
|
19
|
+
<strong>Sponsored by:</strong> <a href="https://www.varvet.com">Varvet<br><br><img src="https://github.com/varvet/pundit/assets/99166/aa9efa0a-6903-4037-abee-1824edc57f1a" alt="Varvet logo" height="120"></div>
|
|
20
|
+
|
|
21
|
+
## Installation
|
|
22
|
+
|
|
23
|
+
> **Please note** that the README on GitHub is accurate with the _latest code on GitHub_. You are most likely using a released version of Pundit, so please refer to the [documentation for the latest released version of Pundit](https://www.rubydoc.info/gems/pundit).
|
|
24
|
+
|
|
25
|
+
``` sh
|
|
26
|
+
bundle add pundit
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Include `Pundit::Authorization` in your application controller:
|
|
30
|
+
|
|
31
|
+
``` ruby
|
|
32
|
+
class ApplicationController < ActionController::Base
|
|
33
|
+
include Pundit::Authorization
|
|
34
|
+
end
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Optionally, you can run the generator, which will set up an application policy
|
|
38
|
+
with some useful defaults for you:
|
|
39
|
+
|
|
40
|
+
``` sh
|
|
41
|
+
rails g pundit:install
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
After generating your application policy, restart the Rails server so that Rails
|
|
45
|
+
can pick up any classes in the new `app/policies/` directory.
|
|
46
|
+
|
|
47
|
+
## Policies
|
|
48
|
+
|
|
49
|
+
Pundit is focused around the notion of policy classes. We suggest that you put
|
|
50
|
+
these classes in `app/policies`. This is an example that allows updating a post
|
|
51
|
+
if the user is an admin, or if the post is unpublished:
|
|
52
|
+
|
|
53
|
+
``` ruby
|
|
54
|
+
class PostPolicy
|
|
55
|
+
attr_reader :user, :post
|
|
56
|
+
|
|
57
|
+
def initialize(user, post)
|
|
58
|
+
@user = user
|
|
59
|
+
@post = post
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def update?
|
|
63
|
+
user.admin? || !post.published?
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
As you can see, this is a plain Ruby class. Pundit makes the following
|
|
69
|
+
assumptions about this class:
|
|
70
|
+
|
|
71
|
+
- The class has the same name as some kind of model class, only suffixed
|
|
72
|
+
with the word "Policy".
|
|
73
|
+
- The first argument is a user. In your controller, Pundit will call the
|
|
74
|
+
`current_user` method to retrieve what to send into this argument
|
|
75
|
+
- The second argument is some kind of model object, whose authorization
|
|
76
|
+
you want to check. This does not need to be an ActiveRecord or even
|
|
77
|
+
an ActiveModel object, it can be anything really.
|
|
78
|
+
- The class implements some kind of query method, in this case `update?`.
|
|
79
|
+
Usually, this will map to the name of a particular controller action.
|
|
80
|
+
|
|
81
|
+
That's it really.
|
|
82
|
+
|
|
83
|
+
Usually you'll want to inherit from the application policy created by the
|
|
84
|
+
generator, or set up your own base class to inherit from:
|
|
85
|
+
|
|
86
|
+
``` ruby
|
|
87
|
+
class PostPolicy < ApplicationPolicy
|
|
88
|
+
def update?
|
|
89
|
+
user.admin? or not record.published?
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
In the generated `ApplicationPolicy`, the model object is called `record`.
|
|
95
|
+
|
|
96
|
+
Supposing that you have an instance of class `Post`, Pundit now lets you do
|
|
97
|
+
this in your controller:
|
|
98
|
+
|
|
99
|
+
``` ruby
|
|
100
|
+
def update
|
|
101
|
+
@post = Post.find(params[:id])
|
|
102
|
+
authorize @post
|
|
103
|
+
if @post.update(post_params)
|
|
104
|
+
redirect_to @post
|
|
105
|
+
else
|
|
106
|
+
render :edit
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
The authorize method automatically infers that `Post` will have a matching
|
|
112
|
+
`PostPolicy` class, and instantiates this class, handing in the current user
|
|
113
|
+
and the given record. It then infers from the action name, that it should call
|
|
114
|
+
`update?` on this instance of the policy. In this case, you can imagine that
|
|
115
|
+
`authorize` would have done something like this:
|
|
116
|
+
|
|
117
|
+
``` ruby
|
|
118
|
+
unless PostPolicy.new(current_user, @post).update?
|
|
119
|
+
raise Pundit::NotAuthorizedError, "not allowed to PostPolicy#update? this Post"
|
|
120
|
+
end
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
You can pass a second argument to `authorize` if the name of the permission you
|
|
124
|
+
want to check doesn't match the action name. For example:
|
|
125
|
+
|
|
126
|
+
``` ruby
|
|
127
|
+
def publish
|
|
128
|
+
@post = Post.find(params[:id])
|
|
129
|
+
authorize @post, :update?
|
|
130
|
+
@post.publish!
|
|
131
|
+
redirect_to @post
|
|
132
|
+
end
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
You can pass an argument to override the policy class if necessary. For example:
|
|
136
|
+
|
|
137
|
+
```ruby
|
|
138
|
+
def create
|
|
139
|
+
@publication = find_publication # assume this method returns any model that behaves like a publication
|
|
140
|
+
# @publication.class => Post
|
|
141
|
+
authorize @publication, policy_class: PublicationPolicy
|
|
142
|
+
@publication.publish!
|
|
143
|
+
redirect_to @publication
|
|
144
|
+
end
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
If you don't have an instance for the first argument to `authorize`, then you can pass
|
|
148
|
+
the class. For example:
|
|
149
|
+
|
|
150
|
+
Policy:
|
|
151
|
+
```ruby
|
|
152
|
+
class PostPolicy < ApplicationPolicy
|
|
153
|
+
def admin_list?
|
|
154
|
+
user.admin?
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Controller:
|
|
160
|
+
```ruby
|
|
161
|
+
def admin_list
|
|
162
|
+
authorize Post # we don't have a particular post to authorize
|
|
163
|
+
# Rest of controller action
|
|
164
|
+
end
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
`authorize` returns the instance passed to it, so you can chain it like this:
|
|
168
|
+
|
|
169
|
+
Controller:
|
|
170
|
+
```ruby
|
|
171
|
+
def show
|
|
172
|
+
@user = authorize User.find(params[:id])
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# return the record even for namespaced policies
|
|
176
|
+
def show
|
|
177
|
+
@user = authorize [:admin, User.find(params[:id])]
|
|
178
|
+
end
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
You can easily get a hold of an instance of the policy through the `policy`
|
|
182
|
+
method in both the view and controller. This is especially useful for
|
|
183
|
+
conditionally showing links or buttons in the view:
|
|
184
|
+
|
|
185
|
+
``` erb
|
|
186
|
+
<% if policy(@post).update? %>
|
|
187
|
+
<%= link_to "Edit post", edit_post_path(@post) %>
|
|
188
|
+
<% end %>
|
|
189
|
+
```
|
|
190
|
+
## Headless policies
|
|
191
|
+
|
|
192
|
+
Given there is a policy without a corresponding model / ruby class,
|
|
193
|
+
you can retrieve it by passing a symbol.
|
|
194
|
+
|
|
195
|
+
```ruby
|
|
196
|
+
# app/policies/dashboard_policy.rb
|
|
197
|
+
class DashboardPolicy
|
|
198
|
+
attr_reader :user
|
|
199
|
+
|
|
200
|
+
# `_record` in this example will be :dashboard
|
|
201
|
+
def initialize(user, _record)
|
|
202
|
+
@user = user
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def show?
|
|
206
|
+
user.admin?
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
Note that the headless policy still needs to accept two arguments. The
|
|
212
|
+
second argument will be the symbol `:dashboard` in this case, which
|
|
213
|
+
is what is passed as the record to `authorize` below.
|
|
214
|
+
|
|
215
|
+
```ruby
|
|
216
|
+
# In controllers
|
|
217
|
+
def show
|
|
218
|
+
authorize :dashboard, :show?
|
|
219
|
+
...
|
|
220
|
+
end
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
```erb
|
|
224
|
+
# In views
|
|
225
|
+
<% if policy(:dashboard).show? %>
|
|
226
|
+
<%= link_to 'Dashboard', dashboard_path %>
|
|
227
|
+
<% end %>
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
## Scopes
|
|
231
|
+
|
|
232
|
+
Often, you will want to have some kind of view listing records which a
|
|
233
|
+
particular user has access to. When using Pundit, you are expected to
|
|
234
|
+
define a class called a policy scope. It can look something like this:
|
|
235
|
+
|
|
236
|
+
``` ruby
|
|
237
|
+
class PostPolicy < ApplicationPolicy
|
|
238
|
+
class Scope
|
|
239
|
+
def initialize(user, scope)
|
|
240
|
+
@user = user
|
|
241
|
+
@scope = scope
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
def resolve
|
|
245
|
+
if user.admin?
|
|
246
|
+
scope.all
|
|
247
|
+
else
|
|
248
|
+
scope.where(published: true)
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
private
|
|
253
|
+
|
|
254
|
+
attr_reader :user, :scope
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
def update?
|
|
258
|
+
user.admin? or not record.published?
|
|
259
|
+
end
|
|
260
|
+
end
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
Pundit makes the following assumptions about this class:
|
|
264
|
+
|
|
265
|
+
- The class has the name `Scope` and is nested under the policy class.
|
|
266
|
+
- The first argument is a user. In your controller, Pundit will call the
|
|
267
|
+
`current_user` method to retrieve what to send into this argument.
|
|
268
|
+
- The second argument is a scope of some kind on which to perform some kind of
|
|
269
|
+
query. It will usually be an ActiveRecord class or a
|
|
270
|
+
`ActiveRecord::Relation`, but it could be something else entirely.
|
|
271
|
+
- Instances of this class respond to the method `resolve`, which should return
|
|
272
|
+
some kind of result which can be iterated over. For ActiveRecord classes,
|
|
273
|
+
this would usually be an `ActiveRecord::Relation`.
|
|
274
|
+
|
|
275
|
+
You'll probably want to inherit from the application policy scope generated by the
|
|
276
|
+
generator, or create your own base class to inherit from:
|
|
277
|
+
|
|
278
|
+
``` ruby
|
|
279
|
+
class PostPolicy < ApplicationPolicy
|
|
280
|
+
class Scope < ApplicationPolicy::Scope
|
|
281
|
+
def resolve
|
|
282
|
+
if user.admin?
|
|
283
|
+
scope.all
|
|
284
|
+
else
|
|
285
|
+
scope.where(published: true)
|
|
286
|
+
end
|
|
287
|
+
end
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
def update?
|
|
291
|
+
user.admin? or not record.published?
|
|
292
|
+
end
|
|
293
|
+
end
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
You can now use this class from your controller via the `policy_scope` method:
|
|
297
|
+
|
|
298
|
+
``` ruby
|
|
299
|
+
def index
|
|
300
|
+
@posts = policy_scope(Post)
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
def show
|
|
304
|
+
@post = policy_scope(Post).find(params[:id])
|
|
305
|
+
end
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
Like with the authorize method, you can also override the policy scope class:
|
|
309
|
+
|
|
310
|
+
``` ruby
|
|
311
|
+
def index
|
|
312
|
+
# publication_class => Post
|
|
313
|
+
@publications = policy_scope(publication_class, policy_scope_class: PublicationPolicy::Scope)
|
|
314
|
+
end
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
In this case it is a shortcut for doing:
|
|
318
|
+
|
|
319
|
+
``` ruby
|
|
320
|
+
def index
|
|
321
|
+
@publications = PublicationPolicy::Scope.new(current_user, Post).resolve
|
|
322
|
+
end
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
You can, and are encouraged to, use this method in views:
|
|
326
|
+
|
|
327
|
+
``` erb
|
|
328
|
+
<% policy_scope(@user.posts).each do |post| %>
|
|
329
|
+
<p><%= link_to post.title, post_path(post) %></p>
|
|
330
|
+
<% end %>
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
## Ensuring policies and scopes are used
|
|
334
|
+
|
|
335
|
+
When you are developing an application with Pundit it can be easy to forget to
|
|
336
|
+
authorize some action. People are forgetful after all. Since Pundit encourages
|
|
337
|
+
you to add the `authorize` call manually to each controller action, it's really
|
|
338
|
+
easy to miss one.
|
|
339
|
+
|
|
340
|
+
Thankfully, Pundit has a handy feature which reminds you in case you forget.
|
|
341
|
+
Pundit tracks whether you have called `authorize` anywhere in your controller
|
|
342
|
+
action. Pundit also adds a method to your controllers called
|
|
343
|
+
`verify_authorized`. This method will raise an exception if `authorize` has not
|
|
344
|
+
yet been called. You should run this method in an `after_action` hook to ensure
|
|
345
|
+
that you haven't forgotten to authorize the action. For example:
|
|
346
|
+
|
|
347
|
+
``` ruby
|
|
348
|
+
class ApplicationController < ActionController::Base
|
|
349
|
+
include Pundit::Authorization
|
|
350
|
+
after_action :verify_authorized
|
|
351
|
+
end
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
Likewise, Pundit also adds `verify_policy_scoped` to your controller. This
|
|
355
|
+
will raise an exception similar to `verify_authorized`. However, it tracks
|
|
356
|
+
if `policy_scope` is used instead of `authorize`. This is mostly useful for
|
|
357
|
+
controller actions like `index` which find collections with a scope and don't
|
|
358
|
+
authorize individual instances.
|
|
359
|
+
|
|
360
|
+
``` ruby
|
|
361
|
+
class ApplicationController < ActionController::Base
|
|
362
|
+
include Pundit::Authorization
|
|
363
|
+
after_action :verify_pundit_authorization
|
|
364
|
+
|
|
365
|
+
def verify_pundit_authorization
|
|
366
|
+
if action_name == "index"
|
|
367
|
+
verify_policy_scoped
|
|
368
|
+
else
|
|
369
|
+
verify_authorized
|
|
370
|
+
end
|
|
371
|
+
end
|
|
372
|
+
end
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
**This verification mechanism only exists to aid you while developing your
|
|
376
|
+
application, so you don't forget to call `authorize`. It is not some kind of
|
|
377
|
+
failsafe mechanism or authorization mechanism. You should be able to remove
|
|
378
|
+
these filters without affecting how your app works in any way.**
|
|
379
|
+
|
|
380
|
+
Some people have found this feature confusing, while many others
|
|
381
|
+
find it extremely helpful. If you fall into the category of people who find it
|
|
382
|
+
confusing then you do not need to use it. Pundit will work fine without
|
|
383
|
+
using `verify_authorized` and `verify_policy_scoped`.
|
|
384
|
+
|
|
385
|
+
### Conditional verification
|
|
386
|
+
|
|
387
|
+
If you're using `verify_authorized` in your controllers but need to
|
|
388
|
+
conditionally bypass verification, you can use `skip_authorization`. For
|
|
389
|
+
bypassing `verify_policy_scoped`, use `skip_policy_scope`. These are useful
|
|
390
|
+
in circumstances where you don't want to disable verification for the
|
|
391
|
+
entire action, but have some cases where you intend to not authorize.
|
|
392
|
+
|
|
393
|
+
```ruby
|
|
394
|
+
def show
|
|
395
|
+
record = Record.find_by(attribute: "value")
|
|
396
|
+
if record.present?
|
|
397
|
+
authorize record
|
|
398
|
+
else
|
|
399
|
+
skip_authorization
|
|
400
|
+
end
|
|
401
|
+
end
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
## Manually specifying policy classes
|
|
405
|
+
|
|
406
|
+
Sometimes you might want to explicitly declare which policy to use for a given
|
|
407
|
+
class, instead of letting Pundit infer it. This can be done like so:
|
|
408
|
+
|
|
409
|
+
``` ruby
|
|
410
|
+
class Post
|
|
411
|
+
def self.policy_class
|
|
412
|
+
PostablePolicy
|
|
413
|
+
end
|
|
414
|
+
end
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
Alternatively, you can declare an instance method:
|
|
418
|
+
|
|
419
|
+
``` ruby
|
|
420
|
+
class Post
|
|
421
|
+
def policy_class
|
|
422
|
+
PostablePolicy
|
|
423
|
+
end
|
|
424
|
+
end
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
## Plain old Ruby
|
|
428
|
+
|
|
429
|
+
Pundit is a very small library on purpose, and it doesn't do anything you can't do yourself. There's no secret sauce here. It does as little as possible, and then gets out of your way.
|
|
430
|
+
|
|
431
|
+
With the few but powerful helpers available in Pundit, you have the power to build a well structured, fully working authorization system without using any special DSLs or funky syntax.
|
|
432
|
+
|
|
433
|
+
Remember that all of the policy and scope classes are plain Ruby classes, which means you can use the same mechanisms you always use to DRY things up. Encapsulate a set of permissions into a module and include them in multiple policies. Use `alias_method` to make some permissions behave the same as others. Inherit from a base set of permissions. Use metaprogramming if you really have to.
|
|
434
|
+
|
|
435
|
+
## Generator
|
|
436
|
+
|
|
437
|
+
Use the supplied generator to generate policies:
|
|
438
|
+
|
|
439
|
+
``` sh
|
|
440
|
+
rails g pundit:policy post
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
## Closed systems
|
|
444
|
+
|
|
445
|
+
In many applications, only logged in users are really able to do anything. If
|
|
446
|
+
you're building such a system, it can be kind of cumbersome to check that the
|
|
447
|
+
user in a policy isn't `nil` for every single permission. Aside from policies,
|
|
448
|
+
you can add this check to the base class for scopes.
|
|
449
|
+
|
|
450
|
+
We suggest that you define a filter that redirects unauthenticated users to the
|
|
451
|
+
login page. As a secondary defence, if you've defined an ApplicationPolicy, it
|
|
452
|
+
might be a good idea to raise an exception if somehow an unauthenticated user
|
|
453
|
+
got through. This way you can fail more gracefully.
|
|
454
|
+
|
|
455
|
+
``` ruby
|
|
456
|
+
class ApplicationPolicy
|
|
457
|
+
def initialize(user, record)
|
|
458
|
+
raise Pundit::NotAuthorizedError, "must be logged in" unless user
|
|
459
|
+
@user = user
|
|
460
|
+
@record = record
|
|
461
|
+
end
|
|
462
|
+
|
|
463
|
+
class Scope
|
|
464
|
+
attr_reader :user, :scope
|
|
465
|
+
|
|
466
|
+
def initialize(user, scope)
|
|
467
|
+
raise Pundit::NotAuthorizedError, "must be logged in" unless user
|
|
468
|
+
@user = user
|
|
469
|
+
@scope = scope
|
|
470
|
+
end
|
|
471
|
+
end
|
|
472
|
+
end
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
## NilClassPolicy
|
|
476
|
+
|
|
477
|
+
To support a [null object pattern](https://en.wikipedia.org/wiki/Null_Object_pattern)
|
|
478
|
+
you may find that you want to implement a `NilClassPolicy`. This might be useful
|
|
479
|
+
where you want to extend your ApplicationPolicy to allow some tolerance of, for
|
|
480
|
+
example, associations which might be `nil`.
|
|
481
|
+
|
|
482
|
+
```ruby
|
|
483
|
+
class NilClassPolicy < ApplicationPolicy
|
|
484
|
+
class Scope < ApplicationPolicy::Scope
|
|
485
|
+
def resolve
|
|
486
|
+
raise Pundit::NotDefinedError, "Cannot scope NilClass"
|
|
487
|
+
end
|
|
488
|
+
end
|
|
489
|
+
|
|
490
|
+
def show?
|
|
491
|
+
false # Nobody can see nothing
|
|
492
|
+
end
|
|
493
|
+
end
|
|
494
|
+
```
|
|
495
|
+
|
|
496
|
+
## Rescuing a denied Authorization in Rails
|
|
497
|
+
|
|
498
|
+
Pundit raises a `Pundit::NotAuthorizedError` you can
|
|
499
|
+
[rescue_from](https://guides.rubyonrails.org/action_controller_overview.html#rescue-from)
|
|
500
|
+
in your `ApplicationController`. You can customize the `user_not_authorized`
|
|
501
|
+
method in every controller.
|
|
502
|
+
|
|
503
|
+
```ruby
|
|
504
|
+
class ApplicationController < ActionController::Base
|
|
505
|
+
include Pundit::Authorization
|
|
506
|
+
|
|
507
|
+
rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized
|
|
508
|
+
|
|
509
|
+
private
|
|
510
|
+
|
|
511
|
+
def user_not_authorized
|
|
512
|
+
flash[:alert] = "You are not authorized to perform this action."
|
|
513
|
+
redirect_back_or_to(root_path)
|
|
514
|
+
end
|
|
515
|
+
end
|
|
516
|
+
```
|
|
517
|
+
|
|
518
|
+
Alternatively, you can globally handle Pundit::NotAuthorizedError's by having rails handle them as a 403 error and serving a 403 error page. Add the following to application.rb:
|
|
519
|
+
|
|
520
|
+
```config.action_dispatch.rescue_responses["Pundit::NotAuthorizedError"] = :forbidden```
|
|
521
|
+
|
|
522
|
+
## Creating custom error messages
|
|
523
|
+
|
|
524
|
+
`NotAuthorizedError`s provide information on what query (e.g. `:create?`), what
|
|
525
|
+
record (e.g. an instance of `Post`), and what policy (e.g. an instance of
|
|
526
|
+
`PostPolicy`) caused the error to be raised.
|
|
527
|
+
|
|
528
|
+
One way to use these `query`, `record`, and `policy` properties is to connect
|
|
529
|
+
them with `I18n` to generate error messages. Here's how you might go about doing
|
|
530
|
+
that.
|
|
531
|
+
|
|
532
|
+
```ruby
|
|
533
|
+
class ApplicationController < ActionController::Base
|
|
534
|
+
rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized
|
|
535
|
+
|
|
536
|
+
private
|
|
537
|
+
|
|
538
|
+
def user_not_authorized(exception)
|
|
539
|
+
policy_name = exception.policy.class.to_s.underscore
|
|
540
|
+
|
|
541
|
+
flash[:error] = t "#{policy_name}.#{exception.query}", scope: "pundit", default: :default
|
|
542
|
+
redirect_back_or_to(root_path)
|
|
543
|
+
end
|
|
544
|
+
end
|
|
545
|
+
```
|
|
546
|
+
|
|
547
|
+
```yaml
|
|
548
|
+
en:
|
|
549
|
+
pundit:
|
|
550
|
+
default: 'You cannot perform this action.'
|
|
551
|
+
post_policy:
|
|
552
|
+
update?: 'You cannot edit this post!'
|
|
553
|
+
create?: 'You cannot create posts!'
|
|
554
|
+
```
|
|
555
|
+
|
|
556
|
+
This is an example. Pundit is agnostic as to how you implement your error messaging.
|
|
557
|
+
|
|
558
|
+
## Manually retrieving policies and scopes
|
|
559
|
+
|
|
560
|
+
Sometimes you want to retrieve a policy for a record outside the controller or
|
|
561
|
+
view. For example when you delegate permissions from one policy to another.
|
|
562
|
+
|
|
563
|
+
You can easily retrieve policies and scopes like this:
|
|
564
|
+
|
|
565
|
+
``` ruby
|
|
566
|
+
Pundit.policy!(user, post)
|
|
567
|
+
Pundit.policy(user, post)
|
|
568
|
+
|
|
569
|
+
Pundit.policy_scope!(user, Post)
|
|
570
|
+
Pundit.policy_scope(user, Post)
|
|
571
|
+
```
|
|
572
|
+
|
|
573
|
+
The bang methods will raise an exception if the policy does not exist, whereas
|
|
574
|
+
those without the bang will return nil.
|
|
575
|
+
|
|
576
|
+
## Customize Pundit user
|
|
577
|
+
|
|
578
|
+
On occasion, your controller may be unable to access `current_user`, or the method that should be invoked by Pundit may not be `current_user`. To address this, you can define a method in your controller named `pundit_user`.
|
|
579
|
+
|
|
580
|
+
```ruby
|
|
581
|
+
def pundit_user
|
|
582
|
+
User.find_by_other_means
|
|
583
|
+
end
|
|
584
|
+
```
|
|
585
|
+
|
|
586
|
+
For instance, Rails 8 includes a built-in [authentication generator](https://github.com/rails/rails/tree/8-0-stable/railties/lib/rails/generators/rails/authentication). If you choose to use it, the currently logged-in user is accessed via `Current.user` instead of `current_user`.
|
|
587
|
+
|
|
588
|
+
To ensure compatibility with Pundit, define a `pundit_user` method in `application_controller.rb` (or another suitable location) as follows:
|
|
589
|
+
|
|
590
|
+
```ruby
|
|
591
|
+
def pundit_user
|
|
592
|
+
Current.user
|
|
593
|
+
end
|
|
594
|
+
```
|
|
595
|
+
|
|
596
|
+
### Handling User Switching in Pundit
|
|
597
|
+
|
|
598
|
+
When switching users in your application, it's important to reset the Pundit user context to ensure that authorization policies are applied correctly for the new user. Pundit caches the user context, so failing to reset it could result in incorrect permissions being applied.
|
|
599
|
+
|
|
600
|
+
To handle user switching, you can use the following pattern in your controller:
|
|
601
|
+
|
|
602
|
+
```ruby
|
|
603
|
+
class ApplicationController
|
|
604
|
+
include Pundit::Authorization
|
|
605
|
+
|
|
606
|
+
def switch_user_to(user)
|
|
607
|
+
terminate_session if authenticated?
|
|
608
|
+
start_new_session_for user
|
|
609
|
+
pundit_reset!
|
|
610
|
+
end
|
|
611
|
+
end
|
|
612
|
+
```
|
|
613
|
+
|
|
614
|
+
Make sure to invoke `pundit_reset!` whenever changing the user. This ensures the cached authorization context is reset, preventing any incorrect permissions from being applied.
|
|
615
|
+
|
|
616
|
+
## Policy Namespacing
|
|
617
|
+
In some cases it might be helpful to have multiple policies that serve different contexts for a
|
|
618
|
+
resource. A prime example of this is the case where User policies differ from Admin policies. To
|
|
619
|
+
authorize with a namespaced policy, pass the namespace into the `authorize` helper in an array:
|
|
620
|
+
|
|
621
|
+
```ruby
|
|
622
|
+
authorize(post) # => will look for a PostPolicy
|
|
623
|
+
authorize([:admin, post]) # => will look for an Admin::PostPolicy
|
|
624
|
+
authorize([:foo, :bar, post]) # => will look for a Foo::Bar::PostPolicy
|
|
625
|
+
|
|
626
|
+
policy_scope(Post) # => will look for a PostPolicy::Scope
|
|
627
|
+
policy_scope([:admin, Post]) # => will look for an Admin::PostPolicy::Scope
|
|
628
|
+
policy_scope([:foo, :bar, Post]) # => will look for a Foo::Bar::PostPolicy::Scope
|
|
629
|
+
```
|
|
630
|
+
|
|
631
|
+
If you are using namespaced policies for something like Admin views, it can be useful to
|
|
632
|
+
override the `policy_scope` and `authorize` helpers in your `AdminController` to automatically
|
|
633
|
+
apply the namespacing:
|
|
634
|
+
|
|
635
|
+
```ruby
|
|
636
|
+
class AdminController < ApplicationController
|
|
637
|
+
def policy_scope(scope)
|
|
638
|
+
super([:admin, scope])
|
|
639
|
+
end
|
|
640
|
+
|
|
641
|
+
def authorize(record, query = nil)
|
|
642
|
+
super([:admin, record], query)
|
|
643
|
+
end
|
|
644
|
+
end
|
|
645
|
+
|
|
646
|
+
class Admin::PostController < AdminController
|
|
647
|
+
def index
|
|
648
|
+
policy_scope(Post)
|
|
649
|
+
end
|
|
650
|
+
|
|
651
|
+
def show
|
|
652
|
+
post = authorize Post.find(params[:id])
|
|
653
|
+
end
|
|
654
|
+
end
|
|
655
|
+
```
|
|
656
|
+
|
|
657
|
+
## Additional context
|
|
658
|
+
|
|
659
|
+
Pundit strongly encourages you to model your application in such a way that the
|
|
660
|
+
only context you need for authorization is a user object and a domain model that
|
|
661
|
+
you want to check authorization for. If you find yourself needing more context than
|
|
662
|
+
that, consider whether you are authorizing the right domain model, maybe another
|
|
663
|
+
domain model (or a wrapper around multiple domain models) can provide the context
|
|
664
|
+
you need.
|
|
665
|
+
|
|
666
|
+
Pundit does not allow you to pass additional arguments to policies for precisely
|
|
667
|
+
this reason.
|
|
668
|
+
|
|
669
|
+
However, in very rare cases, you might need to authorize based on more context than just
|
|
670
|
+
the currently authenticated user. Suppose for example that authorization is dependent
|
|
671
|
+
on IP address in addition to the authenticated user. In that case, one option is to
|
|
672
|
+
create a special class which wraps up both user and IP and passes it to the policy.
|
|
673
|
+
|
|
674
|
+
``` ruby
|
|
675
|
+
class UserContext
|
|
676
|
+
attr_reader :user, :ip
|
|
677
|
+
|
|
678
|
+
def initialize(user, ip)
|
|
679
|
+
@user = user
|
|
680
|
+
@ip = ip
|
|
681
|
+
end
|
|
682
|
+
end
|
|
683
|
+
|
|
684
|
+
class ApplicationController
|
|
685
|
+
include Pundit::Authorization
|
|
686
|
+
|
|
687
|
+
def pundit_user
|
|
688
|
+
UserContext.new(current_user, request.ip)
|
|
689
|
+
end
|
|
690
|
+
end
|
|
691
|
+
```
|
|
692
|
+
|
|
693
|
+
## Strong parameters
|
|
694
|
+
|
|
695
|
+
In Rails,
|
|
696
|
+
mass-assignment protection is handled in the controller. With Pundit you can
|
|
697
|
+
control which attributes a user has access to update via your policies. You can
|
|
698
|
+
set up a `permitted_attributes` method in your policy like this:
|
|
699
|
+
|
|
700
|
+
```ruby
|
|
701
|
+
# app/policies/post_policy.rb
|
|
702
|
+
class PostPolicy < ApplicationPolicy
|
|
703
|
+
def permitted_attributes
|
|
704
|
+
if user.admin? || user.owner_of?(post)
|
|
705
|
+
[:title, :body, :tag_list]
|
|
706
|
+
else
|
|
707
|
+
[:tag_list]
|
|
708
|
+
end
|
|
709
|
+
end
|
|
710
|
+
end
|
|
711
|
+
```
|
|
712
|
+
|
|
713
|
+
You can now retrieve these attributes from the policy:
|
|
714
|
+
|
|
715
|
+
```ruby
|
|
716
|
+
# app/controllers/posts_controller.rb
|
|
717
|
+
class PostsController < ApplicationController
|
|
718
|
+
def update
|
|
719
|
+
@post = Post.find(params[:id])
|
|
720
|
+
if @post.update(post_params)
|
|
721
|
+
redirect_to @post
|
|
722
|
+
else
|
|
723
|
+
render :edit
|
|
724
|
+
end
|
|
725
|
+
end
|
|
726
|
+
|
|
727
|
+
private
|
|
728
|
+
|
|
729
|
+
def post_params
|
|
730
|
+
params.require(:post).permit(policy(@post).permitted_attributes)
|
|
731
|
+
end
|
|
732
|
+
end
|
|
733
|
+
```
|
|
734
|
+
|
|
735
|
+
However, this is a bit cumbersome, so Pundit provides a convenient helper method:
|
|
736
|
+
|
|
737
|
+
```ruby
|
|
738
|
+
# app/controllers/posts_controller.rb
|
|
739
|
+
class PostsController < ApplicationController
|
|
740
|
+
def update
|
|
741
|
+
@post = Post.find(params[:id])
|
|
742
|
+
if @post.update(permitted_attributes(@post))
|
|
743
|
+
redirect_to @post
|
|
744
|
+
else
|
|
745
|
+
render :edit
|
|
746
|
+
end
|
|
747
|
+
end
|
|
748
|
+
end
|
|
749
|
+
```
|
|
750
|
+
|
|
751
|
+
If you want to permit different attributes based on the current action, you can define a `permitted_attributes_for_#{action}` method on your policy:
|
|
752
|
+
|
|
753
|
+
```ruby
|
|
754
|
+
# app/policies/post_policy.rb
|
|
755
|
+
class PostPolicy < ApplicationPolicy
|
|
756
|
+
def permitted_attributes_for_create
|
|
757
|
+
[:title, :body]
|
|
758
|
+
end
|
|
759
|
+
|
|
760
|
+
def permitted_attributes_for_edit
|
|
761
|
+
[:body]
|
|
762
|
+
end
|
|
763
|
+
end
|
|
764
|
+
```
|
|
765
|
+
|
|
766
|
+
If you have defined an action-specific method on your policy for the current action, the `permitted_attributes` helper will call it instead of calling `permitted_attributes` on your controller.
|
|
767
|
+
|
|
768
|
+
If you need to fetch parameters based on namespaces different from the suggested one, override the below method, in your controller, and return an instance of `ActionController::Parameters`.
|
|
769
|
+
|
|
770
|
+
```ruby
|
|
771
|
+
def pundit_params_for(record)
|
|
772
|
+
params.require(PolicyFinder.new(record).param_key)
|
|
773
|
+
end
|
|
774
|
+
```
|
|
775
|
+
|
|
776
|
+
For example:
|
|
777
|
+
|
|
778
|
+
```ruby
|
|
779
|
+
# If you don't want to use require
|
|
780
|
+
def pundit_params_for(record)
|
|
781
|
+
params.fetch(PolicyFinder.new(record).param_key, {})
|
|
782
|
+
end
|
|
783
|
+
|
|
784
|
+
# If you are using something like the JSON API spec
|
|
785
|
+
def pundit_params_for(_record)
|
|
786
|
+
params.fetch(:data, {}).fetch(:attributes, {})
|
|
787
|
+
end
|
|
788
|
+
```
|
|
789
|
+
|
|
790
|
+
## RSpec
|
|
791
|
+
|
|
792
|
+
### Policy Specs
|
|
793
|
+
|
|
794
|
+
> [!TIP]
|
|
795
|
+
> An alternative approach to Pundit policy specs is scoping them to a user context as outlined in this
|
|
796
|
+
[excellent post](https://thunderboltlabs.com/blog/2013/03/27/testing-pundit-policies-with-rspec/) and implemented in the third party [pundit-matchers](https://github.com/punditcommunity/pundit-matchers) gem.
|
|
797
|
+
|
|
798
|
+
Pundit includes a mini-DSL for writing expressive tests for your policies in RSpec.
|
|
799
|
+
Require `pundit/rspec` in your `spec_helper.rb`:
|
|
800
|
+
|
|
801
|
+
``` ruby
|
|
802
|
+
require "pundit/rspec"
|
|
803
|
+
```
|
|
804
|
+
|
|
805
|
+
Then put your policy specs in `spec/policies`, and make them look somewhat like this:
|
|
806
|
+
|
|
807
|
+
``` ruby
|
|
808
|
+
describe PostPolicy do
|
|
809
|
+
subject { described_class }
|
|
810
|
+
|
|
811
|
+
permissions :update?, :edit? do
|
|
812
|
+
it "denies access if post is published" do
|
|
813
|
+
expect(subject).not_to permit(User.new(admin: false), Post.new(published: true))
|
|
814
|
+
end
|
|
815
|
+
|
|
816
|
+
it "grants access if post is published and user is an admin" do
|
|
817
|
+
expect(subject).to permit(User.new(admin: true), Post.new(published: true))
|
|
818
|
+
end
|
|
819
|
+
|
|
820
|
+
it "grants access if post is unpublished" do
|
|
821
|
+
expect(subject).to permit(User.new(admin: false), Post.new(published: false))
|
|
822
|
+
end
|
|
823
|
+
end
|
|
824
|
+
end
|
|
825
|
+
```
|
|
826
|
+
|
|
827
|
+
### Custom matcher description
|
|
828
|
+
|
|
829
|
+
By default rspec includes an inspected `user` and `record` in the matcher description, which might become overly verbose:
|
|
830
|
+
|
|
831
|
+
```
|
|
832
|
+
PostPolicy
|
|
833
|
+
update? and show?
|
|
834
|
+
is expected to permit #<User:0x0000000104aefd80> and #<Post:0x0000000104aef8d0 @user=#<User:0x0000000104aefd80>>
|
|
835
|
+
```
|
|
836
|
+
|
|
837
|
+
You can override the default description with a static string, or a block:
|
|
838
|
+
|
|
839
|
+
```ruby
|
|
840
|
+
# static alternative: Pundit::RSpec::Matchers.description = "permit the user"
|
|
841
|
+
Pundit::RSpec::Matchers.description = ->(user, record) do
|
|
842
|
+
"permit user with role #{user.role} to access record with ID #{record.id}"
|
|
843
|
+
end
|
|
844
|
+
```
|
|
845
|
+
|
|
846
|
+
Which would make for a less chatty output:
|
|
847
|
+
|
|
848
|
+
```
|
|
849
|
+
PostPolicy
|
|
850
|
+
update? and show?
|
|
851
|
+
is expected to permit user with role admin to access record with ID 130
|
|
852
|
+
```
|
|
853
|
+
|
|
854
|
+
### Focus Support
|
|
855
|
+
|
|
856
|
+
If your RSpec config has `filter_run_when_matching :focus`, you may tag the `permissions` helper like so:
|
|
857
|
+
|
|
858
|
+
```
|
|
859
|
+
permissions :show?, :focus do
|
|
860
|
+
```
|
|
861
|
+
|
|
862
|
+
### Scope Specs
|
|
863
|
+
|
|
864
|
+
Pundit does not provide a DSL for testing scopes. Test them like you would a regular Ruby class!
|
|
865
|
+
|
|
866
|
+
### Linting with RuboCop RSpec
|
|
867
|
+
|
|
868
|
+
When you lint your RSpec spec files with `rubocop-rspec`, it will fail to properly detect RSpec constructs that Pundit defines, `permissions`.
|
|
869
|
+
Make sure to use `rubocop-rspec` 2.0 or newer and add the following to your `.rubocop.yml`:
|
|
870
|
+
|
|
871
|
+
```yaml
|
|
872
|
+
inherit_gem:
|
|
873
|
+
pundit: config/rubocop-rspec.yml
|
|
874
|
+
```
|
|
875
|
+
|
|
876
|
+
# External Resources
|
|
877
|
+
|
|
878
|
+
- [RailsApps Example Application: Pundit and Devise](https://github.com/RailsApps/rails-devise-pundit)
|
|
879
|
+
- [Migrating to Pundit from CanCan](https://blog.carbonfive.com/2013/10/21/migrating-to-pundit-from-cancan/)
|
|
880
|
+
- [Testing Pundit Policies with RSpec](https://thunderboltlabs.com/blog/2013/03/27/testing-pundit-policies-with-rspec/)
|
|
881
|
+
- [Testing Pundit with Minitest](https://github.com/varvet/pundit/issues/204#issuecomment-60166450)
|
|
882
|
+
- [Using Pundit outside of a Rails controller](https://github.com/varvet/pundit/pull/136)
|
|
883
|
+
- [Straightforward Rails Authorization with Pundit](https://www.sitepoint.com/straightforward-rails-authorization-with-pundit/)
|
|
884
|
+
|
|
885
|
+
## Other implementations
|
|
886
|
+
|
|
887
|
+
- [Flask-Pundit](https://github.com/anurag90x/flask-pundit) (Python) is a [Flask](https://flask.pocoo.org/) extension "heavily inspired by" Pundit
|
|
888
|
+
|
|
889
|
+
# License
|
|
890
|
+
|
|
891
|
+
Licensed under the MIT license, see the separate LICENSE.txt file.
|