uhees-declarative_authorization 0.3.1

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 (42) hide show
  1. data/CHANGELOG +77 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.rdoc +490 -0
  4. data/Rakefile +43 -0
  5. data/app/controllers/authorization_rules_controller.rb +235 -0
  6. data/app/controllers/authorization_usages_controller.rb +23 -0
  7. data/app/helpers/authorization_rules_helper.rb +183 -0
  8. data/app/views/authorization_rules/_change.erb +49 -0
  9. data/app/views/authorization_rules/_show_graph.erb +37 -0
  10. data/app/views/authorization_rules/_suggestion.erb +9 -0
  11. data/app/views/authorization_rules/_suggestions.erb +24 -0
  12. data/app/views/authorization_rules/change.html.erb +124 -0
  13. data/app/views/authorization_rules/graph.dot.erb +68 -0
  14. data/app/views/authorization_rules/graph.html.erb +40 -0
  15. data/app/views/authorization_rules/index.html.erb +17 -0
  16. data/app/views/authorization_usages/index.html.erb +36 -0
  17. data/authorization_rules.dist.rb +20 -0
  18. data/config/routes.rb +7 -0
  19. data/garlic_example.rb +20 -0
  20. data/init.rb +5 -0
  21. data/lib/declarative_authorization.rb +15 -0
  22. data/lib/declarative_authorization/authorization.rb +630 -0
  23. data/lib/declarative_authorization/development_support/analyzer.rb +252 -0
  24. data/lib/declarative_authorization/development_support/change_analyzer.rb +253 -0
  25. data/lib/declarative_authorization/development_support/change_supporter.rb +578 -0
  26. data/lib/declarative_authorization/development_support/development_support.rb +243 -0
  27. data/lib/declarative_authorization/helper.rb +60 -0
  28. data/lib/declarative_authorization/in_controller.rb +367 -0
  29. data/lib/declarative_authorization/in_model.rb +150 -0
  30. data/lib/declarative_authorization/maintenance.rb +188 -0
  31. data/lib/declarative_authorization/obligation_scope.rb +297 -0
  32. data/lib/declarative_authorization/rails_legacy.rb +14 -0
  33. data/lib/declarative_authorization/reader.rb +438 -0
  34. data/test/authorization_test.rb +823 -0
  35. data/test/controller_test.rb +418 -0
  36. data/test/dsl_reader_test.rb +157 -0
  37. data/test/helper_test.rb +154 -0
  38. data/test/maintenance_test.rb +41 -0
  39. data/test/model_test.rb +1171 -0
  40. data/test/schema.sql +53 -0
  41. data/test/test_helper.rb +103 -0
  42. metadata +104 -0
