acl9 0.11.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.
- data/CHANGELOG.textile +32 -0
- data/MIT-LICENSE +20 -0
- data/README.textile +888 -0
- data/Rakefile +40 -0
- data/TODO +42 -0
- data/VERSION.yml +4 -0
- data/lib/acl9.rb +16 -0
- data/lib/acl9/config.rb +10 -0
- data/lib/acl9/controller_extensions.rb +85 -0
- data/lib/acl9/controller_extensions/dsl_base.rb +229 -0
- data/lib/acl9/controller_extensions/generators.rb +197 -0
- data/lib/acl9/helpers.rb +19 -0
- data/lib/acl9/model_extensions.rb +133 -0
- data/lib/acl9/model_extensions/object.rb +59 -0
- data/lib/acl9/model_extensions/subject.rb +175 -0
- data/test/access_control_test.rb +338 -0
- data/test/dsl_base_test.rb +758 -0
- data/test/helpers_test.rb +93 -0
- data/test/roles_test.rb +310 -0
- data/test/support/controllers.rb +207 -0
- data/test/support/models.rb +47 -0
- data/test/support/schema.rb +69 -0
- data/test/test_helper.rb +31 -0
- metadata +103 -0
data/CHANGELOG.textile
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
h2. 0.11.0 (16-Sep-2009)
|
2
|
+
|
3
|
+
* :protect_global_roles
|
4
|
+
* Subject#roles renamed to Subject#role_objects
|
5
|
+
* Fix namespaced models in roles backend (thanks goes to Tomas Jogin)
|
6
|
+
* Action name override in boolean methods.
|
7
|
+
* @:query_method@ option for @access_control@.
|
8
|
+
|
9
|
+
h2. 0.10.0 (03-May-2009)
|
10
|
+
|
11
|
+
* Use context+matchy combo for testing
|
12
|
+
* Bugfix: unwanted double quote in generated SQL statement
|
13
|
+
|
14
|
+
h2. 0.9.4 (27-Feb-2009)
|
15
|
+
|
16
|
+
* Introduce :if and :unless rule options.
|
17
|
+
|
18
|
+
h2. 0.9.3 (04-Feb-2009)
|
19
|
+
|
20
|
+
* Fix bug in delete_role - didn't work with custom class names
|
21
|
+
* Add @:helper@ option for @access_control@.
|
22
|
+
* Ability to generate helper methods directly (place @include Acl9Helpers@ in your helper module).
|
23
|
+
|
24
|
+
h2. 0.9.2 (11-Jan-2009)
|
25
|
+
|
26
|
+
* @access_control :method do end@ as shorter form for @access_control :as_method => :method do end@.
|
27
|
+
* Boolean method can now receive an objects hash which will be looked into first before taking
|
28
|
+
an instance variable.
|
29
|
+
|
30
|
+
h2. 0.9.1 (03-Jan-2009)
|
31
|
+
|
32
|
+
Initial release.
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2009 Oleg Dashevskii
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.textile
ADDED
@@ -0,0 +1,888 @@
|
|
1
|
+
h1. Introduction
|
2
|
+
|
3
|
+
Acl9 is yet another solution for role-based authorization in Rails. It consists of two
|
4
|
+
subsystems which can be used separately.
|
5
|
+
|
6
|
+
*Role control subsystem* allows you to set and query user roles for various objects.
|
7
|
+
|
8
|
+
*Access control subsystem* allows you to specify different role-based access rules
|
9
|
+
inside controllers.
|
10
|
+
|
11
|
+
A bunch of access rules is translated into a complex
|
12
|
+
boolean expression. Then it's turned into a lambda or a method and can
|
13
|
+
be used with @before_filter@. Thus you can block unprivileged access to certain
|
14
|
+
actions of your controller.
|
15
|
+
|
16
|
+
An example:
|
17
|
+
|
18
|
+
<pre><code>
|
19
|
+
class VerySecretController < ApplicationController
|
20
|
+
access_control do
|
21
|
+
allow :superadmin
|
22
|
+
allow :owner, :of => :secret
|
23
|
+
|
24
|
+
action :index do
|
25
|
+
allow anonymous, logged_in
|
26
|
+
end
|
27
|
+
|
28
|
+
allow logged_in, :to => :show
|
29
|
+
allow :manager, :of => :secret, :except => [:delete, :destroy]
|
30
|
+
deny :thiefs
|
31
|
+
end
|
32
|
+
|
33
|
+
def index
|
34
|
+
# ...
|
35
|
+
end
|
36
|
+
|
37
|
+
# ...
|
38
|
+
end
|
39
|
+
</code></pre>
|
40
|
+
|
41
|
+
h1. Contacts
|
42
|
+
|
43
|
+
Acl9 is hosted "on the GitHub":http://github.com/be9/acl9.
|
44
|
+
|
45
|
+
You may find tutorials and additional docs on the "wiki page":http://wiki.github.com/be9/acl9.
|
46
|
+
|
47
|
+
Rdocs are available "here":http://rdoc.info/projects/be9/acl9.
|
48
|
+
|
49
|
+
If you have questions, please post to the
|
50
|
+
"acl9-discuss group":http://groups.google.com/group/acl9-discuss
|
51
|
+
|
52
|
+
h1. Installation
|
53
|
+
|
54
|
+
Acl9 can be installed as a gem from "GitHub":http://github.com.
|
55
|
+
|
56
|
+
Add the following line to your @config/environment.rb@:
|
57
|
+
|
58
|
+
<pre><code>
|
59
|
+
config.gem "be9-acl9", :source => "http://gems.github.com", :lib => "acl9"
|
60
|
+
</pre></code>
|
61
|
+
|
62
|
+
Then run @rake gems:install@ (with possible @rake gems:unpack@ thereafter) and you're done!
|
63
|
+
|
64
|
+
Alternatively you can install Acl9 as a plugin:
|
65
|
+
|
66
|
+
<pre><code>
|
67
|
+
script/plugin install git://github.com/be9/acl9.git
|
68
|
+
</pre></code>
|
69
|
+
|
70
|
+
h1. Basics
|
71
|
+
|
72
|
+
h2. Authorization is not authentication!
|
73
|
+
|
74
|
+
Both words start with "auth" but have different meaning!
|
75
|
+
|
76
|
+
*Authentication* is basically a mapping of credentials (username, password) or
|
77
|
+
OpenID to specific user account in the system.
|
78
|
+
|
79
|
+
*Authorization* is an _authenticated_ user's permission to perform some
|
80
|
+
specific action somewhere in the system.
|
81
|
+
|
82
|
+
Acl9 is a authorization solution, so you will need to implement authentication
|
83
|
+
by other means. I recommend "Authlogic":http://github.com/binarylogic/authlogic
|
84
|
+
for that purpose, as it's simple, clean and at the same time very configurable.
|
85
|
+
|
86
|
+
h2. Roles
|
87
|
+
|
88
|
+
Role is an abstraction. You could directly assign permissions to user accounts in
|
89
|
+
your system, but you'd not want to! Way more manageable solution is to assign permissions
|
90
|
+
to roles and roles further to users.
|
91
|
+
|
92
|
+
For example, you can have role called _admin_ which has all available permissions. Now
|
93
|
+
you may assign this role to several trusted accounts on your system.
|
94
|
+
|
95
|
+
Acl9 also supports the notion of _object roles_, that is, roles with limited scope.
|
96
|
+
|
97
|
+
Imagine we are building a magazine site and want to develop a permission system. So, what roles
|
98
|
+
and permissions are there?
|
99
|
+
|
100
|
+
_Journalists_ should be able to create articles in their section and edit their own articles.
|
101
|
+
|
102
|
+
_Section editors_ should be able to edit and delete all articles in their sections and
|
103
|
+
change the _published_ flag.
|
104
|
+
|
105
|
+
_Editor-in-chief_ should be able to change everything.
|
106
|
+
|
107
|
+
We clearly see that journalists and section editors are tied to a specific section, whereas
|
108
|
+
editor-in-chief is a role with global scope.
|
109
|
+
|
110
|
+
h2. Role interface
|
111
|
+
|
112
|
+
All permission checks in Acl9 are boiled down to calls of a single method:
|
113
|
+
|
114
|
+
@subject.has_role?(role, object)@
|
115
|
+
|
116
|
+
That should be read as "Does _subject_ have _role_ on _object_?".
|
117
|
+
|
118
|
+
Subject is an instance of a @User@, or @Account@, or whatever model you use for
|
119
|
+
authentication. Object is an instance of any class (including subject class!)
|
120
|
+
or @nil@ (in which case it's a global role).
|
121
|
+
|
122
|
+
Acl9 builtin role control subsystem provides @has_role?@ method for you, but you can
|
123
|
+
also implemented it by hand (see _Coming up with your own role implementation_ below).
|
124
|
+
|
125
|
+
h1. Acl9 role control subsystem
|
126
|
+
|
127
|
+
Role control subsystem has been lifted from
|
128
|
+
"Rails authorization plugin":http://github.com/DocSavage/rails-authorization-plugin,
|
129
|
+
but undergone some modifications.
|
130
|
+
|
131
|
+
It's based on two tables in the database. First, role table, which stores pairs @[role_name, object]@
|
132
|
+
where object is a polymorphic model instance or a class. Second, join table, which joins users and roles.
|
133
|
+
|
134
|
+
To use this subsystem, you should define a @Role@ model.
|
135
|
+
|
136
|
+
h2. Role model
|
137
|
+
|
138
|
+
<pre><code>
|
139
|
+
class Role < ActiveRecord::Base
|
140
|
+
acts_as_authorization_role
|
141
|
+
end
|
142
|
+
</code></pre>
|
143
|
+
|
144
|
+
The structure of @roles@ table is as follows:
|
145
|
+
|
146
|
+
<pre><code>
|
147
|
+
create_table "roles", :force => true do |t|
|
148
|
+
t.string "name", :limit => 40
|
149
|
+
t.string "authorizable_type", :limit => 40
|
150
|
+
t.integer "authorizable_id"
|
151
|
+
t.datetime "created_at"
|
152
|
+
t.datetime "updated_at"
|
153
|
+
end
|
154
|
+
</code></pre>
|
155
|
+
|
156
|
+
Note that you will almost never use the @Role@ class directly.
|
157
|
+
|
158
|
+
h2. Subject model
|
159
|
+
|
160
|
+
<pre><code>
|
161
|
+
class User < ActiveRecord::Base
|
162
|
+
acts_as_authorization_subject
|
163
|
+
end
|
164
|
+
</code></pre>
|
165
|
+
|
166
|
+
You won't need any specific columns in the @users@ table, but
|
167
|
+
there should be a join table:
|
168
|
+
|
169
|
+
<pre><code>
|
170
|
+
create_table "roles_users", :id => false, :force => true do |t|
|
171
|
+
t.integer "user_id"
|
172
|
+
t.integer "role_id"
|
173
|
+
t.datetime "created_at"
|
174
|
+
t.datetime "updated_at"
|
175
|
+
end
|
176
|
+
</code></pre>
|
177
|
+
|
178
|
+
h2. Object model
|
179
|
+
|
180
|
+
Place @acts_as_authorization_object@ call inside any model you want to act
|
181
|
+
as such.
|
182
|
+
|
183
|
+
<pre><code>
|
184
|
+
class Foo < ActiveRecord::Base
|
185
|
+
acts_as_authorization_object
|
186
|
+
end
|
187
|
+
|
188
|
+
class Bar < ActiveRecord::Base
|
189
|
+
acts_as_authorization_object
|
190
|
+
end
|
191
|
+
</code></pre>
|
192
|
+
|
193
|
+
h2. Interface
|
194
|
+
|
195
|
+
h3. Subject model
|
196
|
+
|
197
|
+
A call of @acts_as_authorization_subject@ defines following methods on the model:
|
198
|
+
|
199
|
+
@subject.has_role?(role, object = nil)@. Returns @true@ of @false@ (has or has not).
|
200
|
+
|
201
|
+
@subject.has_role!(role, object = nil)@. Assigns a @role@ for the @object@ to the @subject@.
|
202
|
+
Does nothing is subject already has such a role.
|
203
|
+
|
204
|
+
@subject.has_no_role!(role, object = nil)@. Unassigns a role from the @subject@.
|
205
|
+
|
206
|
+
@subject.has_roles_for?(object)@. Does the @subject@ has any roles for @object@? (@true@ of @false@)
|
207
|
+
|
208
|
+
@subject.has_role_for?(object)@. Same as @has_roles_for?@.
|
209
|
+
|
210
|
+
@subject.roles_for(object)@. Returns an array of @Role@ instances, corresponding to @subject@ 's roles on
|
211
|
+
@object@. E.g. @subject.roles_for(object).map(&:name).sort@ will give you role names in alphabetical order.
|
212
|
+
|
213
|
+
@subject.has_no_roles_for!(object)@. Unassign any @subject@ 's roles for a given @object@.
|
214
|
+
|
215
|
+
@subject.has_no_roles!@. Unassign all roles from @subject@.
|
216
|
+
|
217
|
+
h3. Object model
|
218
|
+
|
219
|
+
A call of @acts_as_authorization_object@ defines following methods on the model:
|
220
|
+
|
221
|
+
@object.accepts_role?(role_name, subject)@. An alias for @subject.has_role?(role_name, object)@.
|
222
|
+
|
223
|
+
@object.accepts_role!(role_name, subject)@. An alias for @subject.has_role!(role_name, object)@.
|
224
|
+
|
225
|
+
@object.accepts_no_role!(role_name, subject)@. An alias for @subject.has_no_role!(role_name, object)@.
|
226
|
+
|
227
|
+
@object.accepts_roles_by?(subject)@. An alias for @subject.has_roles_for?(object)@.
|
228
|
+
|
229
|
+
@object.accepts_role_by?(subject)@. Same as @accepts_roles_by?@.
|
230
|
+
|
231
|
+
@object.accepts_roles_by(subject)@. An alias for @subject.roles_for(object)@.
|
232
|
+
|
233
|
+
h2. Custom class names
|
234
|
+
|
235
|
+
You may want to deviate from default @User@ and @Role@ class names. That can easily be done with
|
236
|
+
arguments to @acts_as_...@.
|
237
|
+
|
238
|
+
Say, you have @Account@ and @AccountRole@:
|
239
|
+
|
240
|
+
<pre><code>
|
241
|
+
class Account < ActiveRecord::Base
|
242
|
+
acts_as_authorization_subject :role_class_name => 'AccountRole'
|
243
|
+
end
|
244
|
+
|
245
|
+
class AccountRole < ActiveRecord::Base
|
246
|
+
acts_as_authorization_role :subject_class_name => 'Account'
|
247
|
+
end
|
248
|
+
|
249
|
+
class FooBar < ActiveRecord::Base
|
250
|
+
acts_as_authorization_object :role_class_name => 'AccountRole', :subject_class_name => 'Account'
|
251
|
+
end
|
252
|
+
</code></pre>
|
253
|
+
|
254
|
+
Or... since Acl9 defaults can be changed in a special hash, you can put the following snippet:
|
255
|
+
|
256
|
+
<pre><code>
|
257
|
+
Acl9::config.merge!({
|
258
|
+
:default_role_class_name => 'AccountRole',
|
259
|
+
:default_subject_class_name => 'Account',
|
260
|
+
})
|
261
|
+
</code></pre>
|
262
|
+
|
263
|
+
... into @config/initializers/acl9.rb@ and get rid of that clunky arguments:
|
264
|
+
|
265
|
+
<pre><code>
|
266
|
+
class Account < ActiveRecord::Base
|
267
|
+
acts_as_authorization_subject
|
268
|
+
end
|
269
|
+
|
270
|
+
class AccountRole < ActiveRecord::Base
|
271
|
+
acts_as_authorization_role
|
272
|
+
end
|
273
|
+
|
274
|
+
class FooBar < ActiveRecord::Base
|
275
|
+
acts_as_authorization_object
|
276
|
+
end
|
277
|
+
</code></pre>
|
278
|
+
|
279
|
+
Note that you'll need to change your database structure appropriately:
|
280
|
+
|
281
|
+
<pre><code>
|
282
|
+
create_table "account_roles", :force => true do |t|
|
283
|
+
t.string "name", :limit => 40
|
284
|
+
t.string "authorizable_type", :limit => 40
|
285
|
+
t.integer "authorizable_id"
|
286
|
+
t.datetime "created_at"
|
287
|
+
t.datetime "updated_at"
|
288
|
+
end
|
289
|
+
|
290
|
+
create_table "account_roles_accounts", :id => false, :force => true do |t|
|
291
|
+
t.integer "account_id"
|
292
|
+
t.integer "account_role_id"
|
293
|
+
t.datetime "created_at"
|
294
|
+
t.datetime "updated_at"
|
295
|
+
end
|
296
|
+
</code></pre>
|
297
|
+
|
298
|
+
h2. Examples
|
299
|
+
|
300
|
+
<pre><code>
|
301
|
+
user = User.create!
|
302
|
+
user.has_role? 'admin' # => false
|
303
|
+
|
304
|
+
user.has_role! :admin
|
305
|
+
|
306
|
+
user.has_role? :admin # => true
|
307
|
+
</code></pre>
|
308
|
+
|
309
|
+
@user@ now has global role _admin_. Note that you can specify role name either
|
310
|
+
as a string or as a symbol.
|
311
|
+
|
312
|
+
<pre><code>
|
313
|
+
foo = Foo.create!
|
314
|
+
|
315
|
+
user.has_role? 'admin', foo # => false
|
316
|
+
|
317
|
+
user.has_role! :manager, foo
|
318
|
+
|
319
|
+
user.has_role? :manager, foo # => true
|
320
|
+
foo.accepts_role? :manager, user # => true
|
321
|
+
|
322
|
+
user.has_roles_for? foo # => true
|
323
|
+
</code></pre>
|
324
|
+
|
325
|
+
You can see here that global and object roles are distinguished from each other. User
|
326
|
+
with global role _admin_ isn't automatically admin of @foo@.
|
327
|
+
|
328
|
+
However,
|
329
|
+
|
330
|
+
<pre><code>
|
331
|
+
user.has_role? :manager # => true
|
332
|
+
</code></pre>
|
333
|
+
|
334
|
+
That is, if you have an object role, it means that you have a global role with the same name too!
|
335
|
+
In other words, you are _manager_ if you manage at least one @foo@ (or a @bar@...).
|
336
|
+
|
337
|
+
<pre><code>
|
338
|
+
bar = Bar.create!
|
339
|
+
|
340
|
+
user.has_role! :manager, bar
|
341
|
+
user.has_no_role! :manager, foo
|
342
|
+
|
343
|
+
user.has_role? :manager, foo # => false
|
344
|
+
user.has_role? :manager # => true
|
345
|
+
</code></pre>
|
346
|
+
|
347
|
+
Our @user@ is no more manager of @foo@, but has become a manager of @bar@.
|
348
|
+
|
349
|
+
<pre><code>
|
350
|
+
user.has_no_roles!
|
351
|
+
|
352
|
+
user.has_role? :manager # => false
|
353
|
+
user.has_role? :admin # => false
|
354
|
+
user.roles # => []
|
355
|
+
</code></pre>
|
356
|
+
|
357
|
+
At this time @user@ has no roles in the system.
|
358
|
+
|
359
|
+
h2. Coming up with your own role implementation
|
360
|
+
|
361
|
+
The described role system with its 2 tables (not counting the @users@ table!)
|
362
|
+
might be an overkill for many cases. If all you want is global roles without
|
363
|
+
any scope, you'd better off implementing it by hand.
|
364
|
+
|
365
|
+
The access control subsystem of Acl9 uses only @subject.has_role?@ method, so
|
366
|
+
there's no need to implement anything else except for own convenience.
|
367
|
+
|
368
|
+
For example, if each your user can have only one global role, just add @role@
|
369
|
+
column to your @User@ class:
|
370
|
+
|
371
|
+
<pre><code>
|
372
|
+
class User < ActiveRecord::Base
|
373
|
+
def has_role?(role_name, obj=nil)
|
374
|
+
self.role == role_name
|
375
|
+
end
|
376
|
+
|
377
|
+
def has_role!(role_name, obj=nil)
|
378
|
+
self.role = role_name
|
379
|
+
save!
|
380
|
+
end
|
381
|
+
end
|
382
|
+
</code></pre>
|
383
|
+
|
384
|
+
If you need to assign multiple roles to your users, you can use @serialize@
|
385
|
+
with role array or a special solution like
|
386
|
+
"preference_fu":http://github.com/brennandunn/preference_fu.
|
387
|
+
|
388
|
+
h1. Access control subsystem
|
389
|
+
|
390
|
+
By means of access control subsystem you can protect actions of your controller
|
391
|
+
from unauthorized access. Acl9 provides a nice DSL for writing access rules.
|
392
|
+
|
393
|
+
h2. Allow and deny
|
394
|
+
|
395
|
+
Access control is mostly about allowing and denying. So there are two
|
396
|
+
basic methods: @allow@ and @deny@. They have the same syntax:
|
397
|
+
|
398
|
+
<pre><code>
|
399
|
+
allow ROLE_LIST, OPTIONS
|
400
|
+
deny ROLE_LIST, OPTIONS
|
401
|
+
</code></pre>
|
402
|
+
|
403
|
+
h3. Specifying roles
|
404
|
+
|
405
|
+
ROLE_LIST is a list of roles (at least 1 role should be there). So,
|
406
|
+
|
407
|
+
<pre><code>
|
408
|
+
allow :manager, :admin
|
409
|
+
deny :banned
|
410
|
+
</code></pre>
|
411
|
+
will match holders of global role _manager_ *and* holders of global role _admin_ as allowed.
|
412
|
+
On the contrary, holders of _banned_ role will match as denied.
|
413
|
+
|
414
|
+
Basically this snippet is equivalent to
|
415
|
+
|
416
|
+
<pre><code>
|
417
|
+
allow :manager
|
418
|
+
allow :admin
|
419
|
+
deny :banned
|
420
|
+
</code></pre>
|
421
|
+
which means that roles in argument list are OR'ed for a match, and not AND'ed.
|
422
|
+
|
423
|
+
Also note that:
|
424
|
+
* You may use both strings and :symbols to specify roles (the latter get converted into strings).
|
425
|
+
* Role names are singularized before check.
|
426
|
+
|
427
|
+
Thus the snippet above can also be written as
|
428
|
+
|
429
|
+
<pre><code>
|
430
|
+
allow :managers, :admins
|
431
|
+
deny 'banned'
|
432
|
+
</code></pre>
|
433
|
+
or even
|
434
|
+
|
435
|
+
<pre><code>
|
436
|
+
allow *%w(managers admins)
|
437
|
+
deny 'banned'
|
438
|
+
</code></pre>
|
439
|
+
|
440
|
+
h3. Object and class roles
|
441
|
+
|
442
|
+
Examples in the previous section were all about global roles. Let's see how we can
|
443
|
+
use object and class roles in the ACL block.
|
444
|
+
|
445
|
+
<pre><code>
|
446
|
+
allow :responsible, :for => Widget
|
447
|
+
allow :possessor, :of => :foo
|
448
|
+
deny :angry, :at => :me
|
449
|
+
allow :interested, :in => Future
|
450
|
+
deny :short, :on => :time
|
451
|
+
deny :hated, :by => :us
|
452
|
+
</code></pre>
|
453
|
+
|
454
|
+
To specify an object you use one of the 6 preposition options:
|
455
|
+
* :of
|
456
|
+
* :at
|
457
|
+
* :on
|
458
|
+
* :by
|
459
|
+
* :for
|
460
|
+
* :in
|
461
|
+
|
462
|
+
They all have the same meaning, use one that makes better English out of your rule.
|
463
|
+
|
464
|
+
Now, each of these prepositions may point to a Class or a :symbol. In the former case we get
|
465
|
+
a class role. E.g. @allow :responsible, :for => Widget@ becomes @subject.has_role?('responsible', Widget)@.
|
466
|
+
|
467
|
+
Symbol is trickier, it means that the appropriate instance variable of the controller is taken
|
468
|
+
as an object.
|
469
|
+
|
470
|
+
@allow :possessor, :of => :foo@ is translated into
|
471
|
+
<code>subject.has_role?('possessor', controller.instance_variable_get('@foo'))</code>.
|
472
|
+
|
473
|
+
Checking against an instance variable has sense when you have another _before filter_ which is executed
|
474
|
+
*before* the one generated by @access_control@, like this:
|
475
|
+
|
476
|
+
<pre><code>
|
477
|
+
class MoorblesController < ApplicationController
|
478
|
+
before_filter :load_moorble, :only => [:edit, :update, :destroy]
|
479
|
+
|
480
|
+
access_control do
|
481
|
+
allow :creator, :of => :moorble
|
482
|
+
|
483
|
+
# ...
|
484
|
+
end
|
485
|
+
|
486
|
+
# ...
|
487
|
+
|
488
|
+
private
|
489
|
+
|
490
|
+
def load_moorble
|
491
|
+
@moorble = Moorble.find(params[:id])
|
492
|
+
end
|
493
|
+
end
|
494
|
+
</code></pre>
|
495
|
+
|
496
|
+
Note that the object option is applied to all of the roles you specify in the argument list.
|
497
|
+
As such,
|
498
|
+
|
499
|
+
<pre><code>
|
500
|
+
allow :devil, :son, :of => God
|
501
|
+
</code></pre>
|
502
|
+
is equivalent to
|
503
|
+
|
504
|
+
<pre><code>
|
505
|
+
allow :devil, :of => God
|
506
|
+
allow :son, :of => God
|
507
|
+
</code></pre>
|
508
|
+
|
509
|
+
but *NOT*
|
510
|
+
|
511
|
+
<pre><code>
|
512
|
+
allow :devil
|
513
|
+
allow :son, :of => God
|
514
|
+
</code></pre>
|
515
|
+
|
516
|
+
h3. Pseudo-roles
|
517
|
+
|
518
|
+
There are three pseudo-roles in the ACL: @all@, @anonymous@ and @logged_in@.
|
519
|
+
|
520
|
+
@allow all@ will always match (as well as @deny all@).
|
521
|
+
|
522
|
+
@allow anonymous@ and @deny anonymous@ will match when user is anonymous, i.e. subject is @nil@.
|
523
|
+
You may also use a shorter notation: @allow nil@ (@deny nil@).
|
524
|
+
|
525
|
+
@logged_in@ is direct opposite of @anonymous@, so @allow logged_in@ will match if the user is logged in
|
526
|
+
(subject is not @nil@).
|
527
|
+
|
528
|
+
No role checks are done in either case.
|
529
|
+
|
530
|
+
h3. Limiting action scope
|
531
|
+
|
532
|
+
By default rules apply to all actions of the controller. There are two options that
|
533
|
+
narrow the scope of the @deny@ or @allow@ rule: @:to@ and @:except@.
|
534
|
+
|
535
|
+
<pre><code>
|
536
|
+
allow :owner, :of => :site, :to => [:delete, :destroy]
|
537
|
+
deny anonymous, :except => [:index, :show]
|
538
|
+
</code></pre>
|
539
|
+
|
540
|
+
For the first rule to match not only the current user should be an _owner_ of the _site_, but also
|
541
|
+
current action should be _delete_ or _destroy_.
|
542
|
+
|
543
|
+
In the second rule anonymous user access is denied for all actions, except _index_ and _show_.
|
544
|
+
|
545
|
+
You may not specify both @:to@ and @:except@.
|
546
|
+
|
547
|
+
Note that you can use actions block instead of @:to@ (see _Actions block_
|
548
|
+
below). You can also use @:only@ and @:except@ options in the
|
549
|
+
@access_control@ call which will serve as options of the @before_filter@ and thus
|
550
|
+
limit the scope of the whole ACL.
|
551
|
+
|
552
|
+
h3. Rule conditions
|
553
|
+
|
554
|
+
You may create conditional rules using @:if@ and @:unless@ options.
|
555
|
+
|
556
|
+
<pre><code>
|
557
|
+
allow :owner, :of => :site, :to => [:delete, :destroy], :if => :chance_to_delete
|
558
|
+
</code></pre>
|
559
|
+
|
560
|
+
Controller's @:chance_to_delete@ method will be called here. The rule will match if the action
|
561
|
+
is 'delete' or 'destroy' AND if the method returned @true@.
|
562
|
+
|
563
|
+
@:unless@ has the opposite meaning and should return @false@ for a rule to match.
|
564
|
+
|
565
|
+
Both options can be specified in the same rule.
|
566
|
+
|
567
|
+
<pre><code>
|
568
|
+
allow :visitor, :to => [:index, :show], :if => :right_phase_of_the_moon?, :unless => :suspicious?
|
569
|
+
</code></pre>
|
570
|
+
|
571
|
+
@right_phase_of_the_moon?@ should return @true@ AND @suspicious?@ should return @false@ for a poor visitor to
|
572
|
+
see a page.
|
573
|
+
|
574
|
+
Currently only controller methods are supported (specify them as :symbols). Lambdas are *not* supported.
|
575
|
+
|
576
|
+
h2. Rule matching order
|
577
|
+
|
578
|
+
Rule matching system is similar to that of Apache web server. There are two modes: _default allow_
|
579
|
+
(corresponding to @Order Deny,Allow@ in Apache) and _default deny_ (@Order Allow,Deny@ in Apache).
|
580
|
+
|
581
|
+
h3. Setting modes
|
582
|
+
|
583
|
+
Mode is set with a @default@ call.
|
584
|
+
|
585
|
+
@default :allow@ will set _default allow_ mode.
|
586
|
+
|
587
|
+
@default :deny@ will set _default deny_ mode. Note that this is the default mode, i.e. it will be on
|
588
|
+
if you don't do a @default@ call at all.
|
589
|
+
|
590
|
+
h3. Matching algorithm
|
591
|
+
|
592
|
+
First of all, regardless of the mode, all @allow@ matches are OR'ed together and all @deny@ matches
|
593
|
+
are OR'ed as well.
|
594
|
+
|
595
|
+
We'll express this in the following manner:
|
596
|
+
|
597
|
+
<pre><code>
|
598
|
+
ALLOWED = (allow rule 1 matches?) OR ((allow rule 2 matches?) OR ...
|
599
|
+
NOT_DENIED = NOT ((deny rule 1 matches?) OR (deny rule 2 matches?) OR ...)
|
600
|
+
</code></pre>
|
601
|
+
|
602
|
+
So, ALLOWED is @true@ when either of the @allow@ rules matches, and NOT_DENIED is @true@ when none
|
603
|
+
of the @deny@ rules matches.
|
604
|
+
|
605
|
+
Let's denote the final result of algorithm as ALLOWANCE. If it's @true@, access is allowed, if @false@ - denied.
|
606
|
+
|
607
|
+
In the case of _default allow_:
|
608
|
+
<pre><code>
|
609
|
+
ALLOWANCE = ALLOWED OR NOT_DENIED
|
610
|
+
</code></pre>
|
611
|
+
|
612
|
+
In the case of _default deny_:
|
613
|
+
<pre><code>
|
614
|
+
ALLOWANCE = ALLOWED AND NOT_DENIED
|
615
|
+
</code></pre>
|
616
|
+
|
617
|
+
Same result as a table:
|
618
|
+
|
619
|
+
|_. Rule matches |_. Default allow mode |_. Default deny mode |
|
620
|
+
| None of the @allow@ and @deny@ rules matched. | Access is allowed. | Access is denied. |
|
621
|
+
| Some of the @allow@ rules matched, none of the @deny@ rules matched. | Access is allowed. | Access is allowed. |
|
622
|
+
| None of the @allow@ rules matched, some of the @deny@ rules matched. | Access is denied. | Access is denied. |
|
623
|
+
| Some of the @allow@ rules matched, some of the @deny@ rules matched. | Access is allowed. | Access is denied. |
|
624
|
+
|
625
|
+
Apparently _default deny_ mode is more strict, and that's because it's on by default.
|
626
|
+
|
627
|
+
h2. Actions block
|
628
|
+
|
629
|
+
You may group rules with the help of the @actions@ block.
|
630
|
+
|
631
|
+
An example from the imaginary @PostsController@:
|
632
|
+
|
633
|
+
<pre><code>
|
634
|
+
allow :admin
|
635
|
+
|
636
|
+
actions :index, :show do
|
637
|
+
allow all
|
638
|
+
end
|
639
|
+
|
640
|
+
actions :new, :create do
|
641
|
+
allow :managers, :of => Post
|
642
|
+
end
|
643
|
+
|
644
|
+
actions :edit, :update do
|
645
|
+
allow :owner, :of => :post
|
646
|
+
end
|
647
|
+
|
648
|
+
action :destroy do
|
649
|
+
allow :owner, :of => :post
|
650
|
+
end
|
651
|
+
</code></pre>
|
652
|
+
|
653
|
+
This is equivalent to:
|
654
|
+
|
655
|
+
<pre><code>
|
656
|
+
allow :admin
|
657
|
+
|
658
|
+
allow all, :to => [:index, :show]
|
659
|
+
allow :managers, :of => Post, :to => [:new, :create]
|
660
|
+
allow :owner, :of => :post, :to => [:edit, :update]
|
661
|
+
allow :owner, :of => :post, :to => :destroy
|
662
|
+
</code></pre>
|
663
|
+
|
664
|
+
Note that only @allow@ and @deny@ calls are available inside @actions@ block, and these may not have
|
665
|
+
@:to@/@:except@ options.
|
666
|
+
|
667
|
+
@action@ is just a synonym for @actions@.
|
668
|
+
|
669
|
+
h2. access_control method
|
670
|
+
|
671
|
+
By calling @access_control@ in your controller you can get your ACL block translated into...
|
672
|
+
|
673
|
+
# a lambda, installed with @before_filter@ and raising @Acl9::AccessDenied@ exception on occasion.
|
674
|
+
# a method, installed with @before_filter@ and raising @Acl9::AccessDenied@ exception on occasion.
|
675
|
+
# a method, returning @true@ or @false@, whether access is allowed or denied.
|
676
|
+
|
677
|
+
First case is by default. You can catch the exception with @rescue_from@ call and do something
|
678
|
+
you like: make a redirect, or render "Access denied" template, or whatever.
|
679
|
+
|
680
|
+
Second case is obtained with specifying method name as an argument to
|
681
|
+
@access_control@ (or using @:as_method@ option, see below) and may be helpful
|
682
|
+
if you want to use @skip_before_filter@ somewhere in the derived controller.
|
683
|
+
|
684
|
+
Third case will take place if you supply @:filter => false@ along with method
|
685
|
+
name. You'll get an ordinary method which you can call anywhere you want.
|
686
|
+
|
687
|
+
h3. :subject_method
|
688
|
+
|
689
|
+
Acl9 obtains the subject instance by calling specific method of the controller. By default it's
|
690
|
+
@:current_user@, but you may change it.
|
691
|
+
|
692
|
+
<pre><code>
|
693
|
+
class MyController < ApplicationController
|
694
|
+
access_control :subject_method => :current_account do
|
695
|
+
allow :nifty
|
696
|
+
# ...
|
697
|
+
end
|
698
|
+
|
699
|
+
# ...
|
700
|
+
end
|
701
|
+
</code></pre>
|
702
|
+
|
703
|
+
Subject method can also be changed globally. Place the following into @config/initializers/acl9.rb@:
|
704
|
+
|
705
|
+
<pre><code>
|
706
|
+
Acl9::config[:default_subject_method] = :current_account
|
707
|
+
</code></pre>
|
708
|
+
|
709
|
+
h3. :debug
|
710
|
+
|
711
|
+
@:debug => true@ will output the filtering expression into the debug log. If
|
712
|
+
Acl9 does something strange, you may look at it as the last resort.
|
713
|
+
|
714
|
+
h3. :as_method
|
715
|
+
|
716
|
+
In the case
|
717
|
+
|
718
|
+
<pre><code>
|
719
|
+
class NiftyController < ApplicationController
|
720
|
+
access_control :as_method => :acl do
|
721
|
+
allow :nifty
|
722
|
+
# ...
|
723
|
+
end
|
724
|
+
|
725
|
+
# ...
|
726
|
+
end
|
727
|
+
</code></pre>
|
728
|
+
|
729
|
+
access control checks will be added as @acl@ method onto MyController, with @before_filter :acl@ call thereafter.
|
730
|
+
|
731
|
+
Instead of using @:as_method@ you may specify the name of the method as a positional argument
|
732
|
+
to @access_control@:
|
733
|
+
|
734
|
+
<pre><code>
|
735
|
+
class MyController < ApplicationController
|
736
|
+
access_control :acl do
|
737
|
+
# ...
|
738
|
+
end
|
739
|
+
|
740
|
+
# ...
|
741
|
+
end
|
742
|
+
</code></pre>
|
743
|
+
|
744
|
+
|
745
|
+
h3. :filter
|
746
|
+
|
747
|
+
If you set @:filter@ to @false@ (it's @true@ by default) and also use
|
748
|
+
@:as_method@ (or method name as 1st argument to @access_control@, you'll get a
|
749
|
+
method which won't raise @Acl9::AccessDenied@ exception, but rather return
|
750
|
+
@true@ or @false@ (access allowed/denied).
|
751
|
+
|
752
|
+
<pre><code>
|
753
|
+
class SecretController < ApplicationController
|
754
|
+
access_control :secret_access?, :filter => false do
|
755
|
+
allow :james_bond
|
756
|
+
# ...
|
757
|
+
end
|
758
|
+
|
759
|
+
def index
|
760
|
+
if secret_access?
|
761
|
+
_secret_index
|
762
|
+
else
|
763
|
+
_ordinary_index
|
764
|
+
end
|
765
|
+
end
|
766
|
+
|
767
|
+
# ...
|
768
|
+
|
769
|
+
private
|
770
|
+
|
771
|
+
def _secret_index
|
772
|
+
# ...
|
773
|
+
end
|
774
|
+
|
775
|
+
def _ordinary_index
|
776
|
+
# ...
|
777
|
+
end
|
778
|
+
end
|
779
|
+
</code></pre>
|
780
|
+
|
781
|
+
The generated method can receive an objects hash as an argument. In this example,
|
782
|
+
|
783
|
+
<pre><code>
|
784
|
+
class LolController < ApplicationController
|
785
|
+
access_control :lolcats?, :filter => false do
|
786
|
+
allow :cats, :by => :lol
|
787
|
+
# ...
|
788
|
+
end
|
789
|
+
end
|
790
|
+
</code></pre>
|
791
|
+
|
792
|
+
you may not only call @lolcats?@ with no arguments, which will basically return
|
793
|
+
|
794
|
+
<pre><code>
|
795
|
+
current_user.has_role?('cats', @lol)
|
796
|
+
</code></pre>
|
797
|
+
|
798
|
+
but also as @lolcats?(:lol => Lol.find(params[:lol]))@. The hash will be looked into first,
|
799
|
+
even if you have an instance variable @lol@.
|
800
|
+
|
801
|
+
h3. :helper
|
802
|
+
|
803
|
+
Sometimes you want to have a boolean method (like @:filter => false@) accessible
|
804
|
+
in your views. Acl9 can call @helper_method@ for you:
|
805
|
+
|
806
|
+
<pre><code>
|
807
|
+
class LolController < ApplicationController
|
808
|
+
access_control :helper => :lolcats? do
|
809
|
+
allow :cats, :by => :lol
|
810
|
+
# ...
|
811
|
+
end
|
812
|
+
end
|
813
|
+
</code></pre>
|
814
|
+
|
815
|
+
That's equivalent to
|
816
|
+
|
817
|
+
<pre><code>
|
818
|
+
class LolController < ApplicationController
|
819
|
+
access_control :lolcats?, :filter => false do
|
820
|
+
allow :cats, :by => :lol
|
821
|
+
# ...
|
822
|
+
end
|
823
|
+
|
824
|
+
helper_method :lolcats?
|
825
|
+
end
|
826
|
+
</code></pre>
|
827
|
+
|
828
|
+
h3. Other options
|
829
|
+
|
830
|
+
Other options will be passed to @before_filter@. As such, you may use @:only@ and @:except@ to narrow
|
831
|
+
the action scope of the whole ACL block.
|
832
|
+
|
833
|
+
<pre><code>
|
834
|
+
class OmgController < ApplicationController
|
835
|
+
access_control :only => [:index, :show] do
|
836
|
+
allow all
|
837
|
+
deny :banned
|
838
|
+
end
|
839
|
+
|
840
|
+
# ...
|
841
|
+
end
|
842
|
+
</code></pre>
|
843
|
+
|
844
|
+
is basically equivalent to
|
845
|
+
|
846
|
+
<pre><code>
|
847
|
+
class OmgController < ApplicationController
|
848
|
+
access_control do
|
849
|
+
actions :index, :show do
|
850
|
+
allow all
|
851
|
+
deny :banned
|
852
|
+
end
|
853
|
+
|
854
|
+
allow all, :except => [:index, :show]
|
855
|
+
end
|
856
|
+
|
857
|
+
# ...
|
858
|
+
end
|
859
|
+
</code></pre>
|
860
|
+
|
861
|
+
h2. access_control in your helpers
|
862
|
+
|
863
|
+
Apart from using @:helper@ option for @access_control@ call inside controller, there's a
|
864
|
+
way to generate helper methods directly, like this:
|
865
|
+
|
866
|
+
<pre><code>
|
867
|
+
module SettingsHelper
|
868
|
+
include Acl9Helpers
|
869
|
+
|
870
|
+
access_control :show_settings? do
|
871
|
+
allow :admin
|
872
|
+
allow :settings_manager
|
873
|
+
end
|
874
|
+
end
|
875
|
+
</code></pre>
|
876
|
+
|
877
|
+
Here we mix in @Acl9Helpers@ module which brings in @access_control@ method and call it,
|
878
|
+
obtaining @show_settings?@ method.
|
879
|
+
|
880
|
+
An imaginary view:
|
881
|
+
|
882
|
+
<pre><code>
|
883
|
+
<% if show_settings? %>
|
884
|
+
<%= link_to 'Settings', settings_path %>
|
885
|
+
<% end %>
|
886
|
+
</code></pre>
|
887
|
+
|
888
|
+
Copyright (c) 2009 Oleg Dashevskii, released under the MIT license.
|