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,39 @@
1
+ module Aegis
2
+ class Sieve
3
+
4
+ def initialize(role_name, effect, block = nil)
5
+ role_name = 'everyone' if role_name.blank?
6
+ @role_name = role_name.to_s
7
+ @effect = effect
8
+ @block = block
9
+ end
10
+
11
+ def may?(context, *args)
12
+ matches_role = @role_name == 'everyone' || @role_name == context.user.role_name
13
+ if matches_role
14
+ if @block
15
+ block_result = context.instance_exec(*args, &@block)
16
+ block_result ? @effect : !@effect
17
+ else
18
+ @effect
19
+ end
20
+ else
21
+ nil
22
+ end
23
+ end
24
+
25
+ def inspect
26
+ "Sieve(#{{:role_name => @role_name, :effect => @effect ? :allow : :deny, :block => @block.present?}.inspect})"
27
+ end
28
+
29
+ def self.allow_to_all
30
+ new('everyone', true)
31
+ end
32
+
33
+ def self.deny_to_all
34
+ new('everyone', false)
35
+ end
36
+
37
+ end
38
+
39
+ end
@@ -0,0 +1,38 @@
1
+ module Aegis
2
+ module ActionController
3
+
4
+ def permissions(resource, options = {})
5
+
6
+ before_filter :check_permissions, options.slice(:except, :only)
7
+
8
+ instance_eval do
9
+
10
+ private
11
+
12
+ actions_map = (options[:map] || {}).stringify_keys
13
+ object_method = options[:object] || :object
14
+ parent_object_method = options[:parent_object] || :parent_object
15
+ user_method = options[:user] || :current_user
16
+ permissions = lambda { Aegis::Permissions.app_permissions(options[:permissions]) }
17
+
18
+ define_method :check_permissions do
19
+ action = permissions.call.guess_action(
20
+ resource,
21
+ action_name.to_s,
22
+ actions_map
23
+ )
24
+ args = []
25
+ args << send(user_method)
26
+ args << send(parent_object_method) if action.takes_parent_object
27
+ args << send(object_method) if action.takes_object
28
+ action.may!(*args)
29
+ end
30
+
31
+ end
32
+
33
+ end
34
+
35
+ end
36
+ end
37
+
38
+ ActionController::Base.extend(Aegis::ActionController)
@@ -1,5 +1 @@
1
- ActiveRecord::Base.class_eval do
2
-
3
- extend Aegis::HasRole
4
-
5
- end
1
+ ActiveRecord::Base.extend(Aegis::HasRole)
@@ -0,0 +1,100 @@
1
+ require File.dirname(__FILE__) + "/spec_helper"
2
+
3
+ describe 'Aegis::ActionController' do
4
+
5
+ before(:each) do
6
+
7
+ permissions_class = @permissions_class = Class.new(Aegis::Permissions) do
8
+ role :user
9
+ resources :posts do
10
+ reading do
11
+ allow :user
12
+ end
13
+ end
14
+ end
15
+
16
+ @user_class = Class.new(ActiveRecord::Base) do
17
+ set_table_name 'users'
18
+ has_role :permissions => permissions_class
19
+ end
20
+
21
+ user = @user = @user_class.new(:role_name => 'user')
22
+
23
+ @controller_class = Class.new(ActionController::Base) do
24
+ define_method :current_user do
25
+ user
26
+ end
27
+ end
28
+
29
+ end
30
+
31
+ describe 'permissions' do
32
+
33
+ it "should fetch the context through #object, #parent_object and #current_user by default" do
34
+ permissions_class = @permissions_class
35
+ @controller_class.class_eval do
36
+ permissions :posts, :permissions => permissions_class
37
+ end
38
+ controller = @controller_class.new
39
+ permissions_class.stub(:guess_action => stub('action', :takes_object => true, :takes_parent_object => true).as_null_object)
40
+ controller.should_receive(:object)
41
+ controller.should_receive(:parent_object)
42
+ controller.should_receive(:current_user)
43
+ controller.send(:check_permissions)
44
+ end
45
+
46
+ it "should allow custom readers for object, parent object and the current user" do
47
+ permissions_class = @permissions_class
48
+ @controller_class.class_eval do
49
+ permissions :posts, :permissions => permissions_class, :user => :my_user, :object => :my_object, :parent_object => :my_parent
50
+ end
51
+ controller = @controller_class.new
52
+ permissions_class.stub(:guess_action => stub('action', :takes_object => true, :takes_parent_object => true).as_null_object)
53
+ controller.should_receive(:my_object)
54
+ controller.should_receive(:my_parent)
55
+ controller.should_receive(:my_user)
56
+ controller.send(:check_permissions)
57
+ end
58
+
59
+ it 'should install a before_filter that checks permissions' do
60
+ @controller_class.should_receive(:before_filter).with(:check_permissions, :only => [:update])
61
+ permissions_class = @permissions_class
62
+ @controller_class.class_eval do
63
+ permissions :posts, :permissions => permissions_class, :only => [:update]
64
+ end
65
+ end
66
+
67
+ end
68
+
69
+ describe 'check_permissions' do
70
+
71
+ before(:each) do
72
+ permissions_class = @permissions_class
73
+ map = @map = { 'controller_action' => 'permission_path' }
74
+ @controller_class.class_eval do
75
+ permissions :posts, :permissions => permissions_class, :map => map
76
+ end
77
+ @controller = @controller_class.new
78
+ @controller.stub(:object => "object", :parent_object => "parent object")
79
+ end
80
+
81
+ it "should guess the aegis-action using the current resource, action name and actions-map" do
82
+ @controller.stub(:action_name => 'update')
83
+ action = stub("action").as_null_object
84
+ @permissions_class.should_receive(:guess_action).with(:posts, 'update', @map).and_return(action)
85
+ @controller.send(:check_permissions)
86
+ end
87
+
88
+ it "should raise an error if permission is denied" do
89
+ @controller.stub(:action_name => 'update')
90
+ lambda { @controller.send(:check_permissions) }.should raise_error(Aegis::AccessDenied)
91
+ end
92
+
93
+ it "should pass silently if permission is granted" do
94
+ @controller.stub(:action_name => 'show')
95
+ lambda { @controller.send(:check_permissions) }.should_not raise_error
96
+ end
97
+
98
+ end
99
+
100
+ end
@@ -0,0 +1,7 @@
1
+ class ApplicationController < ActionController::Base
2
+
3
+ def current_user
4
+ User.new(:role_name => 'user')
5
+ end
6
+
7
+ end
@@ -0,0 +1,36 @@
1
+ class ReviewsController < ApplicationController
2
+
3
+ permissions :property_reviews
4
+
5
+ def show
6
+ end
7
+
8
+ def edit
9
+ end
10
+
11
+ def update
12
+ end
13
+
14
+ def new
15
+ end
16
+
17
+ def create
18
+ end
19
+
20
+ def destroy
21
+ end
22
+
23
+ def index
24
+ end
25
+
26
+ private
27
+
28
+ def object
29
+ @oject ||= parent_object.reviews.find(params[:id])
30
+ end
31
+
32
+ def parent_object
33
+ @parent_object ||= Property.find(params[:id])
34
+ end
35
+
36
+ end
@@ -0,0 +1,14 @@
1
+ class Permissions < Aegis::Permissions
2
+
3
+ role :user
4
+
5
+ resources :properties do
6
+ resources :reviews do
7
+ reading do
8
+ allow :user
9
+ end
10
+ end
11
+ end
12
+
13
+ end
14
+
@@ -0,0 +1,5 @@
1
+ class Property < ActiveRecord::Base
2
+
3
+ has_many :reviews
4
+
5
+ end
@@ -0,0 +1,5 @@
1
+ class Review < ActiveRecord::Base
2
+
3
+ belongs_to :property
4
+
5
+ end
@@ -1,6 +1,5 @@
1
1
  class User < ActiveRecord::Base
