timcharper-declarative_authorization 0.4.1.2

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 +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