declarative_authorization 0.3.2.3

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 +83 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.rdoc +510 -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 +187 -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 +152 -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 +634 -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 +597 -0
  28. data/lib/declarative_authorization/in_model.rb +159 -0
  29. data/lib/declarative_authorization/maintenance.rb +182 -0
  30. data/lib/declarative_authorization/obligation_scope.rb +308 -0
  31. data/lib/declarative_authorization/rails_legacy.rb +14 -0
  32. data/lib/declarative_authorization/reader.rb +441 -0
  33. data/test/authorization_test.rb +827 -0
  34. data/test/controller_filter_resource_access_test.rb +394 -0
  35. data/test/controller_test.rb +386 -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 +1308 -0
  40. data/test/schema.sql +54 -0
  41. data/test/test_helper.rb +118 -0
  42. metadata +105 -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,441 @@
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_file)
70
+ # TODO cache reader in production mode?
71
+ reader = new
72
+ reader.parse(File.read(dsl_file), dsl_file)
73
+ reader
74
+ end
75
+
76
+ # DSL methods
77
+ class DSLMethods # :nodoc:
78
+ def initialize (parent)
79
+ @parent = parent
80
+ end
81
+
82
+ def privileges (&block)
83
+ @parent.privileges_reader.instance_eval(&block)
84
+ end
85
+
86
+ def contexts (&block)
87
+ # Not implemented
88
+ end
89
+
90
+ def authorization (&block)
91
+ @parent.auth_rules_reader.instance_eval(&block)
92
+ end
93
+ end
94
+ end
95
+
96
+ # The PrivilegeReader handles the part of the authorization DSL in
97
+ # a +privileges+ block. Here, privilege hierarchies are defined.
98
+ class PrivilegesReader
99
+ # TODO handle privileges with separated context
100
+ attr_reader :privileges, :privilege_hierarchy # :nodoc:
101
+
102
+ def initialize # :nodoc:
103
+ @current_priv = nil
104
+ @current_context = nil
105
+ @privileges = []
106
+ # {priv => [[priv,ctx], ...]}
107
+ @privilege_hierarchy = {}
108
+ end
109
+
110
+ def append_privilege (priv) # :nodoc:
111
+ @privileges << priv unless @privileges.include?(priv)
112
+ end
113
+
114
+ # Defines part of a privilege hierarchy. For the given +privilege+,
115
+ # included privileges may be defined in the block (through includes)
116
+ # or as option :+includes+. If the optional context is given,
117
+ # the privilege hierarchy is limited to that context.
118
+ #
119
+ def privilege (privilege, context = nil, options = {}, &block)
120
+ if context.is_a?(Hash)
121
+ options = context
122
+ context = nil
123
+ end
124
+ @current_priv = privilege
125
+ @current_context = context
126
+ append_privilege privilege
127
+ instance_eval(&block) if block
128
+ includes(*options[:includes]) if options[:includes]
129
+ ensure
130
+ @current_priv = nil
131
+ @current_context = nil
132
+ end
133
+
134
+ # Specifies +privileges+ that are to be assigned as lower ones. Only to
135
+ # be used inside a privilege block.
136
+ def includes (*privileges)
137
+ raise DSLError, "includes only in privilege block" if @current_priv.nil?
138
+ privileges.each do |priv|
139
+ append_privilege priv
140
+ @privilege_hierarchy[@current_priv] ||= []
141
+ @privilege_hierarchy[@current_priv] << [priv, @current_context]
142
+ end
143
+ end
144
+ end
145
+
146
+ class AuthorizationRulesReader
147
+ attr_reader :roles, :role_hierarchy, :auth_rules,
148
+ :role_descriptions, :role_titles # :nodoc:
149
+
150
+ def initialize # :nodoc:
151
+ @current_role = nil
152
+ @current_rule = nil
153
+ @roles = []
154
+ # higher_role => [lower_roles]
155
+ @role_hierarchy = {}
156
+ @role_titles = {}
157
+ @role_descriptions = {}
158
+ @auth_rules = []
159
+ end
160
+
161
+ def append_role (role, options = {}) # :nodoc:
162
+ @roles << role unless @roles.include? role
163
+ @role_titles[role] = options[:title] if options[:title]
164
+ @role_descriptions[role] = options[:description] if options[:description]
165
+ end
166
+
167
+ # Defines the authorization rules for the given +role+ in the
168
+ # following block.
169
+ # role :admin do
170
+ # has_permissions_on ...
171
+ # end
172
+ #
173
+ def role (role, options = {}, &block)
174
+ append_role role, options
175
+ @current_role = role
176
+ yield
177
+ ensure
178
+ @current_role = nil
179
+ end
180
+
181
+ # Roles may inherit all the rights from subroles. The given +roles+
182
+ # become subroles of the current block's role.
183
+ # role :admin do
184
+ # includes :user
185
+ # has_permission_on :employees, :to => [:update, :create]
186
+ # end
187
+ # role :user do
188
+ # has_permission_on :employees, :to => :read
189
+ # end
190
+ #
191
+ def includes (*roles)
192
+ raise DSLError, "includes only in role blocks" if @current_role.nil?
193
+ @role_hierarchy[@current_role] ||= []
194
+ @role_hierarchy[@current_role] += roles.flatten
195
+ end
196
+
197
+ # Allows the definition of privileges to be allowed for the current role,
198
+ # either in a has_permission_on block or directly in one call.
199
+ # role :admin
200
+ # has_permission_on :employees, :to => :read
201
+ # has_permission_on [:employees, :orders], :to => :read
202
+ # has_permission_on :employees do
203
+ # to :create
204
+ # if_attribute ...
205
+ # end
206
+ # has_permission_on :employees, :to => :delete do
207
+ # if_attribute ...
208
+ # end
209
+ # end
210
+ # The block form allows to describe restrictions on the permissions
211
+ # using if_attribute. Multiple has_permission_on statements are
212
+ # OR'ed when evaluating the permissions. Also, multiple if_attribute
213
+ # statements in one block are OR'ed if no :+join_by+ option is given
214
+ # (see below). To AND conditions, either set :+join_by+ to :and or place
215
+ # them in one if_attribute statement.
216
+ #
217
+ # Available options
218
+ # [:+to+]
219
+ # A symbol or an array of symbols representing the privileges that
220
+ # should be granted in this statement.
221
+ # [:+join_by+]
222
+ # Join operator to logically connect the constraint statements inside
223
+ # of the has_permission_on block. May be :+and+ or :+or+. Defaults to :+or+.
224
+ #
225
+ def has_permission_on (*args, &block)
226
+ options = args.extract_options!
227
+ context = args.flatten
228
+
229
+ raise DSLError, "has_permission_on only allowed in role blocks" if @current_role.nil?
230
+ options = {:to => [], :join_by => :or}.merge(options)
231
+
232
+ privs = options[:to]
233
+ privs = [privs] unless privs.is_a?(Array)
234
+ raise DSLError, "has_permission_on either needs a block or :to option" if !block_given? and privs.empty?
235
+
236
+ file, line = file_and_line_number_from_call_stack
237
+ rule = AuthorizationRule.new(@current_role, privs, context, options[:join_by],
238
+ :source_file => file, :source_line => line)
239
+ @auth_rules << rule
240
+ if block_given?
241
+ @current_rule = rule
242
+ yield
243
+ raise DSLError, "has_permission_on block content specifies no privileges" if rule.privileges.empty?
244
+ # TODO ensure?
245
+ @current_rule = nil
246
+ end
247
+ end
248
+
249
+ # Sets a description for the current role. E.g.
250
+ # role :admin
251
+ # description "To be assigned to administrative personnel"
252
+ # has_permission_on ...
253
+ # end
254
+ def description (text)
255
+ raise DSLError, "description only allowed in role blocks" if @current_role.nil?
256
+ role_descriptions[@current_role] = text
257
+ end
258
+
259
+ # Sets a human-readable title for the current role. E.g.
260
+ # role :admin
261
+ # title "Administrator"
262
+ # has_permission_on ...
263
+ # end
264
+ def title (text)
265
+ raise DSLError, "title only allowed in role blocks" if @current_role.nil?
266
+ role_titles[@current_role] = text
267
+ end
268
+
269
+ # Used in a has_permission_on block, to may be used to specify privileges
270
+ # to be assigned to the current role under the conditions specified in
271
+ # the current block.
272
+ # role :admin
273
+ # has_permission_on :employees do
274
+ # to :create, :read, :update, :delete
275
+ # end
276
+ # end
277
+ def to (*privs)
278
+ raise DSLError, "to only allowed in has_permission_on blocks" if @current_rule.nil?
279
+ @current_rule.append_privileges(privs)
280
+ end
281
+
282
+ # In a has_permission_on block, if_attribute specifies conditions
283
+ # of dynamic parameters that have to be met for the user to meet the
284
+ # privileges in this block. Conditions are evaluated on the context
285
+ # object. Thus, the following allows CRUD for branch admins only on
286
+ # employees that belong to the same branch as the current user.
287
+ # role :branch_admin
288
+ # has_permission_on :employees do
289
+ # to :create, :read, :update, :delete
290
+ # if_attribute :branch => is { user.branch }
291
+ # end
292
+ # end
293
+ # In this case, is is the operator for evaluating the condition. Another
294
+ # operator is contains for collections. In the block supplied to the
295
+ # operator, +user+ specifies the current user for whom the condition
296
+ # is evaluated.
297
+ #
298
+ # Conditions may be nested:
299
+ # role :company_admin
300
+ # has_permission_on :employees do
301
+ # to :create, :read, :update, :delete
302
+ # if_attribute :branch => { :company => is {user.branch.company} }
303
+ # end
304
+ # end
305
+ #
306
+ # Multiple attributes in one :if_attribute statement are AND'ed.
307
+ # Multiple if_attribute statements are OR'ed if the join operator for the
308
+ # has_permission_on block isn't explicitly set. Thus, the following would
309
+ # require the current user either to be of the same branch AND the employee
310
+ # to be "changeable_by_coworker". OR the current user has to be the
311
+ # employee in question.
312
+ # has_permission_on :employees, :to => :manage do
313
+ # if_attribute :branch => is {user.branch}, :changeable_by_coworker => true
314
+ # if_attribute :id => is {user.id}
315
+ # end
316
+ #
317
+ # Arrays and fixed values may be used directly as hash values:
318
+ # if_attribute :id => 1
319
+ # if_attribute :type => "special"
320
+ # if_attribute :id => [1,2]
321
+ #
322
+ def if_attribute (attr_conditions_hash)
323
+ raise DSLError, "if_attribute only in has_permission blocks" if @current_rule.nil?
324
+ parse_attribute_conditions_hash!(attr_conditions_hash)
325
+ @current_rule.append_attribute Attribute.new(attr_conditions_hash)
326
+ end
327
+
328
+ # if_permitted_to allows the has_permission_on block to depend on
329
+ # permissions on associated objects. By using it, the authorization
330
+ # rules may be a lot DRYer. E.g.:
331
+ #
332
+ # role :branch_manager
333
+ # has_permission_on :branches, :to => :manage do
334
+ # if_attribute :employees => includes { user }
335
+ # end
336
+ # has_permission_on :employees, :to => :read do
337
+ # if_permitted_to :read, :branch
338
+ # # instead of
339
+ # # if_attribute :branch => { :employees => includes { user } }
340
+ # end
341
+ # end
342
+ #
343
+ # if_permitted_to associations may be nested as well:
344
+ # if_permitted_to :read, :branch => :company
345
+ #
346
+ # To check permissions based on the current object, the attribute has to
347
+ # be left out:
348
+ # has_permission_on :branches, :to => :manage do
349
+ # if_attribute :employees => includes { user }
350
+ # end
351
+ # has_permission_on :branches, :to => :paint_green do
352
+ # if_permitted_to :update
353
+ # end
354
+ # Normally, one would merge those rules into one. Deviding makes sense
355
+ # if additional if_attribute are used in the second rule or those rules
356
+ # are applied to different roles.
357
+ #
358
+ # Options:
359
+ # [:+context+]
360
+ # If the context of the refered object may not be infered from the
361
+ # associations name, the context may be given explicitly:
362
+ # if_permitted_to :read, :home_branch, :context => :branches
363
+ # if_permitted_to :read, :branch => :main_company, :context => :companies
364
+ #
365
+ def if_permitted_to (privilege, attr_or_hash = nil, options = {})
366
+ raise DSLError, "if_permitted_to only in has_permission blocks" if @current_rule.nil?
367
+ options[:context] ||= attr_or_hash.delete(:context) if attr_or_hash.is_a?(Hash)
368
+ # only :context option in attr_or_hash:
369
+ attr_or_hash = nil if attr_or_hash.is_a?(Hash) and attr_or_hash.empty?
370
+ @current_rule.append_attribute AttributeWithPermission.new(privilege,
371
+ attr_or_hash, options[:context])
372
+ end
373
+
374
+ # In an if_attribute statement, is says that the value has to be
375
+ # met exactly by the if_attribute attribute. For information on the block
376
+ # argument, see if_attribute.
377
+ def is (&block)
378
+ [:is, block]
379
+ end
380
+
381
+ # The negation of is.
382
+ def is_not (&block)
383
+ [:is_not, block]
384
+ end
385
+
386
+ # In an if_attribute statement, contains says that the value has to be
387
+ # part of the collection specified by the if_attribute attribute.
388
+ # For information on the block argument, see if_attribute.
389
+ def contains (&block)
390
+ [:contains, block]
391
+ end
392
+
393
+ # The negation of contains. Currently, query rewriting is disabled
394
+ # for does_not_contain.
395
+ def does_not_contain (&block)
396
+ [:does_not_contain, block]
397
+ end
398
+
399
+ # In an if_attribute statement, intersects_with requires that at least
400
+ # one of the values has to be part of the collection specified by the
401
+ # if_attribute attribute. The value block needs to evaluate to an
402
+ # Enumerable. For information on the block argument, see if_attribute.
403
+ def intersects_with (&block)
404
+ [:intersects_with, block]
405
+ end
406
+
407
+ # In an if_attribute statement, is_in says that the value has to
408
+ # contain the attribute value.
409
+ # For information on the block argument, see if_attribute.
410
+ def is_in (&block)
411
+ [:is_in, block]
412
+ end
413
+
414
+ # The negation of is_in.
415
+ def is_not_in (&block)
416
+ [:is_not_in, block]
417
+ end
418
+
419
+ private
420
+ def parse_attribute_conditions_hash! (hash)
421
+ merge_hash = {}
422
+ hash.each do |key, value|
423
+ if value.is_a?(Hash)
424
+ parse_attribute_conditions_hash!(value)
425
+ elsif !value.is_a?(Array)
426
+ merge_hash[key] = [:is, lambda { value }]
427
+ elsif value.is_a?(Array) and !value[0].is_a?(Symbol)
428
+ merge_hash[key] = [:is_in, lambda { value }]
429
+ end
430
+ end
431
+ hash.merge!(merge_hash)
432
+ end
433
+
434
+ def file_and_line_number_from_call_stack
435
+ caller_parts = caller(2).first.split(':')
436
+ [caller_parts[0] == "(eval)" ? nil : caller_parts[0],
437
+ caller_parts[1] && caller_parts[1].to_i]
438
+ end
439
+ end
440
+ end
441
+ end