stffn-declarative_authorization 0.2.4 → 0.2.5

Sign up to get free protection for your applications and to get access to all the features.
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