joshuaclayton-sentinel 0.1.0 → 0.1.1
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/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
|