stffn-declarative_authorization 0.2.4 → 0.2.5

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.
data/CHANGELOG CHANGED
@@ -1,5 +1,21 @@
1
+ * New option :join_by for has_permission_on to allow AND'ing of statements in one has_permission_on block
2
+
3
+ * Allow using_access_control to be called directly on ActiveRecord::Base, globally enabling model security
4
+
5
+ * New operator: intersects_with, comparing two Enumerables in if_attribute
6
+
7
+ * Improved if_permitted_to syntax: if the attribute is left out, permissions are checked on for the current object
8
+
9
+ * Added #has_role_with_hierarchy? method to retrieve explicit and calculated roles [jeremyf]
10
+
11
+ * Added a simple rules analyzer to help improve authorization rules [sb]
12
+
13
+ * Gemified plugin. Needed to restructure the lib path contents [sb]
14
+
1
15
  * Added handling of Authorization::AuthorizationInController::ClassMethods.filter_access_to parameters that are of the form [:show, :update] instead of just :show, :update. [jeremyf]
2
16
 
17
+ * Added authorization usage helper for checking filter_access_to usage in controllers [sb]
18
+
3
19
  * Added a authorization rules browser. See README for more information [sb]
4
20
 
5
21
  * Added Model.using_access_control? to check if a model has model security activated [sb]
data/README.rdoc CHANGED
@@ -31,7 +31,8 @@ Plugin features
31
31
  Requirements
32
32
  * An authentication mechanism
33
33
  * User object in Controller#current_user
34
- * User object needs to respond to a method :role_symbols that returns an
34
+ * (For model security) Setting Authorization.current_user
35
+ * User objects need to respond to a method :role_symbols that returns an
35
36
  array of role symbols
36
37
  See below for installation instructions.
37
38
 
@@ -362,6 +363,7 @@ The requirements are
362
363
  * An authentication mechanism
363
364
  * A user object returned by controller.current_user
364
365
  * An array of role symbols returned by user.role_symbols
366
+ * (For model security) Setting Authorization.current_user to the request's user
365
367
 
366
368
  Of the various ways to provide these requirements, here is one way employing
367
369
  restful_authentication.
@@ -467,7 +469,10 @@ sbartsch at tzi.org
467
469
  = Contributors
468
470
 
469
471
  Thanks to
472
+ * Erik Dahlstrand
473
+ * Jeremy Friesen
470
474
  * Brian Langenfeld
475
+ * Geoff Longman
471
476
  * Mark Mansour
472
477
  * Mike Vincent
473
478
 
@@ -1,5 +1,7 @@
1
1
  if Authorization::activate_authorization_rules_browser?
2
2
 
3
+ require File.join(File.dirname(__FILE__), %w{.. .. lib declarative_authorization authorization_rules_analyzer})
4
+
3
5
  begin
4
6
  # for nice auth_rules output:
5
7
  require "parse_tree"
@@ -81,7 +83,7 @@ class AuthorizationRulesController < ApplicationController
81
83
  end
82
84
 
83
85
  def dot_to_svg (dot_data)
84
- gv = IO.popen("/usr/bin/dot -q -Tsvg", "w+")
86
+ gv = IO.popen("#{Authorization.dot_path} -q -Tsvg", "w+")
85
87
  gv.puts dot_data
86
88
  gv.close_write
87
89
  gv.read
@@ -100,4 +102,6 @@ class AuthorizationRulesController < ApplicationController
100
102
  end
101
103
  end
102
104
 
105
+ else
106
+ class AuthorizationRulesController < ApplicationController; end
103
107
  end # activate_authorization_rules_browser?
@@ -1,6 +1,6 @@
1
1
  if Authorization::activate_authorization_rules_browser?
2
2
 
3
- require File.join(File.dirname(__FILE__), %w{.. .. lib maintenance})
3
+ require File.join(File.dirname(__FILE__), %w{.. .. lib declarative_authorization maintenance})
4
4
 
