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 +16 -0
- data/README.rdoc +6 -1
- data/app/controllers/authorization_rules_controller.rb +5 -1
- data/app/controllers/authorization_usages_controller.rb +3 -1
- data/app/helpers/authorization_rules_helper.rb +16 -0
- data/app/views/authorization_rules/index.html.erb +2 -1
- data/lib/declarative_authorization/authorization.rb +77 -13
- data/lib/declarative_authorization/authorization_rules_analyzer.rb +138 -0
- data/lib/declarative_authorization/helper.rb +5 -0
- data/lib/declarative_authorization/in_controller.rb +11 -0
- data/lib/declarative_authorization/in_model.rb +12 -17
- data/lib/declarative_authorization/obligation_scope.rb +34 -23
- data/lib/declarative_authorization/reader.rb +48 -9
- data/test/authorization_rules_analyzer_test.rb +123 -0
- data/test/authorization_test.rb +203 -0
- data/test/helper_test.rb +38 -0
- data/test/model_test.rb +323 -1
- data/test/schema.sql +22 -1
- metadata +4 -2
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
|
-
*
|
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("
|
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
|
-
|
198
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
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
|