rubycs-declarative_authorization 0.3.0

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.
Files changed (37) hide show
  1. data/CHANGELOG +70 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.rdoc +9 -0
  4. data/Rakefile +43 -0
  5. data/app/controllers/authorization_rules_controller.rb +114 -0
  6. data/app/controllers/authorization_usages_controller.rb +23 -0
  7. data/app/helpers/authorization_rules_helper.rb +100 -0
  8. data/app/views/authorization_rules/graph.dot.erb +49 -0
  9. data/app/views/authorization_rules/graph.html.erb +39 -0
  10. data/app/views/authorization_rules/index.html.erb +16 -0
  11. data/app/views/authorization_usages/index.html.erb +45 -0
  12. data/authorization_rules.dist.rb +20 -0
  13. data/config/locales/en.declarative_authorization.yml +35 -0
  14. data/config/locales/ro.declarative_authorization.yml +35 -0
  15. data/config/routes.rb +6 -0
  16. data/garlic_example.rb +20 -0
  17. data/init.rb +5 -0
  18. data/lib/declarative_authorization.rb +15 -0
  19. data/lib/declarative_authorization/authorization.rb +578 -0
  20. data/lib/declarative_authorization/authorization_rules_analyzer.rb +138 -0
  21. data/lib/declarative_authorization/helper.rb +56 -0
  22. data/lib/declarative_authorization/in_controller.rb +343 -0
  23. data/lib/declarative_authorization/in_model.rb +125 -0
  24. data/lib/declarative_authorization/maintenance.rb +174 -0
  25. data/lib/declarative_authorization/obligation_scope.rb +292 -0
  26. data/lib/declarative_authorization/rails_legacy.rb +14 -0
  27. data/lib/declarative_authorization/reader.rb +430 -0
  28. data/test/authorization_rules_analyzer_test.rb +123 -0
  29. data/test/authorization_test.rb +779 -0
  30. data/test/controller_test.rb +361 -0
  31. data/test/dsl_reader_test.rb +157 -0
  32. data/test/helper_test.rb +133 -0
  33. data/test/maintenance_test.rb +15 -0
  34. data/test/model_test.rb +1143 -0
  35. data/test/schema.sql +53 -0
  36. data/test/test_helper.rb +99 -0
  37. metadata +97 -0
