tarsolya-declarative_authorization 0.4.1

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