@@ -0,0 +1,150 @@
1
+ # Authorization::AuthorizationInModel
2
+ require File.dirname(__FILE__) + '/authorization.rb'
3
+ require File.dirname(__FILE__) + '/obligation_scope.rb'
4
+
5
+ module Authorization
6
+
7
+ module AuthorizationInModel
8
+
9
+ # If the user meets the given privilege, permitted_to? returns true
10
+ # and yields to the optional block.
11
+ def permitted_to? (privilege, options = {} )
12
+ options = {
13
+ :user => Authorization.current_user,
14
+ :object => self
15
+ }.merge(options)
16
+ Authorization::Engine.instance.permit?(privilege,
17
+ {:user => options[:user],
18
+ :object => options[:object]},
19
+ &block)
20
+ end
21
+
22
+ # Works similar to the permitted_to? method, but doesn't accept a block
23
+ # and throws the authorization exceptions, just like Engine#permit!
24
+ def permitted_to! (privilege, options = {} )
25
+ options = {
26
+ :user => Authorization.current_user,
27
+ :object => self
28
+ }.merge(options)
29
+ Authorization::Engine.instance.permit!(privilege,
30
+ {:user => options[:user],
31
+ :object => options[:object]})
32
+ end
33
+
34
+ def self.included(base) # :nodoc:
35
+ #base.extend(ClassMethods)
36
+ base.module_eval do
37
+ scopes[:with_permissions_to] = lambda do |parent_scope, *args|
38
+ options = args.last.is_a?(Hash) ? args.pop : {}
39
+ privilege = (args[0] || :read).to_sym
40
+ privileges = [privilege]
41
+ context = options[:context] || :"#{parent_scope.table_name}"
42
+
43
+ user = options[:user] || Authorization.current_user
44
+
45
+ engine = options[:engine] || Authorization::Engine.instance
46
+ engine.permit!(privileges, :user => user, :skip_attribute_test => true,
47
+ :context => context)
48
+
49
+ obligation_scope_for( privileges, :user => user,
50
+ :context => context, :engine => engine, :model => parent_scope)
51
+ end
52
+
53
+ # Builds and returns a scope with joins and conditions satisfying all obligations.
54
+ def self.obligation_scope_for( privileges, options = {} )
55
+ options = {
56
+ :user => Authorization.current_user,
57
+ :context => nil,
58
+ :model => self,
59
+ :engine => nil,
60
+ }.merge(options)
61
+ engine = options[:engine] || Authorization::Engine.instance
62
+
63
+ scope = ObligationScope.new( options[:model], {} )
64
+ engine.obligations( privileges, :user => options[:user], :context => options[:context] ).each do |obligation|
65
+ scope.parse!( obligation )
66
+ end
67
+ scope
68
+ end
69
+
70
+ # Named scope for limiting query results according to the authorization
71
+ # of the current user. If no privilege is given, :+read+ is assumed.
72
+ #
73
+ # User.with_permissions_to
74
+ # User.with_permissions_to(:update)
75
+ # User.with_permissions_to(:update, :context => :users)
76
+ #
77
+ # As in the case of other named scopes, this one may be chained:
78
+ # User.with_permission_to.find(:all, :conditions...)
79
+ #
80
+ # Options
81
+ # [:+context+]
82
+ # Context for the privilege to be evaluated in; defaults to the
83
+ # model's table name.
84
+ # [:+user+]
85
+ # User to be used for gathering obligations; defaults to the
86
+ # current user.
87
+ #
88
+ def self.with_permissions_to (*args)
89
+ scopes[:with_permissions_to].call(self, *args)
90
+ end
91
+
92
+ # Activates model security for the current model. Then, CRUD operations
93
+ # are checked against the authorization of the current user. The
94
+ # privileges are :+create+, :+read+, :+update+ and :+delete+ in the
95
+ # context of the model. By default, :+read+ is not checked because of
96
+ # performance impacts, especially with large result sets.
97
+ #
98
+ # class User < ActiveRecord::Base
99
+ # using_access_control
100
+ # end
101
+ #
102
+ # If an operation is not permitted, a Authorization::AuthorizationError
103
+ # is raised.
104
+ #
105
+ # To activate model security on all models, call using_access_control
106
+ # on ActiveRecord::Base
107
+ # ActiveRecord::Base.using_access_control
108
+ #
109
+ # Available options
110
+ # [:+context+] Specify context different from the models table name.
111
+ # [:+include_read+] Also check for :+read+ privilege after find.
112
+ #
113
+ def self.using_access_control (options = {})
114
+ options = {
115
+ :context => nil,
116
+ :include_read => false
117
+ }.merge(options)
118
+
119
+ class_eval do
120
+ [:create, :update, [:destroy, :delete]].each do |action, privilege|
121
+ send(:"before_#{action}") do |object|
122
+ Authorization::Engine.instance.permit!(privilege || action,
123
+ :object => object, :context => options[:context])
124
+ end
125
+ end
126
+
127
+ # after_find is only called if after_find is implemented
128
+ after_find do |object|
129
+ Authorization::Engine.instance.permit!(:read, :object => object,
130
+ :context => options[:context])
131
+ end
132
+
133
+ if options[:include_read]
134
+ def after_find; end
135
+ end
136
+
137
+ def self.using_access_control?
138
+ true
139
+ end
140
+ end
141
+ end
142
+
143
+ # Returns true if the model is using model security.
144
+ def self.using_access_control?
145
+ false
146
+ end
147
+ end
148
+ end
149
+ end
150
+ end
@@ -0,0 +1,188 @@
1
+ # Authorization::Maintenance
2
+ require File.dirname(__FILE__) + '/authorization.rb'
3
+
4
+ module Authorization
5
+
6
+ def self.ignore_access_control (state = nil) # :nodoc:
7
+ Thread.current["ignore_access_control"] = state unless state.nil?
8
+ Thread.current["ignore_access_control"] || false
9
+ end
10
+
11
+ # Provides a few maintenance methods for modifying data without enforcing
12
+ # authorization.
13
+ module Maintenance
14
+ # Disables access control for the given block. Appropriate for
15
+ # maintenance operation at the Rails console or in test case setup.
16
+ #
17
+ # For use in the Rails console:
18
+ # require "vendor/plugins/declarative_authorization/lib/maintenance"
19
+ # include Authorization::Maintenance
20
+ #
21
+ # without_access_control do
22
+ # SomeModel.find(:first).save
23
+ # end
24
+ def without_access_control
25
+ self.class.without_access_control
26
+ end
27
+
28
+ # A class method variant of without_access_control. Thus, one can call
29
+ # Authorization::Maintenance::without_access_control do
30
+ # ...
31
+ # end
32
+ def self.without_access_control
33
+ previous_state = Authorization.ignore_access_control
34
+ begin
35
+ Authorization.ignore_access_control(true)
36
+ yield
37
+ ensure
38
+ Authorization.ignore_access_control(previous_state)
39
+ end
40
+ end
41
+
42
+ # Sets the current user for the declarative authorization plugin to the
43
+ # given one for the execution of the supplied block. Suitable for tests
44
+ # on certain users.
45
+ def with_user (user)
46
+ prev_user = Authorization.current_user
47
+ Authorization.current_user = user
48
+ yield
49
+ ensure
50
+ Authorization.current_user = prev_user
51
+ end
52
+
53
+ # Module for grouping usage-related helper methods
54
+ module Usage
55
+ # Delivers a hash of {ControllerClass => usage_info_hash},
56
+ # where usage_info_hash has the form of
57
+ def self.usages_by_controller
58
+ # load each application controller
59
+ begin
60
+ Dir.foreach(File.join(RAILS_ROOT, %w{app controllers})) do |entry|
61
+ if entry =~ /^\w+_controller\.rb$/
62
+ require File.join(RAILS_ROOT, %w{app controllers}, entry)
63
+ end
64
+ end
65
+ rescue Errno::ENOENT
66
+ end
67
+ controllers = []
68
+ ObjectSpace.each_object(Class) do |obj|
69
+ controllers << obj if obj.ancestors.include?(ActionController::Base) and
70
+ !%w{ActionController::Base ApplicationController}.include?(obj.name)
71
+ end
72
+
73
+ controllers.inject({}) do |memo, controller|
74
+ catchall_permissions = []
75
+ permission_by_action = {}
76
+ controller.all_filter_access_permissions.each do |controller_permissions|
77
+ catchall_permissions << controller_permissions if controller_permissions.actions.include?(:all)
78
+ controller_permissions.actions.reject {|action| action == :all}.each do |action|
79
+ permission_by_action[action] = controller_permissions
80
+ end
81
+ end
82
+
83
+ actions = controller.public_instance_methods(false) - controller.hidden_actions
84
+ memo[controller] = actions.inject({}) do |actions_memo, action|
85
+ action_sym = action.to_sym
86
+ actions_memo[action_sym] =
87
+ if permission_by_action[action_sym]
88
+ {
89
+ :privilege => permission_by_action[action_sym].privilege,
90
+ :context => permission_by_action[action_sym].context,
91
+ :controller_permissions => [permission_by_action[action_sym]]
92
+ }
93
+ elsif !catchall_permissions.empty?
94
+ {
95
+ :privilege => catchall_permissions[0].privilege,
96
+ :context => catchall_permissions[0].context,
97
+ :controller_permissions => catchall_permissions
98
+ }
99
+ else
100
+ {}
101
+ end
102
+ actions_memo
103
+ end
104
+ memo
105
+ end
106
+ end
107
+ end
108
+ end
109
+
110
+ # TestHelper provides assert methods and controller request methods which
111
+ # take authorization into account and set the current user to a specific
112
+ # one.
113
+ #
114
+ # Defines get_with, post_with, get_by_xhr_with etc. for methods
115
+ # get, post, put, delete each with the signature
116
+ # get_with(user, action, params = {}, session = {}, flash = {})
117
+ #
118
+ # Use it by including it in your TestHelper:
119
+ # require File.expand_path(File.dirname(__FILE__) +
120
+ # "/../vendor/plugins/declarative_authorization/lib/maintenance")
121
+ # class Test::Unit::TestCase
122
+ # include Authorization::TestHelper
123
+ # ...
124
+ #
125
+ # def admin
126
+ # # create admin user
127
+ # end
128
+ # end
129
+ #
130
+ # class SomeControllerTest < ActionController::TestCase
131
+ # def test_should_get_index
132
+ # ...
133
+ # get_with admin, :index, :param_1 => "param value"
134
+ # ...
135
+ # end
136
+ # end
137
+ module TestHelper
138
+ include Authorization::Maintenance
139
+
140
+ # Analogue to the Ruby's assert_raise method, only executing the block
141
+ # in the context of the given user.
142
+ def assert_raise_with_user (user, *args, &block)
143
+ assert_raise(*args) do
144
+ with_user(user, &block)
145
+ end
146
+ end
147
+
148
+ def should_be_allowed_to (privilege, object_or_context)
149
+ options = {}
150
+ options[object_or_context.is_a?(Symbol) ? :context : :object] = object_or_context
151
+ assert_nothing_raised do
152
+ Authorization::Engine.instance.permit!(privilege, options)
153
+ end
154
+ end
155
+
156
+ def should_not_be_allowed_to (privilege, object_or_context)
157
+ options = {}
158
+ options[object_or_context.is_a?(Symbol) ? :context : :object] = object_or_context
159
+ assert !Authorization::Engine.instance.permit?(privilege, options)
160
+ end
161
+
162
+ def request_with (user, method, xhr, action, params = {},
163
+ session = {}, flash = {})
164
+ session = session.merge({:user => user, :user_id => user.id})
165
+ with_user(user) do
166
+ if xhr
167
+ xhr method, action, params, session, flash
168
+ else
169
+ send method, action, params, session, flash
170
+ end
171
+ end
172
+ end
173
+
174
+ def self.included (base)
175
+ [:get, :post, :put, :delete].each do |method|
176
+ base.class_eval <<-EOV, __FILE__, __LINE__
177
+ def #{method}_with (user, *args)
178
+ request_with(user, #{method.inspect}, false, *args)
179
+ end
180
+
181
+ def #{method}_by_xhr_with (user, *args)
182
+ request_with(user, #{method.inspect}, true, *args)
183
+ end
184
+ EOV
185
+ end
186
+ end
187
+ end
188
+ end
@@ -0,0 +1,297 @@
1
+ module Authorization
2
+ # The +ObligationScope+ class parses any number of obligations into joins and conditions.
3
+ #
4
+ # In +ObligationScope+ parlance, "association paths" are one-dimensional arrays in which each
5
+ # element represents an attribute or association (or "step"), and "leads" to the next step in the
6
+ # association path.
7
+ #
8
+ # Suppose we have this path defined in the context of model Foo:
9
+ # +{ :bar => { :baz => { :foo => { :attr => is { user } } } } }+
10
+ #
11
+ # To parse this path, +ObligationScope+ evaluates each step in the context of the preceding step.
12
+ # The first step is evaluated in the context of the parent scope, the second step is evaluated in
13
+ # the context of the first, and so forth. Every time we encounter a step representing an
14
+ # association, we make note of the fact by storing the path (up to that point), assigning it a
15
+ # table alias intended to match the one that will eventually be chosen by ActiveRecord when
16
+ # executing the +find+ method on the scope.
17
+ #
18
+ # +@table_aliases = {
19
+ # [] => 'foos',
20
+ # [:bar] => 'bars',
21
+ # [:bar, :baz] => 'bazzes',
22
+ # [:bar, :baz, :foo] => 'foos_bazzes' # Alias avoids collisions with 'foos' (already used)
23
+ # }+
24
+ #
25
+ # At the "end" of each path, we expect to find a comparison operation of some kind, generally
26
+ # comparing an attribute of the most recent association with some other value (such as an ID,
27
+ # constant, or array of values). When we encounter a step representing a comparison, we make
28
+ # note of the fact by storing the path (up to that point) and the comparison operation together.
29
+ # (Note that individual obligations' conditions are kept separate, to allow their conditions to
30
+ # be OR'ed together in the generated scope options.)
31
+ #
32
+ # +@obligation_conditions[<obligation>][[:bar, :baz, :foo]] = [
33
+ # [ :attr, :is, <user.id> ]
34
+ # ]+
35
+ #
36
+ # After successfully parsing an obligation, all of the stored paths and conditions are converted
37
+ # into scope options (stored in +proxy_options+ as +:joins+ and +:conditions+). The resulting
38
+ # scope may then be used to find all scoped objects for which at least one of the parsed
39
+ # obligations is fully met.
40
+ #
41
+ # +@proxy_options[:joins] = { :bar => { :baz => :foo } }
42
+ # @proxy_options[:conditions] = [ 'foos_bazzes.attr = :foos_bazzes__id_0', { :foos_bazzes__id_0 => 1 } ]+
43
+ #
44
+ class ObligationScope < ActiveRecord::NamedScope::Scope
45
+
46
+ # Consumes the given obligation, converting it into scope join and condition options.
47
+ def parse!( obligation )
48
+ @current_obligation = obligation
49
+ obligation_conditions[@current_obligation] ||= {}
50
+ follow_path( obligation )
51
+
52
+ rebuild_condition_options!
53
+ rebuild_join_options!
54
+ end
55
+
56
+ protected
57
+
58
+ # Parses the next step in the association path. If it's an association, we advance down the
59
+ # path. Otherwise, it's an attribute, and we need to evaluate it as a comparison operation.
60
+ def follow_path( steps, past_steps = [] )
61
+ if steps.is_a?( Hash )
62
+ steps.each do |step, next_steps|
63
+ path_to_this_point = [past_steps, step].flatten
64
+ reflection = reflection_for( path_to_this_point ) rescue nil
65
+ if reflection
66
+ follow_path( next_steps, path_to_this_point )
67
+ else
68
+ follow_comparison( next_steps, past_steps, step )
69
+ end
70
+ end
71
+ elsif steps.is_a?( Array ) && steps.length == 2
72
+ if reflection_for( past_steps )
73
+ follow_comparison( steps, past_steps, :id )
74
+ else
75
+ follow_comparison( steps, past_steps[0..-2], past_steps[-1] )
76
+ end
77
+ else
78
+ raise "invalid obligation path #{[past_steps, steps].flatten}"
79
+ end
80
+ end
81
+
82
+ # At the end of every association path, we expect to see a comparison of some kind; for
83
+ # example, +:attr => [ :is, :value ]+.
84
+ #
85
+ # This method parses the comparison and creates an obligation condition from it.
86
+ def follow_comparison( steps, past_steps, attribute )
87
+ operator = steps[0]
88
+ value = steps[1..-1]
89
+ value = value[0] if value.length == 1
90
+
91
+ add_obligation_condition_for( past_steps, [attribute, operator, value] )
92
+ end
93
+
94
+ # Adds the given expression to the current obligation's indicated path's conditions.
95
+ #
96
+ # Condition expressions must follow the format +[ <attribute>, <operator>, <value> ]+.
97
+ def add_obligation_condition_for( path, expression )
98
+ raise "invalid expression #{expression.inspect}" unless expression.is_a?( Array ) && expression.length == 3
99
+ add_obligation_join_for( path )
100
+ obligation_conditions[@current_obligation] ||= {}
101
+ ( obligation_conditions[@current_obligation][path] ||= Set.new ) << expression
102
+ end
103
+
104
+ # Adds the given path to the list of obligation joins, if we haven't seen it before.
105
+ def add_obligation_join_for( path )
106
+ map_reflection_for( path ) if reflections[path].nil?
107
+ end
108
+
109
+ # Returns the model associated with the given path.
110
+ def model_for( path )
111
+ reflection = reflection_for( path )
112
+ reflection.respond_to?( :klass ) ? reflection.klass : reflection
113
+ end
114
+
115
+ # Returns the reflection corresponding to the given path.
116
+ def reflection_for( path )
117
+ reflections[path] ||= map_reflection_for( path )
118
+ end
119
+
120
+ # Returns a proper table alias for the given path. This alias may be used in SQL statements.
121
+ def table_alias_for( path )
122
+ table_aliases[path] ||= map_table_alias_for( path )
123
+ end
124
+
125
+ # Attempts to map a reflection for the given path. Raises if already defined.
126
+ def map_reflection_for( path )
127
+ raise "reflection for #{path.inspect} already exists" unless reflections[path].nil?
128
+
129
+ reflection = path.empty? ? @proxy_scope : begin
130
+ parent = reflection_for( path[0..-2] )
131
+ parent.klass.reflect_on_association( path.last )
132
+ rescue
133
+ parent.reflect_on_association( path.last )
134
+ end
135
+ raise "invalid path #{path.inspect}" if reflection.nil?
136
+
137
+ reflections[path] = reflection
138
+ map_table_alias_for( path ) # Claim a table alias for the path.
139
+
140
+ reflection
141
+ end
142
+
143
+ # Attempts to map a table alias for the given path. Raises if already defined.
144
+ def map_table_alias_for( path )
145
+ return "table alias for #{path.inspect} already exists" unless table_aliases[path].nil?
146
+
147
+ reflection = reflection_for( path )
148
+ table_alias = reflection.table_name
149
+ if table_aliases.values.include?( table_alias )
150
+ max_length = reflection.active_record.connection.table_alias_length
151
+ # Rails seems to pluralize reflection names
152
+ table_alias = "#{reflection.name.to_s.pluralize}_#{reflection.active_record.table_name}".to(max_length-1)
153
+ end
154
+ while table_aliases.values.include?( table_alias )
155
+ if table_alias =~ /\w(_\d+?)$/
156
+ table_index = $1.succ
157
+ table_alias = "#{table_alias[0..-(table_index.length+1)]}_#{table_index}"
158
+ else
159
+ table_alias = "#{table_alias[0..(max_length-3)]}_2"
160
+ end
161
+ end
162
+ table_aliases[path] = table_alias
163
+ end
164
+
165
+ # Returns a hash mapping obligations to zero or more condition path sets.
166
+ def obligation_conditions
167
+ @obligation_conditions ||= {}
168
+ end
169
+
170
+ # Returns a hash mapping paths to reflections.
171
+ def reflections
172
+ # lets try to get the order of joins right
173
+ @reflections ||= ActiveSupport::OrderedHash.new
174
+ end
175
+
176
+ # Returns a hash mapping paths to proper table aliases to use in SQL statements.
177
+ def table_aliases
178
+ @table_aliases ||= {}
179
+ end
180
+
181
+ # Parses all of the defined obligation conditions and defines the scope's :conditions option.
182
+ def rebuild_condition_options!
183
+ conds = []
184
+ binds = {}
185
+ used_paths = Set.new
186
+ delete_paths = Set.new
187
+ obligation_conditions.each_with_index do |array, obligation_index|
188
+ obligation, conditions = array
189
+ obligation_conds = []
190
+ conditions.each do |path, expressions|
191
+ model = model_for( path )
192
+ table_alias = table_alias_for(path)
193
+ parent_model = (path.length > 1 ? model_for(path[0..-2]) : @proxy_scope)
194
+ expressions.each do |expression|
195
+ attribute, operator, value = expression
196
+ # prevent unnecessary joins:
197
+ if attribute == :id and operator == :is and parent_model.columns_hash["#{path.last}_id"]
198
+ attribute_name = :"#{path.last}_id"
199
+ attribute_table_alias = table_alias_for(path[0..-2])
200
+ used_paths << path[0..-2]
201
+ delete_paths << path
202
+ else
203
+ attribute_name = model.columns_hash["#{attribute}_id"] && :"#{attribute}_id" ||
204
+ model.columns_hash[attribute.to_s] && attribute ||
205
+ :id
206
+ attribute_table_alias = table_alias
207
+ used_paths << path
208
+ end
209
+ bindvar = "#{attribute_table_alias}__#{attribute_name}_#{obligation_index}".to_sym
210
+
211
+ sql_attribute = "#{connection.quote_table_name(attribute_table_alias)}.#{connection.quote_table_name(attribute_name)}"
212
+ if value.nil? and [:is, :is_not].include?(operator)
213
+ obligation_conds << "#{sql_attribute} IS #{[:contains, :is].include?(operator) ? '' : 'NOT '}NULL"
214
+ else
215
+ attribute_operator = case operator
216
+ when :contains, :is then "= :#{bindvar}"
217
+ when :does_not_contain, :is_not then "<> :#{bindvar}"
218
+ when :is_in, :intersects_with then "IN (:#{bindvar})"
219
+ when :is_not_in then "NOT IN (:#{bindvar})"
220
+ else raise AuthorizationUsageError, "Unknown operator: #{operator}"
221
+ end
222
+ obligation_conds << "#{sql_attribute} #{attribute_operator}"
223
+ binds[bindvar] = attribute_value(value)
224
+ end
225
+ end
226
+ end
227
+ obligation_conds << "1=1" if obligation_conds.empty?
228
+ conds << "(#{obligation_conds.join(' AND ')})"
229
+ end
230
+ (delete_paths - used_paths).each {|path| reflections.delete(path)}
231
+ @proxy_options[:conditions] = [ conds.join( " OR " ), binds ]
232
+ end
233
+
234
+ def attribute_value (value)
235
+ value.respond_to?( :descends_from_active_record? ) && value.descends_from_active_record? && value.id ||
236
+ value.is_a?( Array ) && value[0].respond_to?( :descends_from_active_record? ) && value[0].descends_from_active_record? && value.map( &:id ) ||
237
+ value
238
+ end
239
+
240
+ # Parses all of the defined obligation joins and defines the scope's :joins or :includes option.
241
+ # TODO: Support non-linear association paths. Right now, we just break down the longest path parsed.
242
+ def rebuild_join_options!
243
+ joins = (@proxy_options[:joins] || []) + (@proxy_options[:includes] || [])
244
+
245
+ reflections.keys.each do |path|
246
+ next if path.empty?
247
+
248
+ existing_join = joins.find do |join|
249
+ existing_path = join_to_path(join)
250
+ min_length = [existing_path.length, path.length].min
251
+ existing_path.first(min_length) == path.first(min_length)
252
+ end
253
+
254
+ if existing_join
255
+ if join_to_path(existing_join).length < path.length
256
+ joins[joins.index(existing_join)] = path_to_join(path)
257
+ end
258
+ else
259
+ joins << path_to_join(path)
260
+ end
261
+ end
262
+
263
+ case obligation_conditions.length
264
+ when 0 then
265
+ # No obligation conditions means we don't have to mess with joins or includes at all.
266
+ when 1 then
267
+ @proxy_options[:joins] = joins
268
+ @proxy_options.delete( :include )
269
+ else
270
+ @proxy_options.delete( :joins )
271
+ @proxy_options[:include] = joins
272
+ end
273
+ end
274
+
275
+ def path_to_join (path)
276
+ case path.length
277
+ when 0 then nil
278
+ when 1 then path[0]
279
+ else
280
+ hash = { path[-2] => path[-1] }
281
+ path[0..-3].reverse.each do |elem|
282
+ hash = { elem => hash }
283
+ end
284
+ hash
285
+ end
286
+ end
287
+
288
+ def join_to_path (join)
289
+ case join
290
+ when Symbol
291
+ [join]
292
+ when Hash
293
+ [join.keys.first] + join_to_path(join[join.keys.first])
294
+ end
295
+ end
296
+ end
297
+ end