2
2
 
3
3
  has_role
4
- validates_role_name
5
4
 
6
- end
5
+ end
File without changes
File without changes
File without changes
@@ -0,0 +1,7 @@
1
+ ActionController::Routing::Routes.draw do |map|
2
+
3
+ map.resources :properties do |properties|
4
+ properties.resources :reviews
5
+ end
6
+
7
+ end
@@ -3,6 +3,7 @@ class CreateUsers < ActiveRecord::Migration
3
3
  def self.up
4
4
  create_table :users do |t|
5
5
  t.string :role_name
6
+ t.string :name
6
7
  t.timestamps
7
8
  end
8
9
  end
@@ -10,5 +11,5 @@ class CreateUsers < ActiveRecord::Migration
10
11
  def self.down
11
12
  drop_table :users
12
13
  end
13
-
14
+
14
15
  end
@@ -0,0 +1,13 @@
1
+ class CreateProperties < ActiveRecord::Migration
2
+
3
+ def self.up
4
+ create_table :properties do |t|
5
+ t.timestamps
6
+ end
7
+ end
8
+
9
+ def self.down
10
+ drop_table :properties
11
+ end
12
+
13
+ end
@@ -0,0 +1,14 @@
1
+ class CreateReviews < ActiveRecord::Migration
2
+
3
+ def self.up
4
+ create_table :reviews do |t|
5
+ t.integer :property_id
6
+ t.timestamps
7
+ end
8
+ end
9
+
10
+ def self.down
11
+ drop_table :reviews
12
+ end
13
+
14
+ end
File without changes
File without changes
@@ -0,0 +1,19 @@
1
+ require File.dirname(__FILE__) + '/../spec_helper'
2
+
3
+ describe ReviewsController do
4
+
5
+ before(:each) do
6
+ Property.stub(:find => stub("a property", :reviews => stub("reviews", :find => "a review")))
7
+ end
8
+
9
+ it "should grant access in a fully integrated scenario" do
10
+ lambda { get :show, :property_id => '10', :id => '20' }.should_not raise_error
11
+ lambda { get :index, :property_id => '10' }.should_not raise_error
12
+ end
13
+
14
+ it "should deny access in a fully integrated scenario" do
15
+ lambda { put :update, :property_id => '10', :id => '20' }.should raise_error(Aegis::AccessDenied)
16
+ lambda { get :new, :property_id => '10' }.should raise_error(Aegis::AccessDenied)
17
+ end
18
+
19
+ end
@@ -0,0 +1,177 @@
1
+ require File.dirname(__FILE__) + "/spec_helper"
2
+
3
+ describe Aegis::HasRole do
4
+
5
+ before(:each) do
6
+
7
+ @permissions_class = permissions_class = Class.new(Aegis::Permissions) do
8
+ role :user
9
+ role :admin
10
+ end
11
+
12
+ @user_class = Class.new(ActiveRecord::Base) do
13
+ set_table_name 'users'
14
+ has_role :permissions => permissions_class
15
+ end
16
+
17
+ end
18
+
19
+ describe 'has_role' do
20
+
21
+ it "should define accessors for the role association" do
22
+ user = @user_class.new
23
+ user.should respond_to(:role)
24
+ user.should respond_to(:role=)
25
+ end
26
+
27
+ end
28
+
29
+ describe 'role' do
30
+
31
+ it "should be nil by default" do
32
+ user = @user_class.new
33
+ user.role.should be_nil
34
+ end
35
+
36
+ it "should return the role corresponding to the role_name" do
37
+ user = @user_class.new(:role_name => 'admin')
38
+ user.role.name.should == 'admin'
39
+ end
40
+
41
+ it "should be nil if the role_name doesn't match a known role" do
42
+ user = @user_class.new(:role_name => 'nonexisting_role_name')
43
+ user.role.should be_nil
44
+ end
45
+
46
+ it "should read the role name from a custom reader" do
47
+ permissions_class = @permissions_class
48
+ @user_class.class_eval { has_role :reader => "role_handle", :permissions => permissions_class }
49
+ user = @user_class.new
50
+ user.should_receive(:role_handle).and_return('admin')
51
+ user.role
52
+ end
53
+
54
+ it "should read the role name from a custom accessor" do
55
+ permissions_class = @permissions_class
56
+ @user_class.class_eval { has_role :accessor => "role_handle", :permissions => permissions_class }
57
+ user = @user_class.new
58
+ user.should_receive(:role_handle).and_return('admin')
59
+ user.role
60
+ end
61
+
62
+ it "should take a default role" do
63
+ permissions_class = @permissions_class
64
+ @user_class.class_eval { has_role :default => "admin", :permissions => permissions_class }
65
+ user = @user_class.new
66
+ user.role.name.should == 'admin'
67
+ end
68
+
69
+ end
70
+
71
+ describe 'role=' do
72
+
73
+ before :each do
74
+ @admin_role = @permissions_class.find_role_by_name('admin')
75
+ end
76
+
77
+ it "should write the role name to the role_name attribute" do
78
+ user = @user_class.new
79
+ user.should_receive(:role_name=).with('admin')
80
+ user.role = @admin_role
81
+ end
82
+
83
+ it "should write the role name to a custom writer" do
84
+ permissions_class = @permissions_class
85
+ @user_class.class_eval { has_role :writer => "role_handle=", :permissions => permissions_class }
86
+ user = @user_class.new
87
+ user.should_receive(:role_handle=).with('admin')
88
+ user.role = @admin_role
89
+ end
90
+
91
+ it "should write the role name to a custom accessor" do
92
+ permissions_class = @permissions_class
93
+ @user_class.class_eval { has_role :accessor => "role_handle", :permissions => permissions_class }
94
+ user = @user_class.new
95
+ user.should_receive(:role_handle=).with('admin')
96
+ user.role = @admin_role
97
+ end
98
+
99
+ end
100
+
101
+ describe 'method_missing' do
102
+
103
+ it "should delegate may...? messages to the permissions class" do
104
+ user = @user_class.new
105
+ @permissions_class.should_receive(:may?).with(user, 'do_action', 'argument')
106
+ user.may_do_action?('argument')
107
+ end
108
+
109
+ it "should delegate may...! messages to the permissions class" do
110
+ user = @user_class.new
111
+ @permissions_class.should_receive(:may!).with(user, 'do_action', 'argument')
112
+ user.may_do_action!('argument')
113
+ end
114
+
115
+ it "should retain its usual behaviour for non-permission methods" do
116
+ user = @user_class.new
117
+ lambda { user.nonexisting_method }.should raise_error(NoMethodError)
118
+ end
119
+
120
+ end
121
+
122
+ describe 'respond_to?' do
123
+
124
+ it "should be true for all permission methods, even if they are not explicitely defined" do
125
+ user = @user_class.new
126
+ user.should respond_to(:may_foo?)
127
+ user.should respond_to(:may_foo!)
128
+ end
129
+
130
+ it "should retain its usual behaviour for non-permission methods" do
131
+ user = @user_class.new
132
+ user.should respond_to(:to_s)
133
+ user.should_not respond_to(:nonexisting_method)
134
+ end
135
+
136
+ end
137
+
138
+ describe 'validates_role' do
139
+
140
+ before(:each) do
141
+ @user_class.class_eval { validates_role }
142
+ end
143
+
144
+ it "should run the validation callback :validate_role" do
145
+ user = @user_class.new
146
+ user.should_receive(:validate_role)
147
+ user.run_callbacks(:validate)
148
+ end
149
+
150
+ it "should add an inclusion error to the role name if the role name is blank" do
151
+ user = @user_class.new(:role_name => '')
152
+ user.errors.should_receive(:add).with(:role_name, I18n.translate('activerecord.errors.messages.inclusion'))
153
+ user.send(:validate_role)
154
+ end
155
+
156
+ it "should add an inclusion error to the role name if the role name doesn't match a role" do
157
+ user = @user_class.new(:role_name => 'nonexisting_role')
158
+ user.errors.should_receive(:add).with(:role_name, I18n.translate('activerecord.errors.messages.inclusion'))
159
+ user.send(:validate_role)
160
+ end
161
+
162
+ it "should add no error if the role name matches a role" do
163
+ user = @user_class.new(:role_name => 'admin')
164
+ user.errors.should_not_receive(:add)
165
+ user.send(:validate_role)
166
+ end
167
+
168
+ it "should allow a custom error message with the :message options" do
169
+ @user_class.class_eval { validates_role :message => "custom message" }
170
+ user = @user_class.new(:role_name => '')
171
+ user.errors.should_receive(:add).with(:role_name, 'custom message')
172
+ user.send(:validate_role)
173
+ end
174
+
175
+ end
176
+
177
+ end