action_access 0.0.3 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +34 -0
- data/CONTRIBUTING.md +19 -14
- data/Gemfile.lock +41 -41
- data/README.md +81 -39
- data/action_access.gemspec +0 -1
- data/lib/action_access/controller_additions.rb +38 -10
- data/lib/action_access/keeper.rb +6 -4
- data/lib/action_access/user_utilities.rb +32 -17
- data/lib/action_access/version.rb +1 -1
- data/test/controllers/articles_controller_test.rb +33 -11
- data/test/controllers/secrets_controller_test.rb +7 -1
- data/test/controllers/static_controller_test.rb +3 -3
- data/test/dummy/app/controllers/application_controller.rb +2 -2
- data/test/dummy/app/controllers/articles_controller.rb +3 -2
- data/test/dummy/app/models/user.rb +2 -2
- data/test/dummy/db/migrate/20140926071026_create_users.rb +1 -1
- data/test/dummy/db/schema.rb +4 -4
- data/test/models/user_test.rb +31 -10
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3018e5d68f21d4a6f66e15a29edc67b654b8f82a
|
4
|
+
data.tar.gz: 0cf2e285d1f2116733bb9ec8c5e99862de9b300a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: daa68edbd6d9f3a91e96d20c4e4e957eb7873b637eac6b0639b20d83069a9d2187152d005a059c34e68512c0be3bded5143f91d8b72919d6e4fd6bec0156f141
|
7
|
+
data.tar.gz: 4578a0f74cf68ab2b9687a710b36991b950073cddfcaf7b31cdcae37444407cc3f755592c9eb481d38a375b3540f7861d4dbbe91698f73a14a7e4f2db1a965ed
|
data/CHANGELOG.md
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
Changelog
|
2
|
+
=========
|
3
|
+
|
4
|
+
0.1.0
|
5
|
+
-----
|
6
|
+
|
7
|
+
**Features:**
|
8
|
+
|
9
|
+
- Support sessions with multiple clearance levels (e.g. users with many roles).
|
10
|
+
- Avoid repetition when defining clearance levels with the same permissions.
|
11
|
+
|
12
|
+
**API:**
|
13
|
+
|
14
|
+
- Use `current_clearance_levels` (plural) instead of `current_clearance_level`.
|
15
|
+
- Use `clearance_levels` (plural) instead of `clearance_level` (model method).
|
16
|
+
|
17
|
+
|
18
|
+
0.0.3
|
19
|
+
-----
|
20
|
+
|
21
|
+
- Update dependencies.
|
22
|
+
- Support Rails 2.2.2.
|
23
|
+
|
24
|
+
|
25
|
+
0.0.2
|
26
|
+
-----
|
27
|
+
|
28
|
+
- Speed up tests with in-memory database.
|
29
|
+
|
30
|
+
|
31
|
+
0.0.1
|
32
|
+
-----
|
33
|
+
|
34
|
+
- Initial release.
|
data/CONTRIBUTING.md
CHANGED
@@ -1,24 +1,29 @@
|
|
1
1
|
## Please read before contributing
|
2
2
|
|
3
3
|
1. If you have questions about Action Access, use the
|
4
|
-
[Mailing List](https://groups.google.com/d/forum/action_access),
|
5
|
-
[Gitter's chat](https://gitter.im/matiasgagliano/action_access) or
|
6
|
-
[Stack Overflow](https://stackoverflow.com/questions/tagged/action_access).
|
7
|
-
**DO NOT** post questions here.
|
4
|
+
[Mailing List](https://groups.google.com/d/forum/action_access),
|
5
|
+
[Gitter's chat](https://gitter.im/matiasgagliano/action_access) or
|
6
|
+
[Stack Overflow](https://stackoverflow.com/questions/tagged/action_access).
|
7
|
+
**DO NOT** post questions here.
|
8
8
|
|
9
9
|
2. If you find a security bug, **DO NOT** submit an issue here.
|
10
|
-
Please send an e-mail to [mgag.issues@gmail.com](mailto:mgag.issues@gmail.com)
|
11
|
-
instead.
|
10
|
+
Please send an e-mail to [mgag.issues@gmail.com](mailto:mgag.issues@gmail.com)
|
11
|
+
instead.
|
12
12
|
|
13
13
|
3. Do a small search on the issues tracker before submitting your issue to
|
14
|
-
see if it was already reported or fixed. In case it wasn't, create your
|
15
|
-
with a **clear description** of the issue and a **code sample** that
|
16
|
-
demonstrates it. Include Rails and Action Access versions, and as much
|
17
|
-
information as possible. Tests or a sample application showing how
|
18
|
-
expected behavior is not occurring will be much appreciated.
|
14
|
+
see if it was already reported or fixed. In case it wasn't, create your
|
15
|
+
report with a **clear description** of the issue and a **code sample** that
|
16
|
+
demonstrates it. Include Rails and Action Access versions, and as much
|
17
|
+
relevant information as possible. Tests or a sample application showing how
|
18
|
+
the expected behavior is not occurring will be much appreciated.
|
19
19
|
|
20
|
-
The more information you give, the easier it becomes to track the issue and
|
21
|
-
it. Your goal should be to make it easy for yourself and others to
|
22
|
-
the bug and figure out a fix.
|
20
|
+
The more information you give, the easier it becomes to track the issue and
|
21
|
+
fix it. Your goal should be to make it easy for yourself and others to
|
22
|
+
replicate the bug and figure out a fix.
|
23
|
+
|
24
|
+
4. **ALWAYS** open an issue (issue tracker) before submitting a pull request to
|
25
|
+
discuss the changes and the best ways to implement them, it won't be accepted
|
26
|
+
otherwise. Describe your proposal, why is it relevant? For which cases are
|
27
|
+
those changes or new features needed?
|
23
28
|
|
24
29
|
Thanks for your time and support!
|
data/Gemfile.lock
CHANGED
@@ -1,94 +1,94 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
action_access (0.0
|
4
|
+
action_access (0.1.0)
|
5
5
|
rails (~> 4.1)
|
6
6
|
|
7
7
|
GEM
|
8
8
|
remote: https://rubygems.org/
|
9
9
|
specs:
|
10
|
-
actionmailer (4.2.
|
11
|
-
actionpack (= 4.2.
|
12
|
-
actionview (= 4.2.
|
13
|
-
activejob (= 4.2.
|
10
|
+
actionmailer (4.2.4)
|
11
|
+
actionpack (= 4.2.4)
|
12
|
+
actionview (= 4.2.4)
|
13
|
+
activejob (= 4.2.4)
|
14
14
|
mail (~> 2.5, >= 2.5.4)
|
15
15
|
rails-dom-testing (~> 1.0, >= 1.0.5)
|
16
|
-
actionpack (4.2.
|
17
|
-
actionview (= 4.2.
|
18
|
-
activesupport (= 4.2.
|
16
|
+
actionpack (4.2.4)
|
17
|
+
actionview (= 4.2.4)
|
18
|
+
activesupport (= 4.2.4)
|
19
19
|
rack (~> 1.6)
|
20
20
|
rack-test (~> 0.6.2)
|
21
21
|
rails-dom-testing (~> 1.0, >= 1.0.5)
|
22
|
-
rails-html-sanitizer (~> 1.0, >= 1.0.
|
23
|
-
actionview (4.2.
|
24
|
-
activesupport (= 4.2.
|
22
|
+
rails-html-sanitizer (~> 1.0, >= 1.0.2)
|
23
|
+
actionview (4.2.4)
|
24
|
+
activesupport (= 4.2.4)
|
25
25
|
builder (~> 3.1)
|
26
26
|
erubis (~> 2.7.0)
|
27
27
|
rails-dom-testing (~> 1.0, >= 1.0.5)
|
28
|
-
rails-html-sanitizer (~> 1.0, >= 1.0.
|
29
|
-
activejob (4.2.
|
30
|
-
activesupport (= 4.2.
|
28
|
+
rails-html-sanitizer (~> 1.0, >= 1.0.2)
|
29
|
+
activejob (4.2.4)
|
30
|
+
activesupport (= 4.2.4)
|
31
31
|
globalid (>= 0.3.0)
|
32
|
-
activemodel (4.2.
|
33
|
-
activesupport (= 4.2.
|
32
|
+
activemodel (4.2.4)
|
33
|
+
activesupport (= 4.2.4)
|
34
34
|
builder (~> 3.1)
|
35
|
-
activerecord (4.2.
|
36
|
-
activemodel (= 4.2.
|
37
|
-
activesupport (= 4.2.
|
35
|
+
activerecord (4.2.4)
|
36
|
+
activemodel (= 4.2.4)
|
37
|
+
activesupport (= 4.2.4)
|
38
38
|
arel (~> 6.0)
|
39
|
-
activesupport (4.2.
|
39
|
+
activesupport (4.2.4)
|
40
40
|
i18n (~> 0.7)
|
41
41
|
json (~> 1.7, >= 1.7.7)
|
42
42
|
minitest (~> 5.1)
|
43
43
|
thread_safe (~> 0.3, >= 0.3.4)
|
44
44
|
tzinfo (~> 1.1)
|
45
|
-
arel (6.0.
|
45
|
+
arel (6.0.3)
|
46
46
|
builder (3.2.2)
|
47
47
|
erubis (2.7.0)
|
48
|
-
globalid (0.3.
|
48
|
+
globalid (0.3.6)
|
49
49
|
activesupport (>= 4.1.0)
|
50
50
|
i18n (0.7.0)
|
51
51
|
json (1.8.3)
|
52
|
-
loofah (2.0.
|
52
|
+
loofah (2.0.3)
|
53
53
|
nokogiri (>= 1.5.9)
|
54
54
|
mail (2.6.3)
|
55
55
|
mime-types (>= 1.16, < 3)
|
56
|
-
mime-types (2.6.
|
56
|
+
mime-types (2.6.2)
|
57
57
|
mini_portile (0.6.2)
|
58
|
-
minitest (5.
|
58
|
+
minitest (5.8.1)
|
59
59
|
nokogiri (1.6.6.2)
|
60
60
|
mini_portile (~> 0.6.0)
|
61
61
|
rack (1.6.4)
|
62
62
|
rack-test (0.6.3)
|
63
63
|
rack (>= 1.0)
|
64
|
-
rails (4.2.
|
65
|
-
actionmailer (= 4.2.
|
66
|
-
actionpack (= 4.2.
|
67
|
-
actionview (= 4.2.
|
68
|
-
activejob (= 4.2.
|
69
|
-
activemodel (= 4.2.
|
70
|
-
activerecord (= 4.2.
|
71
|
-
activesupport (= 4.2.
|
64
|
+
rails (4.2.4)
|
65
|
+
actionmailer (= 4.2.4)
|
66
|
+
actionpack (= 4.2.4)
|
67
|
+
actionview (= 4.2.4)
|
68
|
+
activejob (= 4.2.4)
|
69
|
+
activemodel (= 4.2.4)
|
70
|
+
activerecord (= 4.2.4)
|
71
|
+
activesupport (= 4.2.4)
|
72
72
|
bundler (>= 1.3.0, < 2.0)
|
73
|
-
railties (= 4.2.
|
73
|
+
railties (= 4.2.4)
|
74
74
|
sprockets-rails
|
75
75
|
rails-deprecated_sanitizer (1.0.3)
|
76
76
|
activesupport (>= 4.2.0.alpha)
|
77
|
-
rails-dom-testing (1.0.
|
77
|
+
rails-dom-testing (1.0.7)
|
78
78
|
activesupport (>= 4.2.0.beta, < 5.0)
|
79
79
|
nokogiri (~> 1.6.0)
|
80
80
|
rails-deprecated_sanitizer (>= 1.0.1)
|
81
81
|
rails-html-sanitizer (1.0.2)
|
82
82
|
loofah (~> 2.0)
|
83
|
-
railties (4.2.
|
84
|
-
actionpack (= 4.2.
|
85
|
-
activesupport (= 4.2.
|
83
|
+
railties (4.2.4)
|
84
|
+
actionpack (= 4.2.4)
|
85
|
+
activesupport (= 4.2.4)
|
86
86
|
rake (>= 0.8.7)
|
87
87
|
thor (>= 0.18.1, < 2.0)
|
88
88
|
rake (10.4.2)
|
89
|
-
sprockets (3.
|
90
|
-
rack (
|
91
|
-
sprockets-rails (2.3.
|
89
|
+
sprockets (3.4.0)
|
90
|
+
rack (> 1, < 3)
|
91
|
+
sprockets-rails (2.3.3)
|
92
92
|
actionpack (>= 3.0)
|
93
93
|
activesupport (>= 3.0)
|
94
94
|
sprockets (>= 2.8, < 4.0)
|
data/README.md
CHANGED
@@ -6,7 +6,7 @@ Action Access is an access control system for Ruby on Rails. It provides a
|
|
6
6
|
modular and easy way to secure applications and handle permissions.
|
7
7
|
|
8
8
|
It works at controller level focusing on what **actions** are accessible for
|
9
|
-
the current user instead of
|
9
|
+
the current user instead of messing with models and their attributes.
|
10
10
|
|
11
11
|
It also provides utilities for thorough control and some useful view helpers.
|
12
12
|
|
@@ -24,45 +24,62 @@ gem 'action_access'
|
|
24
24
|
|
25
25
|
## Basic configuration
|
26
26
|
|
27
|
-
The most important setting is
|
28
|
-
user
|
27
|
+
The most important setting is how to get the **clearance levels** (roles,
|
28
|
+
credentials, user groups, etc.) for the current session, other than that it
|
29
|
+
works out of the box.
|
29
30
|
|
30
31
|
Action Access doesn't require users or authentication at all to function so
|
31
32
|
you can get creative with the way you set and identify clearance levels.
|
32
33
|
|
33
|
-
It only needs a `
|
34
|
-
clearance
|
35
|
-
|
34
|
+
It only needs a `current_clearance_levels` method that returns the
|
35
|
+
clearance levels granted for the current request. It can be a single clearance
|
36
|
+
level (string or symbol) or a list of them (array), and it doesn't matter if
|
37
|
+
they're singular or plural (they'll be singularized).
|
36
38
|
|
37
39
|
* With `current_user`:
|
38
40
|
|
39
|
-
The default `
|
40
|
-
`current_user.
|
41
|
+
The default `current_clearance_levels` method tests if it can get
|
42
|
+
`current_user.clearance_levels` and defaults to `:guest` if not.
|
41
43
|
|
42
44
|
So, if you already have a `current_user` method you just need to add a
|
43
|
-
`
|
45
|
+
`clearance_levels` method to the user. With a role based authorization you
|
44
46
|
may add the following to your `User` model:
|
45
47
|
|
46
48
|
```ruby
|
47
49
|
class User < ActiveRecord::Base
|
48
50
|
belongs_to :role
|
49
51
|
|
50
|
-
def
|
52
|
+
def clearance_levels
|
53
|
+
# Single role name
|
51
54
|
role.name
|
52
55
|
end
|
53
56
|
end
|
54
57
|
```
|
55
58
|
|
59
|
+
or
|
60
|
+
|
61
|
+
```ruby
|
62
|
+
class User < ActiveRecord::Base
|
63
|
+
has_and_belongs_to_many :roles
|
64
|
+
|
65
|
+
def clearance_levels
|
66
|
+
# Array of role names
|
67
|
+
roles.pluck(:name)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
```
|
71
|
+
|
72
|
+
|
56
73
|
* No `current_user`:
|
57
74
|
|
58
|
-
If there's no `current_user` you need to override `
|
59
|
-
with whatever logic
|
75
|
+
If there's no `current_user` you need to override `current_clearance_levels`
|
76
|
+
with whatever logic applies to your application.
|
60
77
|
|
61
78
|
Continuing with the role based example, you might do something like this:
|
62
79
|
|
63
80
|
```ruby
|
64
81
|
class ApplicationController < ActionController::Base
|
65
|
-
def
|
82
|
+
def current_clearance_levels
|
66
83
|
session[:role] || :guest
|
67
84
|
end
|
68
85
|
end
|
@@ -72,8 +89,8 @@ it doesn't matter if it's singular or plural, it'll be singularized.
|
|
72
89
|
## Setting permissions
|
73
90
|
|
74
91
|
Permissions are set through authorization statements using the **let** class
|
75
|
-
method available in every controller.
|
76
|
-
|
92
|
+
method available in every controller. It takes the clearance levels (plural or
|
93
|
+
singular) first and the action or list of actions (array) as the last parameter.
|
77
94
|
|
78
95
|
As a simple example, to allow administrators (and only administrators in this
|
79
96
|
case) to delete articles you'd add the following to `ArticlesController`:
|
@@ -94,12 +111,14 @@ This will automatically **lock** the controller and only allow administrators
|
|
94
111
|
accessing the destroy action. **Every other request** pointing to the controller
|
95
112
|
**will be rejected** and redirected with an alert.
|
96
113
|
|
114
|
+
|
97
115
|
### Real-life example:
|
98
116
|
|
99
117
|
```ruby
|
100
118
|
class ArticlesController < ApplicationController
|
101
119
|
let :admins, :all
|
102
|
-
let :editors,
|
120
|
+
let :editors, :reviewers, [:edit, :update]
|
121
|
+
let :editors, :destroy
|
103
122
|
let :all, [:index, :show]
|
104
123
|
|
105
124
|
def index
|
@@ -112,21 +131,27 @@ end
|
|
112
131
|
|
113
132
|
These statements lock the controller and set the following:
|
114
133
|
* _Administrators_ (admins) are authorized to access any action.
|
115
|
-
* _Editors_ can list, view and
|
134
|
+
* _Editors_ can list, view, edit and destroy articles (can't create).
|
135
|
+
* _Reviewers_ can list, view and edit articles.
|
116
136
|
* _Anyone else_ can **only** list and view articles.
|
117
137
|
|
118
138
|
This case uses the special keyword `:all`, it means everyone if passed as the
|
119
|
-
first argument or every action if passed as the
|
139
|
+
first argument or every action if passed as the last one.
|
120
140
|
|
121
141
|
Again, any unauthorized request will be rejected and redirected with an alert.
|
122
142
|
|
143
|
+
|
123
144
|
### Note about clearance levels
|
124
145
|
|
125
146
|
Notice that in the previous examples we didn't need to define clearance levels
|
126
|
-
or roles anywhere else in the application. With the authorization
|
127
|
-
both **define** them and **set their permissions**. The only requirement is
|
128
|
-
that the clearance levels from the authorizations match
|
129
|
-
`
|
147
|
+
or roles anywhere else in the application. With the authorization statements
|
148
|
+
you both **define** them and **set their permissions**. The only requirement is
|
149
|
+
that the clearance levels from the authorizations match at least one from the
|
150
|
+
list returned by `current_clearance_levels`.
|
151
|
+
|
152
|
+
This makes it easier to embrace modular designs, makes controllers
|
153
|
+
self-contained because everything related to a controller is within the
|
154
|
+
controller and avoids leaving unnecessary or unused code after refactoring.
|
130
155
|
|
131
156
|
|
132
157
|
## Advanced configuration
|
@@ -151,6 +176,7 @@ end
|
|
151
176
|
To **unlock** a single controller (to make it "public") add `let :all, :all`,
|
152
177
|
this will allow anyone to access any action in the controller.
|
153
178
|
|
179
|
+
|
154
180
|
### Redirection path
|
155
181
|
|
156
182
|
By default any unauthorized (or not explicitly authorized) access will be
|
@@ -162,18 +188,21 @@ very clear `unauthorized_access_redirection_path` method.
|
|
162
188
|
```ruby
|
163
189
|
class ApplicationController < ActionController::Base
|
164
190
|
|
165
|
-
def
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
191
|
+
def unauthorized_access_redirection_path
|
192
|
+
# Ensure an array of symbols
|
193
|
+
clearance_levels = Array(current_user.clearance_levels).map(&:to_sym)
|
194
|
+
|
195
|
+
# Choose a redirection path
|
196
|
+
return admin_root_path if clearance_levels.include?(:admin)
|
197
|
+
return user_root_path if clearance_levels.include?(:user)
|
198
|
+
root_path
|
171
199
|
end
|
172
200
|
|
173
201
|
# ...
|
174
202
|
end
|
175
203
|
```
|
176
204
|
|
205
|
+
|
177
206
|
### Alert message
|
178
207
|
|
179
208
|
Redirections have a default alert message of "Not authorized.". To customize it
|
@@ -215,6 +244,7 @@ end
|
|
215
244
|
There are better ways to handle this particular case but it serves to outline
|
216
245
|
the use of `not_authorized!` inside actions.
|
217
246
|
|
247
|
+
|
218
248
|
### Model additions
|
219
249
|
|
220
250
|
Action Access is bundled with some model utilities too. By calling
|
@@ -233,20 +263,24 @@ of the resource and the namespace (optional). In the end it returns a boolean.
|
|
233
263
|
@user.can? :edit, :articles, namespace: :admin
|
234
264
|
@user.can? :edit, @admin_article # Admin::Article instance
|
235
265
|
@user.can? :edit, Admin::ArticlesController
|
236
|
-
# True if the user's clearance
|
266
|
+
# True if the user's clearance levels allow her to access 'admin/articles#edit'.
|
237
267
|
```
|
238
268
|
|
239
|
-
|
240
|
-
|
269
|
+
Just like the default `current_clearance_levels` in controllers, `can?`
|
270
|
+
depends on the `clearance_levels` method in the model too.
|
271
|
+
|
272
|
+
Following up the `User` model from before:
|
241
273
|
|
242
274
|
```ruby
|
243
275
|
class User < ActiveRecord::Base
|
244
276
|
add_access_utilities
|
245
277
|
|
246
|
-
|
278
|
+
has_and_belongs_to_many :roles
|
247
279
|
|
248
|
-
|
249
|
-
|
280
|
+
# Don't forget this!
|
281
|
+
def clearance_levels
|
282
|
+
# Array of role names
|
283
|
+
roles.pluck(:name)
|
250
284
|
end
|
251
285
|
end
|
252
286
|
```
|
@@ -264,12 +298,12 @@ permissions and who decides if a clearance level grants access or not.
|
|
264
298
|
|
265
299
|
It's available as `keeper` within controllers and views and as
|
266
300
|
`ActionAccess::Keeper.instance` anywhere else. You can use it to check
|
267
|
-
permissions with the `lets?` method, which takes the clearance level
|
268
|
-
first argument and the rest are the same as for `can?`.
|
301
|
+
permissions with the `lets?` method, which takes the clearance level (only one)
|
302
|
+
as the first argument and the rest are the same as for `can?`.
|
269
303
|
|
270
304
|
```ruby
|
271
|
-
# Filter a list of
|
272
|
-
|
305
|
+
# Filter a list of roles to only those that allow to edit articles.
|
306
|
+
roles.select { |role| keeper.lets? role, :edit, :articles }
|
273
307
|
```
|
274
308
|
|
275
309
|
|
@@ -282,4 +316,12 @@ Copyright (c) 2014 Matías A. Gagliano.
|
|
282
316
|
|
283
317
|
## Contributing
|
284
318
|
|
285
|
-
|
319
|
+
If you have *questions*, found an *issue* or want to submit a *pull request* you
|
320
|
+
must read the [CONTRIBUTING file](CONTRIBUTING.md).
|
321
|
+
|
322
|
+
- **DO NOT** use the issue tracker for **questions** or to require help, there
|
323
|
+
are other means for that (see the CONTRIBUTING file).
|
324
|
+
|
325
|
+
- **ALWAYS** open an issue before submitting a **pull request**, it won't be
|
326
|
+
accepted otherwise. Discussing changes beforehand will make your work much
|
327
|
+
more relevant.
|
data/action_access.gemspec
CHANGED
@@ -16,14 +16,32 @@ module ActionAccess
|
|
16
16
|
# Set an access rule for the current controller.
|
17
17
|
# It will automatically lock the controller if it wasn't already.
|
18
18
|
#
|
19
|
+
#
|
20
|
+
# == Parameters
|
21
|
+
#
|
22
|
+
# +clearance_levels+:: single clearance level (string or symbol) or list
|
23
|
+
# of them (list of parameters or array), either singular or plural.
|
24
|
+
# Accepts the special keyword +:all+ (every clearance level, even none).
|
25
|
+
#
|
26
|
+
# +permissions+:: controller action (string or symbol) or list of them (array).
|
27
|
+
# Accepts the special keyword +:all+ (every action in the controller).
|
28
|
+
#
|
29
|
+
#
|
19
30
|
# == Example:
|
20
|
-
# Add the following to ArticlesController to allow admins to edit articles.
|
21
|
-
# let :admin, [:edit, :update]
|
22
31
|
#
|
23
|
-
|
32
|
+
# class ArticlesControler < ApplicationController
|
33
|
+
# let :admins, :all # admins can do anything
|
34
|
+
# let :editors, :reviewers, [:edit, :update] # editors and reviewers can edit articles
|
35
|
+
# let :all, [:index, :show] # anyone can view articles
|
36
|
+
#
|
37
|
+
# # ...
|
38
|
+
# end
|
39
|
+
#
|
40
|
+
def let(*clearance_levels, permissions)
|
24
41
|
lock_access unless access_locked?
|
25
42
|
keeper = ActionAccess::Keeper.instance
|
26
|
-
|
43
|
+
clearance_levels = Array(clearance_levels).flatten
|
44
|
+
clearance_levels.each { |c| keeper.let c, permissions, self }
|
27
45
|
end
|
28
46
|
end
|
29
47
|
|
@@ -41,10 +59,19 @@ module ActionAccess
|
|
41
59
|
ActionAccess::Keeper.instance
|
42
60
|
end
|
43
61
|
|
44
|
-
#
|
45
|
-
def
|
46
|
-
|
47
|
-
|
62
|
+
# Current user's clearance levels (override to customize).
|
63
|
+
def current_clearance_levels
|
64
|
+
# Notify deprecation of `current_clearance_level` (singular)
|
65
|
+
if defined? current_clearance_level
|
66
|
+
ActiveSupport::Deprecation.warn \
|
67
|
+
'[Action Access] The use of "current_clearance_level" ' +
|
68
|
+
'is going to be deprecated in the next release, rename ' +
|
69
|
+
'it to "current_clearance_levels" (plural).'
|
70
|
+
return current_clearance_level
|
71
|
+
end
|
72
|
+
|
73
|
+
if defined?(current_user) and current_user.respond_to?(:clearance_levels)
|
74
|
+
current_user.clearance_levels
|
48
75
|
else
|
49
76
|
:guest
|
50
77
|
end
|
@@ -57,9 +84,10 @@ module ActionAccess
|
|
57
84
|
|
58
85
|
# Validate access to the current route.
|
59
86
|
def validate_access!
|
60
|
-
clearance_level = current_clearance_level
|
61
87
|
action = self.action_name
|
62
|
-
|
88
|
+
clearance_levels = Array(current_clearance_levels)
|
89
|
+
authorized = clearance_levels.any? { |c| keeper.lets? c, action, self.class }
|
90
|
+
not_authorized! unless authorized
|
63
91
|
end
|
64
92
|
|
65
93
|
# Redirect if not authorized.
|
data/lib/action_access/keeper.rb
CHANGED
@@ -7,10 +7,10 @@ module ActionAccess
|
|
7
7
|
end
|
8
8
|
|
9
9
|
# Set clearance to perform actions over a resource.
|
10
|
-
#
|
11
10
|
# Clearance level and resource can be either plural or singular.
|
12
11
|
#
|
13
12
|
# == Examples:
|
13
|
+
#
|
14
14
|
# let :user, :show, :profile
|
15
15
|
# let :user, :show, @profile
|
16
16
|
# let :user, :show, ProfilesController
|
@@ -26,15 +26,17 @@ module ActionAccess
|
|
26
26
|
actions = Array(actions).map(&:to_sym)
|
27
27
|
controller = get_controller_name(resource, options)
|
28
28
|
@rules[controller] ||= {}
|
29
|
-
@rules[controller][clearance_level]
|
29
|
+
@rules[controller][clearance_level] ||= []
|
30
|
+
@rules[controller][clearance_level] += actions
|
31
|
+
@rules[controller][clearance_level].uniq!
|
30
32
|
return nil
|
31
33
|
end
|
32
34
|
|
33
|
-
# Check if a given clearance level allows to perform
|
34
|
-
#
|
35
|
+
# Check if a given clearance level allows to perform an action on a resource.
|
35
36
|
# Clearance level and resource can be either plural or singular.
|
36
37
|
#
|
37
38
|
# == Examples:
|
39
|
+
#
|
38
40
|
# lets? :users, :create, :profiles
|
39
41
|
# lets? :users, :create, @profile
|
40
42
|
# lets? :users, :create, ProfilesController
|
@@ -1,39 +1,54 @@
|
|
1
1
|
module ActionAccess
|
2
2
|
module UserUtilities
|
3
3
|
# Check if the user is authorized to perform a given action.
|
4
|
-
#
|
5
4
|
# Resource can be either plural or singular.
|
6
5
|
#
|
7
6
|
# == Examples:
|
7
|
+
#
|
8
8
|
# user.can? :show, :articles
|
9
9
|
# user.can? :show, @article
|
10
10
|
# user.can? :show, ArticlesController
|
11
|
-
# # True if the user's clearance
|
11
|
+
# # True if any of the user's clearance levels allows to access 'articles#show'
|
12
12
|
#
|
13
13
|
# user.can? :edit, :articles, namespace: :admin
|
14
14
|
# user.can? :edit, @admin_article
|
15
15
|
# user.can? :edit, Admin::ArticlesController
|
16
|
-
# # True if the user's clearance
|
16
|
+
# # True if any of the user's clearance levels allows to access 'admin/articles#edit'
|
17
17
|
#
|
18
18
|
def can?(action, resource, options = {})
|
19
19
|
keeper = ActionAccess::Keeper.instance
|
20
|
-
|
20
|
+
clearance_levels = Array(clearance_levels())
|
21
|
+
clearance_levels.any? { |c| keeper.lets? c, action, resource, options }
|
21
22
|
end
|
22
23
|
|
23
24
|
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
25
|
+
# Accessor for the user's clearance levels.
|
26
|
+
#
|
27
|
+
# Must be *overridden* to set the proper clearance levels.
|
28
|
+
#
|
29
|
+
# == Examples:
|
30
|
+
#
|
31
|
+
# # Single clearance level (returns string)
|
32
|
+
# def clearance_levels
|
33
|
+
# role.name
|
34
|
+
# end
|
35
|
+
#
|
36
|
+
# # Multiple clearance levels (returns array)
|
37
|
+
# def clearance_levels
|
38
|
+
# roles.pluck(:name)
|
39
|
+
# end
|
40
|
+
#
|
41
|
+
def clearance_levels
|
42
|
+
# Notify deprecation of `clearance_level` (singular)
|
43
|
+
if defined? clearance_level
|
44
|
+
ActiveSupport::Deprecation.warn \
|
45
|
+
'[Action Access] The use of "clearance_level" in models ' +
|
46
|
+
'is going to be deprecated in the next release, rename ' +
|
47
|
+
'it to "clearance_levels" (plural).'
|
48
|
+
return clearance_level
|
37
49
|
end
|
50
|
+
|
51
|
+
:guest
|
52
|
+
end
|
38
53
|
end
|
39
54
|
end
|
@@ -1,7 +1,7 @@
|
|
1
1
|
require 'test_helper'
|
2
2
|
|
3
3
|
class ArticlesControllerTest < ActionController::TestCase
|
4
|
-
test "
|
4
|
+
test "accesses with no clearance level get redirected" do
|
5
5
|
# Undefined role
|
6
6
|
get :new
|
7
7
|
assert_redirected_to root_url
|
@@ -10,32 +10,54 @@ class ArticlesControllerTest < ActionController::TestCase
|
|
10
10
|
assert_redirected_to root_url
|
11
11
|
end
|
12
12
|
|
13
|
-
test "
|
14
|
-
# The role doesn't exist (
|
15
|
-
get :new, nil, {
|
13
|
+
test "accesses with undefined clearance level get redirected" do
|
14
|
+
# The role doesn't exist (haven't been defined)
|
15
|
+
get :new, nil, {roles: :root}
|
16
16
|
assert_redirected_to root_url
|
17
17
|
|
18
|
-
post :create, nil, {
|
18
|
+
post :create, nil, {roles: :root}
|
19
19
|
assert_redirected_to root_url
|
20
20
|
end
|
21
21
|
|
22
|
-
test "
|
22
|
+
test "unauthorized accesses get redirected" do
|
23
23
|
# The roles exist but aren't authorized.
|
24
|
-
|
24
|
+
post :create, nil, {roles: :editor}
|
25
25
|
assert_redirected_to root_url
|
26
26
|
|
27
|
-
|
27
|
+
get :edit, {id: 1}, {roles: :user}
|
28
28
|
assert_redirected_to root_url
|
29
29
|
end
|
30
30
|
|
31
31
|
test "authorized accesses aren't redirected" do
|
32
|
-
get :new, nil, {
|
32
|
+
get :new, nil, {roles: :admin}
|
33
33
|
assert_response :success
|
34
34
|
|
35
|
-
get :edit, {id: 1}, {
|
35
|
+
get :edit, {id: 1}, {roles: :editor}
|
36
|
+
assert_response :success
|
37
|
+
end
|
38
|
+
|
39
|
+
test "many clearance levels can be defined in one statement" do
|
40
|
+
get :index, nil, {roles: :editor}
|
36
41
|
assert_response :success
|
37
42
|
|
38
|
-
get :
|
43
|
+
get :index, nil, {roles: :cleaner}
|
39
44
|
assert_response :success
|
45
|
+
|
46
|
+
get :index, nil, {roles: :user}
|
47
|
+
assert_response :success
|
48
|
+
end
|
49
|
+
|
50
|
+
test "users can have many clearance levels" do
|
51
|
+
post :create, nil, {roles: [:editor, :cleaner]}
|
52
|
+
assert_redirected_to root_url
|
53
|
+
|
54
|
+
get :index, nil, {roles: [:editor, :cleaner]}
|
55
|
+
assert_response :success
|
56
|
+
|
57
|
+
get :edit, {id: 1}, {roles: [:editor, :cleaner]}
|
58
|
+
assert_response :success
|
59
|
+
|
60
|
+
delete :destroy, {id: 1}, {roles: [:editor, :cleaner]}
|
61
|
+
assert_redirected_to articles_url
|
40
62
|
end
|
41
63
|
end
|
@@ -4,7 +4,13 @@ class SecretsControllerTest < ActionController::TestCase
|
|
4
4
|
test "controllers are locked by default" do
|
5
5
|
# Test that the "lock_access" call in ApplicationController works properly.
|
6
6
|
# There are no access rules in SecretsController but it should be locked anyway.
|
7
|
-
get :index, nil, {
|
7
|
+
get :index, nil, {roles: :admin}
|
8
|
+
assert_redirected_to root_url
|
9
|
+
|
10
|
+
get :index, nil, {roles: :editor}
|
11
|
+
assert_redirected_to root_url
|
12
|
+
|
13
|
+
get :index
|
8
14
|
assert_redirected_to root_url
|
9
15
|
end
|
10
16
|
end
|
@@ -1,12 +1,12 @@
|
|
1
1
|
require 'test_helper'
|
2
2
|
|
3
3
|
class StaticControllerTest < ActionController::TestCase
|
4
|
-
# Test that the :all
|
4
|
+
# Test that the :all keyword works properly
|
5
5
|
test "anyone can do anything" do
|
6
|
-
get :home, nil, {
|
6
|
+
get :home, nil, {roles: :admin}
|
7
7
|
assert_response :success
|
8
8
|
|
9
|
-
get :home, nil, {
|
9
|
+
get :home, nil, {roles: :undefined}
|
10
10
|
assert_response :success
|
11
11
|
|
12
12
|
get :home
|
@@ -1,7 +1,8 @@
|
|
1
1
|
class ArticlesController < ApplicationController
|
2
2
|
let :admin, :all
|
3
|
-
let :editor, [:
|
4
|
-
let :
|
3
|
+
let :editor, [:edit, :update]
|
4
|
+
let :cleaner, :destroy
|
5
|
+
let :editor, :cleaner, :user, [:index, :show]
|
5
6
|
|
6
7
|
# GET /articles
|
7
8
|
def index
|
data/test/dummy/db/schema.rb
CHANGED
@@ -13,11 +13,11 @@
|
|
13
13
|
|
14
14
|
ActiveRecord::Schema.define(version: 20140926071026) do
|
15
15
|
|
16
|
-
create_table "users", force:
|
16
|
+
create_table "users", force: :cascade do |t|
|
17
17
|
t.string "username"
|
18
|
-
t.string "
|
19
|
-
t.datetime "created_at"
|
20
|
-
t.datetime "updated_at"
|
18
|
+
t.string "roles"
|
19
|
+
t.datetime "created_at", null: false
|
20
|
+
t.datetime "updated_at", null: false
|
21
21
|
end
|
22
22
|
|
23
23
|
end
|
data/test/models/user_test.rb
CHANGED
@@ -1,22 +1,43 @@
|
|
1
1
|
require 'test_helper'
|
2
2
|
|
3
3
|
class UserTest < ActiveSupport::TestCase
|
4
|
-
setup do
|
5
|
-
@admin ||= User.where({ username: 'flynn', role: 'admin' }).first_or_create
|
6
|
-
@user ||= User.where({ username: 'sam', role: 'user' }).first_or_create
|
7
|
-
end
|
8
|
-
|
9
4
|
test "the model gets extended" do
|
10
|
-
assert
|
5
|
+
assert user.respond_to? :can?
|
11
6
|
end
|
12
7
|
|
13
8
|
test "it allows to check permissions" do
|
14
9
|
# User
|
15
|
-
assert
|
16
|
-
assert_not
|
10
|
+
assert user.can?(:show, :articles)
|
11
|
+
assert_not user.can?(:create, :articles)
|
17
12
|
|
18
13
|
# Admin
|
19
|
-
assert
|
20
|
-
assert
|
14
|
+
assert admin.can?(:show, :articles)
|
15
|
+
assert admin.can?(:create, :articles)
|
16
|
+
end
|
17
|
+
|
18
|
+
test "users can have more than one role" do
|
19
|
+
# Assert the mixed user is both an 'editor' and a 'cleaner'
|
20
|
+
assert mixed.can?(:show, :articles) # Editors and cleaners
|
21
|
+
assert mixed.can?(:edit, :articles) # Editors only
|
22
|
+
assert mixed.can?(:destroy, :articles) # Cleaners only
|
23
|
+
|
24
|
+
# Assert the mixed user is not an 'admin'
|
25
|
+
assert_not mixed.can?(:new, :articles)
|
26
|
+
assert_not mixed.can?(:create, :articles)
|
21
27
|
end
|
28
|
+
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def admin
|
33
|
+
@admin ||= User.where({ username: 'flynn', roles: 'admin' }).first_or_create
|
34
|
+
end
|
35
|
+
|
36
|
+
def mixed
|
37
|
+
@mixed ||= User.where({ username: 'clu', roles: 'editor,cleaner' }).first_or_create
|
38
|
+
end
|
39
|
+
|
40
|
+
def user
|
41
|
+
@user ||= User.where({ username: 'sam', roles: 'user' }).first_or_create
|
42
|
+
end
|
22
43
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: action_access
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
4
|
+
version: 0.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Matías A. Gagliano
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2015-06
|
11
|
+
date: 2015-10-06 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rails
|
@@ -75,6 +75,7 @@ extra_rdoc_files: []
|
|
75
75
|
files:
|
76
76
|
- ".gitignore"
|
77
77
|
- ".travis.yml"
|
78
|
+
- CHANGELOG.md
|
78
79
|
- CONTRIBUTING.md
|
79
80
|
- Gemfile
|
80
81
|
- Gemfile.lock
|