aegis 1.1.8 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. data/.gitignore +4 -0
  2. data/README.rdoc +58 -165
  3. data/Rakefile +20 -12
  4. data/VERSION +1 -1
  5. data/aegis.gemspec +85 -56
  6. data/lib/aegis.rb +9 -6
  7. data/lib/aegis/access_denied.rb +4 -0
  8. data/lib/aegis/action.rb +99 -0
  9. data/lib/aegis/compiler.rb +113 -0
  10. data/lib/aegis/has_role.rb +89 -110
  11. data/lib/aegis/parser.rb +110 -0
  12. data/lib/aegis/permissions.rb +164 -107
  13. data/lib/aegis/resource.rb +158 -0
  14. data/lib/aegis/role.rb +25 -55
  15. data/lib/aegis/sieve.rb +39 -0
  16. data/lib/rails/action_controller.rb +38 -0
  17. data/lib/rails/active_record.rb +1 -5
  18. data/spec/action_controller_spec.rb +100 -0
  19. data/spec/app_root/app/controllers/application_controller.rb +7 -0
  20. data/spec/app_root/app/controllers/reviews_controller.rb +36 -0
  21. data/spec/app_root/app/models/permissions.rb +14 -0
  22. data/spec/app_root/app/models/property.rb +5 -0
  23. data/spec/app_root/app/models/review.rb +5 -0
  24. data/{test → spec}/app_root/app/models/user.rb +1 -2
  25. data/{test → spec}/app_root/config/boot.rb +0 -0
  26. data/{test → spec}/app_root/config/database.yml +0 -0
  27. data/{test → spec}/app_root/config/environment.rb +0 -0
  28. data/{test → spec}/app_root/config/environments/in_memory.rb +0 -0
  29. data/{test → spec}/app_root/config/environments/mysql.rb +0 -0
  30. data/{test → spec}/app_root/config/environments/postgresql.rb +0 -0
  31. data/{test → spec}/app_root/config/environments/sqlite.rb +0 -0
  32. data/{test → spec}/app_root/config/environments/sqlite3.rb +0 -0
  33. data/spec/app_root/config/routes.rb +7 -0
  34. data/{test/app_root/db/migrate/20090408115228_create_users.rb → spec/app_root/db/migrate/001_create_users.rb} +2 -1
  35. data/spec/app_root/db/migrate/002_create_properties.rb +13 -0
  36. data/spec/app_root/db/migrate/003_create_reviews.rb +14 -0
  37. data/{test → spec}/app_root/lib/console_with_fixtures.rb +0 -0
  38. data/{test → spec}/app_root/log/.gitignore +0 -0
  39. data/{test → spec}/app_root/script/console +0 -0
  40. data/spec/controllers/reviews_controller_spec.rb +19 -0
  41. data/spec/has_role_spec.rb +177 -0
  42. data/spec/permissions_spec.rb +550 -0
  43. data/spec/rcov.opts +2 -0
  44. data/spec/spec.opts +4 -0
  45. data/{test/test_helper.rb → spec/spec_helper.rb} +6 -9
  46. metadata +73 -57
  47. data/lib/aegis/constants.rb +0 -6
  48. data/lib/aegis/normalization.rb +0 -26
  49. data/lib/aegis/permission_error.rb +0 -5
  50. data/lib/aegis/permission_evaluator.rb +0 -34
  51. data/test/app_root/app/controllers/application_controller.rb +0 -2
  52. data/test/app_root/app/models/old_soldier.rb +0 -6
  53. data/test/app_root/app/models/permissions.rb +0 -49
  54. data/test/app_root/app/models/soldier.rb +0 -5
  55. data/test/app_root/app/models/trust_fund_kid.rb +0 -5
  56. data/test/app_root/app/models/user_subclass.rb +0 -2
  57. data/test/app_root/app/models/veteran_soldier.rb +0 -6
  58. data/test/app_root/config/routes.rb +0 -4
  59. data/test/app_root/db/migrate/20090429075648_create_soldiers.rb +0 -14
  60. data/test/app_root/db/migrate/20091110075648_create_veteran_soldiers.rb +0 -14
  61. data/test/app_root/db/migrate/20091110075649_create_trust_fund_kids.rb +0 -15
  62. data/test/has_role_options_test.rb +0 -64
  63. data/test/has_role_test.rb +0 -54
  64. data/test/permissions_test.rb +0 -109
  65. data/test/validation_test.rb +0 -55