5
5
  class AuthorizationUsagesController < ApplicationController
6
6
  helper :authorization_rules
@@ -16,4 +16,6 @@ class AuthorizationUsagesController < ApplicationController
16
16
  end
17
17
  end
18
18
 
19
+ else
20
+ class AuthorizationUsagesController < ApplicationController; end
19
21
  end # activate_authorization_rules_browser?
@@ -21,6 +21,22 @@ module AuthorizationRulesHelper
21
21
  end
22
22
  rules
23
23
  end
24
+
25
+ def policy_analysis_hints (marked_up, policy_data)
26
+ analyzer = Authorization::Analyzer.new(controller.authorization_engine)
27
+ analyzer.analyze(policy_data)
28
+ marked_up_by_line = marked_up.split("\n")
29
+ reports_by_line = analyzer.reports.inject({}) do |memo, report|
30
+ memo[report.line] ||= []
31
+ memo[report.line] << report
32
+ memo
33
+ end
34
+ reports_by_line.each do |line, reports|
35
+ note = %Q{<span class="note" title="#{reports.first.type}: #{reports.first.message}">[i]</span>}
36
+ marked_up_by_line[line - 1] = note + marked_up_by_line[line - 1]
37
+ end
38
+ marked_up_by_line * "\n"
39
+ end
24
40
 
25
41
  def link_to_graph (title, options = {})
26
42
  type = options[:type] || ''
