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 +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
|