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