timcharper-declarative_authorization 0.4.1.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. data/CHANGELOG +135 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.rdoc +503 -0
  4. data/Rakefile +43 -0
  5. data/app/controllers/authorization_rules_controller.rb +259 -0
  6. data/app/controllers/authorization_usages_controller.rb +23 -0
  7. data/app/helpers/authorization_rules_helper.rb +218 -0
  8. data/app/views/authorization_rules/_change.erb +58 -0
  9. data/app/views/authorization_rules/_show_graph.erb +37 -0
  10. data/app/views/authorization_rules/_suggestions.erb +48 -0
  11. data/app/views/authorization_rules/change.html.erb +169 -0
  12. data/app/views/authorization_rules/graph.dot.erb +68 -0
  13. data/app/views/authorization_rules/graph.html.erb +40 -0
  14. data/app/views/authorization_rules/index.html.erb +17 -0
  15. data/app/views/authorization_usages/index.html.erb +36 -0
  16. data/authorization_rules.dist.rb +20 -0
  17. data/config/routes.rb +7 -0
  18. data/garlic_example.rb +20 -0
  19. data/init.rb +5 -0
  20. data/lib/declarative_authorization.rb +15 -0
  21. data/lib/declarative_authorization/authorization.rb +683 -0
  22. data/lib/declarative_authorization/development_support/analyzer.rb +252 -0
  23. data/lib/declarative_authorization/development_support/change_analyzer.rb +253 -0
  24. data/lib/declarative_authorization/development_support/change_supporter.rb +620 -0
  25. data/lib/declarative_authorization/development_support/development_support.rb +243 -0
  26. data/lib/declarative_authorization/helper.rb +60 -0
  27. data/lib/declarative_authorization/in_controller.rb +623 -0
  28. data/lib/declarative_authorization/in_model.rb +162 -0
  29. data/lib/declarative_authorization/maintenance.rb +198 -0
  30. data/lib/declarative_authorization/obligation_scope.rb +345 -0
  31. data/lib/declarative_authorization/rails_legacy.rb +14 -0
  32. data/lib/declarative_authorization/reader.rb +472 -0
  33. data/test/authorization_test.rb +971 -0
  34. data/test/controller_filter_resource_access_test.rb +511 -0
  35. data/test/controller_test.rb +465 -0
  36. data/test/dsl_reader_test.rb +157 -0
  37. data/test/helper_test.rb +171 -0
  38. data/test/maintenance_test.rb +46 -0
  39. data/test/model_test.rb +1694 -0
  40. data/test/schema.sql +54 -0
  41. data/test/test_helper.rb +134 -0
  42. metadata +119 -0
