ghart-declarative_authorization 0.3.2.4
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 +83 -0
- data/MIT-LICENSE +20 -0
- data/README.rdoc +510 -0
- data/Rakefile +43 -0
- data/app/controllers/authorization_rules_controller.rb +259 -0
- data/app/controllers/authorization_usages_controller.rb +23 -0
- data/app/helpers/authorization_rules_helper.rb +187 -0
- data/app/views/authorization_rules/_change.erb +58 -0
- data/app/views/authorization_rules/_show_graph.erb +37 -0
- data/app/views/authorization_rules/_suggestions.erb +48 -0
- data/app/views/authorization_rules/change.html.erb +152 -0
- data/app/views/authorization_rules/graph.dot.erb +68 -0
- data/app/views/authorization_rules/graph.html.erb +40 -0
- data/app/views/authorization_rules/index.html.erb +17 -0
- data/app/views/authorization_usages/index.html.erb +36 -0
- data/authorization_rules.dist.rb +20 -0
- data/config/routes.rb +7 -0
- data/garlic_example.rb +20 -0
- data/init.rb +5 -0
- data/lib/declarative_authorization.rb +15 -0
- data/lib/declarative_authorization/authorization.rb +634 -0
- data/lib/declarative_authorization/development_support/analyzer.rb +252 -0
- data/lib/declarative_authorization/development_support/change_analyzer.rb +253 -0
- data/lib/declarative_authorization/development_support/change_supporter.rb +620 -0
- data/lib/declarative_authorization/development_support/development_support.rb +243 -0
- data/lib/declarative_authorization/helper.rb +60 -0
- data/lib/declarative_authorization/in_controller.rb +597 -0
- data/lib/declarative_authorization/in_model.rb +159 -0
- data/lib/declarative_authorization/maintenance.rb +182 -0
- data/lib/declarative_authorization/obligation_scope.rb +308 -0
- data/lib/declarative_authorization/rails_legacy.rb +14 -0
- data/lib/declarative_authorization/reader.rb +441 -0
- data/test/authorization_test.rb +827 -0
- data/test/controller_filter_resource_access_test.rb +394 -0
- data/test/controller_test.rb +386 -0
- data/test/dsl_reader_test.rb +157 -0
- data/test/helper_test.rb +171 -0
- data/test/maintenance_test.rb +46 -0
- data/test/model_test.rb +1308 -0
- data/test/schema.sql +54 -0
- data/test/test_helper.rb +118 -0
- metadata +106 -0
@@ -0,0 +1,20 @@
|
|
1
|
+
authorization do
|
2
|
+
role :guest do
|
3
|
+
# add permissions for guests here, e.g.
|
4
|
+
#has_permission_on :conferences, :to => :read
|
5
|
+
end
|
6
|
+
|
7
|
+
# permissions on other roles, such as
|
8
|
+
#role :admin do
|
9
|
+
# has_permission_on :conferences, :to => :manage
|
10
|
+
#end
|
11
|
+
end
|
12
|
+
|
13
|
+
privileges do
|
14
|
+
# default privilege hierarchies to facilitate RESTful Rails apps
|
15
|
+
privilege :manage, :includes => [:create, :read, :update, :delete]
|
16
|
+
privilege :read, :includes => [:index, :show]
|
17
|
+
privilege :create, :includes => :new
|
18
|
+
privilege :update, :includes => :edit
|
19
|
+
privilege :delete, :includes => :destroy
|
20
|
+
end
|
data/config/routes.rb
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
ActionController::Routing::Routes.draw do |map|
|
2
|
+
if Authorization::activate_authorization_rules_browser?
|
3
|
+
map.resources :authorization_rules, :only => [:index],
|
4
|
+
:collection => {:graph => :get, :change => :get, :suggest_change => :get}
|
5
|
+
map.resources :authorization_usages, :only => :index
|
6
|
+
end
|
7
|
+
end
|
data/garlic_example.rb
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
garlic do
|
2
|
+
repo 'rails', :url => 'git://github.com/rails/rails'#, :local => "~/dev/vendor/rails"
|
3
|
+
repo 'declarative_authorization', :path => '.'
|
4
|
+
|
5
|
+
target 'edge'
|
6
|
+
target '2.1-stable', :branch => 'origin/2-1-stable'
|
7
|
+
target '2.2.0-RC1', :tag => 'v2.2.0'
|
8
|
+
|
9
|
+
all_targets do
|
10
|
+
prepare do
|
11
|
+
plugin 'declarative_authorization', :clone => true
|
12
|
+
end
|
13
|
+
|
14
|
+
run do
|
15
|
+
cd "vendor/plugins/declarative_authorization" do
|
16
|
+
sh "rake"
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
data/init.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
require File.join(%w{declarative_authorization rails_legacy})
|
2
|
+
require File.join(%w{declarative_authorization helper})
|
3
|
+
require File.join(%w{declarative_authorization in_controller})
|
4
|
+
require File.join(%w{declarative_authorization in_model})
|
5
|
+
require File.join(%w{declarative_authorization obligation_scope})
|
6
|
+
|
7
|
+
min_rails_version = "2.1.0"
|
8
|
+
if Rails::VERSION::STRING < min_rails_version
|
9
|
+
raise "declarative_authorization requires Rails #{min_rails_version}. You are using #{Rails::VERSION::STRING}."
|
10
|
+
end
|
11
|
+
|
12
|
+
ActionController::Base.send :include, Authorization::AuthorizationInController
|
13
|
+
ActionController::Base.helper Authorization::AuthorizationHelper
|
14
|
+
|
15
|
+
ActiveRecord::Base.send :include, Authorization::AuthorizationInModel
|
@@ -0,0 +1,634 @@
|
|
1
|
+
# Authorization
|
2
|
+
require File.dirname(__FILE__) + '/reader.rb'
|
3
|
+
require "set"
|
4
|
+
|
5
|
+
|
6
|
+
module Authorization
|
7
|
+
# An exception raised if anything goes wrong in the Authorization realm
|
8
|
+
class AuthorizationError < StandardError ; end
|
9
|
+
# NotAuthorized is raised if the current user is not allowed to perform
|
10
|
+
# the given operation possibly on a specific object.
|
11
|
+
class NotAuthorized < AuthorizationError ; end
|
12
|
+
# AttributeAuthorizationError is more specific than NotAuthorized, signalling
|
13
|
+
# that the access was denied on the grounds of attribute conditions.
|
14
|
+
class AttributeAuthorizationError < NotAuthorized ; end
|
15
|
+
# AuthorizationUsageError is used whenever a situation is encountered
|
16
|
+
# in which the application misused the plugin. That is, if, e.g.,
|
17
|
+
# authorization rules may not be evaluated.
|
18
|
+
class AuthorizationUsageError < AuthorizationError ; end
|
19
|
+
# NilAttributeValueError is raised by Attribute#validate? when it hits a nil attribute value.
|
20
|
+
# The exception is raised to ensure that the entire rule is invalidated.
|
21
|
+
class NilAttributeValueError < AuthorizationError; end
|
22
|
+
|
23
|
+
AUTH_DSL_FILE = "#{RAILS_ROOT}/config/authorization_rules.rb"
|
24
|
+
|
25
|
+
# Controller-independent method for retrieving the current user.
|
26
|
+
# Needed for model security where the current controller is not available.
|
27
|
+
def self.current_user
|
28
|
+
Thread.current["current_user"] || GuestUser.new
|
29
|
+
end
|
30
|
+
|
31
|
+
# Controller-independent method for setting the current user.
|
32
|
+
def self.current_user=(user)
|
33
|
+
Thread.current["current_user"] = user
|
34
|
+
end
|
35
|
+
|
36
|
+
# For use in test cases only
|
37
|
+
def self.ignore_access_control (state = nil) # :nodoc:
|
38
|
+
Thread.current["ignore_access_control"] = state unless state.nil?
|
39
|
+
Thread.current["ignore_access_control"] || false
|
40
|
+
end
|
41
|
+
|
42
|
+
def self.activate_authorization_rules_browser? # :nodoc:
|
43
|
+
::RAILS_ENV == 'development'
|
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
|
54
|
+
|
55
|
+
# Authorization::Engine implements the reference monitor. It may be used
|
56
|
+
# for querying the permission and retrieving obligations under which
|
57
|
+
# a certain privilege is granted for the current user.
|
58
|
+
#
|
59
|
+
class Engine
|
60
|
+
attr_reader :roles, :role_titles, :role_descriptions, :privileges,
|
61
|
+
:privilege_hierarchy, :auth_rules, :role_hierarchy, :rev_priv_hierarchy,
|
62
|
+
:rev_role_hierarchy
|
63
|
+
|
64
|
+
# If +reader+ is not given, a new one is created with the default
|
65
|
+
# authorization configuration of +AUTH_DSL_FILE+. If given, may be either
|
66
|
+
# a Reader object or a path to a configuration file.
|
67
|
+
def initialize (reader = nil)
|
68
|
+
if reader.nil?
|
69
|
+
begin
|
70
|
+
reader = Reader::DSLReader.load(AUTH_DSL_FILE)
|
71
|
+
rescue SystemCallError
|
72
|
+
reader = Reader::DSLReader.new
|
73
|
+
end
|
74
|
+
elsif reader.is_a?(String)
|
75
|
+
reader = Reader::DSLReader.load(reader)
|
76
|
+
end
|
77
|
+
@privileges = reader.privileges_reader.privileges
|
78
|
+
# {priv => [[priv, ctx],...]}
|
79
|
+
@privilege_hierarchy = reader.privileges_reader.privilege_hierarchy
|
80
|
+
@auth_rules = reader.auth_rules_reader.auth_rules
|
81
|
+
@roles = reader.auth_rules_reader.roles
|
82
|
+
@role_hierarchy = reader.auth_rules_reader.role_hierarchy
|
83
|
+
|
84
|
+
@role_titles = reader.auth_rules_reader.role_titles
|
85
|
+
@role_descriptions = reader.auth_rules_reader.role_descriptions
|
86
|
+
@reader = reader
|
87
|
+
|
88
|
+
# {[priv, ctx] => [priv, ...]}
|
89
|
+
@rev_priv_hierarchy = {}
|
90
|
+
@privilege_hierarchy.each do |key, value|
|
91
|
+
value.each do |val|
|
92
|
+
@rev_priv_hierarchy[val] ||= []
|
93
|
+
@rev_priv_hierarchy[val] << key
|
94
|
+
end
|
95
|
+
end
|
96
|
+
@rev_role_hierarchy = {}
|
97
|
+
@role_hierarchy.each do |higher_role, lower_roles|
|
98
|
+
lower_roles.each do |role|
|
99
|
+
(@rev_role_hierarchy[role] ||= []) << higher_role
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def initialize_copy (from) # :nodoc:
|
105
|
+
[
|
106
|
+
:privileges, :privilege_hierarchy, :roles, :role_hierarchy, :role_titles,
|
107
|
+
:role_descriptions, :rev_priv_hierarchy, :rev_role_hierarchy
|
108
|
+
].each {|attr| instance_variable_set(:"@#{attr}", from.send(attr).clone) }
|
109
|
+
@auth_rules = from.auth_rules.collect {|rule| rule.clone}
|
110
|
+
end
|
111
|
+
|
112
|
+
# Returns true if privilege is met by the current user. Raises
|
113
|
+
# AuthorizationError otherwise. +privilege+ may be given with or
|
114
|
+
# without context. In the latter case, the :+context+ option is
|
115
|
+
# required.
|
116
|
+
#
|
117
|
+
# Options:
|
118
|
+
# [:+context+]
|
119
|
+
# The context part of the privilege.
|
120
|
+
# Defaults either to the tableized +class_name+ of the given :+object+, if given.
|
121
|
+
# That is, :+users+ for :+object+ of type User.
|
122
|
+
# Raises AuthorizationUsageError if context is missing and not to be infered.
|
123
|
+
# [:+object+] An context object to test attribute checks against.
|
124
|
+
# [:+skip_attribute_test+]
|
125
|
+
# Skips those attribute checks in the
|
126
|
+
# authorization rules. Defaults to false.
|
127
|
+
# [:+user+]
|
128
|
+
# The user to check the authorization for.
|
129
|
+
# Defaults to Authorization#current_user.
|
130
|
+
#
|
131
|
+
def permit! (privilege, options = {})
|
132
|
+
return true if Authorization.ignore_access_control
|
133
|
+
options = {
|
134
|
+
:object => nil,
|
135
|
+
:skip_attribute_test => false,
|
136
|
+
:context => nil
|
137
|
+
}.merge(options)
|
138
|
+
|
139
|
+
# Make sure we're handling all privileges as symbols.
|
140
|
+
privilege = privilege.is_a?( Array ) ?
|
141
|
+
privilege.flatten.collect { |priv| priv.to_sym } :
|
142
|
+
privilege.to_sym
|
143
|
+
|
144
|
+
#
|
145
|
+
# If the object responds to :proxy_reflection, we're probably working with
|
146
|
+
# an association proxy. Use 'new' to leverage ActiveRecord's builder
|
147
|
+
# functionality to obtain an object against which we can check permissions.
|
148
|
+
#
|
149
|
+
# Example: permit!( :edit, :object => user.posts )
|
150
|
+
#
|
151
|
+
if options[:object].respond_to?( :proxy_reflection ) && options[:object].respond_to?( :new )
|
152
|
+
options[:object] = options[:object].new
|
153
|
+
end
|
154
|
+
|
155
|
+
options[:context] ||= options[:object] && (
|
156
|
+
options[:object].class.respond_to?(:decl_auth_context) ?
|
157
|
+
options[:object].class.decl_auth_context :
|
158
|
+
options[:object].class.name.tableize.to_sym
|
159
|
+
) rescue NoMethodError
|
160
|
+
|
161
|
+
user, roles, privileges = user_roles_privleges_from_options(privilege, options)
|
162
|
+
|
163
|
+
# find a authorization rule that matches for at least one of the roles and
|
164
|
+
# at least one of the given privileges
|
165
|
+
attr_validator = AttributeValidator.new(self, user, options[:object], privilege, options[:context])
|
166
|
+
rules = matching_auth_rules(roles, privileges, options[:context])
|
167
|
+
if rules.empty?
|
168
|
+
raise NotAuthorized, "No matching rules found for #{privilege} for #{user.inspect} " +
|
169
|
+
"(roles #{roles.inspect}, privileges #{privileges.inspect}, " +
|
170
|
+
"context #{options[:context].inspect})."
|
171
|
+
end
|
172
|
+
|
173
|
+
# Test each rule in turn to see whether any one of them is satisfied.
|
174
|
+
unless rules.any? {|rule| rule.validate?(attr_validator, options[:skip_attribute_test])}
|
175
|
+
raise AttributeAuthorizationError, "#{privilege} not allowed for #{user.inspect} on #{(options[:object] || options[:context]).inspect}."
|
176
|
+
end
|
177
|
+
true
|
178
|
+
end
|
179
|
+
|
180
|
+
# Calls permit! but rescues the AuthorizationException and returns false
|
181
|
+
# instead. If no exception is raised, permit? returns true and yields
|
182
|
+
# to the optional block.
|
183
|
+
def permit? (privilege, options = {}, &block) # :yields:
|
184
|
+
permit!(privilege, options)
|
185
|
+
yield if block_given?
|
186
|
+
true
|
187
|
+
rescue NotAuthorized
|
188
|
+
false
|
189
|
+
end
|
190
|
+
|
191
|
+
# Returns the obligations to be met by the current user for the given
|
192
|
+
# privilege as an array of obligation hashes in form of
|
193
|
+
# [{:object_attribute => obligation_value, ...}, ...]
|
194
|
+
# where +obligation_value+ is either (recursively) another obligation hash
|
195
|
+
# or a value spec, such as
|
196
|
+
# [operator, literal_value]
|
197
|
+
# The obligation hashes in the array should be OR'ed, conditions inside
|
198
|
+
# the hashes AND'ed.
|
199
|
+
#
|
200
|
+
# Example
|
201
|
+
# {:branch => {:company => [:is, 24]}, :active => [:is, true]}
|
202
|
+
#
|
203
|
+
# Options
|
204
|
+
# [:+context+] See permit!
|
205
|
+
# [:+user+] See permit!
|
206
|
+
#
|
207
|
+
def obligations (privilege, options = {})
|
208
|
+
options = {:context => nil}.merge(options)
|
209
|
+
user, roles, privileges = user_roles_privleges_from_options(privilege, options)
|
210
|
+
attr_validator = AttributeValidator.new(self, user, nil, privilege, options[:context])
|
211
|
+
matching_auth_rules(roles, privileges, options[:context]).collect do |rule|
|
212
|
+
rule.obligations(attr_validator)
|
213
|
+
end.flatten
|
214
|
+
end
|
215
|
+
|
216
|
+
# Returns the description for the given role. The description may be
|
217
|
+
# specified with the authorization rules. Returns +nil+ if none was
|
218
|
+
# given.
|
219
|
+
def description_for (role)
|
220
|
+
role_descriptions[role]
|
221
|
+
end
|
222
|
+
|
223
|
+
# Returns the title for the given role. The title may be
|
224
|
+
# specified with the authorization rules. Returns +nil+ if none was
|
225
|
+
# given.
|
226
|
+
def title_for (role)
|
227
|
+
role_titles[role]
|
228
|
+
end
|
229
|
+
|
230
|
+
# Returns the role symbols of the given user.
|
231
|
+
def roles_for (user)
|
232
|
+
user ||= Authorization.current_user
|
233
|
+
raise AuthorizationUsageError, "User object doesn't respond to roles (#{user.inspect})" \
|
234
|
+
if !user.respond_to?(:role_symbols) and !user.respond_to?(:roles)
|
235
|
+
|
236
|
+
RAILS_DEFAULT_LOGGER.info("The use of user.roles is deprecated. Please add a method " +
|
237
|
+
"role_symbols to your User model.") if defined?(RAILS_DEFAULT_LOGGER) and !user.respond_to?(:role_symbols)
|
238
|
+
|
239
|
+
roles = user.respond_to?(:role_symbols) ? user.role_symbols : user.roles
|
240
|
+
|
241
|
+
raise AuthorizationUsageError, "User.#{user.respond_to?(:role_symbols) ? 'role_symbols' : 'roles'} " +
|
242
|
+
"doesn't return an Array of Symbols (#{roles.inspect})" \
|
243
|
+
if !roles.is_a?(Array) or (!roles.empty? and !roles[0].is_a?(Symbol))
|
244
|
+
|
245
|
+
(roles.empty? ? [:guest] : roles)
|
246
|
+
end
|
247
|
+
|
248
|
+
# Returns the role symbols and inherritted role symbols for the given user
|
249
|
+
def roles_with_hierarchy_for(user)
|
250
|
+
flatten_roles(roles_for(user))
|
251
|
+
end
|
252
|
+
|
253
|
+
# Returns an instance of Engine, which is created if there isn't one
|
254
|
+
# yet. If +dsl_file+ is given, it is passed on to Engine.new and
|
255
|
+
# a new instance is always created.
|
256
|
+
def self.instance (dsl_file = nil)
|
257
|
+
if dsl_file or ENV['RAILS_ENV'] == 'development'
|
258
|
+
@@instance = new(dsl_file)
|
259
|
+
else
|
260
|
+
@@instance ||= new
|
261
|
+
end
|
262
|
+
end
|
263
|
+
|
264
|
+
class AttributeValidator # :nodoc:
|
265
|
+
attr_reader :user, :object, :engine, :context, :privilege
|
266
|
+
def initialize (engine, user, object = nil, privilege = nil, context = nil)
|
267
|
+
@engine = engine
|
268
|
+
@user = user
|
269
|
+
@object = object
|
270
|
+
@privilege = privilege
|
271
|
+
@context = context
|
272
|
+
end
|
273
|
+
|
274
|
+
def evaluate (value_block)
|
275
|
+
# TODO cache?
|
276
|
+
instance_eval(&value_block)
|
277
|
+
end
|
278
|
+
end
|
279
|
+
|
280
|
+
private
|
281
|
+
def user_roles_privleges_from_options(privilege, options)
|
282
|
+
options = {
|
283
|
+
:user => nil,
|
284
|
+
:context => nil,
|
285
|
+
:user_roles => nil
|
286
|
+
}.merge(options)
|
287
|
+
user = options[:user] || Authorization.current_user
|
288
|
+
privileges = privilege.is_a?(Array) ? privilege : [privilege]
|
289
|
+
|
290
|
+
raise AuthorizationUsageError, "No user object given (#{user.inspect}) or " +
|
291
|
+
"set through Authorization.current_user" unless user
|
292
|
+
|
293
|
+
roles = options[:user_roles] || flatten_roles(roles_for(user))
|
294
|
+
privileges = flatten_privileges privileges, options[:context]
|
295
|
+
[user, roles, privileges]
|
296
|
+
end
|
297
|
+
|
298
|
+
def flatten_roles (roles)
|
299
|
+
# TODO caching?
|
300
|
+
flattened_roles = roles.clone.to_a
|
301
|
+
flattened_roles.each do |role|
|
302
|
+
flattened_roles.concat(@role_hierarchy[role]).uniq! if @role_hierarchy[role]
|
303
|
+
end
|
304
|
+
end
|
305
|
+
|
306
|
+
# Returns the privilege hierarchy flattened for given privileges in context.
|
307
|
+
def flatten_privileges (privileges, context = nil)
|
308
|
+
# TODO caching?
|
309
|
+
#if context.nil?
|
310
|
+
# context = privileges.collect { |p| p.to_s.split('_') }.
|
311
|
+
# reject { |p_p| p_p.length < 2 }.
|
312
|
+
# collect { |p_p| (p_p[1..-1] * '_').to_sym }.first
|
313
|
+
# raise AuthorizationUsageError, "No context given or inferable from privileges #{privileges.inspect}" unless context
|
314
|
+
#end
|
315
|
+
raise AuthorizationUsageError, "No context given or inferable from object" unless context
|
316
|
+
#context_regex = Regexp.new "_#{context}$"
|
317
|
+
# TODO work with contextless privileges
|
318
|
+
#flattened_privileges = privileges.collect {|p| p.to_s.sub(context_regex, '')}
|
319
|
+
flattened_privileges = privileges.clone #collect {|p| p.to_s.end_with?(context.to_s) ?
|
320
|
+
# p : [p, "#{p}_#{context}".to_sym] }.flatten
|
321
|
+
flattened_privileges.each do |priv|
|
322
|
+
flattened_privileges.concat(@rev_priv_hierarchy[[priv, nil]]).uniq! if @rev_priv_hierarchy[[priv, nil]]
|
323
|
+
flattened_privileges.concat(@rev_priv_hierarchy[[priv, context]]).uniq! if @rev_priv_hierarchy[[priv, context]]
|
324
|
+
end
|
325
|
+
end
|
326
|
+
|
327
|
+
def matching_auth_rules (roles, privileges, context)
|
328
|
+
@auth_rules.select {|rule| rule.matches? roles, privileges, context}
|
329
|
+
end
|
330
|
+
end
|
331
|
+
|
332
|
+
class AuthorizationRule
|
333
|
+
attr_reader :attributes, :contexts, :role, :privileges, :join_operator,
|
334
|
+
:source_file, :source_line
|
335
|
+
|
336
|
+
def initialize (role, privileges = [], contexts = nil, join_operator = :or,
|
337
|
+
options = {})
|
338
|
+
@role = role
|
339
|
+
@privileges = Set.new(privileges)
|
340
|
+
@contexts = Set.new((contexts && !contexts.is_a?(Array) ? [contexts] : contexts))
|
341
|
+
@join_operator = join_operator
|
342
|
+
@attributes = []
|
343
|
+
@source_file = options[:source_file]
|
344
|
+
@source_line = options[:source_line]
|
345
|
+
end
|
346
|
+
|
347
|
+
def initialize_copy (from)
|
348
|
+
@privileges = @privileges.clone
|
349
|
+
@contexts = @contexts.clone
|
350
|
+
@attributes = @attributes.collect {|attribute| attribute.clone }
|
351
|
+
end
|
352
|
+
|
353
|
+
def append_privileges (privs)
|
354
|
+
@privileges.merge(privs)
|
355
|
+
end
|
356
|
+
|
357
|
+
def append_attribute (attribute)
|
358
|
+
@attributes << attribute
|
359
|
+
end
|
360
|
+
|
361
|
+
def matches? (roles, privs, context = nil)
|
362
|
+
roles = [roles] unless roles.is_a?(Array)
|
363
|
+
@contexts.include?(context) and roles.include?(@role) and
|
364
|
+
not (@privileges & privs).empty?
|
365
|
+
end
|
366
|
+
|
367
|
+
def validate? (attr_validator, skip_attribute = false)
|
368
|
+
skip_attribute or @attributes.empty? or
|
369
|
+
@attributes.send(@join_operator == :and ? :all? : :any?) do |attr|
|
370
|
+
begin
|
371
|
+
attr.validate?(attr_validator)
|
372
|
+
rescue NilAttributeValueError => e
|
373
|
+
nil # Bumping up against a nil attribute value flunks the rule.
|
374
|
+
end
|
375
|
+
end
|
376
|
+
end
|
377
|
+
|
378
|
+
def obligations (attr_validator)
|
379
|
+
obligations = @attributes.collect {|attr| attr.obligation(attr_validator) }.flatten
|
380
|
+
if @join_operator == :and and !obligations.empty?
|
381
|
+
merged_obligation = obligations.first
|
382
|
+
obligations[1..-1].each do |obligation|
|
383
|
+
merged_obligation = merged_obligation.deep_merge(obligation)
|
384
|
+
end
|
385
|
+
obligations = [merged_obligation]
|
386
|
+
end
|
387
|
+
obligations.empty? ? [{}] : obligations
|
388
|
+
end
|
389
|
+
|
390
|
+
def to_long_s
|
391
|
+
attributes.collect {|attr| attr.to_long_s } * "; "
|
392
|
+
end
|
393
|
+
end
|
394
|
+
|
395
|
+
class Attribute
|
396
|
+
# attr_conditions_hash of form
|
397
|
+
# { :object_attribute => [operator, value_block], ... }
|
398
|
+
# { :object_attribute => { :attr => ... } }
|
399
|
+
def initialize (conditions_hash)
|
400
|
+
@conditions_hash = conditions_hash
|
401
|
+
end
|
402
|
+
|
403
|
+
def initialize_copy (from)
|
404
|
+
@conditions_hash = deep_hash_clone(@conditions_hash)
|
405
|
+
end
|
406
|
+
|
407
|
+
def validate? (attr_validator, object = nil, hash = nil)
|
408
|
+
object ||= attr_validator.object
|
409
|
+
return false unless object
|
410
|
+
|
411
|
+
(hash || @conditions_hash).all? do |attr, value|
|
412
|
+
attr_value = object_attribute_value(object, attr)
|
413
|
+
if value.is_a?(Hash)
|
414
|
+
if attr_value.is_a?(Array)
|
415
|
+
raise AuthorizationUsageError, "Unable evaluate multiple attributes " +
|
416
|
+
"on a collection. Cannot use '=>' operator on #{attr.inspect} " +
|
417
|
+
"(#{attr_value.inspect}) for attributes #{value.inspect}."
|
418
|
+
elsif attr_value.nil?
|
419
|
+
raise NilAttributeValueError, "Attribute #{attr.inspect} is nil in #{object.inspect}."
|
420
|
+
end
|
421
|
+
validate?(attr_validator, attr_value, value)
|
422
|
+
elsif value.is_a?(Array) and value.length == 2
|
423
|
+
evaluated = if value[1].is_a?(Proc)
|
424
|
+
attr_validator.evaluate(value[1])
|
425
|
+
else
|
426
|
+
value[1]
|
427
|
+
end
|
428
|
+
case value[0]
|
429
|
+
when :is
|
430
|
+
attr_value == evaluated
|
431
|
+
when :is_not
|
432
|
+
attr_value != evaluated
|
433
|
+
when :contains
|
434
|
+
begin
|
435
|
+
attr_value.include?(evaluated)
|
436
|
+
rescue NoMethodError => e
|
437
|
+
raise AuthorizationUsageError, "Operator contains requires a " +
|
438
|
+
"subclass of Enumerable as attribute value, got: #{attr_value.inspect} " +
|
439
|
+
"contains #{evaluated.inspect}: #{e}"
|
440
|
+
end
|
441
|
+
when :does_not_contain
|
442
|
+
begin
|
443
|
+
!attr_value.include?(evaluated)
|
444
|
+
rescue NoMethodError => e
|
445
|
+
raise AuthorizationUsageError, "Operator does_not_contain requires a " +
|
446
|
+
"subclass of Enumerable as attribute value, got: #{attr_value.inspect} " +
|
447
|
+
"does_not_contain #{evaluated.inspect}: #{e}"
|
448
|
+
end
|
449
|
+
when :intersects_with
|
450
|
+
begin
|
451
|
+
!(evaluated.to_set & attr_value.to_set).empty?
|
452
|
+
rescue NoMethodError => e
|
453
|
+
raise AuthorizationUsageError, "Operator intersects_with requires " +
|
454
|
+
"subclasses of Enumerable, got: #{attr_value.inspect} " +
|
455
|
+
"intersects_with #{evaluated.inspect}: #{e}"
|
456
|
+
end
|
457
|
+
when :is_in
|
458
|
+
begin
|
459
|
+
evaluated.include?(attr_value)
|
460
|
+
rescue NoMethodError => e
|
461
|
+
raise AuthorizationUsageError, "Operator is_in requires a " +
|
462
|
+
"subclass of Enumerable as value, got: #{attr_value.inspect} " +
|
463
|
+
"is_in #{evaluated.inspect}: #{e}"
|
464
|
+
end
|
465
|
+
when :is_not_in
|
466
|
+
begin
|
467
|
+
!evaluated.include?(attr_value)
|
468
|
+
rescue NoMethodError => e
|
469
|
+
raise AuthorizationUsageError, "Operator is_not_in requires a " +
|
470
|
+
"subclass of Enumerable as value, got: #{attr_value.inspect} " +
|
471
|
+
"is_not_in #{evaluated.inspect}: #{e}"
|
472
|
+
end
|
473
|
+
else
|
474
|
+
raise AuthorizationError, "Unknown operator #{value[0]}"
|
475
|
+
end
|
476
|
+
else
|
477
|
+
raise AuthorizationError, "Wrong conditions hash format"
|
478
|
+
end
|
479
|
+
end
|
480
|
+
end
|
481
|
+
|
482
|
+
# resolves all the values in condition_hash
|
483
|
+
def obligation (attr_validator, hash = nil)
|
484
|
+
hash = (hash || @conditions_hash).clone
|
485
|
+
hash.each do |attr, value|
|
486
|
+
if value.is_a?(Hash)
|
487
|
+
hash[attr] = obligation(attr_validator, value)
|
488
|
+
elsif value.is_a?(Array) and value.length == 2
|
489
|
+
hash[attr] = [value[0], attr_validator.evaluate(value[1])]
|
490
|
+
else
|
491
|
+
raise AuthorizationError, "Wrong conditions hash format"
|
492
|
+
end
|
493
|
+
end
|
494
|
+
hash
|
495
|
+
end
|
496
|
+
|
497
|
+
def to_long_s (hash = nil)
|
498
|
+
if hash
|
499
|
+
hash.inject({}) do |memo, key_val|
|
500
|
+
key, val = key_val
|
501
|
+
memo[key] = case val
|
502
|
+
when Array then "#{val[0]} { #{val[1].respond_to?(:to_ruby) ? val[1].to_ruby.gsub(/^proc \{\n?(.*)\n?\}$/m, '\1') : "..."} }"
|
503
|
+
when Hash then to_long_s(val)
|
504
|
+
end
|
505
|
+
memo
|
506
|
+
end
|
507
|
+
else
|
508
|
+
"if_attribute #{to_long_s(@conditions_hash).inspect}"
|
509
|
+
end
|
510
|
+
end
|
511
|
+
|
512
|
+
protected
|
513
|
+
def object_attribute_value (object, attr)
|
514
|
+
begin
|
515
|
+
object.send(attr)
|
516
|
+
rescue ArgumentError, NoMethodError => e
|
517
|
+
raise AuthorizationUsageError, "Error when calling #{attr} on " +
|
518
|
+
"#{object.inspect} for validating attribute: #{e}"
|
519
|
+
end
|
520
|
+
end
|
521
|
+
|
522
|
+
def deep_hash_clone (hash)
|
523
|
+
hash.inject({}) do |memo, (key, val)|
|
524
|
+
memo[key] = case val
|
525
|
+
when Hash
|
526
|
+
deep_hash_clone(val)
|
527
|
+
when NilClass, Symbol
|
528
|
+
val
|
529
|
+
else
|
530
|
+
val.clone
|
531
|
+
end
|
532
|
+
memo
|
533
|
+
end
|
534
|
+
end
|
535
|
+
end
|
536
|
+
|
537
|
+
# An attribute condition that uses existing rules to decide validation
|
538
|
+
# and create obligations.
|
539
|
+
class AttributeWithPermission < Attribute
|
540
|
+
# E.g. privilege :read, attr_or_hash either :attribute or
|
541
|
+
# { :attribute => :deeper_attribute }
|
542
|
+
def initialize (privilege, attr_or_hash, context = nil)
|
543
|
+
@privilege = privilege
|
544
|
+
@context = context
|
545
|
+
@attr_hash = attr_or_hash
|
546
|
+
end
|
547
|
+
|
548
|
+
def initialize_copy (from)
|
549
|
+
@attr_hash = deep_hash_clone(@attr_hash) if @attr_hash.is_a?(Hash)
|
550
|
+
end
|
551
|
+
|
552
|
+
def validate? (attr_validator, object = nil, hash_or_attr = nil)
|
553
|
+
object ||= attr_validator.object
|
554
|
+
hash_or_attr ||= @attr_hash
|
555
|
+
return false unless object
|
556
|
+
|
557
|
+
case hash_or_attr
|
558
|
+
when Symbol
|
559
|
+
attr_value = object_attribute_value(object, hash_or_attr)
|
560
|
+
if attr_value.nil?
|
561
|
+
raise NilAttributeValueError, "Attribute #{hash_or_attr.inspect} is nil in #{object.inspect}."
|
562
|
+
end
|
563
|
+
attr_validator.engine.permit? @privilege, :object => attr_value, :user => attr_validator.user
|
564
|
+
when Hash
|
565
|
+
hash_or_attr.all? do |attr, sub_hash|
|
566
|
+
attr_value = object_attribute_value(object, attr)
|
567
|
+
if attr_value.nil?
|
568
|
+
raise NilAttributeValueError, "Attribute #{attr.inspect} is nil in #{object.inspect}."
|
569
|
+
end
|
570
|
+
validate?(attr_validator, attr_value, sub_hash)
|
571
|
+
end
|
572
|
+
when NilClass
|
573
|
+
attr_validator.engine.permit? @privilege, :object => object, :user => attr_validator.user
|
574
|
+
else
|
575
|
+
raise AuthorizationError, "Wrong conditions hash format: #{hash_or_attr.inspect}"
|
576
|
+
end
|
577
|
+
end
|
578
|
+
|
579
|
+
# may return an array of obligations to be OR'ed
|
580
|
+
def obligation (attr_validator, hash_or_attr = nil)
|
581
|
+
hash_or_attr ||= @attr_hash
|
582
|
+
case hash_or_attr
|
583
|
+
when Symbol
|
584
|
+
obligations = attr_validator.engine.obligations(@privilege,
|
585
|
+
:context => @context || hash_or_attr.to_s.pluralize.to_sym,
|
586
|
+
:user => attr_validator.user)
|
587
|
+
obligations.collect {|obl| {hash_or_attr => obl} }
|
588
|
+
when Hash
|
589
|
+
obligations_array_attrs = []
|
590
|
+
obligations =
|
591
|
+
hash_or_attr.inject({}) do |all, pair|
|
592
|
+
attr, sub_hash = pair
|
593
|
+
all[attr] = obligation(attr_validator, sub_hash)
|
594
|
+
if all[attr].length > 1
|
595
|
+
obligations_array_attrs << attr
|
596
|
+
else
|
597
|
+
all[attr] = all[attr].first
|
598
|
+
end
|
599
|
+
all
|
600
|
+
end
|
601
|
+
obligations = [obligations]
|
602
|
+
obligations_array_attrs.each do |attr|
|
603
|
+
next_array_size = obligations.first[attr].length
|
604
|
+
obligations = obligations.collect do |obls|
|
605
|
+
(0...next_array_size).collect do |idx|
|
606
|
+
obls_wo_array = obls.clone
|
607
|
+
obls_wo_array[attr] = obls_wo_array[attr][idx]
|
608
|
+
obls_wo_array
|
609
|
+
end
|
610
|
+
end.flatten
|
611
|
+
end
|
612
|
+
obligations
|
613
|
+
when NilClass
|
614
|
+
attr_validator.engine.obligations(@privilege,
|
615
|
+
:context => attr_validator.context,
|
616
|
+
:user => attr_validator.user)
|
617
|
+
else
|
618
|
+
raise AuthorizationError, "Wrong conditions hash format: #{hash_or_attr.inspect}"
|
619
|
+
end
|
620
|
+
end
|
621
|
+
|
622
|
+
def to_long_s
|
623
|
+
"if_permitted_to #{@privilege.inspect}, #{@attr_hash.inspect}"
|
624
|
+
end
|
625
|
+
end
|
626
|
+
|
627
|
+
# Represents a pseudo-user to facilitate guest users in applications
|
628
|
+
class GuestUser
|
629
|
+
attr_reader :role_symbols
|
630
|
+
def initialize (roles = [:guest])
|
631
|
+
@role_symbols = roles
|
632
|
+
end
|
633
|
+
end
|
634
|
+
end
|