@@ -9,7 +9,8 @@
9
9
  pre .proc {color: #0a0;}
10
10
  pre .privilege, pre .context {font-weight: bold}
11
11
  pre .preproc, pre .comment, pre .comment span {color: grey !important;}
12
+ pre .note {color: #a00; position:absolute; cursor: help }
12
13
  </style>
13
14
  <pre>
14
- <%= syntax_highlight(h(@auth_rules_script)) %>
15
+ <%= policy_analysis_hints(syntax_highlight(h(@auth_rules_script)), @auth_rules_script) %>
15
16
  </pre>
@@ -42,6 +42,15 @@ module Authorization
42
42
  def self.activate_authorization_rules_browser? # :nodoc:
43
43
  ::RAILS_ENV == 'development'
44
44
  end
45
+
46
+ @@dot_path = "dot"
47
+ def self.dot_path
48
+ @@dot_path
49
+ end
50
+
51
+ def self.dot_path= (path)
52
+ @@dot_path = path
53
+ end
45
54
 
46
55
  # Authorization::Engine implements the reference monitor. It may be used
47
56
  # for querying the permission and retrieving obligations under which
@@ -147,7 +156,7 @@ module Authorization
147
156
  begin
148
157
  options[:skip_attribute_test] or
149
158
  rule.attributes.empty? or
150
- rule.attributes.any? do |attr|
159
+ rule.attributes.send(rule.join_operator == :and ? :all? : :any?) do |attr|
151
160
  begin
152
161
  attr.validate?( attr_validator )
153
162
  rescue NilAttributeValueError => e
@@ -192,10 +201,17 @@ module Authorization
192
201
  def obligations (privilege, options = {})
193
202
  options = {:context => nil}.merge(options)
194
203
  user, roles, privileges = user_roles_privleges_from_options(privilege, options)
195
- attr_validator = AttributeValidator.new(self, user)
204
+ attr_validator = AttributeValidator.new(self, user, nil, options[:context])
196
205
  matching_auth_rules(roles, privileges, options[:context]).collect do |rule|
197
- obligation = rule.attributes.collect {|attr| attr.obligation(attr_validator) }
198
- obligation.empty? ? [{}] : obligation
206
+ obligations = rule.attributes.collect {|attr| attr.obligation(attr_validator) }
207
+ if rule.join_operator == :and and !obligations.empty?
208
+ merged_obligation = obligations.first
209
+ obligations[1..-1].each do |obligation|
210
+ merged_obligation = merged_obligation.deep_merge(obligation)
211
+ end
212
+ obligations = [merged_obligation]
213
+ end
214
+ obligations.empty? ? [{}] : obligations
199
215
  end.flatten
200
216
  end
201
217
 
@@ -230,6 +246,11 @@ module Authorization
230
246
  (roles.empty? ? [:guest] : roles)
231
247
  end
232
248
 
249
+ # Returns the role symbols and inherritted role symbols for the given user
250
+ def roles_with_hierarchy_for(user)
251
+ flatten_roles(roles_for(user))
252
+ end
253
+
233
254
  # Returns an instance of Engine, which is created if there isn't one
234
255
  # yet. If +dsl_file+ is given, it is passed on to Engine.new and
235
256
  # a new instance is always created.
@@ -242,11 +263,12 @@ module Authorization
242
263
  end
243
264
 
244
265
  class AttributeValidator # :nodoc:
245
- attr_reader :user, :object, :engine
246
- def initialize (engine, user, object = nil)
266
+ attr_reader :user, :object, :engine, :context
267
+ def initialize (engine, user, object = nil, context = nil)
247
268
  @engine = engine
248
269
  @user = user
249
270
  @object = object
271
+ @context = context
250
272
  end
251
273
 
252
274
  def evaluate (value_block)
@@ -307,12 +329,13 @@ module Authorization
307
329
  end
308
330
 
309
331
  class AuthorizationRule
310
- attr_reader :attributes, :contexts, :role, :privileges
332
+ attr_reader :attributes, :contexts, :role, :privileges, :join_operator
311
333
 
312
- def initialize (role, privileges = [], contexts = nil)
334
+ def initialize (role, privileges = [], contexts = nil, join_operator = :or)
313
335
  @role = role
314
336
  @privileges = Set.new(privileges)
315
337
  @contexts = Set.new((contexts && !contexts.is_a?(Array) ? [contexts] : contexts))
338
+ @join_operator = join_operator
316
339
  @attributes = []
317
340
  end
318
341
 
@@ -370,13 +393,45 @@ module Authorization
370
393
  when :is_not
371
394
  attr_value != evaluated
372
395
  when :contains
373
- attr_value.include?(evaluated)
396
+ begin
397
+ attr_value.include?(evaluated)
398
+ rescue NoMethodError => e
399
+ raise AuthorizationUsageError, "Operator contains requires a " +
400
+ "subclass of Enumerable as attribute value, got: #{attr_value.inspect} " +
401
+ "contains #{evaluated.inspect}: #{e}"
402
+ end
374
403
  when :does_not_contain
375
- !attr_value.include?(evaluated)
404
+ begin
405
+ !attr_value.include?(evaluated)
406
+ rescue NoMethodError => e
407
+ raise AuthorizationUsageError, "Operator does_not_contain requires a " +
408
+ "subclass of Enumerable as attribute value, got: #{attr_value.inspect} " +
409
+ "does_not_contain #{evaluated.inspect}: #{e}"
410
+ end
411
+ when :intersects_with
412
+ begin
413
+ !(evaluated.to_set & attr_value.to_set).empty?
414
+ rescue NoMethodError => e
415
+ raise AuthorizationUsageError, "Operator intersects_with requires " +
416
+ "subclasses of Enumerable, got: #{attr_value.inspect} " +
417
+ "intersects_with #{evaluated.inspect}: #{e}"
418
+ end
376
419
  when :is_in
377
- evaluated.include?(attr_value)
420
+ begin
421
+ evaluated.include?(attr_value)
422
+ rescue NoMethodError => e
423
+ raise AuthorizationUsageError, "Operator is_in requires a " +
424
+ "subclass of Enumerable as value, got: #{attr_value.inspect} " +
425
+ "is_in #{evaluated.inspect}: #{e}"
426
+ end
378
427
  when :is_not_in
379
- !evaluated.include?(attr_value)
428
+ begin
429
+ !evaluated.include?(attr_value)
430
+ rescue NoMethodError => e
431
+ raise AuthorizationUsageError, "Operator is_not_in requires a " +
432
+ "subclass of Enumerable as value, got: #{attr_value.inspect} " +
433
+ "is_not_in #{evaluated.inspect}: #{e}"
434
+ end
380
435
  else
381
436
  raise AuthorizationError, "Unknown operator #{value[0]}"
382
437
  end
@@ -446,15 +501,20 @@ module Authorization
446
501
  case hash_or_attr
447
502
  when Symbol
448
503
  attr_value = object_attribute_value(object, hash_or_attr)
504
+ if attr_value.nil?
505
+ raise NilAttributeValueError, "Attribute #{hash_or_attr.inspect} is nil in #{object.inspect}."
506
+ end
449
507
  attr_validator.engine.permit? @privilege, :object => attr_value, :user => attr_validator.user
450
508
  when Hash
451
509
  hash_or_attr.all? do |attr, sub_hash|
452
510
  attr_value = object_attribute_value(object, attr)
453
511
  if attr_value.nil?
454
- raise AuthorizationError, "Attribute #{attr.inspect} is nil in #{object.inspect}."
512
+ raise NilAttributeValueError, "Attribute #{attr.inspect} is nil in #{object.inspect}."
455
513
  end
456
514
  validate?(attr_validator, attr_value, sub_hash)
457
515
  end
516
+ when NilClass
517
+ attr_validator.engine.permit? @privilege, :object => object, :user => attr_validator.user
458
518
  else
459
519
  raise AuthorizationError, "Wrong conditions hash format: #{hash_or_attr.inspect}"
460
520
  end
@@ -494,6 +554,10 @@ module Authorization
494
554
  end.flatten
495
555
  end
496
556
  obligations
557
+ when NilClass
558
+ attr_validator.engine.obligations(@privilege,
559
+ :context => attr_validator.context,
560
+ :user => attr_validator.user)
497
561
  else
498
562
  raise AuthorizationError, "Wrong conditions hash format: #{hash_or_attr.inspect}"
499
563
  end
@@ -0,0 +1,138 @@
1
+ begin
2
+ require "ruby_parser"
3
+ #require "parse_tree"
4
+ #require "parse_tree_extensions"
5
+ require "sexp_processor"
6
+ rescue LoadError
7
+ raise "Authorization::Analyzer requires ruby_parser gem"
8
+ end
9
+
10
+ module Authorization
11
+
12
+ class Analyzer
13
+ attr_reader :engine
14
+
15
+ def initialize (engine)
16
+ @engine = engine
17
+ end
18
+
19
+ def analyze (rules)
20
+ sexp_array = RubyParser.new.parse(rules)
21
+ #sexp_array = ParseTree.translate(rules)
22
+ @reports = []
23
+ [MergeableRulesProcessor].each do |parser|
24
+ parser.new(self).analyze(sexp_array)
25
+ end
26
+ #p @reports
27
+ end
28
+
29
+ def reports
30
+ @reports or raise "No rules analyzed!"
31
+ end
32
+
33
+ class GeneralAuthorizationProcessor < SexpProcessor
34
+ def initialize(analyzer)
35
+ super()
36
+ self.auto_shift_type = true
37
+ self.require_empty = false
38
+ self.strict = false
39
+ @analyzer = analyzer
40
+ end
41
+
42
+ def analyze (sexp_array)
43
+ process(sexp_array)
44
+ analyze_rules
45
+ end
46
+
47
+ def analyze_rules
48
+ # to be implemented by specific processor
49
+ end
50
+
51
+ def process_iter (exp)
52
+ s(:iter, process(exp.shift), process(exp.shift), process(exp.shift))
53
+ end
54
+
55
+ def process_arglist (exp)
56
+ s(exp.collect {|inner_exp| process(inner_exp).shift})
57
+ end
58
+
59
+ def process_hash (exp)
60
+ s(Hash[*exp.collect {|inner_exp| process(inner_exp).shift}])
61
+ end
62
+
63
+ def process_lit (exp)
64
+ s(exp.shift)
65
+ end
66
+ end
67
+
68
+ class MergeableRulesProcessor < GeneralAuthorizationProcessor
69
+ def analyze_rules
70
+ if @has_permission
71
+ #p @has_permission
72
+ permissions_by_context_and_rules = @has_permission.inject({}) do |memo, permission|
73
+ key = [permission[:context], permission[:rules]]
74
+ memo[key] ||= []
75
+ memo[key] << permission
76
+ memo
77
+ end
78
+
79
+ permissions_by_context_and_rules.each do |key, rules|
80
+ if rules.length > 1
81
+ rule_lines = rules.collect {|rule| rule[:line] }
82
+ rules.each do |rule|
83
+ @analyzer.reports << Report.new(:mergeable_rules, "", rule[:line],
84
+ "Similar rules already in line(s) " +
85
+ rule_lines.reject {|l| l == rule[:line] } * ", ")
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end
91
+
92
+ def process_call (exp)
93
+ klass = exp.shift
94
+ name = exp.shift
95
+ case name
96
+ when :role
97
+ analyze_rules
98
+ @has_permission = []
99
+ s(:call, klass, name)
100
+ when :has_permission_on
101
+ arglist_line = exp[0].line
102
+ arglist = process(exp.shift).shift
103
+ context = arglist.shift
104
+ args_hash = arglist.shift
105
+ @has_permission << {
106
+ :context => context,
107
+ :rules => [],
108
+ :privilege => args_hash && args_hash[:to],
109
+ # a hack: call exp line seems to be wrong
110
+ :line => arglist_line
111
+ }
112
+ s(:call, klass, name)
113
+ when :to
114
+ @has_permission.last[:privilege] = process(exp.shift).shift if @has_permission
115
+ s(:call, klass, name)
116
+ when :if_attribute
117
+ @in_if_attribute = true
118
+ rules = process(exp.shift).shift
119
+ @has_permission.last[:rules] << rules if @has_permission
120
+ @in_if_attribute = false
121
+ s(:call, klass, name)
122
+ else
123
+ s(:call, klass, name, process(exp.shift))
124
+ end
125
+ end
126
+ end
127
+
128
+ class Report
129
+ attr_reader :type, :filename, :line, :message
130
+ def initialize (type, filename, line, msg)
131
+ @type = type
132
+ @filename = filename
133
+ @line = line
134
+ @message = msg
135
+ end
136
+ end
137
+ end
138
+ end
@@ -47,5 +47,10 @@ module Authorization
47
47
  def has_role? (*roles, &block)
48
48
  controller.has_role?(*roles, &block)
49
49
  end
50
+
51
+ # As has_role? except checks all roles included in the role hierarchy
52
+ def has_role_with_hierarchy?(*roles, &block)
53
+ controller.has_role_with_hierarchy?(*roles, &block)
54
+ end
50
55
  end
51
56
  end
@@ -69,6 +69,17 @@ module Authorization
69
69
  result
70
70
  end
71
71
 
72
+ # As has_role? except checks all roles included in the role hierarchy
73
+ def has_role_with_hierarchy?(*roles, &block)
74
+ user_roles = authorization_engine.roles_with_hierarchy_for(current_user)
75
+ result = roles.all? do |role|
76
+ user_roles.include?(role)
77
+ end
78
+ yield if result and block_given?
79
+ result
80
+ end
81
+
82
+
72
83
  protected
73
84
  def filter_access_filter # :nodoc:
74
85
  permissions = self.class.all_filter_access_permissions