@@ -0,0 +1,14 @@
1
+ # Groups methods and additions that were used but are not readily available in
2
+ # all supported Rails versions.
3
+ class Hash
4
+ unless {}.respond_to?(:deep_merge)
5
+ # Imported from Rails 2.2
6
+ def deep_merge(other_hash)
7
+ self.merge(other_hash) do |key, oldval, newval|
8
+ oldval = oldval.to_hash if oldval.respond_to?(:to_hash)
9
+ newval = newval.to_hash if newval.respond_to?(:to_hash)
10
+ oldval.class.to_s == 'Hash' && newval.class.to_s == 'Hash' ? oldval.deep_merge(newval) : newval
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,472 @@
1
+ # Authorization::Reader
2
+
3
+ require File.dirname(__FILE__) + '/authorization.rb'
4
+
5
+ module Authorization
6
+ # Parses an authorization configuration file in the authorization DSL and
7
+ # constructs a data model of its contents.
8
+ #
9
+ # For examples and the modelled data model, see the
10
+ # README[link:files/README_rdoc.html].
11
+ #
12
+ # Also, see role definition methods
13
+ # * AuthorizationRulesReader#role,
14
+ # * AuthorizationRulesReader#includes,
15
+ # * AuthorizationRulesReader#title,
16
+ # * AuthorizationRulesReader#description
17
+ #
18
+ # Methods for rule definition in roles
19
+ # * AuthorizationRulesReader#has_permission_on,
20
+ # * AuthorizationRulesReader#to,
21
+ # * AuthorizationRulesReader#if_attribute,
22
+ # * AuthorizationRulesReader#if_permitted_to
23
+ #
24
+ # Methods to be used in if_attribute statements
25
+ # * AuthorizationRulesReader#contains,
26
+ # * AuthorizationRulesReader#does_not_contain,
27
+ # * AuthorizationRulesReader#intersects_with,
28
+ # * AuthorizationRulesReader#is,
29
+ # * AuthorizationRulesReader#is_not,
30
+ # * AuthorizationRulesReader#is_in,
31
+ # * AuthorizationRulesReader#is_not_in
32
+ #
33
+ # And privilege definition methods
34
+ # * PrivilegesReader#privilege,
35
+ # * PrivilegesReader#includes
36
+ #
37
+ module Reader
38
+ # Signals errors that occur while reading and parsing an authorization DSL
39
+ class DSLError < Exception; end
40
+ # Signals errors in the syntax of an authorization DSL.
41
+ class DSLSyntaxError < DSLError; end
42
+
43
+ # Top-level reader, parses the methods +privileges+ and +authorization+.
44
+ # +authorization+ takes a block with authorization rules as described in
45
+ # AuthorizationRulesReader. The block to +privileges+ defines privilege
46
+ # hierarchies, as described in PrivilegesReader.
47
+ #
48
+ class DSLReader
49
+ attr_reader :privileges_reader, :auth_rules_reader # :nodoc:
50
+
51
+ def initialize ()
52
+ @privileges_reader = PrivilegesReader.new
53
+ @auth_rules_reader = AuthorizationRulesReader.new
54
+ end
55
+
56
+ # Parses a authorization DSL specification from the string given
57
+ # in +dsl_data+. Raises DSLSyntaxError if errors occur on parsing.
58
+ def parse (dsl_data, file_name = nil)
59
+ if file_name
60
+ DSLMethods.new(self).instance_eval(dsl_data, file_name)
61
+ else
62
+ DSLMethods.new(self).instance_eval(dsl_data)
63
+ end
64
+ rescue SyntaxError, NoMethodError, NameError => e
65
+ raise DSLSyntaxError, "Illegal DSL syntax: #{e}"
66
+ end
67
+
68
+ # Loads and parses a DSL from the given file name.
69
+ def self.load (dsl_files)
70
+ # TODO cache reader in production mode?
71
+ reader = new
72
+ dsl_files = [dsl_files].flatten
73
+ dsl_files.each do |file|
74
+ reader.parse(File.read(file), file) if File.exist?(file)
75
+ end
76
+ reader
77
+ end
78
+
79
+ # DSL methods
80
+ class DSLMethods # :nodoc:
81
+ def initialize (parent)
82
+ @parent = parent
83
+ end
84
+
85
+ def privileges (&block)
86
+ @parent.privileges_reader.instance_eval(&block)
87
+ end
88
+
89
+ def contexts (&block)
90
+ # Not implemented
91
+ end
92
+
93
+ def authorization (&block)
94
+ @parent.auth_rules_reader.instance_eval(&block)
95
+ end
96
+ end
97
+ end
98
+
99
+ # The PrivilegeReader handles the part of the authorization DSL in
100
+ # a +privileges+ block. Here, privilege hierarchies are defined.
101
+ class PrivilegesReader
102
+ # TODO handle privileges with separated context
103
+ attr_reader :privileges, :privilege_hierarchy # :nodoc:
104
+
105
+ def initialize # :nodoc:
106
+ @current_priv = nil
107
+ @current_context = nil
108
+ @privileges = []
109
+ # {priv => [[priv,ctx], ...]}
110
+ @privilege_hierarchy = {}
111
+ end
112
+
113
+ def append_privilege (priv) # :nodoc:
114
+ @privileges << priv unless @privileges.include?(priv)
115
+ end
116
+
117
+ # Defines part of a privilege hierarchy. For the given +privilege+,
118
+ # included privileges may be defined in the block (through includes)
119
+ # or as option :+includes+. If the optional context is given,
120
+ # the privilege hierarchy is limited to that context.
121
+ #
122
+ def privilege (privilege, context = nil, options = {}, &block)
123
+ if context.is_a?(Hash)
124
+ options = context
125
+ context = nil
126
+ end
127
+ @current_priv = privilege
128
+ @current_context = context
129
+ append_privilege privilege
130
+ instance_eval(&block) if block
131
+ includes(*options[:includes]) if options[:includes]
132
+ ensure
133
+ @current_priv = nil
134
+ @current_context = nil
135
+ end
136
+
137
+ # Specifies +privileges+ that are to be assigned as lower ones. Only to
138
+ # be used inside a privilege block.
139
+ def includes (*privileges)
140
+ raise DSLError, "includes only in privilege block" if @current_priv.nil?
141
+ privileges.each do |priv|
142
+ append_privilege priv
143
+ @privilege_hierarchy[@current_priv] ||= []
144
+ @privilege_hierarchy[@current_priv] << [priv, @current_context]
145
+ end
146
+ end
147
+ end
148
+
149
+ class AuthorizationRulesReader
150
+ attr_reader :roles, :role_hierarchy, :auth_rules,
151
+ :role_descriptions, :role_titles, :omnipotent_roles # :nodoc:
152
+
153
+ def initialize # :nodoc:
154
+ @current_role = nil
155
+ @current_rule = nil
156
+ @roles = []
157
+ @omnipotent_roles = []
158
+ # higher_role => [lower_roles]
159
+ @role_hierarchy = {}
160
+ @role_titles = {}
161
+ @role_descriptions = {}
162
+ @auth_rules = []
163
+ end
164
+
165
+ def append_role (role, options = {}) # :nodoc:
166
+ @roles << role unless @roles.include? role
167
+ @role_titles[role] = options[:title] if options[:title]
168
+ @role_descriptions[role] = options[:description] if options[:description]
169
+ end
170
+
171
+ # Defines the authorization rules for the given +role+ in the
172
+ # following block.
173
+ # role :admin do
174
+ # has_permissions_on ...
175
+ # end
176
+ #
177
+ def role (role, options = {}, &block)
178
+ append_role role, options
179
+ @current_role = role
180
+ yield
181
+ ensure
182
+ @current_role = nil
183
+ end
184
+
185
+ # Roles may inherit all the rights from subroles. The given +roles+
186
+ # become subroles of the current block's role.
187
+ # role :admin do
188
+ # includes :user
189
+ # has_permission_on :employees, :to => [:update, :create]
190
+ # end
191
+ # role :user do
192
+ # has_permission_on :employees, :to => :read
193
+ # end
194
+ #
195
+ def includes (*roles)
196
+ raise DSLError, "includes only in role blocks" if @current_role.nil?
197
+ @role_hierarchy[@current_role] ||= []
198
+ @role_hierarchy[@current_role] += roles.flatten
199
+ end
200
+
201
+ # Allows the definition of privileges to be allowed for the current role,
202
+ # either in a has_permission_on block or directly in one call.
203
+ # role :admin
204
+ # has_permission_on :employees, :to => :read
205
+ # has_permission_on [:employees, :orders], :to => :read
206
+ # has_permission_on :employees do
207
+ # to :create
208
+ # if_attribute ...
209
+ # end
210
+ # has_permission_on :employees, :to => :delete do
211
+ # if_attribute ...
212
+ # end
213
+ # end
214
+ # The block form allows to describe restrictions on the permissions
215
+ # using if_attribute. Multiple has_permission_on statements are
216
+ # OR'ed when evaluating the permissions. Also, multiple if_attribute
217
+ # statements in one block are OR'ed if no :+join_by+ option is given
218
+ # (see below). To AND conditions, either set :+join_by+ to :and or place
219
+ # them in one if_attribute statement.
220
+ #
221
+ # Available options
222
+ # [:+to+]
223
+ # A symbol or an array of symbols representing the privileges that
224
+ # should be granted in this statement.
225
+ # [:+join_by+]
226
+ # Join operator to logically connect the constraint statements inside
227
+ # of the has_permission_on block. May be :+and+ or :+or+. Defaults to :+or+.
228
+ #
229
+ def has_permission_on (*args, &block)
230
+ options = args.extract_options!
231
+ context = args.flatten
232
+
233
+ raise DSLError, "has_permission_on only allowed in role blocks" if @current_role.nil?
234
+ options = {:to => [], :join_by => :or}.merge(options)
235
+
236
+ privs = options[:to]
237
+ privs = [privs] unless privs.is_a?(Array)
238
+ raise DSLError, "has_permission_on either needs a block or :to option" if !block_given? and privs.empty?
239
+
240
+ file, line = file_and_line_number_from_call_stack
241
+ rule = AuthorizationRule.new(@current_role, privs, context, options[:join_by],
242
+ :source_file => file, :source_line => line)
243
+ @auth_rules << rule
244
+ if block_given?
245
+ @current_rule = rule
246
+ yield
247
+ raise DSLError, "has_permission_on block content specifies no privileges" if rule.privileges.empty?
248
+ # TODO ensure?
249
+ @current_rule = nil
250
+ end
251
+ end
252
+
253
+ def has_omnipotence
254
+ @omnipotent_roles << @current_role
255
+ end
256
+
257
+ # Sets a description for the current role. E.g.
258
+ # role :admin
259
+ # description "To be assigned to administrative personnel"
260
+ # has_permission_on ...
261
+ # end
262
+ def description (text)
263
+ raise DSLError, "description only allowed in role blocks" if @current_role.nil?
264
+ role_descriptions[@current_role] = text
265
+ end
266
+
267
+ # Sets a human-readable title for the current role. E.g.
268
+ # role :admin
269
+ # title "Administrator"
270
+ # has_permission_on ...
271
+ # end
272
+ def title (text)
273
+ raise DSLError, "title only allowed in role blocks" if @current_role.nil?
274
+ role_titles[@current_role] = text
275
+ end
276
+
277
+ # Used in a has_permission_on block, to may be used to specify privileges
278
+ # to be assigned to the current role under the conditions specified in
279
+ # the current block.
280
+ # role :admin
281
+ # has_permission_on :employees do
282
+ # to :create, :read, :update, :delete
283
+ # end
284
+ # end
285
+ def to (*privs)
286
+ raise DSLError, "to only allowed in has_permission_on blocks" if @current_rule.nil?
287
+ @current_rule.append_privileges(privs)
288
+ end
289
+
290
+ # In a has_permission_on block, if_attribute specifies conditions
291
+ # of dynamic parameters that have to be met for the user to meet the
292
+ # privileges in this block. Conditions are evaluated on the context
293
+ # object. Thus, the following allows CRUD for branch admins only on
294
+ # employees that belong to the same branch as the current user.
295
+ # role :branch_admin
296
+ # has_permission_on :employees do
297
+ # to :create, :read, :update, :delete
298
+ # if_attribute :branch => is { user.branch }
299
+ # end
300
+ # end
301
+ # In this case, is is the operator for evaluating the condition. Another
302
+ # operator is contains for collections. In the block supplied to the
303
+ # operator, +user+ specifies the current user for whom the condition
304
+ # is evaluated.
305
+ #
306
+ # Conditions may be nested:
307
+ # role :company_admin
308
+ # has_permission_on :employees do
309
+ # to :create, :read, :update, :delete
310
+ # if_attribute :branch => { :company => is {user.branch.company} }
311
+ # end
312
+ # end
313
+ #
314
+ # has_many and has_many through associations may also be nested.
315
+ # Then, at least one item in the association needs to fulfill the
316
+ # subsequent condition:
317
+ # if_attribute :company => { :branches => { :manager => { :last_name => is { user.last_name } } }
318
+ # Beware of possible performance issues when using has_many associations in
319
+ # permitted_to? checks. For
320
+ # permitted_to? :read, object
321
+ # a check like
322
+ # object.company.branches.any? { |branch| branch.manager ... }
323
+ # will be executed. with_permission_to scopes construct efficient SQL
324
+ # joins, though.
325
+ #
326
+ # Multiple attributes in one :if_attribute statement are AND'ed.
327
+ # Multiple if_attribute statements are OR'ed if the join operator for the
328
+ # has_permission_on block isn't explicitly set. Thus, the following would
329
+ # require the current user either to be of the same branch AND the employee
330
+ # to be "changeable_by_coworker". OR the current user has to be the
331
+ # employee in question.
332
+ # has_permission_on :employees, :to => :manage do
333
+ # if_attribute :branch => is {user.branch}, :changeable_by_coworker => true
334
+ # if_attribute :id => is {user.id}
335
+ # end
336
+ # The join operator for if_attribute rules can explicitly set to AND, though.
337
+ # See has_permission_on for details.
338
+ #
339
+ # Arrays and fixed values may be used directly as hash values:
340
+ # if_attribute :id => 1
341
+ # if_attribute :type => "special"
342
+ # if_attribute :id => [1,2]
343
+ #
344
+ def if_attribute (attr_conditions_hash)
345
+ raise DSLError, "if_attribute only in has_permission blocks" if @current_rule.nil?
346
+ parse_attribute_conditions_hash!(attr_conditions_hash)
347
+ @current_rule.append_attribute Attribute.new(attr_conditions_hash)
348
+ end
349
+
350
+ # if_permitted_to allows the has_permission_on block to depend on
351
+ # permissions on associated objects. By using it, the authorization
352
+ # rules may be a lot DRYer. E.g.:
353
+ #
354
+ # role :branch_manager
355
+ # has_permission_on :branches, :to => :manage do
356
+ # if_attribute :employees => includes { user }
357
+ # end
358
+ # has_permission_on :employees, :to => :read do
359
+ # if_permitted_to :read, :branch
360
+ # # instead of
361
+ # # if_attribute :branch => { :employees => includes { user } }
362
+ # end
363
+ # end
364
+ #
365
+ # if_permitted_to associations may be nested as well:
366
+ # if_permitted_to :read, :branch => :company
367
+ #
368
+ # You can even use has_many associations as target. Then, it is checked
369
+ # if the current user has the required privilege on *any* of the target objects.
370
+ # if_permitted_to :read, :branch => :employees
371
+ # Beware of performance issues with permission checks. In the current implementation,
372
+ # all employees are checked until the first permitted is found.
373
+ # with_permissions_to, on the other hand, constructs more efficient SQL
374
+ # instead.
375
+ #
376
+ # To check permissions based on the current object, the attribute has to
377
+ # be left out:
378
+ # has_permission_on :branches, :to => :manage do
379
+ # if_attribute :employees => includes { user }
380
+ # end
381
+ # has_permission_on :branches, :to => :paint_green do
382
+ # if_permitted_to :update
383
+ # end
384
+ # Normally, one would merge those rules into one. Deviding makes sense
385
+ # if additional if_attribute are used in the second rule or those rules
386
+ # are applied to different roles.
387
+ #
388
+ # Options:
389
+ # [:+context+]
390
+ # When using with_permissions_to, the target context of the if_permitted_to
391
+ # statement is infered from the last reflections target class. Still,
392
+ # you may override this algorithm by setting the context explicitly.
393
+ # if_permitted_to :read, :home_branch, :context => :branches
394
+ # if_permitted_to :read, :branch => :main_company, :context => :companies
395
+ #
396
+ def if_permitted_to (privilege, attr_or_hash = nil, options = {})
397
+ raise DSLError, "if_permitted_to only in has_permission blocks" if @current_rule.nil?
398
+ options[:context] ||= attr_or_hash.delete(:context) if attr_or_hash.is_a?(Hash)
399
+ # only :context option in attr_or_hash:
400
+ attr_or_hash = nil if attr_or_hash.is_a?(Hash) and attr_or_hash.empty?
401
+ @current_rule.append_attribute AttributeWithPermission.new(privilege,
402
+ attr_or_hash, options[:context])
403
+ end
404
+
405
+ # In an if_attribute statement, is says that the value has to be
406
+ # met exactly by the if_attribute attribute. For information on the block
407
+ # argument, see if_attribute.
408
+ def is (&block)
409
+ [:is, block]
410
+ end
411
+
412
+ # The negation of is.
413
+ def is_not (&block)
414
+ [:is_not, block]
415
+ end
416
+
417
+ # In an if_attribute statement, contains says that the value has to be
418
+ # part of the collection specified by the if_attribute attribute.
419
+ # For information on the block argument, see if_attribute.
420
+ def contains (&block)
421
+ [:contains, block]
422
+ end
423
+
424
+ # The negation of contains. Currently, query rewriting is disabled
425
+ # for does_not_contain.
426
+ def does_not_contain (&block)
427
+ [:does_not_contain, block]
428
+ end
429
+
430
+ # In an if_attribute statement, intersects_with requires that at least
431
+ # one of the values has to be part of the collection specified by the
432
+ # if_attribute attribute. The value block needs to evaluate to an
433
+ # Enumerable. For information on the block argument, see if_attribute.
434
+ def intersects_with (&block)
435
+ [:intersects_with, block]
436
+ end
437
+
438
+ # In an if_attribute statement, is_in says that the value has to
439
+ # contain the attribute value.
440
+ # For information on the block argument, see if_attribute.
441
+ def is_in (&block)
442
+ [:is_in, block]
443
+ end
444
+
445
+ # The negation of is_in.
446
+ def is_not_in (&block)
447
+ [:is_not_in, block]
448
+ end
449
+
450
+ private
451
+ def parse_attribute_conditions_hash! (hash)
452
+ merge_hash = {}
453
+ hash.each do |key, value|
454
+ if value.is_a?(Hash)
455
+ parse_attribute_conditions_hash!(value)
456
+ elsif !value.is_a?(Array)
457
+ merge_hash[key] = [:is, lambda { value }]
458
+ elsif value.is_a?(Array) and !value[0].is_a?(Symbol)
459
+ merge_hash[key] = [:is_in, lambda { value }]
460
+ end
461
+ end
462
+ hash.merge!(merge_hash)
463
+ end
464
+
465
+ def file_and_line_number_from_call_stack
466
+ caller_parts = caller(2).first.split(':')
467
+ [caller_parts[0] == "(eval)" ? nil : caller_parts[0],
468
+ caller_parts[1] && caller_parts[1].to_i]
469
+ end
470
+ end
471
+ end
472
+ end