graylog2-declarative_authorization 0.5.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. data/CHANGELOG +153 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.rdoc +529 -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 +44 -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 +47 -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 +20 -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 +705 -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 +68 -0
  27. data/lib/declarative_authorization/in_controller.rb +645 -0
  28. data/lib/declarative_authorization/in_model.rb +162 -0
  29. data/lib/declarative_authorization/maintenance.rb +212 -0
  30. data/lib/declarative_authorization/obligation_scope.rb +354 -0
  31. data/lib/declarative_authorization/rails_legacy.rb +22 -0
  32. data/lib/declarative_authorization/railsengine.rb +6 -0
  33. data/lib/declarative_authorization/reader.rb +521 -0
  34. data/lib/tasks/authorization_tasks.rake +82 -0
  35. data/test/authorization_test.rb +1104 -0
  36. data/test/controller_filter_resource_access_test.rb +511 -0
  37. data/test/controller_test.rb +480 -0
  38. data/test/dsl_reader_test.rb +178 -0
  39. data/test/helper_test.rb +247 -0
  40. data/test/maintenance_test.rb +46 -0
  41. data/test/model_test.rb +1883 -0
  42. data/test/schema.sql +55 -0
  43. data/test/test_helper.rb +152 -0
  44. metadata +112 -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 => contains { 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 => contains { 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 => contains { 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
+