tarsolya-declarative_authorization 0.4.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.
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