stonewall 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.document +5 -0
- data/.gitignore +21 -0
- data/LICENSE +20 -0
- data/README.rdoc +27 -0
- data/Rakefile +56 -0
- data/VERSION +1 -0
- data/design_notes.txt +427 -0
- data/lib/stonewall/access_controller.rb +58 -0
- data/lib/stonewall/helpers.rb +24 -0
- data/lib/stonewall/parser.rb +60 -0
- data/lib/stonewall/stonewall.rb +37 -0
- data/lib/stonewall/user_extensions.rb +48 -0
- data/lib/stonewall.rb +6 -0
- data/test/helper.rb +10 -0
- data/test/test_stonewall.rb +7 -0
- metadata +100 -0
data/.document
ADDED
data/.gitignore
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2009 bokmann
|
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.rdoc
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
= stonewall
|
2
|
+
|
3
|
+
Stonewall is a state-based access control language extracted from the StonePath Stateful Workflow Modeling Gem (which was itself an extraction on a couple of projects, which were based on an internal Java workflow tool dating back to ~1998). Long roots, but the ideas are being freshly re-evaluated in the context of an awesome language.
|
4
|
+
|
5
|
+
StoneWall is not meant to be stand-alone - it will likely make assumptions about ActiveRecord (or ActiveModel when I start thinking about Rails 3). It will be wedded into some rails concepts like controlers, etc.
|
6
|
+
|
7
|
+
== Why another ACL? Aren't there like, 30 of em?
|
8
|
+
StoneWall wants to be a complimentary dsl in the model that works alongside the StonePath DSL, allowing access control to be based on not just objects, users, and roles, but also on the state of the object being accessed... as in "A data entry clerk has read/write access while we are in the data entry state, read-only while we are in the data validation state, and no access at all in any other state."
|
9
|
+
|
10
|
+
Further, I have some opinions about where access control belongs. Access control is a business logic thing, and belongs in the model. How to deal with blocked access is a controller thing, and how to vary the display of data when your rights change is a view thing. Several acl projects out there act as if it belongs in the controller.
|
11
|
+
|
12
|
+
== What does it look like?
|
13
|
+
There was an earlier, functional version in StonePath gems prior to 0.3.0. This work will be based on that work, but I'm re-evaluating everything about that earlier implementation. The first attempt at an extraction wasn't 'opinionated' enough, and it suffered because of it.
|
14
|
+
|
15
|
+
== Note on Patches/Pull Requests
|
16
|
+
|
17
|
+
* Fork the project.
|
18
|
+
* Make your feature addition or bug fix.
|
19
|
+
* Add tests for it. This is important so I don't break it in a
|
20
|
+
future version unintentionally.
|
21
|
+
* Commit, do not mess with rakefile, version, or history.
|
22
|
+
(if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
|
23
|
+
* Send me a pull request. Bonus points for topic branches.
|
24
|
+
|
25
|
+
== Copyright
|
26
|
+
|
27
|
+
Copyright (c) 2010 bokmann. See LICENSE for details.
|
data/Rakefile
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
|
4
|
+
begin
|
5
|
+
require 'jeweler'
|
6
|
+
Jeweler::Tasks.new do |gem|
|
7
|
+
gem.name = "stonewall"
|
8
|
+
gem.summary = %Q{extracting the acl constructs from stonepath}
|
9
|
+
gem.description = %Q{The acl from StoneWall, now as a shiny new gem!}
|
10
|
+
gem.email = "dbock@codesherpas.com"
|
11
|
+
gem.homepage = "http://github.com/bokmann/stonewall"
|
12
|
+
gem.authors = ["bokmann"]
|
13
|
+
gem.add_dependency('activerecord','>= 2.0.0')
|
14
|
+
gem.add_dependency('sentient_user','>= 0.1.0')
|
15
|
+
|
16
|
+
gem.add_development_dependency "thoughtbot-shoulda", ">= 0"
|
17
|
+
# gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
|
18
|
+
end
|
19
|
+
Jeweler::GemcutterTasks.new
|
20
|
+
rescue LoadError
|
21
|
+
puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
|
22
|
+
end
|
23
|
+
|
24
|
+
require 'rake/testtask'
|
25
|
+
Rake::TestTask.new(:test) do |test|
|
26
|
+
test.libs << 'lib' << 'test'
|
27
|
+
test.pattern = 'test/**/test_*.rb'
|
28
|
+
test.verbose = true
|
29
|
+
end
|
30
|
+
|
31
|
+
begin
|
32
|
+
require 'rcov/rcovtask'
|
33
|
+
Rcov::RcovTask.new do |test|
|
34
|
+
test.libs << 'test'
|
35
|
+
test.pattern = 'test/**/test_*.rb'
|
36
|
+
test.verbose = true
|
37
|
+
end
|
38
|
+
rescue LoadError
|
39
|
+
task :rcov do
|
40
|
+
abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
task :test => :check_dependencies
|
45
|
+
|
46
|
+
task :default => :test
|
47
|
+
|
48
|
+
require 'rake/rdoctask'
|
49
|
+
Rake::RDocTask.new do |rdoc|
|
50
|
+
version = File.exist?('VERSION') ? File.read('VERSION') : ""
|
51
|
+
|
52
|
+
rdoc.rdoc_dir = 'rdoc'
|
53
|
+
rdoc.title = "stonewall #{version}"
|
54
|
+
rdoc.rdoc_files.include('README*')
|
55
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
56
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.0.1
|
data/design_notes.txt
ADDED
@@ -0,0 +1,427 @@
|
|
1
|
+
class Schpoo
|
2
|
+
|
3
|
+
stonewall do |acl|
|
4
|
+
#guarded methods are now deniedd unless specifically allowed.
|
5
|
+
acl.guard :method_name
|
6
|
+
acl.guard :group_name, [:method_names, :method2, :method3, :etc]
|
7
|
+
|
8
|
+
#multiple_role_policy :any | :all - this should actually be defined in an initializer, not the class
|
9
|
+
|
10
|
+
acl.access_for :assistant_manager do |role|
|
11
|
+
role.allow :all #magic that undoes all guards
|
12
|
+
role.allow :method_name
|
13
|
+
role.check :group_name, do |o, u| #pass in the object being guarded & the person requesting access
|
14
|
+
#return true or false
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
acl.authorization :verb do |user, object|
|
19
|
+
# return true/false
|
20
|
+
# can now say current_user.may_verb_schpoo?(my_schpoo) just like Aegis
|
21
|
+
# note that if you don't pass in a schpoo, you can check for class-level access. Thats up to you.
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
|
28
|
+
# if someone calls a method on a model that they aren't allowed to access, it throws an AccessViolationException
|
29
|
+
|
30
|
+
|
31
|
+
|
32
|
+
# overly verbose, but nice. no exception, just true, false.
|
33
|
+
user.may_call(:method).on(object)
|
34
|
+
user.may_call_method_on_schpoo?(my_schpoo) #less practical
|
35
|
+
user.may_send(object, :method) # probably easiest to implement
|
36
|
+
user.may_auth_verb_schpoo(my_schpoo)
|
37
|
+
|
38
|
+
|
39
|
+
class Schpoo
|
40
|
+
|
41
|
+
stonewall do |acl|
|
42
|
+
#guarded methods are now deniedd unless specifically allowed.
|
43
|
+
acl.guard :method_name
|
44
|
+
acl.guard :group_name, [:method_names, :method2, :method3, :etc]
|
45
|
+
|
46
|
+
#multiple_role_policy :any | :all
|
47
|
+
|
48
|
+
acl.varies_on(:aasm_state) do |variant|
|
49
|
+
variant.value("created") do |acl|
|
50
|
+
acl.access_for :assistant_manager do |role|
|
51
|
+
role.allow :all #magic that undoes all guards
|
52
|
+
role.allow :method_name
|
53
|
+
role.check :group_name, do |o, u| #pass in the object being guarded & the person requesting access
|
54
|
+
#return true or false
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
variant.value("approved") do |acl|
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
|
66
|
+
|
67
|
+
|
68
|
+
#This shows a variant that gets data out of the db.
|
69
|
+
|
70
|
+
class Schpoo
|
71
|
+
|
72
|
+
stonewall do |acl|
|
73
|
+
|
74
|
+
# This is the same dsl, but since it is just ruby code, you can mix it
|
75
|
+
# with ruby code that gets the data from a database.
|
76
|
+
# This code below is notional, but we coudl set it up as part of the
|
77
|
+
# whole framework if we want to get that complicated.
|
78
|
+
|
79
|
+
#acl.load_protected_groups_for(self.to_sym)
|
80
|
+
protected_groups = ProtectedGroups.find_by_class_name(self.to_sym)
|
81
|
+
protected_groups.each do |group|
|
82
|
+
acl.guard group.name.to_sym, group.methods
|
83
|
+
end
|
84
|
+
|
85
|
+
#acl.load_grants_policy_for(self.to_sym)
|
86
|
+
acl.varies_on(:aasm_state) do |variant|
|
87
|
+
states.each do |state|
|
88
|
+
variant.value(state) do |acl|
|
89
|
+
roles.each do |role|
|
90
|
+
acl.access_for role do |r|
|
91
|
+
allowed_methods = Grant.find_by_class_name_and_role(self.to_sym, role)
|
92
|
+
r.allow allowed_methods.collect{ |m| m.name.to_sym }
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
# from the db perspective, to keep things simple, we only support methods in
|
101
|
+
# groups. You can use a default group named "all" if you don't want to take
|
102
|
+
# avantage of groups.
|
103
|
+
#protected_groups_table # grouped_methods_table
|
104
|
+
#id name stonewall_id # protected_group_id method
|
105
|
+
|
106
|
+
|
107
|
+
#stonewalls_table
|
108
|
+
# class_name variant_attribute
|
109
|
+
#(the Stonewall object will also list all roles, methods, and method groups)
|
110
|
+
|
111
|
+
#grants_table
|
112
|
+
#stonewall_id variant role method method_group
|
113
|
+
|
114
|
+
|
115
|
+
class Schpoo
|
116
|
+
require stonewall
|
117
|
+
|
118
|
+
stonewall.load_from_db
|
119
|
+
|
120
|
+
stonewall do |acl|
|
121
|
+
acl.authorization :view do |user, object|
|
122
|
+
return ["administrator", "Developer", "Manager"].include?(user.role)
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
<% if current_user.may_view_schpoo? %>
|
128
|
+
<%# render link to view here %>
|
129
|
+
<% end %>
|
130
|
+
|
131
|
+
and use in your own before_filters to guard things like the show method.
|
132
|
+
|
133
|
+
Schpoo.reload_stonewall_policy
|
134
|
+
|
135
|
+
|
136
|
+
|
137
|
+
|
138
|
+
|
139
|
+
|
140
|
+
|
141
|
+
|
142
|
+
|
143
|
+
|
144
|
+
|
145
|
+
|
146
|
+
|
147
|
+
class Schpoo
|
148
|
+
|
149
|
+
stonewall do |acl|
|
150
|
+
# tells the framework that if roles specify nothing for a guarded method,
|
151
|
+
# default answer is allow (this is the default setting)
|
152
|
+
check_default :allow
|
153
|
+
|
154
|
+
# tells the framework that if roles specify nothing for a guarded method,
|
155
|
+
# default answer is denied
|
156
|
+
check_default :deny
|
157
|
+
|
158
|
+
|
159
|
+
# tells the framework users have multiple roles to check, and
|
160
|
+
# if default mode is allow:
|
161
|
+
# any deny is a denial
|
162
|
+
# if default mode is deny:
|
163
|
+
# any allow is an allowal
|
164
|
+
multiple_role_policy :any
|
165
|
+
|
166
|
+
# tells the framework users have multiple roles to check, and
|
167
|
+
# if default mode is allow:
|
168
|
+
# all must deny for denial
|
169
|
+
# if default mode is deny:
|
170
|
+
# all must allow for allowal
|
171
|
+
multiple_role_policy :all #this is the default setting
|
172
|
+
|
173
|
+
|
174
|
+
# declare what you want to guard everything else is unchecked
|
175
|
+
acl.guard_method :save
|
176
|
+
acl.guard_method :name
|
177
|
+
acl.guard_method :name=
|
178
|
+
acl.guard_method :description
|
179
|
+
acl.guard_method :description=
|
180
|
+
|
181
|
+
#you can group them for easy reference below
|
182
|
+
acl.method_group :modifiers, [:save, :name=, :description=]
|
183
|
+
acl.method_group :accessors, [:name, :description]
|
184
|
+
|
185
|
+
acl.access_for :assistant_manager do |role_definition|
|
186
|
+
role_definition.deny :name= #use deny when :check_default is :allow
|
187
|
+
role_definition.allow :name= #use allow when :check_default is deny
|
188
|
+
role_Definition.check :name=, { |object, user|
|
189
|
+
# used when you want to make up your mind at runtime.
|
190
|
+
return false
|
191
|
+
}
|
192
|
+
end
|
193
|
+
|
194
|
+
acl.access_for :peon do |role_definition}
|
195
|
+
role_definition.field_variant(:aasm_state) do |state|
|
196
|
+
state.variant(:in_process) do |variant_definition|
|
197
|
+
variant_definition.deny :name=
|
198
|
+
variant_definition.allow :name=
|
199
|
+
variant_definition.check :name=, { |object, user, variant, value|
|
200
|
+
return false
|
201
|
+
}
|
202
|
+
|
203
|
+
state.variant(:catch_all) ...
|
204
|
+
end
|
205
|
+
end
|
206
|
+
|
207
|
+
end
|
208
|
+
|
209
|
+
|
210
|
+
|
211
|
+
|
212
|
+
we could even have code blocks for getting the role when given a user, etc.
|
213
|
+
|
214
|
+
|
215
|
+
|
216
|
+
|
217
|
+
|
218
|
+
|
219
|
+
|
220
|
+
|
221
|
+
|
222
|
+
|
223
|
+
- access control is business logic and should be defined in the domain
|
224
|
+
- this is the kind of problem that can be specified with a terse internal dsl
|
225
|
+
- access is granular on methods on the instance or class level (on domain objects)
|
226
|
+
- the framework should stay out of the way unless I want it
|
227
|
+
- when I mark a method as guarded, it should be denied unless the access control specifically allows it.
|
228
|
+
- I will have a current_user attribute in my session
|
229
|
+
- my current_user will have either:
|
230
|
+
a role method that returns:
|
231
|
+
a string of the role name
|
232
|
+
or
|
233
|
+
an object that has a .name method that returns the role name
|
234
|
+
or
|
235
|
+
a roles method that returns a collection of objects that match the above description
|
236
|
+
- those role names will be used as part of the access control dsl
|
237
|
+
- if any of a users roles grants them access, they have access
|
238
|
+
- access control is governed by intersection of the users role and the method they are accessing
|
239
|
+
- optionally, access control is also governed by some state of the accessed object
|
240
|
+
- exceptions are reserved for exceptional circumstance
|
241
|
+
- models should throw exceptions if an access violation is attempted
|
242
|
+
- models should have a "can?" or 'may I?" methods that return true or false, so a prudent developer can avoid access exceptions
|
243
|
+
- controllers and views should have a terse method of checking access so that they can have logic flows to avoid exposing illegal operations.
|
244
|
+
- if the UI is designed well, an end user should not be able to generate access violation exceptions on the models
|
245
|
+
- the framework should give a minimal api to allow good ui design.
|
246
|
+
|
247
|
+
<% if current_user.is_allowed?(:access_to => "save", :on => this_complaint) %>
|
248
|
+
|
249
|
+
<% if current_user.may_print?(this_complaint) %>
|
250
|
+
|
251
|
+
<% if current_user.may_<authorization>?(this_complaint) %>
|
252
|
+
|
253
|
+
|
254
|
+
my_model.allows?(:method_symbol) #uses current user
|
255
|
+
my.model.allows_for_user?(:method_symbol, user)
|
256
|
+
|
257
|
+
|
258
|
+
|
259
|
+
|
260
|
+
|
261
|
+
# acl thoughts -
|
262
|
+
# I might want to pull acl out into its own gem.
|
263
|
+
# If I do, then the acl.for_state will get complicated.
|
264
|
+
# I might make it something that can be modeled like this:
|
265
|
+
#
|
266
|
+
# acl.for_field(:aasm_state).has_value("in_process") do
|
267
|
+
# end
|
268
|
+
#
|
269
|
+
# which would make this a *lot* more reusable in different
|
270
|
+
# contexts.
|
271
|
+
|
272
|
+
# -
|
273
|
+
# I think there is scizophrenia caused by the framework not
|
274
|
+
# committing to "deny unless access allowed" vs. "allow unless
|
275
|
+
# access denied". I think the principal of least surprise would
|
276
|
+
# dictate we allow unless specifically denied, and I'm almost
|
277
|
+
# ready to commit to that.
|
278
|
+
|
279
|
+
# -
|
280
|
+
# There are also issues when a use has one role vs. many roles.
|
281
|
+
# When there is one role, things are easy.
|
282
|
+
# When they have multiple roles, do we deny if _any_ of them are
|
283
|
+
# denied, or allow unless _all_ are denied? I think POLS would
|
284
|
+
# say allow unless all denied... but then, which return block do
|
285
|
+
# we return? Perhaps something simple like "the first one defined".
|
286
|
+
#
|
287
|
+
# -
|
288
|
+
# as its own gem though, stonepath acl would need a name... hmm...
|
289
|
+
# what do you call something near a stonepath that restricts access
|
290
|
+
# in certain directions? a stonewall!
|
291
|
+
#
|
292
|
+
# -
|
293
|
+
# defining things nested like state/role makes certain sense (as would
|
294
|
+
# role/state), but it makes answering some questions hard... for instance:
|
295
|
+
# "What are all the permissions that managers have? and "what are all the
|
296
|
+
# access_restrictions on the "in_process" state? You have to look across
|
297
|
+
# several tasks. PErhaps a rake task could help generate a report with
|
298
|
+
# this info for auditing purposes.
|
299
|
+
stonepath_acl do |acl|
|
300
|
+
|
301
|
+
acl.guard_method :save
|
302
|
+
acl.guard_method :name=
|
303
|
+
acl.guard_method :name
|
304
|
+
acl.guard_method :regarding=
|
305
|
+
acl.guard_method :regarding
|
306
|
+
acl.guard_method :description
|
307
|
+
acl.guard_method :description=
|
308
|
+
acl.method_group :modifiers, [:save, :name=, :regarding=, :description=]
|
309
|
+
acl.method_group :accessors, [:name, :regarding, :description]
|
310
|
+
|
311
|
+
acl.for_state :in_process do |state|
|
312
|
+
state.access_for_role :manager do |role|
|
313
|
+
role.allow_method :name #allow works if we are in deny first mode.
|
314
|
+
end
|
315
|
+
|
316
|
+
state.access_for_role :peon do |role|
|
317
|
+
role.deny_method :name=
|
318
|
+
role.deny_method :regarding=
|
319
|
+
role.deny_method(:description){ #value of block is returned. might this need to be a proc?
|
320
|
+
"#{user.name}, You do not have access to look at the description, you peon!"
|
321
|
+
}
|
322
|
+
end
|
323
|
+
|
324
|
+
state.access_for_role :assistant_manager do |role|
|
325
|
+
role.check_method_access :method_symbol do
|
326
|
+
# you can implement conditional logic here.
|
327
|
+
# maybe you want to defer the true/false
|
328
|
+
# access decision and check some attribute of
|
329
|
+
# the user.
|
330
|
+
return false #your conditional answer.
|
331
|
+
end
|
332
|
+
end
|
333
|
+
end
|
334
|
+
|
335
|
+
acl.for_state :completed do |state|
|
336
|
+
state.access_for_role :manager do |role|
|
337
|
+
role.deny_method_group :modifiers
|
338
|
+
end
|
339
|
+
|
340
|
+
state.access_for_role :peon do |role|
|
341
|
+
role.deny_method_group :modifiers
|
342
|
+
role.deny_method_group :accessors
|
343
|
+
end
|
344
|
+
|
345
|
+
end
|
346
|
+
end
|
347
|
+
|
348
|
+
|
349
|
+
|
350
|
+
|
351
|
+
|
352
|
+
in memory, the acl can be unrolled to a data structure like:
|
353
|
+
[method][role][pivot]
|
354
|
+
[method][role][pivot] = true | false
|
355
|
+
|
356
|
+
|
357
|
+
|
358
|
+
class Person < ActiveRecord::Base
|
359
|
+
include StoneWall
|
360
|
+
|
361
|
+
stonewall do |acl|
|
362
|
+
acl.guard :age
|
363
|
+
acl.guard :favorite_color
|
364
|
+
acl.method_group :privacy_factors, [:age, :favorite_color]
|
365
|
+
acl.varies_on :aasm_state
|
366
|
+
|
367
|
+
acl.action :edit
|
368
|
+
acl.action :print
|
369
|
+
acl.action :subscribe_to
|
370
|
+
|
371
|
+
role :manager do |manager|
|
372
|
+
manager.methods :privacy_factors
|
373
|
+
manager.actions [:edit, :print, :subscribe_to]
|
374
|
+
end
|
375
|
+
|
376
|
+
role :clerk do |clerk|
|
377
|
+
clerk.variant :data_entry do |data_entry|
|
378
|
+
data_entry.methods :age
|
379
|
+
data_entry.actions :edit
|
380
|
+
end
|
381
|
+
end
|
382
|
+
end
|
383
|
+
|
384
|
+
stonewall.permissions do |permissions|
|
385
|
+
permissions.add :edit |user, post| do
|
386
|
+
post.owner == user || user.is_an_admin?
|
387
|
+
end
|
388
|
+
end
|
389
|
+
|
390
|
+
end
|
391
|
+
|
392
|
+
|
393
|
+
class Person < ActiveRecord::Base
|
394
|
+
include StoneWall
|
395
|
+
|
396
|
+
stonewall do
|
397
|
+
varies_on :aasm_state
|
398
|
+
guard :age
|
399
|
+
guard :favorite_color
|
400
|
+
method_group :privacy_factors, [:age, :favorite_color]
|
401
|
+
|
402
|
+
role :manager do
|
403
|
+
allowed_methods :privacy_factors
|
404
|
+
end
|
405
|
+
|
406
|
+
role :clerk do
|
407
|
+
variant :data_entry do
|
408
|
+
allowed_method :age
|
409
|
+
end
|
410
|
+
end
|
411
|
+
|
412
|
+
action :edit do |person, user|
|
413
|
+
person.owner == user || user.is_administrator?
|
414
|
+
end
|
415
|
+
|
416
|
+
action :print do |person, user|
|
417
|
+
person.complete? && user.is_ogc_attorney?
|
418
|
+
end
|
419
|
+
end
|
420
|
+
end
|
421
|
+
|
422
|
+
|
423
|
+
user.may_edit_person? person
|
424
|
+
|
425
|
+
|
426
|
+
|
427
|
+
|
@@ -0,0 +1,58 @@
|
|
1
|
+
module StoneWall
|
2
|
+
|
3
|
+
# a word of caution, use this framework to guard 'normal user space' methods
|
4
|
+
# only - don't try to guard meta stuff like 'send'. If you have ever seen
|
5
|
+
# 'Being John Malkovich', you can understand why.
|
6
|
+
class AccessController
|
7
|
+
attr_reader :guarded_class
|
8
|
+
attr_reader :variant_field
|
9
|
+
attr_accessor :actions
|
10
|
+
attr_accessor :guarded_methods
|
11
|
+
attr_accessor :method_groups
|
12
|
+
|
13
|
+
# the matrix is the money-shot of the access controller. You can set it
|
14
|
+
# through add_grant, and query it through 'granted', but you can also
|
15
|
+
# access the data structure directly if you want to avoid the dsl.
|
16
|
+
# It is in the format [role][varient][member]
|
17
|
+
attr_accessor :matrix
|
18
|
+
|
19
|
+
def initialize(guarded_class)
|
20
|
+
@guarded_class = guarded_class
|
21
|
+
@guarded_methods = Array.new
|
22
|
+
@actions = Hash.new
|
23
|
+
@matrix = Hash.new
|
24
|
+
@method_groups = Hash.new
|
25
|
+
end
|
26
|
+
|
27
|
+
def set_variant_field(field)
|
28
|
+
if @variant_field.nil?
|
29
|
+
@variant_field = field
|
30
|
+
else
|
31
|
+
raise "no redefinition of variant field"
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def add_grant(r, v, m)
|
36
|
+
@matrix[r] ||= Hash.new
|
37
|
+
@matrix[r][v] ||= Array.new
|
38
|
+
@matrix[r][v] << m
|
39
|
+
end
|
40
|
+
|
41
|
+
def granted?(r, v, m)
|
42
|
+
matrix[r] && matrix[r][v] && matrix[r][v].include?(m)
|
43
|
+
end
|
44
|
+
|
45
|
+
# --------------
|
46
|
+
# This is 1/3rd of the magic in this gem. Every method you guard is
|
47
|
+
# checked by this method. It looks at the matrix of permissions you built
|
48
|
+
# in the dsl and allows or denies based on the guarded object, the user,
|
49
|
+
# and the method being accessed. #should we fail secure?
|
50
|
+
def allowed?(guarded_object, user, method)
|
51
|
+
return true if (guarded_object.nil? || user.nil? || method.nil?)
|
52
|
+
v = variant_field ? guarded_object.send(variant_field).to_sym : :all
|
53
|
+
user.stonepath_role_info.detect do |r|
|
54
|
+
granted?(r, v, method)
|
55
|
+
end || false
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module StoneWall
|
2
|
+
module Helpers
|
3
|
+
|
4
|
+
def self.symbolize_role(role)
|
5
|
+
[String, Symbol].include?(role.class) ? role.to_sym : role.name.to_sym
|
6
|
+
end
|
7
|
+
|
8
|
+
# --------------
|
9
|
+
# This is 1/3rd of the magic in this gem. We earlier built a
|
10
|
+
# 'schpoo_with_stonepath' method on your class, and now we use
|
11
|
+
# alias_method_chain to wrap your original 'schpoo' method.
|
12
|
+
# You will have no hope of understanding this if you don't understand
|
13
|
+
# alias_method_chain, so go memorize that documentation.
|
14
|
+
# We have to do this after ActoveRecord synthesizes the attribute methods
|
15
|
+
# with a call to 'define_attribute_methods'; you'll see some magic in the
|
16
|
+
# base.instance_eval in the other file to make that magic happen.
|
17
|
+
def self.fix_aliases_for(guarded_class)
|
18
|
+
guarded_class.stonewall.guarded_methods.each do |m|
|
19
|
+
guarded_class.send(:alias_method_chain, m, :stonewall)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
module StoneWall
|
2
|
+
class Parser
|
3
|
+
def initialize(parent, role = :all, variant = :all)
|
4
|
+
@parent, @role, @variant = parent, role, variant
|
5
|
+
@method_groups = Hash.new
|
6
|
+
end
|
7
|
+
|
8
|
+
def allowed_method(method)
|
9
|
+
@parent.stonewall.add_grant(@role, @variant, method)
|
10
|
+
end
|
11
|
+
|
12
|
+
def allowed_methods(method_names)
|
13
|
+
@parent.stonewall.method_groups[method_names].each do |m|
|
14
|
+
allowed_method m
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def method_group(name, methods)
|
19
|
+
@parent.stonewall.method_groups[name] = methods
|
20
|
+
end
|
21
|
+
|
22
|
+
def varies_on(field)
|
23
|
+
@parent.stonewall.set_variant_field(field)
|
24
|
+
end
|
25
|
+
|
26
|
+
def action(action_name, &guard)
|
27
|
+
@parent.stonewall.actions[action_name] = guard
|
28
|
+
end
|
29
|
+
|
30
|
+
def role(role_name)
|
31
|
+
yield Parser.new(@parent, role_name)
|
32
|
+
end
|
33
|
+
|
34
|
+
def variant(variant_name)
|
35
|
+
yield Parser.new(@parent, @role, variant_name)
|
36
|
+
end
|
37
|
+
|
38
|
+
def guard(method)
|
39
|
+
@parent.stonewall.guarded_methods << method
|
40
|
+
aliased_target, punctuation = method.to_s.sub(/([?!=])$/, ''), $1
|
41
|
+
checked_method = "#{aliased_target}_with_stonewall#{punctuation}"
|
42
|
+
unchecked_method = "#{aliased_target}_without_stonewall#{punctuation}"
|
43
|
+
# --------------
|
44
|
+
# This method is defined on the guarded class, so it is callable on
|
45
|
+
# objects of that class. This is 1/3rd of the magic of this gem-
|
46
|
+
# if you declare 'schpoo' a guarded method, we generate this
|
47
|
+
# 'schpoo_with_stonewall' method. Elsewhere, we use alias_method_chain
|
48
|
+
# to wrap your original 'schpoo' method.
|
49
|
+
@parent.send(:define_method, checked_method) do |*args|
|
50
|
+
if stonewall.allowed?(self, User.current, method)
|
51
|
+
self.send(unchecked_method, *args)
|
52
|
+
else
|
53
|
+
raise "Access Violation"
|
54
|
+
end
|
55
|
+
end
|
56
|
+
# -------------- end of bizzaro meta-juju
|
57
|
+
end
|
58
|
+
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module StoneWall
|
2
|
+
def self.included(base)
|
3
|
+
base.instance_eval do
|
4
|
+
def stonewall()
|
5
|
+
require File.expand_path(File.dirname(__FILE__)) +
|
6
|
+
"/access_controller.rb"
|
7
|
+
require File.expand_path(File.dirname(__FILE__)) +
|
8
|
+
"/parser.rb"
|
9
|
+
require File.expand_path(File.dirname(__FILE__)) +
|
10
|
+
"/helpers.rb"
|
11
|
+
require File.expand_path(File.dirname(__FILE__)) +
|
12
|
+
"/user_extensions.rb"
|
13
|
+
cattr_accessor :stonewall
|
14
|
+
self.stonewall = StoneWall::AccessController.new(self)
|
15
|
+
yield StoneWall::Parser.new(self)
|
16
|
+
end
|
17
|
+
|
18
|
+
# --------------
|
19
|
+
# This is 1/3rd of the magic in this gem. After ActiceRecord defines
|
20
|
+
# the classes attribute methods, we come along and alias all of the
|
21
|
+
# guarded methods defined in the dsl in the class.
|
22
|
+
def self.define_attribute_methods_with_stonewall
|
23
|
+
define_attribute_methods_without_stonewall
|
24
|
+
StoneWall::Helpers.fix_aliases_for(self)
|
25
|
+
end
|
26
|
+
|
27
|
+
class << self
|
28
|
+
alias_method_chain :define_attribute_methods, :stonewall
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
# mimicking the send method, we want to ask permission first with send?
|
33
|
+
def send?(method, user = User.current)
|
34
|
+
self.class.stonewall.allowed?(self, user, method)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module StoneWall
|
2
|
+
module UserExtensions
|
3
|
+
require File.expand_path(File.dirname(__FILE__)) + "/helpers.rb"
|
4
|
+
|
5
|
+
# This is the ugliest method in the whole gem.
|
6
|
+
# You have a lot of freedom in how you implement your role system, and
|
7
|
+
# I have to adapt to that. You can have:
|
8
|
+
# - role as a string on your class
|
9
|
+
# - role as a singular has_one or belongs_to relationship
|
10
|
+
# - your user can has_many :roles, and they can be any object you want.
|
11
|
+
# The only thing we require is that, if your role is a separate object,
|
12
|
+
# it responds to a 'name' method with a string or a symbol that matches
|
13
|
+
# the symbol you use when defining the permissions in your dsl.
|
14
|
+
def stonepath_role_info
|
15
|
+
return @role_symbols if @role_symbols
|
16
|
+
@role_symbols = Array.new
|
17
|
+
if self.respond_to?(:role)
|
18
|
+
@role_symbols << StoneWall::Helpers.symbolize_role(role)
|
19
|
+
end
|
20
|
+
|
21
|
+
if self.respond_to?(:roles)
|
22
|
+
roles.each do |role|
|
23
|
+
@role_symbols << StoneWall::Helpers.symbolize_role(role)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
@role_symbols
|
27
|
+
end
|
28
|
+
|
29
|
+
# I like Aegis so much, I stole their idea for this part of the gem. I
|
30
|
+
# hope you don't mind guys, but I really need an ACL that triggers off
|
31
|
+
# of object state!
|
32
|
+
def may_send?(object, method)
|
33
|
+
object.class.stonewall.allowed?(object, self, method)
|
34
|
+
end
|
35
|
+
|
36
|
+
def method_missing_with_stonewall(symb, *args)
|
37
|
+
method_name = symb.to_s
|
38
|
+
if method_name =~ /^may_(.+?)[\!\?]$/
|
39
|
+
args.first.class.stonewall.actions[$1.to_sym].call(args.first, self)
|
40
|
+
else
|
41
|
+
method_missing_without_stonewall(symb, *args)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
alias_method_chain :method_missing, :stonewall
|
46
|
+
|
47
|
+
end
|
48
|
+
end
|
data/lib/stonewall.rb
ADDED
@@ -0,0 +1,6 @@
|
|
1
|
+
$:.unshift(File.dirname(__FILE__)) unless
|
2
|
+
$:.include?(File.dirname(__FILE__)) ||
|
3
|
+
$:.include?(File.expand_path(File.dirname(__FILE__)))
|
4
|
+
|
5
|
+
require File.expand_path(File.dirname(__FILE__)) + "/stonewall/stonewall.rb"
|
6
|
+
require File.expand_path(File.dirname(__FILE__)) + "/stonewall/user_extensions.rb"
|
data/test/helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,100 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: stonewall
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- bokmann
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2010-03-20 00:00:00 -04:00
|
13
|
+
default_executable:
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: activerecord
|
17
|
+
type: :runtime
|
18
|
+
version_requirement:
|
19
|
+
version_requirements: !ruby/object:Gem::Requirement
|
20
|
+
requirements:
|
21
|
+
- - ">="
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: 2.0.0
|
24
|
+
version:
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: sentient_user
|
27
|
+
type: :runtime
|
28
|
+
version_requirement:
|
29
|
+
version_requirements: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 0.1.0
|
34
|
+
version:
|
35
|
+
- !ruby/object:Gem::Dependency
|
36
|
+
name: thoughtbot-shoulda
|
37
|
+
type: :development
|
38
|
+
version_requirement:
|
39
|
+
version_requirements: !ruby/object:Gem::Requirement
|
40
|
+
requirements:
|
41
|
+
- - ">="
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: "0"
|
44
|
+
version:
|
45
|
+
description: The acl from StoneWall, now as a shiny new gem!
|
46
|
+
email: dbock@codesherpas.com
|
47
|
+
executables: []
|
48
|
+
|
49
|
+
extensions: []
|
50
|
+
|
51
|
+
extra_rdoc_files:
|
52
|
+
- LICENSE
|
53
|
+
- README.rdoc
|
54
|
+
files:
|
55
|
+
- .document
|
56
|
+
- .gitignore
|
57
|
+
- LICENSE
|
58
|
+
- README.rdoc
|
59
|
+
- Rakefile
|
60
|
+
- VERSION
|
61
|
+
- design_notes.txt
|
62
|
+
- lib/stonewall.rb
|
63
|
+
- lib/stonewall/access_controller.rb
|
64
|
+
- lib/stonewall/helpers.rb
|
65
|
+
- lib/stonewall/parser.rb
|
66
|
+
- lib/stonewall/stonewall.rb
|
67
|
+
- lib/stonewall/user_extensions.rb
|
68
|
+
- test/helper.rb
|
69
|
+
- test/test_stonewall.rb
|
70
|
+
has_rdoc: true
|
71
|
+
homepage: http://github.com/bokmann/stonewall
|
72
|
+
licenses: []
|
73
|
+
|
74
|
+
post_install_message:
|
75
|
+
rdoc_options:
|
76
|
+
- --charset=UTF-8
|
77
|
+
require_paths:
|
78
|
+
- lib
|
79
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
80
|
+
requirements:
|
81
|
+
- - ">="
|
82
|
+
- !ruby/object:Gem::Version
|
83
|
+
version: "0"
|
84
|
+
version:
|
85
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: "0"
|
90
|
+
version:
|
91
|
+
requirements: []
|
92
|
+
|
93
|
+
rubyforge_project:
|
94
|
+
rubygems_version: 1.3.5
|
95
|
+
signing_key:
|
96
|
+
specification_version: 3
|
97
|
+
summary: extracting the acl constructs from stonepath
|
98
|
+
test_files:
|
99
|
+
- test/helper.rb
|
100
|
+
- test/test_stonewall.rb
|