rubycs-declarative_authorization 0.3.0

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 (37) hide show
  1. data/CHANGELOG +70 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.rdoc +9 -0
  4. data/Rakefile +43 -0
  5. data/app/controllers/authorization_rules_controller.rb +114 -0
  6. data/app/controllers/authorization_usages_controller.rb +23 -0
  7. data/app/helpers/authorization_rules_helper.rb +100 -0
  8. data/app/views/authorization_rules/graph.dot.erb +49 -0
  9. data/app/views/authorization_rules/graph.html.erb +39 -0
  10. data/app/views/authorization_rules/index.html.erb +16 -0
  11. data/app/views/authorization_usages/index.html.erb +45 -0
  12. data/authorization_rules.dist.rb +20 -0
  13. data/config/locales/en.declarative_authorization.yml +35 -0
  14. data/config/locales/ro.declarative_authorization.yml +35 -0
  15. data/config/routes.rb +6 -0
  16. data/garlic_example.rb +20 -0
  17. data/init.rb +5 -0
  18. data/lib/declarative_authorization.rb +15 -0
  19. data/lib/declarative_authorization/authorization.rb +578 -0
  20. data/lib/declarative_authorization/authorization_rules_analyzer.rb +138 -0
  21. data/lib/declarative_authorization/helper.rb +56 -0
  22. data/lib/declarative_authorization/in_controller.rb +343 -0
  23. data/lib/declarative_authorization/in_model.rb +125 -0
  24. data/lib/declarative_authorization/maintenance.rb +174 -0
  25. data/lib/declarative_authorization/obligation_scope.rb +292 -0
  26. data/lib/declarative_authorization/rails_legacy.rb +14 -0
  27. data/lib/declarative_authorization/reader.rb +430 -0
  28. data/test/authorization_rules_analyzer_test.rb +123 -0
  29. data/test/authorization_test.rb +779 -0
  30. data/test/controller_test.rb +361 -0
  31. data/test/dsl_reader_test.rb +157 -0
  32. data/test/helper_test.rb +133 -0
  33. data/test/maintenance_test.rb +15 -0
  34. data/test/model_test.rb +1143 -0
  35. data/test/schema.sql +53 -0
  36. data/test/test_helper.rb +99 -0
  37. metadata +97 -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,430 @@
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 (context, options = {}, &block)
226
+ raise DSLError, "has_permission_on only allowed in role blocks" if @current_role.nil?
227
+ options = {:to => [], :join_by => :or}.merge(options)
228
+
229
+ privs = options[:to]
230
+ privs = [privs] unless privs.is_a?(Array)
231
+ raise DSLError, "has_permission_on either needs a block or :to option" if !block_given? and privs.empty?
232
+
233
+ rule = AuthorizationRule.new(@current_role, privs, context, options[:join_by])
234
+ @auth_rules << rule
235
+ if block_given?
236
+ @current_rule = rule
237
+ yield
238
+ raise DSLError, "has_permission_on block content specifies no privileges" if rule.privileges.empty?
239
+ # TODO ensure?
240
+ @current_rule = nil
241
+ end
242
+ end
243
+
244
+ # Sets a description for the current role. E.g.
245
+ # role :admin
246
+ # description "To be assigned to administrative personnel"
247
+ # has_permission_on ...
248
+ # end
249
+ def description (text)
250
+ raise DSLError, "description only allowed in role blocks" if @current_role.nil?
251
+ role_descriptions[@current_role] = text
252
+ end
253
+
254
+ # Sets a human-readable title for the current role. E.g.
255
+ # role :admin
256
+ # title "Administrator"
257
+ # has_permission_on ...
258
+ # end
259
+ def title (text)
260
+ raise DSLError, "title only allowed in role blocks" if @current_role.nil?
261
+ role_titles[@current_role] = text
262
+ end
263
+
264
+ # Used in a has_permission_on block, to may be used to specify privileges
265
+ # to be assigned to the current role under the conditions specified in
266
+ # the current block.
267
+ # role :admin
268
+ # has_permission_on :employees do
269
+ # to :create, :read, :update, :delete
270
+ # end
271
+ # end
272
+ def to (*privs)
273
+ raise DSLError, "to only allowed in has_permission_on blocks" if @current_rule.nil?
274
+ @current_rule.append_privileges(privs)
275
+ end
276
+
277
+ # In a has_permission_on block, if_attribute specifies conditions
278
+ # of dynamic parameters that have to be met for the user to meet the
279
+ # privileges in this block. Conditions are evaluated on the context
280
+ # object. Thus, the following allows CRUD for branch admins only on
281
+ # employees that belong to the same branch as the current user.
282
+ # role :branch_admin
283
+ # has_permission_on :employees do
284
+ # to :create, :read, :update, :delete
285
+ # if_attribute :branch => is { user.branch }
286
+ # end
287
+ # end
288
+ # In this case, is is the operator for evaluating the condition. Another
289
+ # operator is contains for collections. In the block supplied to the
290
+ # operator, +user+ specifies the current user for whom the condition
291
+ # is evaluated.
292
+ #
293
+ # Conditions may be nested:
294
+ # role :company_admin
295
+ # has_permission_on :employees do
296
+ # to :create, :read, :update, :delete
297
+ # if_attribute :branch => { :company => is {user.branch.company} }
298
+ # end
299
+ # end
300
+ #
301
+ # Multiple attributes in one :if_attribute statement are AND'ed.
302
+ # Multiple if_attribute statements are OR'ed if the join operator for the
303
+ # has_permission_on block isn't explicitly set. Thus, the following would
304
+ # require the current user either to be of the same branch AND the employee
305
+ # to be "changeable_by_coworker". OR the current user has to be the
306
+ # employee in question.
307
+ # has_permission_on :employees, :to => :manage do
308
+ # if_attribute :branch => is {user.branch}, :changeable_by_coworker => true
309
+ # if_attribute :id => is {user.id}
310
+ # end
311
+ #
312
+ # Arrays and fixed values may be used directly as hash values:
313
+ # if_attribute :id => 1
314
+ # if_attribute :type => "special"
315
+ # if_attribute :id => [1,2]
316
+ #
317
+ def if_attribute (attr_conditions_hash)
318
+ raise DSLError, "if_attribute only in has_permission blocks" if @current_rule.nil?
319
+ parse_attribute_conditions_hash!(attr_conditions_hash)
320
+ @current_rule.append_attribute Attribute.new(attr_conditions_hash)
321
+ end
322
+
323
+ # if_permitted_to allows the has_permission_on block to depend on
324
+ # permissions on associated objects. By using it, the authorization
325
+ # rules may be a lot DRYer. E.g.:
326
+ #
327
+ # role :branch_manager
328
+ # has_permission_on :branches, :to => :manage do
329
+ # if_attribute :employees => includes { user }
330
+ # end
331
+ # has_permission_on :employees, :to => :read do
332
+ # if_permitted_to :read, :branch
333
+ # # instead of
334
+ # # if_attribute :branch => { :employees => includes { user } }
335
+ # end
336
+ # end
337
+ #
338
+ # if_permitted_to associations may be nested as well:
339
+ # if_permitted_to :read, :branch => :company
340
+ #
341
+ # To check permissions based on the current object, the attribute has to
342
+ # be left out:
343
+ # has_permission_on :branches, :to => :manage do
344
+ # if_attribute :employees => includes { user }
345
+ # end
346
+ # has_permission_on :branches, :to => :paint_green do
347
+ # if_permitted_to :update
348
+ # end
349
+ # Normally, one would merge those rules into one. Deviding makes sense
350
+ # if additional if_attribute are used in the second rule or those rules
351
+ # are applied to different roles.
352
+ #
353
+ # Options:
354
+ # [:+context+]
355
+ # If the context of the refered object may not be infered from the
356
+ # associations name, the context may be given explicitly:
357
+ # if_permitted_to :read, :home_branch, :context => :branches
358
+ # if_permitted_to :read, :branch => :main_company, :context => :companies
359
+ #
360
+ def if_permitted_to (privilege, attr_or_hash = nil, options = {})
361
+ raise DSLError, "if_permitted_to only in has_permission blocks" if @current_rule.nil?
362
+ options[:context] ||= attr_or_hash.delete(:context) if attr_or_hash.is_a?(Hash)
363
+ # only :context option in attr_or_hash:
364
+ attr_or_hash = nil if attr_or_hash.is_a?(Hash) and attr_or_hash.empty?
365
+ @current_rule.append_attribute AttributeWithPermission.new(privilege,
366
+ attr_or_hash, options[:context])
367
+ end
368
+
369
+ # In an if_attribute statement, is says that the value has to be
370
+ # met exactly by the if_attribute attribute. For information on the block
371
+ # argument, see if_attribute.
372
+ def is (&block)
373
+ [:is, block]
374
+ end
375
+
376
+ # The negation of is.
377
+ def is_not (&block)
378
+ [:is_not, block]
379
+ end
380
+
381
+ # In an if_attribute statement, contains says that the value has to be
382
+ # part of the collection specified by the if_attribute attribute.
383
+ # For information on the block argument, see if_attribute.
384
+ def contains (&block)
385
+ [:contains, block]
386
+ end
387
+
388
+ # The negation of contains. Currently, query rewriting is disabled
389
+ # for does_not_contain.
390
+ def does_not_contain (&block)
391
+ [:does_not_contain, block]
392
+ end
393
+
394
+ # In an if_attribute statement, intersects_with requires that at least
395
+ # one of the values has to be part of the collection specified by the
396
+ # if_attribute attribute. The value block needs to evaluate to an
397
+ # Enumerable. For information on the block argument, see if_attribute.
398
+ def intersects_with (&block)
399
+ [:intersects_with, block]
400
+ end
401
+
402
+ # In an if_attribute statement, is_in says that the value has to
403
+ # contain the attribute value.
404
+ # For information on the block argument, see if_attribute.
405
+ def is_in (&block)
406
+ [:is_in, block]
407
+ end
408
+
409
+ # The negation of is_in.
410
+ def is_not_in (&block)
411
+ [:is_not_in, block]
412
+ end
413
+
414
+ private
415
+ def parse_attribute_conditions_hash! (hash)
416
+ merge_hash = {}
417
+ hash.each do |key, value|
418
+ if value.is_a?(Hash)
419
+ parse_attribute_conditions_hash!(value)
420
+ elsif !value.is_a?(Array)
421
+ merge_hash[key] = [:is, lambda { value }]
422
+ elsif value.is_a?(Array) and !value[0].is_a?(Symbol)
423
+ merge_hash[key] = [:is_in, lambda { value }]
424
+ end
425
+ end
426
+ hash.merge!(merge_hash)
427
+ end
428
+ end
429
+ end
430
+ end
@@ -0,0 +1,123 @@
1
+ require File.join(File.dirname(__FILE__), 'test_helper.rb')
2
+ require File.join(File.dirname(__FILE__), %w{.. lib declarative_authorization authorization_rules_analyzer})
3
+
4
+ class AuthorizationRulesAnalyzerTest < Test::Unit::TestCase
5
+
6
+ def test_analyzing_complex_rules
7
+ assert_nothing_raised do
8
+ engine, analyzer = engine_analyzer_for %{
9
+ authorization do
10
+ role :guest do
11
+ has_permission_on :conferences, :to => :read do
12
+ if_attribute :published => true
13
+ end
14
+ has_permission_on :talks, :to => :read do
15
+ if_permitted_to :read, :conference
16
+ end
17
+ has_permission_on :users, :to => :create
18
+ has_permission_on :authorization_rules, :to => :read
19
+ has_permission_on :authorization_usages, :to => :read
20
+ end
21
+
22
+ role :user do
23
+ includes :guest
24
+ has_permission_on :conference_attendees, :to => :create do
25
+ if_attribute :user => is {user},
26
+ :conference => { :published => true }
27
+ end
28
+ has_permission_on :conference_attendees, :to => :delete do
29
+ if_attribute :user => is {user},
30
+ :conference => { :attendees => contains {user} }
31
+ end
32
+ has_permission_on :talk_attendees, :to => :create do
33
+ if_attribute :talk => { :conference => { :attendees => contains {user} }}
34
+ end
35
+ has_permission_on :talk_attendees, :to => :delete do
36
+ if_attribute :user => is {user}
37
+ end
38
+ end
39
+
40
+ role :conference_organizer do
41
+ has_permission_on :conferences do
42
+ to :manage
43
+ # if...
44
+ end
45
+ has_permission_on [:conference_attendees, :talks, :talk_attendees], :to => :manage
46
+ end
47
+
48
+ role :admin do
49
+ has_permission_on [:conferences, :users, :talks], :to => :manage
50
+ has_permission_on :authorization_rules, :to => :read
51
+ has_permission_on :authorization_usages, :to => :read
52
+ end
53
+ end
54
+
55
+ privileges do
56
+ privilege :manage, :includes => [:create, :read, :update, :delete]
57
+ privilege :read, :includes => [:index, :show]
58
+ privilege :create, :includes => :new
59
+ privilege :update, :includes => :edit
60
+ privilege :delete, :includes => :destroy
61
+ end
62
+ }
63
+ end
64
+ end
65
+
66
+ def test_mergeable_rules_without_constraints
67
+ engine, analyzer = engine_analyzer_for %{
68
+ authorization do
69
+ role :test_role do
70
+ has_permission_on :permissions, :to => :test
71
+ has_permission_on :permissions, :to => :test2
72
+ end
73
+ end
74
+ }
75
+
76
+ report = analyzer.reports.find {|report| report.type == :mergeable_rules}
77
+ assert report
78
+ assert_equal 4, report.line
79
+ end
80
+
81
+ def test_mergeable_rules_with_in_block_to
82
+ assert_nothing_raised do
83
+ engine, analyzer = engine_analyzer_for %{
84
+ authorization do
85
+ role :test_role do
86
+ has_permission_on :permissions do
87
+ to :test
88
+ end
89
+ end
90
+ end
91
+ }
92
+ end
93
+ end
94
+
95
+ def test_no_mergeable_rules_with_constraints
96
+ engine, analyzer = engine_analyzer_for %{
97
+ authorization do
98
+ role :test_role do
99
+ has_permission_on :permissions, :to => :test do
100
+ if_attribute :some_attr => is {bla}
101
+ end
102
+ has_permission_on :permissions, :to => :test2 do
103
+ if_attribute :some_attr_2 => is {bla}
104
+ end
105
+ end
106
+ end
107
+ }
108
+
109
+ assert !analyzer.reports.find {|report| report.type == :mergeable_rules}
110
+ end
111
+
112
+ protected
113
+ def engine_analyzer_for (rules)
114
+ reader = Authorization::Reader::DSLReader.new
115
+ reader.parse rules
116
+ engine = Authorization::Engine.new(reader)
117
+
118
+ analyzer = Authorization::Analyzer.new(engine)
119
+ analyzer.analyze rules
120
+
121
+ [engine, analyzer]
122
+ end
123
+ end