@@ -0,0 +1,550 @@
1
+ require File.dirname(__FILE__) + "/spec_helper"
2
+
3
+ describe Aegis::Permissions do
4
+
5
+ before(:each) do
6
+
7
+ permissions = @permissions = Class.new(Aegis::Permissions) do
8
+ role :user
9
+ role :moderator
10
+ role :admin, :default_permission => :allow
11
+ end
12
+
13
+ @user_class = Class.new(ActiveRecord::Base) do
14
+ set_table_name 'users'
15
+ has_role :permissions => permissions
16
+ end
17
+
18
+ @user = @user_class.new(:role_name => 'user')
19
+ @moderator = @user_class.new(:role_name => 'moderator')
20
+ @admin = @user_class.new(:role_name => 'admin')
21
+
22
+ end
23
+
24
+ describe 'find_role_by_name' do
25
+
26
+ it "should look up previously defined roles" do
27
+
28
+ user_role = @permissions.find_role_by_name('user')
29
+ admin_role = @permissions.find_role_by_name('admin')
30
+
31
+ user_role.name.should == 'user'
32
+ user_role.may_by_default?.should be_false
33
+
34
+ admin_role.name.should == 'admin'
35
+ admin_role.may_by_default?.should be_true
36
+
37
+ end
38
+
39
+ it "should be nil if no role with that name was defined" do
40
+ @permissions.find_role_by_name('nonexisting_role').should be_nil
41
+ end
42
+
43
+ end
44
+
45
+ describe 'permission definition' do
46
+
47
+ it "should allow the definition of simple actions" do
48
+
49
+ @permissions.class_eval do
50
+ action :action_name do
51
+ allow :user
52
+ end
53
+ end
54
+
55
+ @permissions.may?(@user, 'action_name').should be_true
56
+
57
+ end
58
+
59
+ it "should allow the definition of multiple actions at once" do
60
+
61
+ @permissions.class_eval do
62
+ action :action1, :action2 do
63
+ allow
64
+ end
65
+ end
66
+
67
+ @permissions.may?(@user, 'action1').should be_true
68
+ @permissions.may?(@user, 'action2').should be_true
69
+ @permissions.may?(@user, 'action3').should be_false
70
+
71
+ end
72
+
73
+ it "should match an allow/deny directive to everyone is no role is named" do
74
+ @permissions.class_eval do
75
+ action :allowed_to_all do
76
+ allow
77
+ end
78
+ action :denied_to_all do
79
+ deny
80
+ end
81
+ end
82
+
83
+ @permissions.may?(@user, 'allowed_to_all').should be_true
84
+ @permissions.may?(@admin, 'denied_to_all').should be_false
85
+ end
86
+
87
+ it "should allow to grant permissions to multiple roles at once" do
88
+
89
+ @permissions.class_eval do
90
+ action :action_name do
91
+ allow :user, :moderator
92
+ end
93
+ end
94
+
95
+ @permissions.may?(@user, 'action_name').should be_true
96
+ @permissions.may?(@moderator, 'action_name').should be_true
97
+
98
+ end
99
+
100
+ it "should return the default permission when queried for undefined actions" do
101
+
102
+ @permissions.may?(@user, 'undefined_action').should be_false
103
+ @permissions.may?(@admin, 'undefined_action').should be_true
104
+
105
+ end
106
+
107
+ it "should distinguish between roles" do
108
+
109
+ @permissions.class_eval do
110
+ action :update_news do
111
+ allow :moderator
112
+ end
113
+ end
114
+
115
+ @permissions.may?(@user, 'update_news').should be_false
116
+ @permissions.may?(@moderator, 'update_news').should be_true
117
+
118
+ end
119
+
120
+ it "should run sieves in a sequence, the result being the last matching sieve" do
121
+
122
+ @permissions.class_eval do
123
+ action :update_news do
124
+ allow :everyone
125
+ deny :user
126
+ end
127
+ end
128
+
129
+ @permissions.may?(@user, 'update_news').should be_false
130
+ @permissions.may?(@moderator, 'update_news').should be_true
131
+
132
+ end
133
+
134
+ it "should evaluate collection resources" do
135
+
136
+ @permissions.class_eval do
137
+ resources :posts do
138
+ allow :moderator
139
+ end
140
+ end
141
+
142
+ @permissions.may?(@moderator, 'update_post', "the post").should be_true
143
+ @permissions.may?(@moderator, 'show_post', "the post").should be_true
144
+ @permissions.may?(@moderator, 'create_post', "the post").should be_true
145
+ @permissions.may?(@moderator, 'destroy_post', "the post").should be_true
146
+ @permissions.may?(@moderator, 'index_posts').should be_true
147
+
148
+ end
149
+
150
+ it "should allow to configure generated resource actions" do
151
+
152
+ @permissions.class_eval do
153
+ resources :posts do
154
+ action :index do
155
+ allow :user
156
+ end
157
+ action :show do
158
+ allow :user
159
+ end
160
+ end
161
+ end
162
+
163
+ @permissions.may?(@user, 'update_post', "the post").should be_false
164
+ @permissions.may?(@user, 'show_post', "the post").should be_true
165
+ @permissions.may?(@user, 'create_post', "the post").should be_false
166
+ @permissions.may?(@user, 'destroy_post', "the post").should be_false
167
+ @permissions.may?(@user, 'index_posts').should be_true
168
+
169
+ @permissions.find_action_by_path('index_posts').takes_object.should be_false
170
+ @permissions.find_action_by_path('show_post').takes_object.should be_true
171
+
172
+ end
173
+
174
+ it "should raise an error if an action takes an object but does not get it in the arguments" do
175
+
176
+ @permissions.class_eval do
177
+ resources :posts do
178
+ allow :moderator
179
+ end
180
+ end
181
+
182
+ @permissions.may?(@moderator, 'update_post', "the post").should be_true
183
+
184
+ end
185
+
186
+ it "should evaluate sieves with blocks" do
187
+
188
+ @permissions.class_eval do
189
+ resources :posts do
190
+ allow :user do
191
+ user.name == 'Waldo'
192
+ end
193
+ end
194
+ end
195
+
196
+ frank = @user_class.new(:name => 'Frank', :role_name => 'user')
197
+ waldo = @user_class.new(:name => 'Waldo', :role_name => 'user')
198
+
199
+ frank.may_update_post?('the post').should be_false
200
+ waldo.may_update_post?('the post').should be_true
201
+
202
+ end
203
+
204
+ it "should evaluate singleton resources, which take no object" do
205
+
206
+ @permissions.class_eval do
207
+ resource :session do
208
+ allow :moderator
209
+ end
210
+ end
211
+
212
+ @permissions.may?(@moderator, 'update_session').should be_true
213
+ @permissions.may?(@moderator, 'show_session').should be_true
214
+ @permissions.may?(@moderator, 'create_session').should be_true
215
+ @permissions.may?(@moderator, 'destroy_session').should be_true
216
+
217
+ end
218
+
219
+ it "should allow to nest resources into collection resources" do
220
+
221
+ @permissions.class_eval do
222
+ resources :properties do
223
+ resources :comments do
224
+ allow :moderator
225
+ end
226
+ end
227
+ end
228
+
229
+ @permissions.may?(@moderator, 'update_property_comment', "the property", "the comment").should be_true
230
+ @permissions.may?(@moderator, 'show_property_comment', "the property", "the comment").should be_true
231
+ @permissions.may?(@moderator, 'create_property_comment', "the property").should be_true
232
+ @permissions.may?(@moderator, 'destroy_property_comment', "the property", "the comment").should be_true
233
+ @permissions.may?(@moderator, 'index_property_comments', "the property").should be_true
234
+
235
+ end
236
+
237
+ it "should allow to nest resources into singleton resources" do
238
+
239
+ @permissions.class_eval do
240
+ resource :account do
241
+ resources :bookings do
242
+ allow :moderator
243
+ end
244
+ end
245
+ end
246
+
247
+ @permissions.may?(@moderator, 'update_account_booking', "the booking").should be_true
248
+ @permissions.may?(@moderator, 'show_account_booking', "the booking").should be_true
249
+ @permissions.may?(@moderator, 'create_account_booking').should be_true
250
+ @permissions.may?(@moderator, 'destroy_account_booking', "the booking").should be_true
251
+ @permissions.may?(@moderator, 'index_account_bookings').should be_true
252
+
253
+ @permissions.find_action_by_path('update_account').should_not be_abstract
254
+
255
+ end
256
+
257
+ it "should support namespaces, which act like singleton resources but don't generate actions by default" do
258
+
259
+ @permissions.class_eval do
260
+ namespace :admin do
261
+ resources :bookings do
262
+ allow :moderator
263
+ end
264
+ end
265
+ end
266
+
267
+ @permissions.may?(@moderator, 'update_admin_booking', "the booking").should be_true
268
+ @permissions.may?(@moderator, 'show_admin_booking', "the booking").should be_true
269
+ @permissions.may?(@moderator, 'create_admin_booking').should be_true
270
+ @permissions.may?(@moderator, 'destroy_admin_booking', "the booking").should be_true
271
+ @permissions.may?(@moderator, 'index_admin_bookings').should be_true
272
+
273
+ @permissions.find_action_by_path('update_admin').should be_abstract
274
+
275
+ end
276
+
277
+ it "should allow multiple levels of resource-nesting" do
278
+
279
+ @permissions.class_eval do
280
+ resources :properties do
281
+ resources :reviews do
282
+ resources :comments do
283
+ allow :moderator
284
+ end
285
+ end
286
+ end
287
+ end
288
+
289
+ @permissions.may?(@moderator, 'update_property_review_comment', "the review", "the comment").should be_true
290
+ @permissions.may?(@moderator, 'show_property_review_comment', "the review", "the comment").should be_true
291
+ @permissions.may?(@moderator, 'create_property_review_comment', "the review").should be_true
292
+ @permissions.may?(@moderator, 'destroy_property_review_comment', "the review", "the comment").should be_true
293
+ @permissions.may?(@moderator, 'index_property_review_comments', "the review").should be_true
294
+
295
+ end
296
+
297
+ it "should raise an error if an action takes a parent object but does not get it in the arguments" do
298
+
299
+ @permissions.class_eval do
300
+ resources :properties do
301
+ resources :comments
302
+ end
303
+ end
304
+
305
+ lambda { @permissions.may?(@moderator, 'update_property_comment', "the comment") }.should raise_error(ArgumentError)
306
+
307
+ end
308
+
309
+ it "should allow sieves with blocks and arguments" do
310
+
311
+ @permissions.class_eval do
312
+ action :sign_in do
313
+ allow do |password|
314
+ user.password == password
315
+ end
316
+ end
317
+ end
318
+
319
+ @user.stub(:password => "secret")
320
+ @user.may_sign_in?("wrong_password").should be_false
321
+ @user.may_sign_in?("secret").should be_true
322
+
323
+ end
324
+
325
+ it "should provide the object and parent_object for a sieve block" do
326
+ spy = stub("spy")
327
+ @permissions.class_eval do
328
+ resources :properties do
329
+ resources :comments do
330
+ allow :moderator do |additional_argument|
331
+ spy.observe(parent_object, object, additional_argument)
332
+ end
333
+ end
334
+ end
335
+ end
336
+
337
+ spy.should_receive(:observe).with("the property", "the comment", "additional argument")
338
+ @moderator.may_update_property_comment?("the property", "the comment", "additional argument")
339
+ end
340
+
341
+ it "should evaluate additional resource actions" do
342
+
343
+ @permissions.class_eval do
344
+ resources :properties do
345
+ action :zoom_into do
346
+ allow :user
347
+ end
348
+ action :view_all, :collection => true do
349
+ allow :user
350
+ end
351
+ end
352
+ end
353
+
354
+ @permissions.may?(@user, "zoom_into_property", "the property").should be_true
355
+ @permissions.may?(@user, "view_all_properties", "the property").should be_true
356
+
357
+ end
358
+
359
+ it "should allow rules that only affect reading actions" do
360
+
361
+ @permissions.class_eval do
362
+ resources :posts do
363
+ reading do
364
+ allow :user
365
+ end
366
+ action :syndicate, :writing => false
367
+ action :close
368
+ end
369
+ end
370
+
371
+ @permissions.may?(@user, 'update_post', "the post").should be_false
372
+ @permissions.may?(@user, 'show_post', "the post").should be_true
373
+ @permissions.may?(@user, 'create_post', "the post").should be_false
374
+ @permissions.may?(@user, 'destroy_post', "the post").should be_false
375
+ @permissions.may?(@user, 'index_posts').should be_true
376
+ @permissions.may?(@user, 'syndicate_post', "the post").should be_true
377
+ @permissions.may?(@user, 'close_post", "the post').should be_false
378
+
379
+ end
380
+
381
+ it "should allow rules that only affect writing actions" do
382
+
383
+ @permissions.class_eval do
384
+ resources :posts do
385
+ writing do
386
+ allow :moderator
387
+ end
388
+ action :syndicate, :writing => false
389
+ action :close
390
+ end
391
+ end
392
+
393
+ # debugger
394
+
395
+ @permissions.may?(@moderator, 'update_post', "the post").should be_true
396
+ @permissions.may?(@moderator, 'show_post', "the post").should be_false
397
+ @permissions.may?(@moderator, 'create_post', "the post").should be_true
398
+ @permissions.may?(@moderator, 'destroy_post', "the post").should be_true
399
+ @permissions.may?(@moderator, 'index_posts').should be_false
400
+ @permissions.may?(@moderator, 'syndicate_post', "the post").should be_false
401
+ @permissions.may?(@moderator, "close_post", "the post").should be_true
402
+
403
+ end
404
+
405
+ it "should allow resources with only selected actions" do
406
+ @permissions.class_eval do
407
+ resources :posts, :only => [:show, :update]
408
+ end
409
+ @permissions.find_action_by_path('update_post').should_not be_abstract
410
+ @permissions.find_action_by_path('show_post').should_not be_abstract
411
+ @permissions.find_action_by_path('create_post').should be_abstract
412
+ @permissions.find_action_by_path('destroy_post').should be_abstract
413
+ @permissions.find_action_by_path('index_posts').should be_abstract
414
+ end
415
+
416
+ it "should allow resources with all actions except a selected few" do
417
+ @permissions.class_eval do
418
+ resources :posts, :except => [:show, :update]
419
+ end
420
+ @permissions.find_action_by_path('update_post').should be_abstract
421
+ @permissions.find_action_by_path('show_post').should be_abstract
422
+ @permissions.find_action_by_path('create_post').should_not be_abstract
423
+ @permissions.find_action_by_path('destroy_post').should_not be_abstract
424
+ @permissions.find_action_by_path('index_posts').should_not be_abstract
425
+ end
426
+
427
+
428
+ it "should alias action names for all actions and resources, aliasing #new and #edit by default" do
429
+
430
+ @permissions.class_eval do
431
+
432
+ alias_action :delete => :destroy
433
+
434
+ resources :properties do
435
+ resources :comments
436
+ end
437
+ end
438
+
439
+ @permissions.find_action_by_path('delete_property').should_not be_abstract
440
+ @permissions.find_action_by_path('new_property').should_not be_abstract
441
+ @permissions.find_action_by_path('edit_property').should_not be_abstract
442
+
443
+ @permissions.find_action_by_path('delete_property_comment').should_not be_abstract
444
+ @permissions.find_action_by_path('new_property_comment').should_not be_abstract
445
+ @permissions.find_action_by_path('edit_property_comment').should_not be_abstract
446
+
447
+ end
448
+ end
449
+
450
+ describe 'may!' do
451
+
452
+ it "should return if permission is granted" do
453
+ lambda { @permissions.may!(@admin, :delete_everything) }.should_not raise_error
454
+ end
455
+
456
+ it "should raise an error if permission is denied" do
457
+ lambda { @permissions.may!(@user, :delete_everything) }.should raise_error(Aegis::AccessDenied)
458
+ end
459
+
460
+ end
461
+
462
+ describe 'behavior when checking permissions without a user' do
463
+
464
+ it "should raise an error if the user is nil" do
465
+ lambda { @permissions.may?(nil, :some_action) }.should raise_error
466
+ end
467
+
468
+ it "should substitute the results from the blank user strategy" do
469
+ @permissions.class_eval do
470
+ missing_user_means { User.new(:role_name => 'user') }
471
+ action :create_post do
472
+ allow :moderator
473
+ end
474
+ action :show_post do
475
+ allow :user
476
+ end
477
+ end
478
+ @permissions.may?(nil, :create_post).should be_false
479
+ @permissions.may?(nil, :show_post).should be_true
480
+ end
481
+
482
+ end
483
+
484
+ describe 'behavior when a permission is not defined' do
485
+
486
+ it "should use the default permission if the strategy is :default_permission" do
487
+ @permissions.class_eval do
488
+ missing_action_means :default_permission
489
+ end
490
+ @user.may_missing_action?.should be_false
491
+ @admin.may_missing_action?.should be_true
492
+ end
493
+
494
+ it "should grant everyone access if the strategy is :allow" do
495
+ @permissions.class_eval do
496
+ missing_action_means :allow
497
+ end
498
+ @user.may_missing_action?.should be_true
499
+ @admin.may_missing_action?.should be_true
500
+ end
501
+
502
+ it "should deny everyone access if the strategy is :deny" do
503
+ @permissions.class_eval do
504
+ missing_action_means :deny
505
+ end
506
+ @user.may_missing_action?.should be_false
507
+ @admin.may_missing_action?.should be_false
508
+ end
509
+
510
+ it "should raise an error if the strategy is :error" do
511
+ @permissions.class_eval do
512
+ missing_action_means :error
513
+ end
514
+ lambda { @user.may_missing_action? }.should raise_error
515
+ lambda { @admin.may_missing_action? }.should raise_error
516
+ end
517
+
518
+ end
519
+
520
+ describe 'guess_action' do
521
+
522
+ it "should guess an action based on the given resource and action name, trying both singular and plural" do
523
+
524
+ @permissions.class_eval do
525
+ resources :posts
526
+ end
527
+
528
+ @permissions.guess_action(:posts, :index).should_not be_abstract
529
+ @permissions.guess_action(:posts, :update).should_not be_abstract
530
+ @permissions.guess_action(:posts, :unknown_action).should be_abstract
531
+
532
+ end
533
+
534
+ it "should consult the actions map first and use the the default behaviour for unmapped actions" do
535
+
536
+ @permissions.class_eval do
537
+ resources :posts, :only => [:update] do
538
+ action :view_all, :collection => true
539
+ end
540
+ end
541
+
542
+ @permissions.guess_action(:posts, 'index', 'index' => 'view_all_posts').should_not be_abstract
543
+ @permissions.guess_action(:posts, 'update', 'index' => 'view_all_posts').should_not be_abstract
544
+
545
+ end
546
+
547
+ end
548
+
549
+ end
550
+