timcharper-declarative_authorization 0.4.1.2
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +135 -0
- data/MIT-LICENSE +20 -0
- data/README.rdoc +503 -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 +218 -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 +169 -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 +683 -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 +623 -0
- data/lib/declarative_authorization/in_model.rb +162 -0
- data/lib/declarative_authorization/maintenance.rb +198 -0
- data/lib/declarative_authorization/obligation_scope.rb +345 -0
- data/lib/declarative_authorization/rails_legacy.rb +14 -0
- data/lib/declarative_authorization/reader.rb +472 -0
- data/test/authorization_test.rb +971 -0
- data/test/controller_filter_resource_access_test.rb +511 -0
- data/test/controller_test.rb +465 -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 +1694 -0
- data/test/schema.sql +54 -0
- data/test/test_helper.rb +134 -0
- metadata +119 -0
@@ -0,0 +1,243 @@
|
|
1
|
+
|
2
|
+
module Authorization
|
3
|
+
module DevelopmentSupport
|
4
|
+
class AbstractAnalyzer
|
5
|
+
attr_reader :engine
|
6
|
+
|
7
|
+
def initialize (engine)
|
8
|
+
@engine = engine
|
9
|
+
end
|
10
|
+
|
11
|
+
def roles
|
12
|
+
AnalyzerEngine.roles(engine)
|
13
|
+
end
|
14
|
+
|
15
|
+
def rules
|
16
|
+
roles.collect {|role| role.rules }.flatten
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
# Groups utility methods and classes to better work with authorization object
|
21
|
+
# model.
|
22
|
+
module AnalyzerEngine
|
23
|
+
|
24
|
+
def self.roles (engine)
|
25
|
+
Role.all(engine)
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.relevant_roles (engine, users)
|
29
|
+
users.collect {|user| user.role_symbols.map {|role_sym| Role.for_sym(role_sym, engine)}}.
|
30
|
+
flatten.uniq.collect {|role| [role] + role.ancestors}.flatten.uniq
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.rule_for_permission (engine, privilege, context, role)
|
34
|
+
AnalyzerEngine.roles(engine).
|
35
|
+
find {|cloned_role| cloned_role.to_sym == role.to_sym}.rules.find do |rule|
|
36
|
+
rule.contexts.include?(context) and rule.privileges.include?(privilege)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def self.apply_change (engine, change)
|
41
|
+
case change[0]
|
42
|
+
when :add_role
|
43
|
+
role_symbol = change[1]
|
44
|
+
if engine.roles.include?(role_symbol)
|
45
|
+
false
|
46
|
+
else
|
47
|
+
engine.roles << role_symbol
|
48
|
+
true
|
49
|
+
end
|
50
|
+
when :add_privilege
|
51
|
+
privilege, context, role = change[1,3]
|
52
|
+
role = Role.for_sym(role.to_sym, engine)
|
53
|
+
privilege = Privilege.for_sym(privilege.to_sym, engine)
|
54
|
+
if ([privilege] + privilege.ancestors).any? {|ancestor_privilege| ([role] + role.ancestors).any? {|ancestor_role| !ancestor_role.rules_for_permission(ancestor_privilege, context).empty?}}
|
55
|
+
false
|
56
|
+
else
|
57
|
+
engine.auth_rules << AuthorizationRule.new(role.to_sym,
|
58
|
+
[privilege.to_sym], [context])
|
59
|
+
true
|
60
|
+
end
|
61
|
+
when :remove_privilege
|
62
|
+
privilege, context, role = change[1,3]
|
63
|
+
role = Role.for_sym(role.to_sym, engine)
|
64
|
+
privilege = Privilege.for_sym(privilege.to_sym, engine)
|
65
|
+
rules_with_priv = role.rules_for_permission(privilege, context)
|
66
|
+
if rules_with_priv.empty?
|
67
|
+
false
|
68
|
+
else
|
69
|
+
rules_with_priv.each do |rule|
|
70
|
+
rule.rule.privileges.delete(privilege.to_sym)
|
71
|
+
engine.auth_rules.delete(rule.rule) if rule.rule.privileges.empty?
|
72
|
+
end
|
73
|
+
true
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
class Role
|
79
|
+
@@role_objects = {}
|
80
|
+
attr_reader :role
|
81
|
+
def initialize (role, rules, engine)
|
82
|
+
@role = role
|
83
|
+
@rules = rules
|
84
|
+
@engine = engine
|
85
|
+
end
|
86
|
+
|
87
|
+
def source_line
|
88
|
+
@rules.empty? ? nil : @rules.first.source_line
|
89
|
+
end
|
90
|
+
def source_file
|
91
|
+
@rules.empty? ? nil : @rules.first.source_file
|
92
|
+
end
|
93
|
+
|
94
|
+
# ancestors' privileges are included in in the current role
|
95
|
+
def ancestors (role_symbol = nil)
|
96
|
+
role_symbol ||= @role
|
97
|
+
(@engine.role_hierarchy[role_symbol] || []).
|
98
|
+
collect {|lower_role| ancestors(lower_role) }.flatten +
|
99
|
+
(role_symbol == @role ? [] : [Role.for_sym(role_symbol, @engine)])
|
100
|
+
end
|
101
|
+
def descendants (role_symbol = nil)
|
102
|
+
role_symbol ||= @role
|
103
|
+
(@engine.rev_role_hierarchy[role_symbol] || []).
|
104
|
+
collect {|higher_role| descendants(higher_role) }.flatten +
|
105
|
+
(role_symbol == @role ? [] : [Role.for_sym(role_symbol, @engine)])
|
106
|
+
end
|
107
|
+
|
108
|
+
def rules
|
109
|
+
@rules ||= @engine.auth_rules.select {|rule| rule.role == @role}.
|
110
|
+
collect {|rule| Rule.new(rule, @engine)}
|
111
|
+
end
|
112
|
+
def rules_for_permission (privilege, context)
|
113
|
+
rules.select do |rule|
|
114
|
+
rule.matches?([@role], [privilege.to_sym], context)
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
def to_sym
|
119
|
+
@role
|
120
|
+
end
|
121
|
+
def self.for_sym (role_sym, engine)
|
122
|
+
@@role_objects[[role_sym, engine]] ||= new(role_sym, nil, engine)
|
123
|
+
end
|
124
|
+
|
125
|
+
def self.all (engine)
|
126
|
+
rules_by_role = engine.auth_rules.inject({}) do |memo, rule|
|
127
|
+
memo[rule.role] ||= []
|
128
|
+
memo[rule.role] << rule
|
129
|
+
memo
|
130
|
+
end
|
131
|
+
engine.roles.collect do |role|
|
132
|
+
new(role, (rules_by_role[role] || []).
|
133
|
+
collect {|rule| Rule.new(rule, engine)}, engine)
|
134
|
+
end
|
135
|
+
end
|
136
|
+
def self.all_for_privilege (privilege, context, engine)
|
137
|
+
privilege = privilege.is_a?(Symbol) ? Privilege.for_sym(privilege, engine) : privilege
|
138
|
+
privilege_symbols = ([privilege] + privilege.ancestors).map(&:to_sym)
|
139
|
+
all(engine).select {|role| role.rules.any? {|rule| rule.matches?([role.to_sym], privilege_symbols, context)}}.
|
140
|
+
collect {|role| [role] + role.descendants}.flatten.uniq
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
class Rule
|
145
|
+
@@rule_objects = {}
|
146
|
+
delegate :source_line, :source_file, :contexts, :matches?, :to => :@rule
|
147
|
+
attr_reader :rule
|
148
|
+
def initialize (rule, engine)
|
149
|
+
@rule = rule
|
150
|
+
@engine = engine
|
151
|
+
end
|
152
|
+
def privileges
|
153
|
+
PrivilegesSet.new(self, @engine, @rule.privileges.collect {|privilege| Privilege.for_sym(privilege, @engine) })
|
154
|
+
end
|
155
|
+
def self.for_rule (rule, engine)
|
156
|
+
@@rule_objects[[rule, engine]] ||= new(rule, engine)
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
class Privilege
|
161
|
+
@@privilege_objects = {}
|
162
|
+
def initialize (privilege, engine)
|
163
|
+
@privilege = privilege
|
164
|
+
@engine = engine
|
165
|
+
end
|
166
|
+
|
167
|
+
# Ancestor privileges are higher in the hierarchy.
|
168
|
+
# Doesn't take context into account.
|
169
|
+
def ancestors (priv_symbol = nil)
|
170
|
+
priv_symbol ||= @privilege
|
171
|
+
# context-specific?
|
172
|
+
(@engine.rev_priv_hierarchy[[priv_symbol, nil]] || []).
|
173
|
+
collect {|higher_priv| ancestors(higher_priv) }.flatten +
|
174
|
+
(priv_symbol == @privilege ? [] : [Privilege.for_sym(priv_symbol, @engine)])
|
175
|
+
end
|
176
|
+
def descendants (priv_symbol = nil)
|
177
|
+
priv_symbol ||= @privilege
|
178
|
+
# context-specific?
|
179
|
+
(@engine.privilege_hierarchy[priv_symbol] || []).
|
180
|
+
collect {|lower_priv, context| descendants(lower_priv) }.flatten +
|
181
|
+
(priv_symbol == @privilege ? [] : [Privilege.for_sym(priv_symbol, @engine)])
|
182
|
+
end
|
183
|
+
|
184
|
+
def rules
|
185
|
+
@rules ||= find_rules_for_privilege
|
186
|
+
end
|
187
|
+
def source_line
|
188
|
+
rules.empty? ? nil : rules.first.source_line
|
189
|
+
end
|
190
|
+
def source_file
|
191
|
+
rules.empty? ? nil : rules.first.source_file
|
192
|
+
end
|
193
|
+
|
194
|
+
def to_sym
|
195
|
+
@privilege
|
196
|
+
end
|
197
|
+
def self.for_sym (privilege_sym, engine)
|
198
|
+
@@privilege_objects[[privilege_sym, engine]] ||= new(privilege_sym, engine)
|
199
|
+
end
|
200
|
+
|
201
|
+
private
|
202
|
+
def find_rules_for_privilege
|
203
|
+
@engine.auth_rules.select {|rule| rule.privileges.include?(@privilege)}.
|
204
|
+
collect {|rule| Rule.for_rule(rule, @engine)}
|
205
|
+
end
|
206
|
+
end
|
207
|
+
|
208
|
+
class PrivilegesSet < Set
|
209
|
+
def initialize (*args)
|
210
|
+
if args.length > 2
|
211
|
+
@rule = args.shift
|
212
|
+
@engine = args.shift
|
213
|
+
end
|
214
|
+
super(*args)
|
215
|
+
end
|
216
|
+
def include? (privilege)
|
217
|
+
if privilege.is_a?(Symbol)
|
218
|
+
super(privilege_from_symbol(privilege))
|
219
|
+
else
|
220
|
+
super
|
221
|
+
end
|
222
|
+
end
|
223
|
+
def delete (privilege)
|
224
|
+
@rule.rule.privileges.delete(privilege.to_sym)
|
225
|
+
if privilege.is_a?(Symbol)
|
226
|
+
super(privilege_from_symbol(privilege))
|
227
|
+
else
|
228
|
+
super
|
229
|
+
end
|
230
|
+
end
|
231
|
+
|
232
|
+
def intersects? (privileges)
|
233
|
+
intersection(privileges).length > 0
|
234
|
+
end
|
235
|
+
|
236
|
+
private
|
237
|
+
def privilege_from_symbol (privilege_sym)
|
238
|
+
Privilege.for_sym(privilege_sym, @engine)
|
239
|
+
end
|
240
|
+
end
|
241
|
+
end
|
242
|
+
end
|
243
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
# Authorization::AuthorizationHelper
|
2
|
+
require File.dirname(__FILE__) + '/authorization.rb'
|
3
|
+
|
4
|
+
module Authorization
|
5
|
+
module AuthorizationHelper
|
6
|
+
|
7
|
+
# If the current user meets the given privilege, permitted_to? returns true
|
8
|
+
# and yields to the optional block. The attribute checks that are defined
|
9
|
+
# in the authorization rules are only evaluated if an object is given
|
10
|
+
# for context.
|
11
|
+
#
|
12
|
+
# Examples:
|
13
|
+
# <% permitted_to? :create, :users do %>
|
14
|
+
# <%= link_to 'New', new_user_path %>
|
15
|
+
# <% end %>
|
16
|
+
# ...
|
17
|
+
# <% if permitted_to? :create, :users %>
|
18
|
+
# <%= link_to 'New', new_user_path %>
|
19
|
+
# <% else %>
|
20
|
+
# You are not allowed to create new users!
|
21
|
+
# <% end %>
|
22
|
+
# ...
|
23
|
+
# <% for user in @users %>
|
24
|
+
# <%= link_to 'Edit', edit_user_path(user) if permitted_to? :update, user %>
|
25
|
+
# <% end %>
|
26
|
+
#
|
27
|
+
# To pass in an object and override the context, you can use the optional
|
28
|
+
# options:
|
29
|
+
# permitted_to? :update, user, :context => :account
|
30
|
+
#
|
31
|
+
def permitted_to? (privilege, object_or_sym = nil, options = {}, &block)
|
32
|
+
controller.permitted_to?(privilege, object_or_sym, options, &block)
|
33
|
+
end
|
34
|
+
|
35
|
+
# While permitted_to? is used for authorization in views, in some cases
|
36
|
+
# content should only be shown to some users without being concerned
|
37
|
+
# with authorization. E.g. to only show the most relevant menu options
|
38
|
+
# to a certain group of users. That is what has_role? should be used for.
|
39
|
+
#
|
40
|
+
# Examples:
|
41
|
+
# <% has_role?(:sales) do %>
|
42
|
+
# <%= link_to 'All contacts', contacts_path %>
|
43
|
+
# <% end %>
|
44
|
+
# ...
|
45
|
+
# <% if has_role?(:sales) %>
|
46
|
+
# <%= link_to 'Customer contacts', contacts_path %>
|
47
|
+
# <% else %>
|
48
|
+
# ...
|
49
|
+
# <% end %>
|
50
|
+
#
|
51
|
+
def has_role? (*roles, &block)
|
52
|
+
controller.has_role?(*roles, &block)
|
53
|
+
end
|
54
|
+
|
55
|
+
# As has_role? except checks all roles included in the role hierarchy
|
56
|
+
def has_role_with_hierarchy?(*roles, &block)
|
57
|
+
controller.has_role_with_hierarchy?(*roles, &block)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,623 @@
|
|
1
|
+
# Authorization::AuthorizationInController
|
2
|
+
require File.dirname(__FILE__) + '/authorization.rb'
|
3
|
+
|
4
|
+
module Authorization
|
5
|
+
module AuthorizationInController
|
6
|
+
|
7
|
+
def self.included(base) # :nodoc:
|
8
|
+
base.extend(ClassMethods)
|
9
|
+
base.hide_action :authorization_engine, :permitted_to?,
|
10
|
+
:permitted_to!
|
11
|
+
end
|
12
|
+
|
13
|
+
DEFAULT_DENY = false
|
14
|
+
|
15
|
+
# If attribute_check is set for filter_access_to, decl_auth will try to
|
16
|
+
# load the appropriate object from the current controller's model with
|
17
|
+
# the id from params[:id]. If that fails, a 404 Not Found is often the
|
18
|
+
# right way to handle the error. If you have additional measures in place
|
19
|
+
# that restricts the find scope, handling this error as a permission denied
|
20
|
+
# might be a better way. Set failed_auto_loading_is_not_found to false
|
21
|
+
# for the latter behaviour.
|
22
|
+
@@failed_auto_loading_is_not_found = true
|
23
|
+
def self.failed_auto_loading_is_not_found?
|
24
|
+
@@failed_auto_loading_is_not_found
|
25
|
+
end
|
26
|
+
def self.failed_auto_loading_is_not_found= (new_value)
|
27
|
+
@@failed_auto_loading_is_not_found = new_value
|
28
|
+
end
|
29
|
+
|
30
|
+
# Returns the Authorization::Engine for the current controller.
|
31
|
+
def authorization_engine
|
32
|
+
@authorization_engine ||= Authorization::Engine.instance
|
33
|
+
end
|
34
|
+
|
35
|
+
# If the current user meets the given privilege, permitted_to? returns true
|
36
|
+
# and yields to the optional block. The attribute checks that are defined
|
37
|
+
# in the authorization rules are only evaluated if an object is given
|
38
|
+
# for context.
|
39
|
+
#
|
40
|
+
# See examples for Authorization::AuthorizationHelper #permitted_to?
|
41
|
+
#
|
42
|
+
# If no object or context is specified, the controller_name is used as
|
43
|
+
# context.
|
44
|
+
#
|
45
|
+
def permitted_to? (privilege, object_or_sym = nil, options = {}, &block)
|
46
|
+
permitted_to!(privilege, object_or_sym, options.merge(:non_bang => true), &block)
|
47
|
+
end
|
48
|
+
|
49
|
+
# Works similar to the permitted_to? method, but
|
50
|
+
# throws the authorization exceptions, just like Engine#permit!
|
51
|
+
def permitted_to! (privilege, object_or_sym = nil, options = {}, &block)
|
52
|
+
context = object = nil
|
53
|
+
if object_or_sym.nil?
|
54
|
+
context = self.class.decl_auth_context
|
55
|
+
elsif object_or_sym.is_a?(Symbol)
|
56
|
+
context = object_or_sym
|
57
|
+
else
|
58
|
+
object = object_or_sym
|
59
|
+
end
|
60
|
+
|
61
|
+
non_bang = options.delete(:non_bang)
|
62
|
+
args = [
|
63
|
+
privilege,
|
64
|
+
{:user => current_user,
|
65
|
+
:object => object,
|
66
|
+
:context => context,
|
67
|
+
:skip_attribute_test => object.nil?}.merge(options)
|
68
|
+
]
|
69
|
+
if non_bang
|
70
|
+
authorization_engine.permit?(*args, &block)
|
71
|
+
else
|
72
|
+
authorization_engine.permit!(*args, &block)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
# While permitted_to? is used for authorization, in some cases
|
77
|
+
# content should only be shown to some users without being concerned
|
78
|
+
# with authorization. E.g. to only show the most relevant menu options
|
79
|
+
# to a certain group of users. That is what has_role? should be used for.
|
80
|
+
def has_role? (*roles, &block)
|
81
|
+
user_roles = authorization_engine.roles_for(current_user)
|
82
|
+
result = roles.all? do |role|
|
83
|
+
user_roles.include?(role)
|
84
|
+
end
|
85
|
+
yield if result and block_given?
|
86
|
+
result
|
87
|
+
end
|
88
|
+
|
89
|
+
# As has_role? except checks all roles included in the role hierarchy
|
90
|
+
def has_role_with_hierarchy?(*roles, &block)
|
91
|
+
user_roles = authorization_engine.roles_with_hierarchy_for(current_user)
|
92
|
+
result = roles.all? do |role|
|
93
|
+
user_roles.include?(role)
|
94
|
+
end
|
95
|
+
yield if result and block_given?
|
96
|
+
result
|
97
|
+
end
|
98
|
+
|
99
|
+
|
100
|
+
protected
|
101
|
+
def filter_access_filter # :nodoc:
|
102
|
+
permissions = self.class.all_filter_access_permissions
|
103
|
+
all_permissions = permissions.select {|p| p.actions.include?(:all)}
|
104
|
+
matching_permissions = permissions.select {|p| p.matches?(action_name)}
|
105
|
+
allowed = false
|
106
|
+
auth_exception = nil
|
107
|
+
begin
|
108
|
+
allowed = if !matching_permissions.empty?
|
109
|
+
matching_permissions.all? {|perm| perm.permit!(self)}
|
110
|
+
elsif !all_permissions.empty?
|
111
|
+
all_permissions.all? {|perm| perm.permit!(self)}
|
112
|
+
else
|
113
|
+
!DEFAULT_DENY
|
114
|
+
end
|
115
|
+
rescue AuthorizationError => e
|
116
|
+
auth_exception = e
|
117
|
+
end
|
118
|
+
|
119
|
+
unless allowed
|
120
|
+
if all_permissions.empty? and matching_permissions.empty?
|
121
|
+
logger.warn "Permission denied: No matching filter access " +
|
122
|
+
"rule found for #{self.class.controller_name}.#{action_name}"
|
123
|
+
elsif auth_exception
|
124
|
+
logger.info "Permission denied: #{auth_exception}"
|
125
|
+
end
|
126
|
+
if respond_to?(:permission_denied)
|
127
|
+
# permission_denied needs to render or redirect
|
128
|
+
send(:permission_denied)
|
129
|
+
else
|
130
|
+
send(:render, :text => "You are not allowed to access this action.",
|
131
|
+
:status => :forbidden)
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
def load_controller_object (context_without_namespace = nil) # :nodoc:
|
137
|
+
instance_var = :"@#{context_without_namespace.to_s.singularize}"
|
138
|
+
model = context_without_namespace.to_s.classify.constantize
|
139
|
+
instance_variable_set(instance_var, model.find(params[:id]))
|
140
|
+
end
|
141
|
+
|
142
|
+
def load_parent_controller_object (parent_context_without_namespace) # :nodoc:
|
143
|
+
instance_var = :"@#{parent_context_without_namespace.to_s.singularize}"
|
144
|
+
model = parent_context_without_namespace.to_s.classify.constantize
|
145
|
+
instance_variable_set(instance_var, model.find(params[:"#{parent_context_without_namespace.to_s.singularize}_id"]))
|
146
|
+
end
|
147
|
+
|
148
|
+
def new_controller_object_from_params (context_without_namespace, parent_context_without_namespace) # :nodoc:
|
149
|
+
model_or_proxy = parent_context_without_namespace ?
|
150
|
+
instance_variable_get(:"@#{parent_context_without_namespace.to_s.singularize}").send(context_without_namespace.to_sym) :
|
151
|
+
context_without_namespace.to_s.classify.constantize
|
152
|
+
instance_var = :"@#{context_without_namespace.to_s.singularize}"
|
153
|
+
instance_variable_set(instance_var,
|
154
|
+
model_or_proxy.new(params[context_without_namespace.to_s.singularize]))
|
155
|
+
end
|
156
|
+
|
157
|
+
def new_controller_object_for_collection (context_without_namespace, parent_context_without_namespace) # :nodoc:
|
158
|
+
model_or_proxy = parent_context_without_namespace ?
|
159
|
+
instance_variable_get(:"@#{parent_context_without_namespace.to_s.singularize}").send(context_without_namespace.to_sym) :
|
160
|
+
context_without_namespace.to_s.classify.constantize
|
161
|
+
instance_var = :"@#{context_without_namespace.to_s.singularize}"
|
162
|
+
instance_variable_set(instance_var, model_or_proxy.new)
|
163
|
+
end
|
164
|
+
|
165
|
+
module ClassMethods
|
166
|
+
#
|
167
|
+
# Defines a filter to be applied according to the authorization of the
|
168
|
+
# current user. Requires at least one symbol corresponding to an
|
169
|
+
# action as parameter. The special symbol :+all+ refers to all action.
|
170
|
+
# The all :+all+ statement is only employed if no specific statement is
|
171
|
+
# present.
|
172
|
+
# class UserController < ApplicationController
|
173
|
+
# filter_access_to :index
|
174
|
+
# filter_access_to :new, :edit
|
175
|
+
# filter_access_to :all
|
176
|
+
# ...
|
177
|
+
# end
|
178
|
+
#
|
179
|
+
# The default is to allow access unconditionally if no rule matches.
|
180
|
+
# Thus, including the +filter_access_to+ :+all+ statement is a good
|
181
|
+
# idea, implementing a default-deny policy.
|
182
|
+
#
|
183
|
+
# When the access is denied, the method +permission_denied+ is called
|
184
|
+
# on the current controller, if defined. Else, a simple "you are not
|
185
|
+
# allowed" string is output. Log.info is given more information on the
|
186
|
+
# reasons of denial.
|
187
|
+
#
|
188
|
+
# def permission_denied
|
189
|
+
# flash[:error] = 'Sorry, you are not allowed to the requested page.'
|
190
|
+
# respond_to do |format|
|
191
|
+
# format.html { redirect_to(:back) rescue redirect_to('/') }
|
192
|
+
# format.xml { head :unauthorized }
|
193
|
+
# format.js { head :unauthorized }
|
194
|
+
# end
|
195
|
+
# end
|
196
|
+
#
|
197
|
+
# By default, required privileges are infered from the action name and
|
198
|
+
# the controller name. Thus, in UserController :+edit+ requires
|
199
|
+
# :+edit+ +users+. To specify required privilege, use the option :+require+
|
200
|
+
# filter_access_to :new, :create, :require => :create, :context => :users
|
201
|
+
#
|
202
|
+
# Without the :+attribute_check+ option, no constraints from the
|
203
|
+
# authorization rules are enforced because for some actions (collections,
|
204
|
+
# +new+, +create+), there is no object to evaluate conditions against. To
|
205
|
+
# allow attribute checks on all actions, it is a common pattern to provide
|
206
|
+
# custom objects through +before_filters+:
|
207
|
+
# class BranchesController < ApplicationController
|
208
|
+
# before_filter :load_company
|
209
|
+
# before_filter :new_branch_from_company_and_params,
|
210
|
+
# :only => [:index, :new, :create]
|
211
|
+
# filter_access_to :all, :attribute_check => true
|
212
|
+
#
|
213
|
+
# protected
|
214
|
+
# def new_branch_from_company_and_params
|
215
|
+
# @branch = @company.branches.new(params[:branch])
|
216
|
+
# end
|
217
|
+
# end
|
218
|
+
# NOTE: +before_filters+ need to be defined before the first
|
219
|
+
# +filter_access_to+ call.
|
220
|
+
#
|
221
|
+
# For further customization, a custom filter expression may be formulated
|
222
|
+
# in a block, which is then evaluated in the context of the controller
|
223
|
+
# on a matching request. That is, for checking two objects, use the
|
224
|
+
# following:
|
225
|
+
# filter_access_to :merge do
|
226
|
+
# permitted_to!(:update, User.find(params[:original_id])) and
|
227
|
+
# permitted_to!(:delete, User.find(params[:id]))
|
228
|
+
# end
|
229
|
+
# The block should raise a Authorization::AuthorizationError or return
|
230
|
+
# false if the access is to be denied.
|
231
|
+
#
|
232
|
+
# Later calls to filter_access_to with overlapping actions overwrite
|
233
|
+
# previous ones for that action.
|
234
|
+
#
|
235
|
+
# All options:
|
236
|
+
# [:+require+]
|
237
|
+
# Privilege required; defaults to action_name
|
238
|
+
# [:+context+]
|
239
|
+
# The privilege's context, defaults to decl_auth_context, which consists
|
240
|
+
# of controller_name, prepended by any namespaces
|
241
|
+
# [:+attribute_check+]
|
242
|
+
# Enables the check of attributes defined in the authorization rules.
|
243
|
+
# Defaults to false. If enabled, filter_access_to will use a context
|
244
|
+
# object from one of the following sources (in that order):
|
245
|
+
# * the method from the :+load_method+ option,
|
246
|
+
# * an instance variable named after the singular of the context
|
247
|
+
# (by default from the controller name, e.g. @post for PostsController),
|
248
|
+
# * a find on the context model, using +params+[:id] as id value.
|
249
|
+
# Any of these methods will only be employed if :+attribute_check+
|
250
|
+
# is enabled.
|
251
|
+
# [:+model+]
|
252
|
+
# The data model to load a context object from. Defaults to the
|
253
|
+
# context, singularized.
|
254
|
+
# [:+load_method+]
|
255
|
+
# Specify a method by symbol or a Proc object which should be used
|
256
|
+
# to load the object. Both should return the loaded object.
|
257
|
+
# If a Proc object is given, e.g. by way of
|
258
|
+
# +lambda+, it is called in the instance of the controller.
|
259
|
+
# Example demonstrating the default behaviour:
|
260
|
+
# filter_access_to :show, :attribute_check => true,
|
261
|
+
# :load_method => lambda { User.find(params[:id]) }
|
262
|
+
#
|
263
|
+
|
264
|
+
def filter_access_to (*args, &filter_block)
|
265
|
+
options = args.last.is_a?(Hash) ? args.pop : {}
|
266
|
+
options = {
|
267
|
+
:require => nil,
|
268
|
+
:context => nil,
|
269
|
+
:attribute_check => false,
|
270
|
+
:model => nil,
|
271
|
+
:load_method => nil
|
272
|
+
}.merge!(options)
|
273
|
+
privilege = options[:require]
|
274
|
+
context = options[:context]
|
275
|
+
actions = args.flatten
|
276
|
+
|
277
|
+
before_filter :filter_access_filter
|
278
|
+
|
279
|
+
filter_access_permissions.each do |perm|
|
280
|
+
perm.remove_actions(actions)
|
281
|
+
end
|
282
|
+
filter_access_permissions <<
|
283
|
+
ControllerPermission.new(actions, privilege, context,
|
284
|
+
options[:attribute_check],
|
285
|
+
options[:model],
|
286
|
+
options[:load_method],
|
287
|
+
filter_block)
|
288
|
+
end
|
289
|
+
|
290
|
+
# Collecting all the ControllerPermission objects from the controller
|
291
|
+
# hierarchy. Permissions for actions are overwritten by calls to
|
292
|
+
# filter_access_to in child controllers with the same action.
|
293
|
+
def all_filter_access_permissions # :nodoc:
|
294
|
+
ancestors.inject([]) do |perms, mod|
|
295
|
+
if mod.respond_to?(:filter_access_permissions)
|
296
|
+
perms +
|
297
|
+
mod.filter_access_permissions.collect do |p1|
|
298
|
+
p1.clone.remove_actions(perms.inject(Set.new) {|actions, p2| actions + p2.actions})
|
299
|
+
end
|
300
|
+
else
|
301
|
+
perms
|
302
|
+
end
|
303
|
+
end
|
304
|
+
end
|
305
|
+
|
306
|
+
# To DRY up the filter_access_to statements in restful controllers,
|
307
|
+
# filter_resource_access combines typical filter_access_to and
|
308
|
+
# before_filter calls, which set up the instance variables.
|
309
|
+
#
|
310
|
+
# The simplest case are top-level resource controllers with only the
|
311
|
+
# seven CRUD methods, e.g.
|
312
|
+
# class CompanyController < ApplicationController
|
313
|
+
# filter_resource_access
|
314
|
+
#
|
315
|
+
# def index...
|
316
|
+
# end
|
317
|
+
# Here, all CRUD actions are protected through a filter_access_to :all
|
318
|
+
# statement. :+attribute_check+ is enabled for all actions except for
|
319
|
+
# the collection action :+index+. To have an object for attribute checks
|
320
|
+
# available, filter_resource_access will set the instance variable
|
321
|
+
# @+company+ in before filters. For the member actions (:+show+, :+edit+,
|
322
|
+
# :+update+, :+destroy+) @company is set to Company.find(params[:id]).
|
323
|
+
# For +new+ actions (:+new+, :+create+), filter_resource_access creates
|
324
|
+
# a new object from company parameters: Company.new(params[:company].
|
325
|
+
#
|
326
|
+
# For nested resources, the parent object may be loaded automatically.
|
327
|
+
# class BranchController < ApplicationController
|
328
|
+
# filter_resource_access :nested_in => :companies
|
329
|
+
# end
|
330
|
+
# Again, the CRUD actions are protected. Now, for all CRUD actions,
|
331
|
+
# the parent object @company is loaded from params[:company_id]. It is
|
332
|
+
# also used when creating @branch for +new+ actions. Here, attribute_check
|
333
|
+
# is enabled for the collection :+index+ as well, checking attributes on a
|
334
|
+
# @company.branches.new method.
|
335
|
+
#
|
336
|
+
# In many cases, the default seven CRUD actions are not sufficient. As in
|
337
|
+
# the resource definition for routing you may thus give additional member,
|
338
|
+
# new and collection methods. The options allow you to specify the
|
339
|
+
# required privileges for each action by providing a hash or an array of
|
340
|
+
# pairs. By default, for each action the action name is taken as privilege
|
341
|
+
# (action search in the example below requires the privilege :index
|
342
|
+
# :companies). Any controller action that is not specified and does not
|
343
|
+
# belong to the seven CRUD actions is handled as a member method.
|
344
|
+
# class CompanyController < ApplicationController
|
345
|
+
# filter_resource_access :collection => [[:search, :index], :index],
|
346
|
+
# :additional_member => {:mark_as_key_company => :update}
|
347
|
+
# end
|
348
|
+
# The +additional_+* options add to the respective CRUD actions,
|
349
|
+
# the other options replace the respective CRUD actions.
|
350
|
+
#
|
351
|
+
# You can override the default object loading by implementing any of the
|
352
|
+
# following instance methods on the controller. Examples are given for the
|
353
|
+
# BranchController (with +nested_in+ set to :+companies+):
|
354
|
+
# [+new_branch_from_params+]
|
355
|
+
# Used for +new+ actions.
|
356
|
+
# [+new_branch_for_collection+]
|
357
|
+
# Used for +collection+ actions if the +nested_in+ option is set.
|
358
|
+
# [+load_branch+]
|
359
|
+
# Used for +member+ actions.
|
360
|
+
# [+load_company+]
|
361
|
+
# Used for all +new+, +member+, and +collection+ actions if the
|
362
|
+
# +nested_in+ option is set.
|
363
|
+
#
|
364
|
+
# All options:
|
365
|
+
# [:+member+]
|
366
|
+
# Member methods are actions like +show+, which have an params[:id] from
|
367
|
+
# which to load the controller object and assign it to @controller_name,
|
368
|
+
# e.g. @+branch+.
|
369
|
+
#
|
370
|
+
# By default, member actions are [:+show+, :+edit+, :+update+,
|
371
|
+
# :+destroy+]. Also, any action not belonging to the seven CRUD actions
|
372
|
+
# are handled as member actions.
|
373
|
+
#
|
374
|
+
# There are three different syntax to specify member, collection and
|
375
|
+
# new actions.
|
376
|
+
# * Hash: Lets you set the required privilege for each action:
|
377
|
+
# {:+show+ => :+show+, :+mark_as_important+ => :+update+}
|
378
|
+
# * Array of actions or pairs: [:+show+, [:+mark_as_important+, :+update+]],
|
379
|
+
# with single actions requiring the privilege of the same name as the method.
|
380
|
+
# * Single method symbol: :+show+
|
381
|
+
# [:+additional_member+]
|
382
|
+
# Allows to add additional member actions to the default resource +member+
|
383
|
+
# actions.
|
384
|
+
# [:+collection+]
|
385
|
+
# Collection actions are like :+index+, actions without any controller object
|
386
|
+
# to check attributes of. If +nested_in+ is given, a new object is
|
387
|
+
# created from the parent object, e.g. @company.branches.new. Without
|
388
|
+
# +nested_in+, attribute check is deactivated for these actions. By
|
389
|
+
# default, collection is set to :+index+.
|
390
|
+
# [:+additional_collection+]
|
391
|
+
# Allows to add additional collaction actions to the default resource +collection+
|
392
|
+
# actions.
|
393
|
+
# [:+new+]
|
394
|
+
# +new+ methods are actions such as +new+ and +create+, which don't
|
395
|
+
# receive a params[:id] to load an object from, but
|
396
|
+
# a params[:controller_name_singular] hash with attributes for a new
|
397
|
+
# object. The attributes will be used here to create a new object and
|
398
|
+
# check the object against the authorization rules. The object is
|
399
|
+
# assigned to @controller_name_singular, e.g. @branch.
|
400
|
+
#
|
401
|
+
# If +nested_in+ is given, the new object
|
402
|
+
# is created from the parent_object.controller_name
|
403
|
+
# proxy, e.g. company.branches.new(params[:branch]). By default,
|
404
|
+
# +new+ is set to [:new, :create].
|
405
|
+
# [:+additional_new+]
|
406
|
+
# Allows to add additional new actions to the default resource +new+ actions.
|
407
|
+
# [:+context+]
|
408
|
+
# The context is used to determine the model to load objects from for the
|
409
|
+
# before_filters and the context of privileges to use in authorization
|
410
|
+
# checks.
|
411
|
+
# [:+nested_in+]
|
412
|
+
# Specifies the parent controller if the resource is nested in another
|
413
|
+
# one. This is used to automatically load the parent object, e.g.
|
414
|
+
# @+company+ from params[:company_id] for a BranchController nested in
|
415
|
+
# a CompanyController.
|
416
|
+
# [:+shallow+]
|
417
|
+
# Only relevant when used in conjunction with +nested_in+. Specifies a nested resource
|
418
|
+
# as being a shallow nested resource, resulting in the controller not attempting to
|
419
|
+
# load a parent object for all member actions defined by +member+ and
|
420
|
+
# +additional_member+ or rather the default member actions (:+show+, :+edit+,
|
421
|
+
# :+update+, :+destroy+).
|
422
|
+
# [:+no_attribute_check+]
|
423
|
+
# Allows to set actions for which no attribute check should be perfomed.
|
424
|
+
# See filter_access_to on details. By default, with no +nested_in+,
|
425
|
+
# +no_attribute_check+ is set to all collections. If +nested_in+ is given
|
426
|
+
# +no_attribute_check+ is empty by default.
|
427
|
+
#
|
428
|
+
def filter_resource_access(options = {})
|
429
|
+
options = {
|
430
|
+
:new => [:new, :create],
|
431
|
+
:additional_new => nil,
|
432
|
+
:member => [:show, :edit, :update, :destroy],
|
433
|
+
:additional_member => nil,
|
434
|
+
:collection => [:index],
|
435
|
+
:additional_collection => nil,
|
436
|
+
#:new_method_for_collection => nil, # only symbol method name
|
437
|
+
#:new_method => nil, # only symbol method name
|
438
|
+
#:load_method => nil, # only symbol method name
|
439
|
+
:no_attribute_check => nil,
|
440
|
+
:context => nil,
|
441
|
+
:nested_in => nil,
|
442
|
+
}.merge(options)
|
443
|
+
|
444
|
+
new_actions = actions_from_option(options[:new]).merge(
|
445
|
+
actions_from_option(options[:additional_new]))
|
446
|
+
members = actions_from_option(options[:member]).merge(
|
447
|
+
actions_from_option(options[:additional_member]))
|
448
|
+
collections = actions_from_option(options[:collection]).merge(
|
449
|
+
actions_from_option(options[:additional_collection]))
|
450
|
+
|
451
|
+
options[:no_attribute_check] ||= collections.keys unless options[:nested_in]
|
452
|
+
|
453
|
+
unless options[:nested_in].blank?
|
454
|
+
load_parent_method = :"load_#{options[:nested_in].to_s.singularize}"
|
455
|
+
shallow_exceptions = options[:shallow] ? {:except => members.keys} : {}
|
456
|
+
before_filter shallow_exceptions do |controller|
|
457
|
+
if controller.respond_to?(load_parent_method)
|
458
|
+
controller.send(load_parent_method)
|
459
|
+
else
|
460
|
+
controller.send(:load_parent_controller_object, options[:nested_in])
|
461
|
+
end
|
462
|
+
end
|
463
|
+
|
464
|
+
new_for_collection_method = :"new_#{controller_name.singularize}_for_collection"
|
465
|
+
before_filter :only => collections.keys do |controller|
|
466
|
+
# new_for_collection
|
467
|
+
if controller.respond_to?(new_for_collection_method)
|
468
|
+
controller.send(new_for_collection_method)
|
469
|
+
else
|
470
|
+
controller.send(:new_controller_object_for_collection,
|
471
|
+
options[:context] || controller_name, options[:nested_in])
|
472
|
+
end
|
473
|
+
end
|
474
|
+
end
|
475
|
+
|
476
|
+
new_from_params_method = :"new_#{controller_name.singularize}_from_params"
|
477
|
+
before_filter :only => new_actions.keys do |controller|
|
478
|
+
# new_from_params
|
479
|
+
if controller.respond_to?(new_from_params_method)
|
480
|
+
controller.send(new_from_params_method)
|
481
|
+
else
|
482
|
+
controller.send(:new_controller_object_from_params,
|
483
|
+
options[:context] || controller_name, options[:nested_in])
|
484
|
+
end
|
485
|
+
end
|
486
|
+
load_method = :"load_#{controller_name.singularize}"
|
487
|
+
before_filter :only => members.keys do |controller|
|
488
|
+
# load controller object
|
489
|
+
if controller.respond_to?(load_method)
|
490
|
+
controller.send(load_method)
|
491
|
+
else
|
492
|
+
controller.send(:load_controller_object, options[:context] || controller_name)
|
493
|
+
end
|
494
|
+
end
|
495
|
+
filter_access_to :all, :attribute_check => true, :context => options[:context]
|
496
|
+
|
497
|
+
members.merge(new_actions).merge(collections).each do |action, privilege|
|
498
|
+
if action != privilege or (options[:no_attribute_check] and options[:no_attribute_check].include?(action))
|
499
|
+
filter_options = {
|
500
|
+
:context => options[:context],
|
501
|
+
:attribute_check => !options[:no_attribute_check] || !options[:no_attribute_check].include?(action)
|
502
|
+
}
|
503
|
+
filter_options[:require] = privilege if action != privilege
|
504
|
+
filter_access_to(action, filter_options)
|
505
|
+
end
|
506
|
+
end
|
507
|
+
end
|
508
|
+
|
509
|
+
# Returns the context for authorization checks in the current controller.
|
510
|
+
# Uses the controller_name and prepends any namespaces underscored and
|
511
|
+
# joined with underscores.
|
512
|
+
#
|
513
|
+
# E.g.
|
514
|
+
# AllThosePeopleController => :all_those_people
|
515
|
+
# AnyName::Space::ThingsController => :any_name_space_things
|
516
|
+
#
|
517
|
+
def decl_auth_context
|
518
|
+
prefixes = name.split('::')[0..-2].map(&:underscore)
|
519
|
+
((prefixes + [controller_name]) * '_').to_sym
|
520
|
+
end
|
521
|
+
|
522
|
+
protected
|
523
|
+
def filter_access_permissions # :nodoc:
|
524
|
+
unless filter_access_permissions?
|
525
|
+
ancestors[1..-1].reverse.each do |mod|
|
526
|
+
mod.filter_access_permissions if mod.respond_to?(:filter_access_permissions)
|
527
|
+
end
|
528
|
+
end
|
529
|
+
class_variable_set(:@@declarative_authorization_permissions, {}) unless filter_access_permissions?
|
530
|
+
class_variable_get(:@@declarative_authorization_permissions)[self.name] ||= []
|
531
|
+
end
|
532
|
+
|
533
|
+
def filter_access_permissions? # :nodoc:
|
534
|
+
class_variable_defined?(:@@declarative_authorization_permissions)
|
535
|
+
end
|
536
|
+
|
537
|
+
def actions_from_option (option) # :nodoc:
|
538
|
+
case option
|
539
|
+
when nil
|
540
|
+
{}
|
541
|
+
when Symbol, String
|
542
|
+
{option.to_sym => option.to_sym}
|
543
|
+
when Hash
|
544
|
+
option
|
545
|
+
when Enumerable
|
546
|
+
option.each_with_object({}) do |action, hash|
|
547
|
+
if action.is_a?(Array)
|
548
|
+
raise "Unexpected option format: #{option.inspect}" if action.length != 2
|
549
|
+
hash[action.first] = action.last
|
550
|
+
else
|
551
|
+
hash[action.to_sym] = action.to_sym
|
552
|
+
end
|
553
|
+
end
|
554
|
+
end
|
555
|
+
end
|
556
|
+
end
|
557
|
+
end
|
558
|
+
|
559
|
+
class ControllerPermission # :nodoc:
|
560
|
+
attr_reader :actions, :privilege, :context, :attribute_check
|
561
|
+
def initialize (actions, privilege, context, attribute_check = false,
|
562
|
+
load_object_model = nil, load_object_method = nil,
|
563
|
+
filter_block = nil)
|
564
|
+
@actions = actions.to_set
|
565
|
+
@privilege = privilege
|
566
|
+
@context = context
|
567
|
+
@load_object_model = load_object_model
|
568
|
+
@load_object_method = load_object_method
|
569
|
+
@filter_block = filter_block
|
570
|
+
@attribute_check = attribute_check
|
571
|
+
end
|
572
|
+
|
573
|
+
def matches? (action_name)
|
574
|
+
@actions.include?(action_name.to_sym)
|
575
|
+
end
|
576
|
+
|
577
|
+
def permit! (contr)
|
578
|
+
if @filter_block
|
579
|
+
return contr.instance_eval(&@filter_block)
|
580
|
+
end
|
581
|
+
object = @attribute_check ? load_object(contr) : nil
|
582
|
+
privilege = @privilege || :"#{contr.action_name}"
|
583
|
+
|
584
|
+
contr.authorization_engine.permit!(privilege,
|
585
|
+
:user => contr.send(:current_user),
|
586
|
+
:object => object,
|
587
|
+
:skip_attribute_test => !@attribute_check,
|
588
|
+
:context => @context || contr.class.decl_auth_context)
|
589
|
+
end
|
590
|
+
|
591
|
+
def remove_actions (actions)
|
592
|
+
@actions -= actions
|
593
|
+
self
|
594
|
+
end
|
595
|
+
|
596
|
+
private
|
597
|
+
def load_object(contr)
|
598
|
+
if @load_object_method and @load_object_method.is_a?(Symbol)
|
599
|
+
contr.send(@load_object_method)
|
600
|
+
elsif @load_object_method and @load_object_method.is_a?(Proc)
|
601
|
+
contr.instance_eval(&@load_object_method)
|
602
|
+
else
|
603
|
+
load_object_model = @load_object_model ||
|
604
|
+
(@context ? @context.to_s.classify.constantize : contr.class.controller_name.classify.constantize)
|
605
|
+
instance_var = :"@#{load_object_model.name.underscore}"
|
606
|
+
object = contr.instance_variable_get(instance_var)
|
607
|
+
unless object
|
608
|
+
begin
|
609
|
+
object = load_object_model.find(contr.params[:id])
|
610
|
+
rescue RuntimeError => e
|
611
|
+
contr.logger.debug("filter_access_to tried to find " +
|
612
|
+
"#{load_object_model} from params[:id] " +
|
613
|
+
"(#{contr.params[:id].inspect}), because attribute_check is enabled " +
|
614
|
+
"and #{instance_var.to_s} isn't set, but failed: #{e.class.name}: #{e}")
|
615
|
+
raise if AuthorizationInController.failed_auto_loading_is_not_found?
|
616
|
+
end
|
617
|
+
contr.instance_variable_set(instance_var, object)
|
618
|
+
end
|
619
|
+
object
|
620
|
+
end
|
621
|
+
end
|
622
|
+
end
|
623
|
+
end
|