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