permissive 0.0.1 → 0.2.0.alpha

Sign up to get free protection for your applications and to get access to all the features.
data/README.markdown.html DELETED
@@ -1,191 +0,0 @@
1
- <h1>Permissive gives your ActiveRecord models granular permission support</h1>
2
-
3
- <p>Permissive combines a model-based permissions system with bitmasking to
4
- create a flexible approach to maintaining permissions on your ActiveRecord
5
- models. It supports an easy-to-use set of methods for accessing and
6
- determining permissions, including some fun metaprogramming.</p>
7
-
8
- <h2>Installation</h2>
9
-
10
- <ol>
11
- <li><p>Get yourself some code. You can install as a gem:</p>
12
-
13
- <p><code>gem install permissive</code></p>
14
-
15
- <p>or as a plugin:</p>
16
-
17
- <p><code>script/plugin install git://github.com/flipsasser/permissive.git</code></p></li>
18
- <li><p>Generate a migration so you can get some sweet table action:</p>
19
-
20
- <p><code>script/generate permissive_migration</code></p>
21
-
22
- <p><code>rake db:migrate</code></p></li>
23
- </ol>
24
-
25
-
26
- <h2>Usage</h2>
27
-
28
- <p>First, define a few permissions constants. We'll define them in <code>Rails.root/config/initializers/permissive.rb</code>. The best practice is to name them in a verb format that follows this pattern: "Object can <code>DO_PERMISSION_NAME</code>".</p>
29
-
30
- <p>Permission constants need to be int values counting up from zero. We use ints because Permissive uses bit masking to keep permissions data compact and performant.</p>
31
-
32
- <pre><code>module Permissive::Permissions
33
- MANAGE_GAMES = 0
34
- CONTROL_RIDES = 1
35
- PUNCH = 2
36
- end
37
- </code></pre>
38
-
39
- <p>And that's all it takes to configure permissions! Now that we have them, let's grant them to a model or two:</p>
40
-
41
- <pre><code>class Employee &lt; ActiveRecord::Base
42
- acts_as_permissive
43
- validates_presence_of :first_name, :last_name
44
- end
45
-
46
- class Company &lt; ActiveRecord::Base
47
- validates_presence_of :name
48
- end
49
- </code></pre>
50
-
51
- <p>Easy-peasy, right? Let's try granting a few permissions:</p>
52
-
53
- <pre><code>@james = Employee.create(:first_name =&gt; 'James', :last_name =&gt; 'Brennan')
54
- @frigo = Employee.create(:first_name =&gt; 'Tommy', :last_name =&gt; 'Frigo')
55
- @adventureland = Company.create(:name =&gt; 'Adventureland')
56
-
57
- # Okay, let's do some granting. We'll start by scoping to a specific company.
58
- @james.can!(:manage_games, :on =&gt; @adventureland)
59
-
60
- # Now let's do some permission checking.
61
- @james.can?(:manage_games, :on =&gt; @adventureland) #=&gt; true
62
-
63
- # We can also use the metaprogramming syntax:
64
- @james.can_manage_games_on?(@adventureland) #=&gt; true
65
- @james.can_control_rides_on?(@adventureland) #=&gt; false
66
-
67
- # We can check for multiple permissions, too:
68
- @james.can?(:manage_games, :control_rides) #=&gt; false
69
- # OR:
70
- @james.can_manage_games_and_control_rides?
71
-
72
- # Scoping can be done through any object
73
- @frigo.can!(:punch, :on =&gt; @james)
74
- @frigo.can_punch_on?(@james) #=&gt; true
75
-
76
- # And the permissions aren't reciprocal
77
- @james.can_punch_on?(@frigo) #=&gt; false
78
-
79
- # Of course, we can grant global (non-scoped) permissions, too:
80
- @frigo.can!(:control_rides)
81
- @frigo.can_control_rides? #=&gt; true
82
-
83
- # BUT! Global permissions don't override scoped permissions.
84
- @frigo.can_control_rides_on?(@adventureland) #=&gt; false
85
-
86
- # Likewise, scoped permissions don't bubble up globally:
87
- @james.can_manage_games? #=&gt; false
88
-
89
- # And, last but not least, let's take all of those great permissions away:
90
- @james.revoke(:manage_games, :on =&gt; @adventureland)
91
-
92
- # We can revoke all permissions, in any scope, too:
93
- @frigo.revoke(:all)
94
- </code></pre>
95
-
96
- <p>And that's it!</p>
97
-
98
- <h2>Scoping</h2>
99
-
100
- <p>Permissive supports scoping at the class-configuration level, which adds relationships to permitted objects:</p>
101
-
102
- <pre><code>class Employee &lt; ActiveRecord::Base
103
- acts_as_permissive :scope =&gt; :company
104
- end
105
-
106
- @frigo.permissive_companies #=&gt; [Company 1, Company 2]
107
- </code></pre>
108
-
109
- <h2>Replacing Permissions</h2>
110
-
111
- <p>Sometimes you want to overwrite all previous permissions in a can! method. That's pretty easy: just add :reset => true to the options.</p>
112
-
113
- <pre><code>@frigo.can!(:control_rides, :on =&gt; @adventureland, :reset =&gt; true)
114
- </code></pre>
115
-
116
- <h2>Next Steps</h2>
117
-
118
- <p>There's a number of things I want to add to the permissive settings. At the moment, Permissive currently support scoping at the class level, BUT all it really does is add a <code>has_many</code> relationship. <code>@employee.can!(:do_anything)</code> will still work, as will <code>@employee.can!(:do_something, :on =&gt; @something_that_isnt_a_company)</code>. That's pretty confusing to me. Adding more granular permissions might be cooler:</p>
119
-
120
- <pre><code>class Employee &lt; ActiveRecord::Base
121
- has_permissions do
122
- on :companies
123
- on :employees
124
- end
125
- end
126
- </code></pre>
127
-
128
- <p>which might yield something like</p>
129
-
130
- <pre><code>@employee.permissive_companies
131
- # and
132
- @employee.can_control_rides_in_company @adventureland
133
- </code></pre>
134
-
135
- <p>I'd also like to support a more intelligent grammar:</p>
136
-
137
- <pre><code>@james.can_punch? @frigo
138
- @frigo.can!(:control_rides, :in =&gt; @adventureland)
139
- </code></pre>
140
-
141
- <p>Meta-programmed methods for granting and revoking would be cool, too:</p>
142
-
143
- <pre><code>@james.can_punch! @frigo
144
- @frigo.cannot_control_rides_in! @adventureland
145
- </code></pre>
146
-
147
- <p>And while we're on the subject of metaprogramming, let's add some OR-ing to the whole thing:</p>
148
-
149
- <pre><code>@james.can_control_rides_or_manage_games_in? @adventureland
150
- </code></pre>
151
-
152
- <p>I'd also like to enable Permissive::Templates (pre-set permission groups, like roles):</p>
153
-
154
- <pre><code>administrator = Permissive::Template.named('Administrator')
155
- @james.acts_like administrator
156
- </code></pre>
157
-
158
- <p>Next up! I currently use a manual reset to grant permissions through a controller. It would by great to DRY this stuff up and provide some decent path for moving permissions into HTML forms. Right now, it looks something like this:</p>
159
-
160
- <pre><code>&lt;%= check_box_tag("employee[permissions][]", Permissive::Permissions::CONTROL_RIDES, @employee.can_control_rides?) %&gt; Control rides
161
- </code></pre>
162
-
163
- <p>.. and in the controller:</p>
164
-
165
- <pre><code>def update
166
- @employee.can!(params[:employees].delete(:permissions), :revert =&gt; true)
167
- respond_to do |format|
168
- ...
169
- end
170
- end
171
- </code></pre>
172
-
173
- <p>Finally, I'd like to use the <code>grant_mask</code> support that exists on the Permissive::Permission model to control what people can or cannot allow others to do. This would necessitate one of two things - first, a quick way of iterating over a person's granting permissions, e.g.:</p>
174
-
175
- <pre><code>&lt;% current_user.grant_permissions.each do |permission| %&gt;
176
- &lt;!-- Do something! --&gt;
177
- &lt;% end %&gt;
178
- </code></pre>
179
-
180
- <p>and second, write-time checking of grantor permissions. Something like this, maybe:</p>
181
-
182
- <pre><code>def update
183
- current_user.grant(params[:employees][:permissions], :to =&gt; @employee)
184
- end
185
- </code></pre>
186
-
187
- <p>which would allow the Permissive::Permission model to make sure whatever <code>current_user</code> is granting to @employee, they're <strong>allowed</strong> to grant to @employee.</p>
188
-
189
- <p>And that's it! Like all of my projects, I extracted it from some live development - which means it, too, is still in development. So please feel free to contribute!</p>
190
-
191
- <p>Copyright (c) 2009 Flip Sasser, released under the MIT license</p>
@@ -1,134 +0,0 @@
1
- module Permissive
2
- module ActsAsPermissive
3
- def self.included(base)
4
- base.class_eval do
5
- # This is the core of the Permissive module. It allows you to define a
6
- # permissive model structure complete with :scope. This will dynamically
7
- # generate scoped, polymorphic relationships across one or more models.
8
- def self.acts_as_permissive(options = {})
9
- options.assert_valid_keys(:scope)
10
- has_many :permissions, :class_name => 'Permissive::Permission', :as => :permitted_object do
11
- def can!(*args)
12
- options = args.last.is_a?(Hash) ? args.pop : {}
13
- options.assert_valid_keys(:on, :reset)
14
- if options[:on]
15
- permission = proxy_owner.permissions.find_or_initialize_by_scoped_object_id_and_scoped_object_type(options[:on].id, options[:on].class.to_s)
16
- else
17
- permission = Permissive::Permission.find_or_initialize_by_permitted_object_id_and_permitted_object_type(proxy_owner.id, proxy_owner.class.to_s)
18
- end
19
- if options[:reset]
20
- permission.mask = 0
21
- permission.grant_mask = 0
22
- end
23
- args.flatten.each do |name|
24
- bit = bit_for(name)
25
- unless permission.mask & bit != 0
26
- permission.mask = permission.mask | bit
27
- end
28
- end
29
- permission.save!
30
- end
31
-
32
- def can?(*args)
33
- options = args.last.is_a?(Hash) ? args.pop : {}
34
- bits = args.map{|name| bit_for(name) }
35
- # scope = nil
36
- # if options[:on]
37
- # scope = scoped(:conditions => ['scoped_object_id = ? AND scoped_object_type = ?', options[:on].id, options[:on].class.to_s])
38
- # else
39
- # scope = scoped(:conditions => ['scoped_object_id IS NULL AND scoped_object_type IS NULL'])
40
- # end
41
- # Skip the trip to the database if the proxy has been loaded up already...
42
- # TODO: Fix this per-scope ... somehow ... probably beyond the scope of this project.
43
- # if @loaded
44
- # bits.all?{|bit| self.select{|permission| permission.mask & bit != 0} }
45
- # else
46
- on(options[:on]).count(:conditions => [bits.map { 'permissive_permissions.mask & ?' }.join(' AND '), *bits]) > 0
47
- # end
48
- end
49
-
50
- def revoke(*args)
51
- options = args.last.is_a?(Hash) ? args.pop : {}
52
- if args.length == 1 && args.first == :all
53
- on(options[:on]).destroy_all
54
- else
55
- bits = args.map{|name| bit_for(name) }
56
- on(options[:on]).each do |permission|
57
- bits.each do |bit|
58
- if permission.mask & bit
59
- permission.mask = permission.mask ^ bit
60
- end
61
- end
62
- permission.save!
63
- end
64
- end
65
- end
66
- end
67
-
68
- if options[:scope]
69
- scope_name = "permissive_#{options[:scope].to_s}"
70
- unless reflection = reflect_on_association(scope_name)
71
- # TODO: There's just no way this should be working. It's WAY too
72
- # fragile. We need support for something more intelligent here,
73
- # like an options hash that includes :scope_type.
74
- namespace = self.to_s.split('::')
75
- if namespace.length > 1
76
- namespace.pop
77
- class_name = namespace.join('::')
78
- else
79
- class_name = ''
80
- end
81
- class_name << "::#{options[:scope].to_s.classify}"
82
- has_many scope_name, :through => :permissions, :source => :scoped_object, :source_type => class_name
83
- end
84
- end
85
-
86
- class_eval do
87
- # Pass calls to the instance down to its permissions collection
88
- # e.g. current_user.can(:view_comments) will bubble to
89
- # current_user.permissions.can(:view_comments)
90
- def can!(*args)
91
- permissions.can!(*args)
92
- end
93
-
94
- # Pass calls to the instance down to its permissions collection
95
- # e.g. current_user.can(:view_comments) will bubble to
96
- # current_user.permissions.can(:view_comments)
97
- def can?(*args)
98
- permissions.can?(*args)
99
- end
100
-
101
- def revoke(*args)
102
- permissions.revoke(*args)
103
- end
104
-
105
- def method_missing(method, *args)
106
- if method.to_s =~ /^can_([^\?]+)\?$/
107
- permissions = $1
108
- options = {}
109
- if permissions =~ /_on$/
110
- permissions.chomp!('_on')
111
- options[:on] = args.shift
112
- end
113
- permissions = permissions.split('_and_')
114
- if permissions.all? {|permission| Permissive::Permissions.hash.has_key?(permission.downcase.to_sym) }
115
- class_eval <<-end_eval
116
- def #{method}#{"(scope)" if options[:on]}
117
- can?(#{[permissions, args].flatten.join(', ').inspect}#{", :on => scope" if options[:on]})
118
- end
119
- end_eval
120
- return can?(*[permissions, options].flatten)
121
- end
122
- end
123
- super
124
- end
125
- end
126
- end
127
- end
128
- end
129
- end
130
- end
131
-
132
- if defined?(ActiveRecord::Base)
133
- ActiveRecord::Base.send :include, Permissive::ActsAsPermissive
134
- end
@@ -1,29 +0,0 @@
1
- # TODO: Abstract this module more later
2
- module Permissive
3
- class PermissionError < StandardError; end;
4
-
5
- module Permissions
6
- class << self
7
- def const_set(*args)
8
- @@hash = nil
9
- super
10
- end
11
-
12
- def hash
13
- @@hash ||= begin
14
- bitwise_hash = constants.inject({}) do |hash, constant_name|
15
- hash[constant_name.downcase] = 2 ** Permissive::Permissions.const_get(constant_name.to_sym)
16
- hash
17
- end
18
- inverted_hash = bitwise_hash.invert
19
- bitwise_hash.values.sort.inject(ActiveSupport::OrderedHash.new) do |hash, value|
20
- hash[inverted_hash[value].to_sym] = value
21
- hash
22
- end
23
- rescue ArgumentError
24
- raise Permissive::PermissionError.new("Permissions must be integers or longs. Strings, symbols, and floats are not currently supported.")
25
- end
26
- end
27
- end
28
- end
29
- end
@@ -1,192 +0,0 @@
1
- require File.join(File.dirname(__FILE__), 'spec_helper')
2
-
3
- # Setup some basic models to test with. We'll set permissions on both,
4
- # and then test :scope'd permissions through both.
5
- class Permissive::Organization < ActiveRecord::Base
6
- set_table_name :permissive_organizations
7
- end
8
-
9
- class Permissive::User < ActiveRecord::Base
10
- set_table_name :permissive_users
11
- end
12
-
13
- describe Permissive::Permission do
14
- before :each do
15
- PermissiveSpecHelper.db_up
16
- end
17
-
18
- after :each do
19
- PermissiveSpecHelper.db_down
20
- end
21
-
22
- describe "`acts_as_permissive' default class method" do
23
- [Permissive::User, Permissive::Organization].each do |model|
24
- before :each do
25
- model.acts_as_permissive
26
- end
27
-
28
- describe model do
29
- it "should create a permissions reflection" do
30
- model.new.should respond_to(:permissions)
31
- end
32
-
33
- it "should create a `can?' method" do
34
- model.new.should respond_to(:can?)
35
- end
36
-
37
- it "should create a `revoke' method" do
38
- model.new.should respond_to(:revoke)
39
- end
40
- end
41
- end
42
- end
43
-
44
- describe "permissions checking" do
45
- before :each do
46
- Permissive::User.acts_as_permissive
47
- @user = Permissive::User.create
48
- end
49
-
50
- it "should allow permissions checks through the `can?' method" do
51
- @user.can?(:edit_organizations).should be_false
52
- end
53
-
54
- it "should respond to the `can!' method" do
55
- @user.should respond_to(:can!)
56
- end
57
-
58
- it "should allow permissions setting through the `can!' method" do
59
- count = @user.permissions.count
60
- @user.can!(:view_users)
61
- @user.permissions.count.should == count.next
62
- end
63
-
64
- it "should return correct permissions through the `can?' method" do
65
- @user.can!(:view_users)
66
- @user.can?(:view_users).should be_true
67
- ['FINALIZE_LAB_SELECTION_LIST', 'SEARCH_APPLICANTS', 'CREATE_BASIC_USER', 'VIEW_BUDGET_REPORT'].each do |permission|
68
- @user.can?(permission).should be_false
69
- end
70
- end
71
-
72
- it "should return correct permissions on multiple requests" do
73
- @user.can!(:view_users)
74
- @user.can!(:view_budget_report)
75
- @user.can?(:view_users, :view_budget_report).should be_true
76
- ['FINALIZE_LAB_SELECTION_LIST', 'SEARCH_APPLICANTS', 'CREATE_BASIC_USER'].each do |permission|
77
- @user.can?(:view_users, permission).should be_false
78
- end
79
- end
80
-
81
- it "should revoke the correct permissions through the `revoke' method" do
82
- @user.can!(:view_users, :view_budget_report)
83
- @user.can?(:view_users).should be_true
84
- @user.can?(:view_budget_report).should be_true
85
- @user.revoke(:view_users)
86
- @user.can?(:view_users).should be_false
87
- @user.can?(:view_budget_report).should be_true
88
- end
89
-
90
- it "should revoke the full permissions through the `revoke' method w/an :all argument" do
91
- @user.can!(:view_users, :view_budget_report)
92
- @user.can?(:view_users).should be_true
93
- @user.can?(:view_budget_report).should be_true
94
- @user.revoke(:all)
95
- @user.can?(:view_users).should be_false
96
- @user.can?(:view_budget_report).should be_false
97
- end
98
- end
99
-
100
- describe "scoped permissions" do
101
- before :each do
102
- Permissive::User.acts_as_permissive(:scope => :organizations)
103
- @user = Permissive::User.create
104
- @organization = Permissive::Organization.create
105
- end
106
-
107
- it "should allow scoped permissions checks through the `can?' method" do
108
- @user.can?(:view_users, :on => @organization).should be_false
109
- end
110
-
111
- it "should return correct permissions through a scoped `can?' method" do
112
- @user.can!(:view_users, :on => @organization)
113
- @user.can?(:view_users, :on => @organization).should be_true
114
- end
115
-
116
- it "should not respond to generic permissions on scoped permissions" do
117
- @user.can!(:view_users, :on => @organization)
118
- @user.can?(:view_users).should be_false
119
- @user.can?(:view_users, :on => @organization).should be_true
120
- end
121
-
122
- it "should revoke the correct permissions through the `revoke' method" do
123
- @user.can!(:view_users, :view_budget_report, :on => @organization)
124
- @user.can?(:view_users, :on => @organization).should be_true
125
- @user.can?(:view_budget_report, :on => @organization).should be_true
126
- @user.revoke(:view_users, :on => @organization)
127
- @user.can?(:view_users, :on => @organization).should be_false
128
- @user.can?(:view_budget_report, :on => @organization).should be_true
129
- end
130
-
131
- it "should revoke the full permissions through the `revoke' method w/an :all argument" do
132
- @user.can!(:create_basic_user)
133
- @user.can!(:view_users, :view_budget_report, :on => @organization)
134
- @user.can?(:view_users, :on => @organization).should be_true
135
- @user.can?(:view_budget_report, :on => @organization).should be_true
136
- @user.can?(:create_basic_user).should be_true
137
- @user.revoke(:all, :on => @organization)
138
- !@user.can?(:view_users, :on => @organization).should be_false
139
- !@user.can?(:view_budget_report, :on => @organization).should be_false
140
- @user.can?(:create_basic_user).should be_true
141
- end
142
-
143
- end
144
-
145
- describe "automatic method creation" do
146
- before :each do
147
- Permissive::User.acts_as_permissive(:scope => :organizations)
148
- @user = Permissive::User.create
149
- @organization = Permissive::Organization.create
150
- @user.can!(:finalize_lab_selection_list)
151
- @user.can!(:create_basic_user)
152
- @user.can!(:view_users, :on => @organization)
153
- end
154
-
155
- it "should not respond to invalid permission methods" do
156
- lambda {
157
- @user.can_finalize_lab_selection_list_fu?
158
- }.should raise_error(NoMethodError)
159
- end
160
-
161
- it "should cache chained methods" do
162
- @user.respond_to?(:can_finalize_lab_selection_list_and_view_users?).should be_false
163
- @user.can_finalize_lab_selection_list_and_view_users?.should be_false
164
- @user.should respond_to(:can_finalize_lab_selection_list_and_view_users?)
165
- end
166
-
167
- it "should respond to valid permission methods" do
168
- @user.can_finalize_lab_selection_list?.should be_true
169
- @user.can_create_basic_user?.should be_true
170
- ['SEARCH_APPLICANTS', 'VIEW_USERS', 'VIEW_BUDGET_REPORT'].each do |permission|
171
- @user.send("can_#{permission.downcase}?").should be_false
172
- end
173
- end
174
-
175
- it "should respond to chained permission methods" do
176
- @user.can_finalize_lab_selection_list_and_create_basic_user?.should be_true
177
- ['SEARCH_APPLICANTS', 'VIEW_USERS', 'VIEW_BUDGET_REPORT'].each do |permission|
178
- @user.send("can_finalize_lab_selection_list_and_#{permission.downcase}?").should be_false
179
- end
180
- end
181
-
182
- it "should respond to scoped permission methods" do
183
- @user.can_view_users_on?(@organization).should be_true
184
- ['FINALIZE_LAB_SELECTION_LIST', 'SEARCH_APPLICANTS', 'CREATE_BASIC_USER', 'VIEW_BUDGET_REPORT'].each do |permission|
185
- @user.send("can_#{permission.downcase}_on?", @organization).should be_false
186
- end
187
- end
188
-
189
- end
190
- end
191
-
192
- PermissiveSpecHelper.clear_log