joshuaclayton-sentinel 0.1.0 → 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- data/README.textile +22 -22
- data/Rakefile +1 -1
- data/lib/sentinel/controller.rb +1 -0
- data/sentinel.gemspec +2 -2
- data/shoulda_macros/sentinel.rb +29 -4
- data/test/functional/sentinel_controller_test.rb +18 -9
- data/test/partial_rails/controllers/forums_controller.rb +19 -1
- metadata +2 -2
data/README.textile
CHANGED
@@ -8,7 +8,7 @@ Sentinels are objects that track permissions. They're flexible, handy, and very
|
|
8
8
|
|
9
9
|
For example, here's a ForumSentinel:
|
10
10
|
|
11
|
-
<code
|
11
|
+
<pre><code>
|
12
12
|
class ForumSentinel < Sentinel::Sentinel
|
13
13
|
def creatable?
|
14
14
|
current_user_admin?
|
@@ -40,7 +40,7 @@ class ForumSentinel < Sentinel::Sentinel
|
|
40
40
|
current_user? && self.current_user.admin?
|
41
41
|
end
|
42
42
|
end
|
43
|
-
</pre
|
43
|
+
</code></pre>
|
44
44
|
|
45
45
|
So, what's this guy do? He personally tracks ability to essentially CRUD a forum, based on the current user.
|
46
46
|
|
@@ -54,7 +54,7 @@ But, there's more.
|
|
54
54
|
|
55
55
|
You may be asking, "What about when I'm looping through a recordset and want to determine permissions on the fly?" That's a legitimate question, really. I've got an easy answer for you. Imagine your view looks something like this:
|
56
56
|
|
57
|
-
<code
|
57
|
+
<pre><code>
|
58
58
|
<% @forums.each do |forum| %>
|
59
59
|
<% sentinel = ForumSentinel.new(:current_user => current_user, :forum => forum) %>
|
60
60
|
<% if sentinel.viewable? %>
|
@@ -64,11 +64,11 @@ You may be asking, "What about when I'm looping through a recordset and want to
|
|
64
64
|
</div>
|
65
65
|
<% end %>
|
66
66
|
<% end %>
|
67
|
-
</pre
|
67
|
+
</code></pre>
|
68
68
|
|
69
69
|
You get the idea. This is still pretty nasty though, since we're instantiating a new sentinel for each item in the recordset. Let's handle this in the controller.
|
70
70
|
|
71
|
-
<code
|
71
|
+
<pre><code>
|
72
72
|
class ForumsController < ApplicationController
|
73
73
|
controls_access_with do
|
74
74
|
ForumSentinel.new :current_user => current_user, :forum => @forum
|
@@ -76,11 +76,11 @@ class ForumsController < ApplicationController
|
|
76
76
|
|
77
77
|
# ...etc
|
78
78
|
end
|
79
|
-
</pre
|
79
|
+
</code></pre>
|
80
80
|
|
81
|
-
Here, we setup the sentinel in the controller and make a @sentinel@ view helper to access the instantiated object. So, if
|
81
|
+
Here, we setup the sentinel in the controller and make a @sentinel@ view helper to access the instantiated object. So, if @@forum@ is set up in the show action, we'll have access to it. The index action, not so much. Not to fear.
|
82
82
|
|
83
|
-
<code
|
83
|
+
<pre><code>
|
84
84
|
<% @forums.each do |forum| %>
|
85
85
|
<% if sentinel[:forum => forum].viewable? %>
|
86
86
|
<div id="<%= dom_id(forum) %>">
|
@@ -89,17 +89,17 @@ Here, we setup the sentinel in the controller and make a @sentinel@ view helper
|
|
89
89
|
</div>
|
90
90
|
<% end %>
|
91
91
|
<% end %>
|
92
|
-
</pre
|
92
|
+
</code></pre>
|
93
93
|
|
94
|
-
Essentially the same view as before, except we're not instantiating on every line and it keeps the view nice and clean. Notice we call
|
94
|
+
Essentially the same view as before, except we're not instantiating on every line and it keeps the view nice and clean. Notice we call @[]@, passing in a hash? Those are _temporary_ (as in, that call only) overrides. We assign forum to the current forum we're looping through and have the sentinel return permissions scoped to itself with whatever overrides.
|
95
95
|
|
96
|
-
So, handling permissions in the views are pretty easy now; hell, testing should be pretty simple too, since stubbing out simple methods like
|
96
|
+
So, handling permissions in the views are pretty easy now; hell, testing should be pretty simple too, since stubbing out simple methods like @viewable?@, @editable?@, etc will be cake.
|
97
97
|
|
98
98
|
"What about the controllers?" you may ask. Don't worry about the controllers; this is just as easy.
|
99
99
|
|
100
100
|
I introduce to you... @grants_access_to@.
|
101
101
|
|
102
|
-
<code
|
102
|
+
<pre><code>
|
103
103
|
class ForumsController < ApplicationController
|
104
104
|
controls_access_with do
|
105
105
|
ForumSentinel.new :current_user => current_user, :forum => @forum
|
@@ -110,13 +110,13 @@ class ForumsController < ApplicationController
|
|
110
110
|
grants_access_to :viewable?, :only => [:show]
|
111
111
|
grants_access_to :destroyable?, :only => [:destroy]
|
112
112
|
end
|
113
|
-
</pre
|
113
|
+
</code></pre>
|
114
114
|
|
115
115
|
@grants_access_to@ is essentially a @before_filter@ on crack. It uses the sentinel we've set up and calls methods on it. So, if the sentinel returns true when :reorderable? is called, it won't deny the request. Other filters, however, may.
|
116
116
|
|
117
117
|
You need not call methods on the sentinel if you don't want to. Let's say you want to check if a user is logged in and an admin (contrived example, I know).
|
118
118
|
|
119
|
-
<code
|
119
|
+
<pre><code>
|
120
120
|
class ForumsController < ApplicationController
|
121
121
|
controls_access_with do
|
122
122
|
ForumSentinel.new :current_user => current_user, :forum => @forum
|
@@ -130,13 +130,13 @@ class ForumsController < ApplicationController
|
|
130
130
|
s.creatable? && s.forum.private?
|
131
131
|
end
|
132
132
|
end
|
133
|
-
</pre
|
133
|
+
</code></pre>
|
134
134
|
|
135
135
|
The first @grants_access_to@ evaluates in the scope of the controller. If the block passed has an arity of 1 (one required block-level variable), it evaluates in the context of the sentinel.
|
136
136
|
|
137
137
|
When granting access, you may want to handle different checks differently. You can essentially how the controller handles how things are denied. For example, you may want to include a couple basics within @ApplicationController@.
|
138
138
|
|
139
|
-
<code
|
139
|
+
<pre><code>
|
140
140
|
class ApplicationController < ActionController::Base
|
141
141
|
on_denied_with :forbid_access do
|
142
142
|
respond_to do |wants|
|
@@ -157,11 +157,11 @@ class ApplicationController < ActionController::Base
|
|
157
157
|
end
|
158
158
|
end
|
159
159
|
end
|
160
|
-
</pre
|
160
|
+
</code></pre>
|
161
161
|
|
162
162
|
If these are set up, you can then have your actions deny with whatever you want, like so:
|
163
163
|
|
164
|
-
<code
|
164
|
+
<pre><code>
|
165
165
|
class ForumsController < ApplicationController
|
166
166
|
controls_access_with do
|
167
167
|
ForumSentinel.new :current_user => current_user, :forum => @forum
|
@@ -172,7 +172,7 @@ class ForumsController < ApplicationController
|
|
172
172
|
grants_access_to :viewable?, :only => [:show], :denies_with => :unauthorized
|
173
173
|
grants_access_to :destroyable?, :only => [:destroy], :denies_with => :forbidden
|
174
174
|
end
|
175
|
-
</pre
|
175
|
+
</code></pre>
|
176
176
|
|
177
177
|
Testing the sentinels themselves are fairly easy to do; I won't go into detail with that.
|
178
178
|
|
@@ -180,7 +180,7 @@ Testing the controllers, however, can be a bit tricky. Luckily, there are a han
|
|
180
180
|
|
181
181
|
Here's a short example of what you may want to test:
|
182
182
|
|
183
|
-
<code
|
183
|
+
<pre><code>
|
184
184
|
class SentinelControllerTest < ActionController::TestCase
|
185
185
|
include ActionView::Helpers::UrlHelper
|
186
186
|
include ActionView::Helpers::TagHelper
|
@@ -207,9 +207,9 @@ class SentinelControllerTest < ActionController::TestCase
|
|
207
207
|
should_grant_access_to "post :create, :forum => {:name => 'My New Forum'}"
|
208
208
|
end
|
209
209
|
end
|
210
|
-
</pre
|
210
|
+
</code></pre>
|
211
211
|
|
212
|
-
@sentinel_context@ allows you to stub out responses for whatever methods you want on the sentinel. Assign attributes (
|
212
|
+
@sentinel_context@ allows you to stub out responses for whatever methods you want on the sentinel. Assign attributes (@:current_user@, @:forum@, etc) or stub the permission methods themselves (that's what I would recommend, since your sentinel unit tests should check what the permissions return).
|
213
213
|
|
214
214
|
@should_not_guard@ ensures that @grants_access_to@ never gets called on that action. @should_grant_access_to@ and @should_deny_access_to@ are fairly straightforward. If @grants_access_to@ denies with a certain handler, you'll want to pass that handler name in (otherwise, you'll have failing tests).
|
215
215
|
|
data/Rakefile
CHANGED
@@ -3,7 +3,7 @@ require 'rake'
|
|
3
3
|
require 'echoe'
|
4
4
|
require 'rake/rdoctask'
|
5
5
|
|
6
|
-
Echoe.new("sentinel", "0.1.
|
6
|
+
Echoe.new("sentinel", "0.1.1") do |p|
|
7
7
|
p.description = "Simple authorization for Rails"
|
8
8
|
p.url = "http://github.com/joshuaclayton/sentinel"
|
9
9
|
p.author = "Joshua Clayton"
|
data/lib/sentinel/controller.rb
CHANGED
data/sentinel.gemspec
CHANGED
@@ -2,11 +2,11 @@
|
|
2
2
|
|
3
3
|
Gem::Specification.new do |s|
|
4
4
|
s.name = %q{sentinel}
|
5
|
-
s.version = "0.1.
|
5
|
+
s.version = "0.1.1"
|
6
6
|
|
7
7
|
s.required_rubygems_version = Gem::Requirement.new(">= 1.2") if s.respond_to? :required_rubygems_version=
|
8
8
|
s.authors = ["Joshua Clayton"]
|
9
|
-
s.date = %q{2009-04-
|
9
|
+
s.date = %q{2009-04-08}
|
10
10
|
s.description = %q{Simple authorization for Rails}
|
11
11
|
s.email = %q{joshua.clayton@gmail.com}
|
12
12
|
s.extra_rdoc_files = ["lib/sentinel/controller.rb", "lib/sentinel/sentinel.rb", "lib/sentinel.rb", "README.textile"]
|
data/shoulda_macros/sentinel.rb
CHANGED
@@ -18,10 +18,27 @@ module Sentinel
|
|
18
18
|
|
19
19
|
def denied_with(denied_name, &block)
|
20
20
|
context "denied_with #{denied_name}" do
|
21
|
+
without_before_filters do # this strips out any other preconditions so we can properly test the handler
|
22
|
+
setup do
|
23
|
+
action = "action_#{Digest::MD5.hexdigest(Time.now.to_s.split(//).sort_by {rand}.join)}"
|
24
|
+
@controller.class.grants_access_to lambda { false }, :only => [action.to_sym], :denies_with => denied_name
|
25
|
+
get action.to_sym
|
26
|
+
end
|
27
|
+
|
28
|
+
merge_block(&block)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def without_before_filters(&block)
|
34
|
+
context "" do
|
21
35
|
setup do
|
22
|
-
|
23
|
-
@controller.class.
|
24
|
-
|
36
|
+
@filter_chain = @controller.class.filter_chain
|
37
|
+
@controller.class.write_inheritable_attribute("filter_chain", ActionController::Filters::FilterChain.new)
|
38
|
+
end
|
39
|
+
|
40
|
+
teardown do
|
41
|
+
@controller.class.write_inheritable_attribute("filter_chain", @filter_chain)
|
25
42
|
end
|
26
43
|
|
27
44
|
merge_block(&block)
|
@@ -51,12 +68,20 @@ module Sentinel
|
|
51
68
|
|
52
69
|
def should_not_guard(command)
|
53
70
|
context "performing `#{command}`" do
|
54
|
-
|
71
|
+
setup do
|
55
72
|
@controller.class.expects(:access_granted).never
|
56
73
|
@controller.class.expects(:access_denied).never
|
74
|
+
@controller.class_eval do
|
75
|
+
def rescue_action(e) raise e end; # force the controller to reraise the exception error
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
should "not use guard with a sentinel" do
|
80
|
+
eval command
|
57
81
|
end
|
58
82
|
end
|
59
83
|
end
|
84
|
+
|
60
85
|
end
|
61
86
|
end
|
62
87
|
|
@@ -9,7 +9,9 @@ class SentinelControllerTest < ActionController::TestCase
|
|
9
9
|
end
|
10
10
|
|
11
11
|
sentinel_context do
|
12
|
-
|
12
|
+
without_before_filters do
|
13
|
+
should_not_guard "get :index"
|
14
|
+
end
|
13
15
|
end
|
14
16
|
|
15
17
|
sentinel_context({:viewable? => true}) do
|
@@ -30,15 +32,22 @@ class SentinelControllerTest < ActionController::TestCase
|
|
30
32
|
should_deny_access_to "get :show", :with => :sentinel_unauthorized
|
31
33
|
end
|
32
34
|
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
should_respond_with :forbidden
|
35
|
+
context "A controller-global grants_access_to that denies access" do
|
36
|
+
# this ensures that, even with a failing grants_access_to, we can properly test all denied_with handlers
|
37
|
+
setup do
|
38
|
+
@controller.stubs(:stubbed_method).returns(false)
|
39
|
+
end
|
39
40
|
|
40
|
-
|
41
|
-
|
41
|
+
denied_with :redirect_to_index do
|
42
|
+
should_redirect_to("forums root") { url_for(:controller => "forums", :action => "secondary_index")}
|
43
|
+
end
|
44
|
+
|
45
|
+
denied_with :sentinel_unauthorized do
|
46
|
+
should_respond_with :forbidden
|
47
|
+
|
48
|
+
should "render text as response" do
|
49
|
+
assert_equal "This is an even more unique default restricted warning", @response.body
|
50
|
+
end
|
42
51
|
end
|
43
52
|
end
|
44
53
|
end
|
@@ -3,13 +3,19 @@ class ForumsController < ApplicationController
|
|
3
3
|
ForumSentinel.new :current_user => current_user, :forum => @forum
|
4
4
|
end
|
5
5
|
|
6
|
+
grants_access_to lambda { stubbed_method }, :denies_with => :redirect_to_index
|
7
|
+
|
8
|
+
grants_access_to :denies_with => :sentinel_unauthorized do
|
9
|
+
stubbed_method_two
|
10
|
+
end
|
11
|
+
|
6
12
|
grants_access_to :reorderable?, :only => [:reorder]
|
7
13
|
grants_access_to :creatable?, :only => [:new, :create], :denies_with => :redirect_to_index
|
8
14
|
grants_access_to :viewable?, :only => [:show], :denies_with => :sentinel_unauthorized
|
9
15
|
grants_access_to :destroyable?, :only => [:destroy]
|
10
16
|
|
11
17
|
on_denied_with :redirect_to_index do
|
12
|
-
redirect_to url_for(:controller => "forums")
|
18
|
+
redirect_to url_for(:controller => "forums", :action => "secondary_index")
|
13
19
|
end
|
14
20
|
|
15
21
|
on_denied_with :sentinel_unauthorized do
|
@@ -23,6 +29,10 @@ class ForumsController < ApplicationController
|
|
23
29
|
handle_successfully
|
24
30
|
end
|
25
31
|
|
32
|
+
def secondary_index
|
33
|
+
handle_successfully
|
34
|
+
end
|
35
|
+
|
26
36
|
def new
|
27
37
|
handle_successfully
|
28
38
|
end
|
@@ -48,4 +58,12 @@ class ForumsController < ApplicationController
|
|
48
58
|
def handle_successfully
|
49
59
|
render :text => "forums"
|
50
60
|
end
|
61
|
+
|
62
|
+
def stubbed_method
|
63
|
+
true
|
64
|
+
end
|
65
|
+
|
66
|
+
def stubbed_method_two
|
67
|
+
true
|
68
|
+
end
|
51
69
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: joshuaclayton-sentinel
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Joshua Clayton
|
@@ -9,7 +9,7 @@ autorequire:
|
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
11
|
|
12
|
-
date: 2009-04-
|
12
|
+
date: 2009-04-08 00:00:00 -07:00
|
13
13
|
default_executable:
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|