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