@@ -0,0 +1,16 @@
1
+ <h1><%= I18n.t(:authorization_rules, :scope => [:declarative_authorization]) %></h1>
2
+ <p><%= I18n.t(:currently_active_rules_in_this_application, :scope => [:declarative_authorization]) %></p>
3
+ <p><%= navigation %></p>
4
+ <style type="text/css">
5
+ pre .constant {color: #a00;}
6
+ pre .special {color: red;}
7
+ pre .operator {color: red;}
8
+ pre .statement {color: #00a;}
9
+ pre .proc {color: #0a0;}
10
+ pre .privilege, pre .context {font-weight: bold}
11
+ pre .preproc, pre .comment, pre .comment span {color: grey !important;}
12
+ pre .note {color: #a00; position:absolute; cursor: help }
13
+ </style>
14
+ <pre>
15
+ <%= policy_analysis_hints(syntax_highlight(h(@auth_rules_script)), @auth_rules_script) %>
16
+ </pre>
@@ -0,0 +1,45 @@
1
+ <h1><%= I18n.t(:authorization_usage, :scope => [:declarative_authorization]) %></h1>
2
+ <div style="margin: 1em;border:1px solid #ccc;max-width:50%;position:fixed;right:0;display:none">
3
+ <object id="graph" data="<%= url_for :format => 'svg' %>" type="image/svg+xml" style="max-width:100%"/>
4
+ </div>
5
+ <p><%= I18n.t(:filter_rules_in_actions_by_controller, :scope => [:declarative_authorization]) %>:</p>
6
+ <p><%= navigation %></p>
7
+ <style type="text/css">
8
+ .auth-usages th { text-align: left; padding-top: 1em }
9
+ .auth-usages td { padding-right: 1em }
10
+ .auth-usages tr.action { cursor: pointer }
11
+ .auth-usages tr.unprotected { background: #FFA399 }
12
+ .auth-usages tr.no-attribute-check { background: #FFE599 }
13
+ /*.auth-usages tr.catch-all td.privilege,*/
14
+ .auth-usages tr.default-privilege td.privilege,
15
+ .auth-usages tr.default-context td.context { color: #888888 }
16
+ </style>
17
+ <% javascript_tag do %>
18
+ function show_graph (privilege, context) {
19
+ base_url = "<%= graph_authorization_rules_path('svg') %>";
20
+ $('graph').data = base_url + '?privilege_hierarchy=1&highlight_privilege=' +
21
+ privilege + '&filter_contexts=' + context;
22
+ $('graph').up().show();
23
+ }
24
+ <% end %>
25
+ <table class="auth-usages">
26
+ <% @auth_usages_by_controller.keys.sort {|c1, c2| c1.name <=> c2.name}.each do |controller| %>
27
+ <% default_context = controller.controller_name.to_sym rescue nil %>
28
+ <tr>
29
+ <th colspan="3"><%= h controller.controller_name %></th>
30
+ </tr>
31
+ <% @auth_usages_by_controller[controller].keys.sort {|c1, c2| c1.to_s <=> c2.to_s}.each do |action| %>
32
+ <% auth_info = @auth_usages_by_controller[controller][action] %>
33
+ <% first_permission = auth_info[:controller_permissions] && auth_info[:controller_permissions][0] %>
34
+ <tr class="action <%= auth_usage_info_classes(auth_info) %>" title="<%= auth_usage_info_title(auth_info) %>" onclick="show_graph('<%= auth_info[:privilege] || action %>','<%= auth_info[:context] || default_context %>')">
35
+ <td><%= h action %></td>
36
+ <% if first_permission %>
37
+ <td class="privilege"><%= h auth_info[:privilege] || action %></td>
38
+ <td class="context"><%= h auth_info[:context] || default_context %></td>
39
+ <% else %>
40
+ <td></td><td></td>
41
+ <% end %>
42
+ </tr>
43
+ <% end %>
44
+ <% end %>
45
+ </table>
@@ -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
@@ -0,0 +1,35 @@
1
+ en:
2
+ declarative_authorization:
3
+
4
+ # app/controllers/authorization_rules_controller.rb
5
+ error_in_call_to_graphviz: Error in call to graphviz
6
+
7
+ # app/helpers/authorization_rules_helper.rb
8
+ rules: Rules
9
+ graphical_view: Graphical view
10
+ usages: Usages
11
+ no_filter_access_to_call_protects_this_action: No filter_access_to call protects this action
12
+ action_is_not_protected_with_attribute_check: Action is not protected with attribute check
13
+ privilege_set_automatically_from_action_name_by_all_rule: Privilege set automatically from action name by :all rule
14
+ context_set_automatically_from_controller_name_by_filter_access_to_call_without_context_option: Context set automatically from controller name by filter_access_to call without :context option
15
+
16
+ # app/views/authorization_rules/graph.html.erb
17
+ authorization_rules_graph: Authorization Rules Graph
18
+ privilege_hierarchy: Privilege hierarchy
19
+ currently_active_rules_in_this_application: Currently active rules in this application.
20
+ all_rules: All rules
21
+ all_contexts: All contexts
22
+ effective_privileges: Effective privileges
23
+ show_full_privilege_hierarchy: Show full privilege hierarchy
24
+ zoom_in: Zoom in
25
+ zoom_out: Zoom out
26
+
27
+ # app/views/authorization_rules/index.html.erb
28
+ authorization_rules: Authorization Rules
29
+
30
+ # app/views/authorization_usages/index.html.erb
31
+ authorization_usage: Authorization Usage
32
+ filter_rules_in_actions_by_controller: Filter rules in actions by controller
33
+
34
+ # lib/declarative_authorization/in_controller.rb
35
+ you_are_not_allowed_to_access_this_action: You are not allowed to access this action
@@ -0,0 +1,35 @@
1
+ ro:
2
+ declarative_authorization:
3
+
4
+ # app/controllers/authorization_rules_controller.rb
5
+ error_in_call_to_graphviz: Eroare generare imagine cu Graphviz
6
+
7
+ # app/helpers/authorization_rules_helper.rb
8
+ rules: Reguli
9
+ graphical_view: Vizualizare grafica
10
+ usages: Utilizari
11
+ no_filter_access_to_call_protects_this_action: Fara filter_access_to pentru a proteja aceasta actiune
12
+ action_is_not_protected_with_attribute_check: Aceasta actiune nu este protejat de attribute_check
13
+ privilege_set_automatically_from_action_name_by_all_rule: Privilegiile sunt setate intr-un mod automat din numele actiune de catre regula :all
14
+ context_set_automatically_from_controller_name_by_filter_access_to_call_without_context_option: Contextul este setat intr-un mod automatic de catre numele actiunii cu filter_access_to fara optiune de context setata
15
+
16
+ # app/views/authorization_rules/graph.html.erb
17
+ authorization_rules_graph: Grafica pentru regurile de autorizare
18
+ privilege_hierarchy: Ierarhia privilegiilor
19
+ currently_active_rules_in_this_application: Reguli active in aceasta aplicatie.
20
+ all_rules: Toate regurile
21
+ all_contexts: Toate contexturile
22
+ effective_privileges: Privilegii efective
23
+ show_full_privilege_hierarchy: Arata ierarhia completa de privilegii
24
+ zoom_in: Marire
25
+ zoom_out: Micsorare
26
+
27
+ # app/views/authorization_rules/index.html.erb
28
+ authorization_rules: Reguli de autorizare
29
+
30
+ # app/views/authorization_usages/index.html.erb
31
+ authorization_usage: Utilizarea regurilor de autorizare
32
+ filter_rules_in_actions_by_controller: Filtrare reguli de autorizare pe baza de controlleri
33
+
34
+ # lib/declarative_authorization/in_controller.rb
35
+ you_are_not_allowed_to_access_this_action: Nu aveti permisiunea de a acesa aceasta pagina
data/config/routes.rb ADDED
@@ -0,0 +1,6 @@
1
+ ActionController::Routing::Routes.draw do |map|
2
+ if Authorization::activate_authorization_rules_browser?
3
+ map.resources :authorization_rules, :only => :index, :collection => {:graph => :get}
4
+ map.resources :authorization_usages, :only => :index
5
+ end
6
+ 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,5 @@
1
+ begin
2
+ require File.join(File.dirname(__FILE__), 'lib', 'declarative_authorization') # From here
3
+ rescue LoadError
4
+ require 'declarative_authorization' # From gem
5
+ end
@@ -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,578 @@
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
+ @@ignore_access_control = false
37
+ # For use in test cases only
38
+ def self.ignore_access_control (state = nil) # :nodoc:
39
+ 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
+
63
+ # If +reader+ is not given, a new one is created with the default
64
+ # authorization configuration of +AUTH_DSL_FILE+. If given, may be either
65
+ # a Reader object or a path to a configuration file.
66
+ def initialize (reader = nil)
67
+ if reader.nil?
68
+ begin
69
+ reader = Reader::DSLReader.load(AUTH_DSL_FILE)
70
+ rescue SystemCallError
71
+ reader = Reader::DSLReader.new
72
+ end
73
+ elsif reader.is_a?(String)
74
+ reader = Reader::DSLReader.load(reader)
75
+ end
76
+ @privileges = reader.privileges_reader.privileges
77
+ # {priv => [[priv, ctx],...]}
78
+ @privilege_hierarchy = reader.privileges_reader.privilege_hierarchy
79
+ @auth_rules = reader.auth_rules_reader.auth_rules
80
+ @roles = reader.auth_rules_reader.roles
81
+ @role_hierarchy = reader.auth_rules_reader.role_hierarchy
82
+
83
+ @role_titles = reader.auth_rules_reader.role_titles
84
+ @role_descriptions = reader.auth_rules_reader.role_descriptions
85
+
86
+ # {[priv, ctx] => [priv, ...]}
87
+ @rev_priv_hierarchy = {}
88
+ @privilege_hierarchy.each do |key, value|
89
+ value.each do |val|
90
+ @rev_priv_hierarchy[val] ||= []
91
+ @rev_priv_hierarchy[val] << key
92
+ end
93
+ end
94
+ end
95
+
96
+ # Returns true if privilege is met by the current user. Raises
97
+ # AuthorizationError otherwise. +privilege+ may be given with or
98
+ # without context. In the latter case, the :+context+ option is
99
+ # required.
100
+ #
101
+ # Options:
102
+ # [:+context+]
103
+ # The context part of the privilege.
104
+ # Defaults either to the +table_name+ of the given :+object+, if given.
105
+ # That is, either :+users+ for :+object+ of type User.
106
+ # Raises AuthorizationUsageError if
107
+ # context is missing and not to be infered.
108
+ # [:+object+] An context object to test attribute checks against.
109
+ # [:+skip_attribute_test+]
110
+ # Skips those attribute checks in the
111
+ # authorization rules. Defaults to false.
112
+ # [:+user+]
113
+ # The user to check the authorization for.
114
+ # Defaults to Authorization#current_user.
115
+ #
116
+ def permit! (privilege, options = {})
117
+ return true if Authorization.ignore_access_control
118
+ options = {
119
+ :object => nil,
120
+ :skip_attribute_test => false,
121
+ :context => nil
122
+ }.merge(options)
123
+
124
+ # Make sure we're handling all privileges as symbols.
125
+ privilege = privilege.is_a?( Array ) ?
126
+ privilege.flatten.collect { |priv| priv.to_sym } :
127
+ privilege.to_sym
128
+
129
+ #
130
+ # If the object responds to :proxy_reflection, we're probably working with
131
+ # an association proxy. Use 'new' to leverage ActiveRecord's builder
132
+ # functionality to obtain an object against which we can check permissions.
133
+ #
134
+ # Example: permit!( :edit, :object => user.posts )
135
+ #
136
+ if options[:object].respond_to?( :proxy_reflection ) && options[:object].respond_to?( :new )
137
+ options[:object] = options[:object].new
138
+ end
139
+
140
+ options[:context] ||= options[:object] && options[:object].class.table_name.to_sym rescue NoMethodError
141
+
142
+ user, roles, privileges = user_roles_privleges_from_options(privilege, options)
143
+
144
+ # find a authorization rule that matches for at least one of the roles and
145
+ # at least one of the given privileges
146
+ attr_validator = AttributeValidator.new(self, user, options[:object])
147
+ rules = matching_auth_rules(roles, privileges, options[:context])
148
+ if rules.empty?
149
+ raise NotAuthorized, "No matching rules found for #{privilege} for #{user.inspect} " +
150
+ "(roles #{roles.inspect}, privileges #{privileges.inspect}, " +
151
+ "context #{options[:context].inspect})."
152
+ end
153
+
154
+ # Test each rule in turn to see whether any one of them is satisfied.
155
+ grant_permission = rules.any? do |rule|
156
+ begin
157
+ options[:skip_attribute_test] or
158
+ rule.attributes.empty? or
159
+ rule.attributes.send(rule.join_operator == :and ? :all? : :any?) do |attr|
160
+ begin
161
+ attr.validate?( attr_validator )
162
+ rescue NilAttributeValueError => e
163
+ nil # Bumping up against a nil attribute value flunks the rule.
164
+ end
165
+ end
166
+ end
167
+ end
168
+ unless grant_permission
169
+ raise AttributeAuthorizationError, "#{privilege} not allowed for #{user.inspect} on #{options[:object].inspect}."
170
+ end
171
+ true
172
+ end
173
+
174
+ # Calls permit! but rescues the AuthorizationException and returns false
175
+ # instead. If no exception is raised, permit? returns true and yields
176
+ # to the optional block.
177
+ def permit? (privilege, options = {}, &block) # :yields:
178
+ permit!(privilege, options)
179
+ yield if block_given?
180
+ true
181
+ rescue NotAuthorized
182
+ false
183
+ end
184
+
185
+ # Returns the obligations to be met by the current user for the given
186
+ # privilege as an array of obligation hashes in form of
187
+ # [{:object_attribute => obligation_value, ...}, ...]
188
+ # where +obligation_value+ is either (recursively) another obligation hash
189
+ # or a value spec, such as
190
+ # [operator, literal_value]
191
+ # The obligation hashes in the array should be OR'ed, conditions inside
192
+ # the hashes AND'ed.
193
+ #
194
+ # Example
195
+ # {:branch => {:company => [:is, 24]}, :active => [:is, true]}
196
+ #
197
+ # Options
198
+ # [:+context+] See permit!
199
+ # [:+user+] See permit!
200
+ #
201
+ def obligations (privilege, options = {})
202
+ options = {:context => nil}.merge(options)
203
+ user, roles, privileges = user_roles_privleges_from_options(privilege, options)
204
+ attr_validator = AttributeValidator.new(self, user, nil, options[:context])
205
+ matching_auth_rules(roles, privileges, options[:context]).collect do |rule|
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
215
+ end.flatten
216
+ end
217
+
218
+ # Returns the description for the given role. The description may be
219
+ # specified with the authorization rules. Returns +nil+ if none was
220
+ # given.
221
+ def description_for (role)
222
+ role_descriptions[role]
223
+ end
224
+
225
+ # Returns the title for the given role. The title may be
226
+ # specified with the authorization rules. Returns +nil+ if none was
227
+ # given.
228
+ def title_for (role)
229
+ role_titles[role]
230
+ end
231
+
232
+ # Returns the role symbols of the given user.
233
+ def roles_for (user)
234
+ raise AuthorizationUsageError, "User object doesn't respond to roles" \
235
+ if !user.respond_to?(:role_symbols) and !user.respond_to?(:roles)
236
+
237
+ RAILS_DEFAULT_LOGGER.info("The use of user.roles is deprecated. Please add a method " +
238
+ "role_symbols to your User model.") if defined?(RAILS_DEFAULT_LOGGER) and !user.respond_to?(:role_symbols)
239
+
240
+ roles = user.respond_to?(:role_symbols) ? user.role_symbols : user.roles
241
+
242
+ raise AuthorizationUsageError, "User.#{user.respond_to?(:role_symbols) ? 'role_symbols' : 'roles'} " +
243
+ "doesn't return an Array of Symbols (#{roles.inspect})" \
244
+ if !roles.is_a?(Array) or (!roles.empty? and !roles[0].is_a?(Symbol))
245
+
246
+ (roles.empty? ? [:guest] : roles)
247
+ end
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
+
254
+ # Returns an instance of Engine, which is created if there isn't one
255
+ # yet. If +dsl_file+ is given, it is passed on to Engine.new and
256
+ # a new instance is always created.
257
+ def self.instance (dsl_file = nil)
258
+ if dsl_file or ENV['RAILS_ENV'] == 'development'
259
+ @@instance = new(dsl_file)
260
+ else
261
+ @@instance ||= new
262
+ end
263
+ end
264
+
265
+ class AttributeValidator # :nodoc:
266
+ attr_reader :user, :object, :engine, :context
267
+ def initialize (engine, user, object = nil, context = nil)
268
+ @engine = engine
269
+ @user = user
270
+ @object = object
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
+ }.merge(options)
286
+ user = options[:user] || Authorization.current_user
287
+ privileges = privilege.is_a?(Array) ? privilege : [privilege]
288
+
289
+ raise AuthorizationUsageError, "No user object given (#{user.inspect})" \
290
+ unless user
291
+
292
+ roles = flatten_roles(roles_for(user))
293
+ privileges = flatten_privileges privileges, options[:context]
294
+ [user, roles, privileges]
295
+ end
296
+
297
+ def flatten_roles (roles)
298
+ # TODO caching?
299
+ flattened_roles = roles.clone.to_a
300
+ flattened_roles.each do |role|
301
+ flattened_roles.concat(@role_hierarchy[role]).uniq! if @role_hierarchy[role]
302
+ end
303
+ end
304
+
305
+ # Returns the privilege hierarchy flattened for given privileges in context.
306
+ def flatten_privileges (privileges, context = nil)
307
+ # TODO caching?
308
+ #if context.nil?
309
+ # context = privileges.collect { |p| p.to_s.split('_') }.
310
+ # reject { |p_p| p_p.length < 2 }.
311
+ # collect { |p_p| (p_p[1..-1] * '_').to_sym }.first
312
+ # raise AuthorizationUsageError, "No context given or inferable from privileges #{privileges.inspect}" unless context
313
+ #end
314
+ raise AuthorizationUsageError, "No context given or inferable from object" unless context
315
+ #context_regex = Regexp.new "_#{context}$"
316
+ # TODO work with contextless privileges
317
+ #flattened_privileges = privileges.collect {|p| p.to_s.sub(context_regex, '')}
318
+ flattened_privileges = privileges.clone #collect {|p| p.to_s.end_with?(context.to_s) ?
319
+ # p : [p, "#{p}_#{context}".to_sym] }.flatten
320
+ flattened_privileges.each do |priv|
321
+ flattened_privileges.concat(@rev_priv_hierarchy[[priv, nil]]).uniq! if @rev_priv_hierarchy[[priv, nil]]
322
+ flattened_privileges.concat(@rev_priv_hierarchy[[priv, context]]).uniq! if @rev_priv_hierarchy[[priv, context]]
323
+ end
324
+ end
325
+
326
+ def matching_auth_rules (roles, privileges, context)
327
+ @auth_rules.select {|rule| rule.matches? roles, privileges, context}
328
+ end
329
+ end
330
+
331
+ class AuthorizationRule
332
+ attr_reader :attributes, :contexts, :role, :privileges, :join_operator
333
+
334
+ def initialize (role, privileges = [], contexts = nil, join_operator = :or)
335
+ @role = role
336
+ @privileges = Set.new(privileges)
337
+ @contexts = Set.new((contexts && !contexts.is_a?(Array) ? [contexts] : contexts))
338
+ @join_operator = join_operator
339
+ @attributes = []
340
+ end
341
+
342
+ def append_privileges (privs)
343
+ @privileges.merge(privs)
344
+ end
345
+
346
+ def append_attribute (attribute)
347
+ @attributes << attribute
348
+ end
349
+
350
+ def matches? (roles, privs, context = nil)
351
+ roles = [roles] unless roles.is_a?(Array)
352
+ @contexts.include?(context) and roles.include?(@role) and
353
+ not (@privileges & privs).empty?
354
+ end
355
+
356
+ def to_long_s
357
+ attributes.collect {|attr| attr.to_long_s } * "; "
358
+ end
359
+ end
360
+
361
+ class Attribute
362
+ # attr_conditions_hash of form
363
+ # { :object_attribute => [operator, value_block], ... }
364
+ # { :object_attribute => { :attr => ... } }
365
+ def initialize (conditions_hash)
366
+ @conditions_hash = conditions_hash
367
+ end
368
+
369
+ def validate? (attr_validator, object = nil, hash = nil)
370
+ object ||= attr_validator.object
371
+ return false unless object
372
+
373
+ (hash || @conditions_hash).all? do |attr, value|
374
+ attr_value = object_attribute_value(object, attr)
375
+ if value.is_a?(Hash)
376
+ if attr_value.is_a?(Array)
377
+ raise AuthorizationUsageError, "Unable evaluate multiple attributes " +
378
+ "on a collection. Cannot use '=>' operator on #{attr.inspect} " +
379
+ "(#{attr_value.inspect}) for attributes #{value.inspect}."
380
+ elsif attr_value.nil?
381
+ raise NilAttributeValueError, "Attribute #{attr.inspect} is nil in #{object.inspect}."
382
+ end
383
+ validate?(attr_validator, attr_value, value)
384
+ elsif value.is_a?(Array) and value.length == 2
385
+ evaluated = if value[1].is_a?(Proc)
386
+ attr_validator.evaluate(value[1])
387
+ else
388
+ value[1]
389
+ end
390
+ case value[0]
391
+ when :is
392
+ attr_value == evaluated
393
+ when :is_not
394
+ attr_value != evaluated
395
+ when :contains
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
403
+ when :does_not_contain
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
419
+ when :is_in
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
427
+ when :is_not_in
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
435
+ else
436
+ raise AuthorizationError, "Unknown operator #{value[0]}"
437
+ end
438
+ else
439
+ raise AuthorizationError, "Wrong conditions hash format"
440
+ end
441
+ end
442
+ end
443
+
444
+ # resolves all the values in condition_hash
445
+ def obligation (attr_validator, hash = nil)
446
+ hash = (hash || @conditions_hash).clone
447
+ hash.each do |attr, value|
448
+ if value.is_a?(Hash)
449
+ hash[attr] = obligation(attr_validator, value)
450
+ elsif value.is_a?(Array) and value.length == 2
451
+ hash[attr] = [value[0], attr_validator.evaluate(value[1])]
452
+ else
453
+ raise AuthorizationError, "Wrong conditions hash format"
454
+ end
455
+ end
456
+ hash
457
+ end
458
+
459
+ def to_long_s (hash = nil)
460
+ if hash
461
+ hash.inject({}) do |memo, key_val|
462
+ key, val = key_val
463
+ memo[key] = case val
464
+ when Array then "#{val[0]} { #{val[1].respond_to?(:to_ruby) ? val[1].to_ruby.gsub(/^proc \{\n?(.*)\n?\}$/m, '\1') : "..."} }"
465
+ when Hash then to_long_s(val)
466
+ end
467
+ memo
468
+ end
469
+ else
470
+ "if_attribute #{to_long_s(@conditions_hash).inspect}"
471
+ end
472
+ end
473
+
474
+ protected
475
+ def object_attribute_value (object, attr)
476
+ begin
477
+ object.send(attr)
478
+ rescue ArgumentError, NoMethodError => e
479
+ raise AuthorizationUsageError, "Error when calling #{attr} on " +
480
+ "#{object.inspect} for validating attribute: #{e}"
481
+ end
482
+ end
483
+ end
484
+
485
+ # An attribute condition that uses existing rules to decide validation
486
+ # and create obligations.
487
+ class AttributeWithPermission < Attribute
488
+ # E.g. privilege :read, attr_or_hash either :attribute or
489
+ # { :attribute => :deeper_attribute }
490
+ def initialize (privilege, attr_or_hash, context = nil)
491
+ @privilege = privilege
492
+ @context = context
493
+ @attr_hash = attr_or_hash
494
+ end
495
+
496
+ def validate? (attr_validator, object = nil, hash_or_attr = nil)
497
+ object ||= attr_validator.object
498
+ hash_or_attr ||= @attr_hash
499
+ return false unless object
500
+
501
+ case hash_or_attr
502
+ when Symbol
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
507
+ attr_validator.engine.permit? @privilege, :object => attr_value, :user => attr_validator.user
508
+ when Hash
509
+ hash_or_attr.all? do |attr, sub_hash|
510
+ attr_value = object_attribute_value(object, attr)
511
+ if attr_value.nil?
512
+ raise NilAttributeValueError, "Attribute #{attr.inspect} is nil in #{object.inspect}."
513
+ end
514
+ validate?(attr_validator, attr_value, sub_hash)
515
+ end
516
+ when NilClass
517
+ attr_validator.engine.permit? @privilege, :object => object, :user => attr_validator.user
518
+ else
519
+ raise AuthorizationError, "Wrong conditions hash format: #{hash_or_attr.inspect}"
520
+ end
521
+ end
522
+
523
+ # may return an array of obligations to be OR'ed
524
+ def obligation (attr_validator, hash_or_attr = nil)
525
+ hash_or_attr ||= @attr_hash
526
+ case hash_or_attr
527
+ when Symbol
528
+ obligations = attr_validator.engine.obligations(@privilege,
529
+ :context => @context || hash_or_attr.to_s.pluralize.to_sym,
530
+ :user => attr_validator.user)
531
+ obligations.collect {|obl| {hash_or_attr => obl} }
532
+ when Hash
533
+ obligations_array_attrs = []
534
+ obligations =
535
+ hash_or_attr.inject({}) do |all, pair|
536
+ attr, sub_hash = pair
537
+ all[attr] = obligation(attr_validator, sub_hash)
538
+ if all[attr].length > 1
539
+ obligations_array_attrs << attr
540
+ else
541
+ all[attr] = all[attr].first
542
+ end
543
+ all
544
+ end
545
+ obligations = [obligations]
546
+ obligations_array_attrs.each do |attr|
547
+ next_array_size = obligations.first[attr].length
548
+ obligations = obligations.collect do |obls|
549
+ (0...next_array_size).collect do |idx|
550
+ obls_wo_array = obls.clone
551
+ obls_wo_array[attr] = obls_wo_array[attr][idx]
552
+ obls_wo_array
553
+ end
554
+ end.flatten
555
+ end
556
+ obligations
557
+ when NilClass
558
+ attr_validator.engine.obligations(@privilege,
559
+ :context => attr_validator.context,
560
+ :user => attr_validator.user)
561
+ else
562
+ raise AuthorizationError, "Wrong conditions hash format: #{hash_or_attr.inspect}"
563
+ end
564
+ end
565
+
566
+ def to_long_s
567
+ "if_permitted_to #{@privilege.inspect}, #{@attr_hash.inspect}"
568
+ end
569
+ end
570
+
571
+ # Represents a pseudo-user to facilitate guest users in applications
572
+ class GuestUser
573
+ attr_reader :role_symbols
574
+ def initialize (roles = [:guest])
575
+ @role_symbols = roles
576
+ end
577
+ end
578
+ end