consul 0.12.0 → 0.12.1
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of consul might be problematic. Click here for more details.
- checksums.yaml +5 -13
- data/README.md +392 -291
- data/lib/consul/power.rb +5 -3
- data/lib/consul/spec/matchers.rb +3 -1
- data/lib/consul/version.rb +1 -1
- data/spec/rails-3.0/Gemfile.lock +1 -1
- data/spec/rails-3.2/Gemfile.lock +1 -1
- data/spec/rails-4.1/Gemfile.lock +1 -1
- data/spec/shared/consul/power_spec.rb +12 -0
- metadata +14 -130
checksums.yaml
CHANGED
@@ -1,15 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
|
5
|
-
data.tar.gz: !binary |-
|
6
|
-
NjEwOTM0ZjMwNzllMWQxMTgzNzZmMDAwZWQ3ODk0NTA0NTU0N2U3YQ==
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 0400eaba4100fccd2fa6ce219fbef662b0539a68
|
4
|
+
data.tar.gz: c37bcc13bc449b5fdbfd75e0cd10921450d93145
|
7
5
|
SHA512:
|
8
|
-
metadata.gz:
|
9
|
-
|
10
|
-
ODg0NTUzNmE4MmI3NzM5YWNmMjkzMjI3NjUzMzA3N2U5ZTZlNjM4YmE1OWJk
|
11
|
-
YTg5NGQ5NjNlYjM3Y2RhODZiZTUyZDNlZWIwZjQ3OGYyNTI2MWM=
|
12
|
-
data.tar.gz: !binary |-
|
13
|
-
Nzg4Nzc2NGU2Y2FlZjA5N2Y1YzBmMGYyZjhlMjIxZTU0ZDgyOGQ3YjZkNTM5
|
14
|
-
MWZkNDA0OTJhNjVjZTVkMTZlM2E0MDg4NWIwODFkZjQ5MGJiYzE4NWEyYTA2
|
15
|
-
ZjQ2NjJjMjc2N2RhZGE1NmZiZWVlZDdmYmY4NmIyOGRmMzQ3Mjg=
|
6
|
+
metadata.gz: c311180fbd19ac398075e151e8b734edca9dabe4a53de83bdf80cbe16498764b3726b856a36d9ab6926e541b9b4f5ddefe52a8aa42173ad204e3e79d6fe5989a
|
7
|
+
data.tar.gz: 5524169c519ddf6461b6c9925e718b0b560408d0514cad3cba6fd881f576963528b2b54d0e417adfa9446c25135b9c924da2ceff70e586145a9b579ca3be5571
|
data/README.md
CHANGED
@@ -23,26 +23,28 @@ Inside your `Power` you can talk about what is accessible for the current user,
|
|
23
23
|
|
24
24
|
A `Power` might look like this:
|
25
25
|
|
26
|
-
|
27
|
-
|
26
|
+
```rb
|
27
|
+
class Power
|
28
|
+
include Consul::Power
|
28
29
|
|
29
|
-
|
30
|
-
|
31
|
-
|
30
|
+
def initialize(user)
|
31
|
+
@user = user
|
32
|
+
end
|
32
33
|
|
33
|
-
|
34
|
-
|
35
|
-
|
34
|
+
power :users do
|
35
|
+
User if @user.admin?
|
36
|
+
end
|
36
37
|
|
37
|
-
|
38
|
-
|
39
|
-
|
38
|
+
power :notes do
|
39
|
+
Note.by_author(@user)
|
40
|
+
end
|
40
41
|
|
41
|
-
|
42
|
-
|
43
|
-
|
42
|
+
power :dashboard do
|
43
|
+
true # not a scope, but a boolean power. This is useful to control access to stuff that doesn't live in the database.
|
44
|
+
end
|
44
45
|
|
45
|
-
|
46
|
+
end
|
47
|
+
```
|
46
48
|
|
47
49
|
There are no restrictions on the name or constructor arguments of your this class.
|
48
50
|
|
@@ -60,39 +62,51 @@ A typical use case in a Rails application is to restrict access to your ActiveRe
|
|
60
62
|
|
61
63
|
You do this by making your powers return an ActiveRecord scope (or "relation"):
|
62
64
|
|
63
|
-
|
64
|
-
|
65
|
+
```rb
|
66
|
+
class Power
|
67
|
+
...
|
65
68
|
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
+
power :notes do
|
70
|
+
Note.by_author(@user)
|
71
|
+
end
|
69
72
|
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
+
power :users do
|
74
|
+
User if @user.admin?
|
75
|
+
end
|
73
76
|
|
74
|
-
|
77
|
+
end
|
78
|
+
```
|
75
79
|
|
76
80
|
You can now query these powers in order to retrieve the scope:
|
77
81
|
|
78
|
-
|
79
|
-
|
82
|
+
```rb
|
83
|
+
power = Power.new(user)
|
84
|
+
power.notes # => returns an ActiveRecord::Scope
|
85
|
+
```
|
80
86
|
|
81
87
|
Or you can ask if the power is given (meaning it's not `nil`):
|
82
88
|
|
83
|
-
|
89
|
+
```rb
|
90
|
+
power.notes? # => returns true if Power#notes returns a scope and not nil
|
91
|
+
```
|
84
92
|
|
85
93
|
Or you can raise an error unless a power its given, e.g. to guard access into a controller action:
|
86
94
|
|
87
|
-
|
95
|
+
```rb
|
96
|
+
power.notes! # => raises Consul::Powerless unless Power#notes returns a scope (even if it's empty)
|
97
|
+
```
|
88
98
|
|
89
99
|
Or you ask whether a given record is included in its scope (can be [optimized](#optimizing-record-checks-for-scope-powers)):
|
90
100
|
|
91
|
-
|
101
|
+
```rb
|
102
|
+
power.note?(Note.last) # => returns whether the given Note is in the Power#notes scope. Caches the result for subsequent queries.
|
103
|
+
```
|
92
104
|
|
93
105
|
Or you can raise an error unless a given record is included in its scope:
|
94
106
|
|
95
|
-
|
107
|
+
```rb
|
108
|
+
power.note!(Note.last) # => raises Consul::Powerless unless the given Note is in the Power#notes scope
|
109
|
+
```
|
96
110
|
|
97
111
|
See our crash course video [Solving bizare authorization requirements with Rails](http://bizarre-authorization.talks.makandra.com/) for many different use cases you can cover with this pattern.
|
98
112
|
|
@@ -103,22 +117,24 @@ See our crash course video [Solving bizare authorization requirements with Rails
|
|
103
117
|
If you have different access rights for e.g. viewing or updating posts, simply use different powers:
|
104
118
|
|
105
119
|
|
106
|
-
|
107
|
-
|
120
|
+
```rb
|
121
|
+
class Power
|
122
|
+
...
|
108
123
|
|
109
|
-
|
110
|
-
|
111
|
-
|
124
|
+
power :notes do
|
125
|
+
Note.published
|
126
|
+
end
|
112
127
|
|
113
|
-
|
114
|
-
|
115
|
-
|
128
|
+
power :updatable_notes do
|
129
|
+
Note.by_author(@user)
|
130
|
+
end
|
116
131
|
|
117
|
-
|
118
|
-
|
119
|
-
|
132
|
+
power :destroyable_notes do
|
133
|
+
Note if @user.admin?
|
134
|
+
end
|
120
135
|
|
121
|
-
|
136
|
+
end
|
137
|
+
```
|
122
138
|
|
123
139
|
There is also a [shortcut to map different powers to RESTful controller actions](#protect-entry-into-controller-actions).
|
124
140
|
|
@@ -128,20 +144,24 @@ There is also a [shortcut to map different powers to RESTful controller actions]
|
|
128
144
|
|
129
145
|
Boolean powers are useful to control access to stuff that doesn't live in the database:
|
130
146
|
|
131
|
-
|
132
|
-
|
147
|
+
```rb
|
148
|
+
class Power
|
149
|
+
...
|
133
150
|
|
134
|
-
|
135
|
-
|
136
|
-
|
151
|
+
power :dashboard do
|
152
|
+
true
|
153
|
+
end
|
137
154
|
|
138
|
-
|
155
|
+
end
|
156
|
+
```
|
139
157
|
|
140
158
|
You can query it like the other powers:
|
141
159
|
|
142
|
-
|
143
|
-
|
144
|
-
|
160
|
+
```rb
|
161
|
+
power = Power.new(@user)
|
162
|
+
power.dashboard? # => true
|
163
|
+
power.dashboard! # => raises Consul::Powerless unless Power#dashboard? returns true
|
164
|
+
```
|
145
165
|
|
146
166
|
|
147
167
|
### Powers that give no access at all
|
@@ -151,23 +171,27 @@ If you want to express that a user has no access at all, make the respective pow
|
|
151
171
|
|
152
172
|
Note how the power in the example below returns `nil` unless the user is an admin:
|
153
173
|
|
154
|
-
|
155
|
-
|
174
|
+
```rb
|
175
|
+
class Power
|
176
|
+
...
|
156
177
|
|
157
|
-
|
158
|
-
|
159
|
-
|
178
|
+
power :users do
|
179
|
+
User if @user.admin?
|
180
|
+
end
|
160
181
|
|
161
|
-
|
182
|
+
end
|
183
|
+
```
|
162
184
|
|
163
185
|
When a non-admin queries the `:users` power, she will get the following behavior:
|
164
186
|
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
187
|
+
```rb
|
188
|
+
power = Power.new(@user)
|
189
|
+
power.users # => returns nil
|
190
|
+
power.users? # => returns false
|
191
|
+
power.users! # => raises Consul::Powerless
|
192
|
+
power.user?(User.last) # => returns false
|
193
|
+
power.user!(User.last) # => raises Consul::Powerless
|
194
|
+
```
|
171
195
|
|
172
196
|
|
173
197
|
|
@@ -179,97 +203,113 @@ Sometimes you only want to store a method that checks whether a given object is
|
|
179
203
|
To do so, simply define a power that ends in a question mark:
|
180
204
|
|
181
205
|
|
182
|
-
|
183
|
-
|
206
|
+
```rb
|
207
|
+
class Power
|
208
|
+
...
|
184
209
|
|
185
|
-
|
186
|
-
|
187
|
-
|
210
|
+
power :updatable_post? do |post|
|
211
|
+
post.author == @user
|
212
|
+
end
|
188
213
|
|
189
|
-
|
214
|
+
end
|
215
|
+
```
|
190
216
|
|
191
217
|
You can query such an power as always:
|
192
218
|
|
193
|
-
|
194
|
-
|
195
|
-
|
219
|
+
```rb
|
220
|
+
power = Power.new(@user)
|
221
|
+
power.updatable_post?(Post.last) # return true if the author of the post is @user
|
222
|
+
power.updatable_post!(Post.last) # raises Consul::Powerless unless the author of the post is @user
|
223
|
+
```
|
196
224
|
|
197
225
|
|
198
226
|
### Other types of powers
|
199
227
|
|
200
228
|
A power can return any type of object. For instance, you often want to return an array:
|
201
229
|
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
power :assignable_note_states do
|
206
|
-
if admin?
|
207
|
-
%w[draft pending published retracted]
|
208
|
-
else
|
209
|
-
%w[draft pending]
|
210
|
-
end
|
211
|
-
end
|
230
|
+
```rb
|
231
|
+
class Power
|
232
|
+
...
|
212
233
|
|
234
|
+
power :assignable_note_states do
|
235
|
+
if admin?
|
236
|
+
%w[draft pending published retracted]
|
237
|
+
else
|
238
|
+
%w[draft pending]
|
213
239
|
end
|
240
|
+
end
|
241
|
+
|
242
|
+
end
|
243
|
+
```
|
214
244
|
|
215
245
|
You can query it like any other power. E.g. if a non-admin queries this power she will get the following behavior:
|
216
246
|
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
247
|
+
```rb
|
248
|
+
power.assignable_note_states # => ['draft', 'pending']
|
249
|
+
power.assignable_note_states? # => returns true
|
250
|
+
power.assignable_note_states! # => does nothing (because the power isn't nil)
|
251
|
+
power.assignable_note_state?('draft') # => returns true
|
252
|
+
power.assignable_note_state?('published') # => returns false
|
253
|
+
power.assignable_note_state!('published') # => raises Consul::Powerless
|
254
|
+
```
|
223
255
|
|
224
256
|
|
225
257
|
### Defining multiple powers at once
|
226
258
|
|
227
259
|
You can define multiple powers at once by giving multiple power names:
|
228
260
|
|
229
|
-
|
230
|
-
|
261
|
+
```rb
|
262
|
+
class Power
|
263
|
+
...
|
231
264
|
|
232
|
-
|
233
|
-
|
234
|
-
|
265
|
+
power :destroyable_users, :updatable_users do
|
266
|
+
User if admin?
|
267
|
+
end
|
235
268
|
|
236
|
-
|
269
|
+
end
|
270
|
+
```
|
237
271
|
|
238
272
|
|
239
273
|
### Powers that require context (arguments)
|
240
274
|
|
241
275
|
Sometimes it can be useful to define powers that require context. To do so, just take an argument in your `power` block:
|
242
276
|
|
243
|
-
|
244
|
-
|
277
|
+
```rb
|
278
|
+
class Power
|
279
|
+
...
|
245
280
|
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
281
|
+
power :client_notes do |client|
|
282
|
+
client.notes.where(:state => 'published')
|
283
|
+
end
|
284
|
+
|
285
|
+
end
|
286
|
+
```
|
251
287
|
|
252
288
|
When querying such a power, you always need to provide the context, e.g.:
|
253
289
|
|
254
|
-
|
255
|
-
|
256
|
-
|
290
|
+
```rb
|
291
|
+
client = ...
|
292
|
+
note = ...
|
293
|
+
Power.current.client_note?(client, note)
|
294
|
+
```
|
257
295
|
|
258
296
|
|
259
297
|
### Optimizing record checks for scope powers
|
260
298
|
|
261
299
|
You can query a scope power for a given record, e.g.
|
262
300
|
|
263
|
-
|
264
|
-
|
301
|
+
```rb
|
302
|
+
class Power
|
303
|
+
...
|
265
304
|
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
305
|
+
power :posts do |post|
|
306
|
+
Post.where(:author_id => @user.id)
|
307
|
+
end
|
308
|
+
end
|
270
309
|
|
271
|
-
|
272
|
-
|
310
|
+
power = Power.new(@user)
|
311
|
+
power.post?(Post.last)
|
312
|
+
```
|
273
313
|
|
274
314
|
What Consul does internally is fetch **all** the IDs of the `power.posts` scope and test if the given
|
275
315
|
record's ID is among them. This list of IDs is cached for subsequent calls, so you will only touch the database once.
|
@@ -279,18 +319,20 @@ however be the point where you want to optimize this.
|
|
279
319
|
|
280
320
|
What you can do in Consul is to define a second power that checks a given record in plain Ruby:
|
281
321
|
|
282
|
-
|
283
|
-
|
322
|
+
```rb
|
323
|
+
class Power
|
324
|
+
...
|
284
325
|
|
285
|
-
|
286
|
-
|
287
|
-
|
326
|
+
power :posts do |post|
|
327
|
+
Post.where(:author_id => @user.id)
|
328
|
+
end
|
288
329
|
|
289
|
-
|
290
|
-
|
291
|
-
|
330
|
+
power :post? do |post|
|
331
|
+
post.author_id == @user.id
|
332
|
+
end
|
292
333
|
|
293
|
-
|
334
|
+
end
|
335
|
+
```
|
294
336
|
|
295
337
|
This way you do not need to touch the database at all.
|
296
338
|
|
@@ -300,27 +342,29 @@ Role-based permissions
|
|
300
342
|
|
301
343
|
Consul has no built-in support for role-based permissions, but you can easily implement it yourself. Let's say your `User` model has a string column `role` which can be `"author"` or `"admin"`:
|
302
344
|
|
303
|
-
|
304
|
-
|
345
|
+
```rb
|
346
|
+
class Power
|
347
|
+
include Consul::Power
|
305
348
|
|
306
|
-
|
307
|
-
|
308
|
-
|
349
|
+
def initialize(user)
|
350
|
+
@user = user
|
351
|
+
end
|
309
352
|
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
353
|
+
power :notes do
|
354
|
+
case role
|
355
|
+
when :admin then Note
|
356
|
+
when :author then Note.by_author
|
357
|
+
end
|
358
|
+
end
|
316
359
|
|
317
|
-
|
360
|
+
private
|
318
361
|
|
319
|
-
|
320
|
-
|
321
|
-
|
362
|
+
def role
|
363
|
+
@user.role.to_sym
|
364
|
+
end
|
322
365
|
|
323
|
-
|
366
|
+
end
|
367
|
+
```
|
324
368
|
|
325
369
|
|
326
370
|
Controller integration
|
@@ -328,76 +372,90 @@ Controller integration
|
|
328
372
|
|
329
373
|
It is convenient to expose the power for the current request to the rest of the application. Consul will help you with that if you tell it how to instantiate a power for the current request:
|
330
374
|
|
331
|
-
|
332
|
-
|
375
|
+
```rb
|
376
|
+
class ApplicationController < ActionController::Base
|
377
|
+
include Consul::Controller
|
333
378
|
|
334
|
-
|
335
|
-
|
336
|
-
|
379
|
+
current_power do
|
380
|
+
Power.new(current_user)
|
381
|
+
end
|
337
382
|
|
338
|
-
|
383
|
+
end
|
384
|
+
```
|
339
385
|
|
340
386
|
You now have a helper method `current_power` for your controller and views. Everywhere else, you can access it from `Power.current`. The power will be instantiated when the request is handed over from routing to `ApplicationController`, and will be nilified once the request was processed.
|
341
387
|
|
342
388
|
You can now use power scopes to control access:
|
343
389
|
|
344
|
-
|
390
|
+
```rb
|
391
|
+
class NotesController < ApplicationController
|
345
392
|
|
346
|
-
|
347
|
-
|
348
|
-
|
393
|
+
def show
|
394
|
+
@note = current_power.notes.find(params[:id])
|
395
|
+
end
|
349
396
|
|
350
|
-
|
397
|
+
end
|
398
|
+
```
|
351
399
|
|
352
400
|
|
353
401
|
### Protect entry into controller actions
|
354
402
|
|
355
403
|
To make sure a power is given before every action in a controller:
|
356
404
|
|
357
|
-
|
358
|
-
|
359
|
-
|
405
|
+
```rb
|
406
|
+
class NotesController < ApplicationController
|
407
|
+
power :notes
|
408
|
+
end
|
409
|
+
```
|
360
410
|
|
361
411
|
You can use `:except` and `:only` options like in before filters.
|
362
412
|
|
363
413
|
You can also map different powers to different actions:
|
364
414
|
|
365
|
-
|
366
|
-
|
367
|
-
|
415
|
+
```rb
|
416
|
+
class NotesController < ApplicationController
|
417
|
+
power :notes, :map => { [:edit, :update, :destroy] => :changeable_notes }
|
418
|
+
end
|
419
|
+
```
|
368
420
|
|
369
421
|
Actions that are not listed in `:map` will get the default action `:notes`.
|
370
422
|
|
371
423
|
Note that in moderately complex authorization scenarios you will often find yourself writing a map like this:
|
372
424
|
|
373
|
-
|
374
|
-
|
375
|
-
|
376
|
-
|
377
|
-
|
378
|
-
|
379
|
-
|
425
|
+
```rb
|
426
|
+
class NotesController < ApplicationController
|
427
|
+
power :notes, :map => {
|
428
|
+
[:edit, :update] => :updatable_notes,
|
429
|
+
[:new, :create] => :creatable_notes,
|
430
|
+
[:destroy] => :destroyable_notes
|
431
|
+
}
|
432
|
+
end
|
433
|
+
```
|
380
434
|
|
381
435
|
Because this pattern is so common, there is a shortcut `:crud` to do the same:
|
382
436
|
|
383
|
-
|
384
|
-
|
385
|
-
|
437
|
+
```rb
|
438
|
+
class NotesController < ApplicationController
|
439
|
+
power :crud => :notes
|
440
|
+
end
|
441
|
+
```
|
386
442
|
|
387
443
|
|
388
444
|
And if your power [requires context](#powers-that-require-context-arguments) (is parametrized), you can give it using the `:context` method:
|
389
445
|
|
390
|
-
|
446
|
+
```rb
|
447
|
+
class ClientNotesController < ApplicationController
|
391
448
|
|
392
|
-
|
449
|
+
power :client_notes, :context => :load_client
|
393
450
|
|
394
|
-
|
451
|
+
private
|
395
452
|
|
396
|
-
|
397
|
-
|
398
|
-
|
453
|
+
def load_client
|
454
|
+
@client ||= Client.find(params[:client_id])
|
455
|
+
end
|
399
456
|
|
400
|
-
|
457
|
+
end
|
458
|
+
```
|
401
459
|
|
402
460
|
|
403
461
|
|
@@ -405,15 +463,17 @@ And if your power [requires context](#powers-that-require-context-arguments) (is
|
|
405
463
|
|
406
464
|
It is often convenient to map a power scope to a private controller method:
|
407
465
|
|
408
|
-
|
466
|
+
```rb
|
467
|
+
class NotesController < ApplicationController
|
409
468
|
|
410
|
-
|
469
|
+
power :notes, :as => note_scope
|
411
470
|
|
412
|
-
|
413
|
-
|
414
|
-
|
471
|
+
def show
|
472
|
+
@note = note_scope.find(params[:id])
|
473
|
+
end
|
415
474
|
|
416
|
-
|
475
|
+
end
|
476
|
+
```
|
417
477
|
|
418
478
|
This is especially useful when you are using a RESTful controller library like [resource_controller](https://github.com/jamesgolick/resource_controller). The mapped method is aware of the `:map` option.
|
419
479
|
|
@@ -425,47 +485,53 @@ checks and method mappings: One for the parent resource, another for the child r
|
|
425
485
|
|
426
486
|
Say you have the following routes:
|
427
487
|
|
428
|
-
|
429
|
-
|
430
|
-
|
488
|
+
```rb
|
489
|
+
resources :clients do
|
490
|
+
resources :notes
|
491
|
+
end
|
492
|
+
```
|
431
493
|
|
432
494
|
And the following power definitions:
|
433
495
|
|
434
|
-
|
435
|
-
|
496
|
+
```rb
|
497
|
+
class Power
|
498
|
+
...
|
436
499
|
|
437
|
-
|
438
|
-
|
439
|
-
|
500
|
+
power :clients do |client|
|
501
|
+
Client.active if signed_in?
|
502
|
+
end
|
440
503
|
|
441
|
-
|
442
|
-
|
443
|
-
|
504
|
+
power :client_notes do |client|
|
505
|
+
client.notes.where(:state => 'published')
|
506
|
+
end
|
444
507
|
|
445
|
-
|
508
|
+
end
|
509
|
+
```
|
446
510
|
|
447
511
|
You can now check and map both powers in the nested `NotesController`:
|
448
512
|
|
449
|
-
|
513
|
+
```rb
|
514
|
+
class NotesController < ApplicationController
|
450
515
|
|
451
|
-
|
452
|
-
|
516
|
+
power :clients, :as => :client_scope
|
517
|
+
power :client_notes, :context => :load_client, :as => :note_scope
|
453
518
|
|
454
|
-
|
455
|
-
|
456
|
-
|
519
|
+
def show
|
520
|
+
load_note
|
521
|
+
end
|
457
522
|
|
458
|
-
|
523
|
+
private
|
459
524
|
|
460
|
-
|
461
|
-
|
462
|
-
|
525
|
+
def load_client
|
526
|
+
@client ||= client_scope.find(params[:client_id])
|
527
|
+
end
|
463
528
|
|
464
|
-
|
465
|
-
|
466
|
-
|
529
|
+
def load_note
|
530
|
+
@note ||= note_scope.find(params[:id])
|
531
|
+
end
|
467
532
|
|
468
|
-
|
533
|
+
end
|
534
|
+
```
|
469
535
|
|
470
536
|
Note how we provide the `Client` parameter for the `:client_notes` power by using the `:context => :load_client`
|
471
537
|
option in the `power` directive.
|
@@ -474,16 +540,20 @@ option in the `power` directive.
|
|
474
540
|
|
475
541
|
You can force yourself to use a `power` check in every controller. This will raise `Consul::UncheckedPower` if you ever forget it:
|
476
542
|
|
477
|
-
|
478
|
-
|
479
|
-
|
480
|
-
|
543
|
+
```rb
|
544
|
+
class ApplicationController < ActionController::Base
|
545
|
+
include Consul::Controller
|
546
|
+
require_power_check
|
547
|
+
end
|
548
|
+
```
|
481
549
|
|
482
550
|
Should you for some obscure reason want to forego the power check:
|
483
551
|
|
484
|
-
|
485
|
-
|
486
|
-
|
552
|
+
```rb
|
553
|
+
class ApiController < ApplicationController
|
554
|
+
skip_power_check
|
555
|
+
end
|
556
|
+
```
|
487
557
|
|
488
558
|
|
489
559
|
Validating assignable values
|
@@ -495,59 +565,68 @@ Consul leverages the [assignable_values](https://github.com/makandra/assignable_
|
|
495
565
|
|
496
566
|
You can enable the authorization layer by using the macro `authorize_values_for`:
|
497
567
|
|
498
|
-
|
499
|
-
|
500
|
-
|
568
|
+
```rb
|
569
|
+
class Story < ActiveRecord::Base
|
570
|
+
authorize_values_for :state
|
571
|
+
end
|
572
|
+
```
|
501
573
|
|
502
574
|
The macro defines an accessor `power` on instances of `Story`. If that field is set to a power, the values of `state` will be validated against a whitelist of values provided by that power. If that field is `nil`, the validation is skipped.
|
503
575
|
|
504
576
|
Here is a power implementation that can provide a list of assignable values for the example above:
|
505
577
|
|
506
|
-
|
507
|
-
|
508
|
-
|
509
|
-
def assignable_story_states(story)
|
510
|
-
if admin?
|
511
|
-
['delivered', 'accepted', 'rejected']
|
512
|
-
else
|
513
|
-
['delivered']
|
514
|
-
end
|
515
|
-
end
|
578
|
+
```rb
|
579
|
+
class Power
|
580
|
+
...
|
516
581
|
|
582
|
+
def assignable_story_states(story)
|
583
|
+
if admin?
|
584
|
+
['delivered', 'accepted', 'rejected']
|
585
|
+
else
|
586
|
+
['delivered']
|
517
587
|
end
|
588
|
+
end
|
589
|
+
|
590
|
+
end
|
591
|
+
```
|
518
592
|
|
519
593
|
Here you can see how to activate the authorization layer and use the new validations:
|
520
594
|
|
521
|
-
|
522
|
-
|
595
|
+
```rb
|
596
|
+
story = Story.new
|
597
|
+
Power.current = Power.new(:role => :guest) # activate the authorization layer
|
523
598
|
|
524
|
-
|
599
|
+
story.assignable_states # ['delivered'] # apparently we're not admins
|
525
600
|
|
526
|
-
|
527
|
-
|
601
|
+
story.state = 'accepted' # a disallowed value
|
602
|
+
story.valid? # => false
|
528
603
|
|
529
|
-
|
530
|
-
|
604
|
+
story.state = 'delivered' # an allowed value
|
605
|
+
story.valid? # => true
|
606
|
+
```
|
531
607
|
|
532
608
|
You can not only authorize scalar attributes like strings or integers that way, you can also authorize `belongs_to` associations:
|
533
609
|
|
534
|
-
|
535
|
-
|
536
|
-
|
537
|
-
|
610
|
+
```rb
|
611
|
+
class Story < ActiveRecord::Base
|
612
|
+
belongs_to :project
|
613
|
+
authorize_values_for :project
|
614
|
+
end
|
538
615
|
|
539
|
-
|
540
|
-
|
616
|
+
class Power
|
617
|
+
...
|
541
618
|
|
542
|
-
|
543
|
-
|
544
|
-
|
545
|
-
|
619
|
+
power :assignable_story_projects do |story|
|
620
|
+
user.account.projects
|
621
|
+
end
|
622
|
+
end
|
623
|
+
```
|
546
624
|
|
547
625
|
The `authorize_values_for` macro comes with many useful options and details best explained in the [assignable_values README](https://github.com/makandra/assignable_values), so head over there for more. The macro is basically a shortcut for this:
|
548
626
|
|
549
|
-
|
550
|
-
|
627
|
+
```rb
|
628
|
+
assignable_values_for :field, :through => lambda { Power.current }
|
629
|
+
```
|
551
630
|
|
552
631
|
Dynamic power access
|
553
632
|
--------------------
|
@@ -556,15 +635,17 @@ Consul gives you a way to dynamically access and query powers for a given name,
|
|
556
635
|
A common use case for this are generic helper methods, e.g. a method to display an "edit" link for any given record
|
557
636
|
if the user is authorized to change that record:
|
558
637
|
|
559
|
-
|
560
|
-
|
561
|
-
def edit_record_action(record)
|
562
|
-
if current_power.include_record?(:updatable, record)
|
563
|
-
link_to 'Edit', [:edit, record]
|
564
|
-
end
|
565
|
-
end
|
638
|
+
```rb
|
639
|
+
module CrudHelper
|
566
640
|
|
641
|
+
def edit_record_action(record)
|
642
|
+
if current_power.include_record?(:updatable, record)
|
643
|
+
link_to 'Edit', [:edit, record]
|
567
644
|
end
|
645
|
+
end
|
646
|
+
|
647
|
+
end
|
648
|
+
```
|
568
649
|
|
569
650
|
You can find a full list of available dynamic calls below:
|
570
651
|
|
@@ -599,43 +680,49 @@ Querying a power that might be nil
|
|
599
680
|
|
600
681
|
You will often want to access `Power.current` from another model, to e.g. iterate through the list of accessible users:
|
601
682
|
|
602
|
-
|
603
|
-
|
604
|
-
def data
|
605
|
-
Power.current.users.collect do |user|
|
606
|
-
[user.name, user.email, user.income]
|
607
|
-
end
|
608
|
-
end
|
683
|
+
```rb
|
684
|
+
class UserReport
|
609
685
|
|
686
|
+
def data
|
687
|
+
Power.current.users.collect do |user|
|
688
|
+
[user.name, user.email, user.income]
|
610
689
|
end
|
690
|
+
end
|
691
|
+
|
692
|
+
end
|
693
|
+
```
|
611
694
|
|
612
695
|
Good practice is for your model to not crash when `Power.current` is `nil`. This is the case when your model isn't
|
613
696
|
called as part of processing a browser request, e.g. on the console, during tests and during batch processes.
|
614
697
|
In such cases your model should simply skip authorization and assume that all users are accessible:
|
615
698
|
|
616
|
-
|
617
|
-
|
618
|
-
def data
|
619
|
-
accessible_users = Power.current.present? ? Power.current.users : User
|
620
|
-
accessible_users.collect do |user|
|
621
|
-
[user.name, user.email, user.income]
|
622
|
-
end
|
623
|
-
end
|
699
|
+
```rb
|
700
|
+
class UserReport
|
624
701
|
|
702
|
+
def data
|
703
|
+
accessible_users = Power.current.present? ? Power.current.users : User
|
704
|
+
accessible_users.collect do |user|
|
705
|
+
[user.name, user.email, user.income]
|
625
706
|
end
|
707
|
+
end
|
708
|
+
|
709
|
+
end
|
710
|
+
```
|
626
711
|
|
627
712
|
Because this pattern is so common, the `Power` class comes with a number of class methods you can use to either query
|
628
713
|
`Power.current` or, if it is not set, just assume that everything is accessible:
|
629
714
|
|
630
|
-
|
631
|
-
|
632
|
-
def data
|
633
|
-
Power.for_model(User).collect do |user|
|
634
|
-
[user.name, user.email, user.income]
|
635
|
-
end
|
636
|
-
end
|
715
|
+
```rb
|
716
|
+
class UserReport
|
637
717
|
|
718
|
+
def data
|
719
|
+
Power.for_model(User).collect do |user|
|
720
|
+
[user.name, user.email, user.income]
|
638
721
|
end
|
722
|
+
end
|
723
|
+
|
724
|
+
end
|
725
|
+
```
|
639
726
|
|
640
727
|
There is a long selection of class methods that behave neutrally in case `Power.current` is `nil`:
|
641
728
|
|
@@ -665,19 +752,23 @@ This section Some hints for testing authorization with Consul.
|
|
665
752
|
|
666
753
|
You can say this in any controller spec:
|
667
754
|
|
668
|
-
|
755
|
+
```rb
|
756
|
+
describe CakesController do
|
669
757
|
|
670
|
-
|
758
|
+
it { should check_power(:cakes) }
|
671
759
|
|
672
|
-
|
760
|
+
end
|
761
|
+
```
|
673
762
|
|
674
763
|
You can test against all options of the `power` macro:
|
675
764
|
|
676
|
-
|
765
|
+
```rb
|
766
|
+
describe CakesController do
|
677
767
|
|
678
|
-
|
768
|
+
it { should check_power(:cakes, :map => { [:edit, :update] => :updatable_cakes }) }
|
679
769
|
|
680
|
-
|
770
|
+
end
|
771
|
+
```
|
681
772
|
|
682
773
|
### Temporarily change the current power
|
683
774
|
|
@@ -685,28 +776,34 @@ When you set `Power.current` to a power in an RSpec example, you must remember t
|
|
685
776
|
|
686
777
|
A better way is to use the `.with_power` method to change the current power for the duration of a block:
|
687
778
|
|
688
|
-
|
689
|
-
|
779
|
+
```rb
|
780
|
+
admin = User.new(:role => 'admin')
|
781
|
+
admin_power = Power.new(admin)
|
690
782
|
|
691
|
-
|
692
|
-
|
693
|
-
|
783
|
+
Power.with_power(admin_power) do
|
784
|
+
# run code that uses Power.current
|
785
|
+
end
|
786
|
+
```
|
694
787
|
|
695
788
|
`Power.current` will be `nil` (or its former value) after the block has ended.
|
696
789
|
|
697
790
|
A nice shortcut is that when you call `with_power` with an argument that is not already a `Power`, Consul will instantiate a `Power` for you:
|
698
791
|
|
699
|
-
|
792
|
+
```rb
|
793
|
+
admin = User.new(:role => 'admin')
|
700
794
|
|
701
|
-
|
702
|
-
|
703
|
-
|
795
|
+
Power.with_power(admin) do
|
796
|
+
# run code that uses Power.current
|
797
|
+
end
|
798
|
+
```
|
704
799
|
|
705
800
|
There is also a method `.without_power` that runs a block without a current Power:
|
706
801
|
|
707
|
-
|
708
|
-
|
709
|
-
|
802
|
+
```rb
|
803
|
+
Power.without_power do
|
804
|
+
# run code that should not see a Power
|
805
|
+
end
|
806
|
+
```
|
710
807
|
|
711
808
|
|
712
809
|
Installation
|
@@ -714,7 +811,9 @@ Installation
|
|
714
811
|
|
715
812
|
Add the following to your `Gemfile`:
|
716
813
|
|
717
|
-
|
814
|
+
```
|
815
|
+
gem 'consul'
|
816
|
+
```
|
718
817
|
|
719
818
|
Now run `bundle install` to lock the gem into your project.
|
720
819
|
|
@@ -724,7 +823,9 @@ Development
|
|
724
823
|
|
725
824
|
Test applications for various Rails versions lives in `spec`. You can run specs from the project root by saying:
|
726
825
|
|
727
|
-
|
826
|
+
```
|
827
|
+
bundle exec rake all:spec
|
828
|
+
```
|
728
829
|
|
729
830
|
If you would like to contribute:
|
730
831
|
|