annotation_security 1.0.2 → 1.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/CHANGELOG +22 -0
- data/HOW-TO +261 -0
- data/{LICENSE → MIT-LICENSE} +1 -1
- data/README +39 -0
- data/Rakefile +53 -62
- data/assets/app/helpers/annotation_security_helper.rb +8 -8
- data/assets/config/initializers/annotation_security.rb +11 -11
- data/assets/config/security/relations.rb +20 -20
- data/assets/vendor/plugins/annotation_security/init.rb +14 -14
- data/bin/annotation_security +7 -7
- data/lib/annotation_security.rb +94 -103
- data/lib/annotation_security/exceptions.rb +124 -124
- data/lib/annotation_security/exec.rb +188 -188
- data/lib/annotation_security/includes/helper.rb +215 -215
- data/lib/annotation_security/includes/resource.rb +84 -84
- data/lib/annotation_security/includes/role.rb +30 -30
- data/lib/annotation_security/includes/user.rb +26 -26
- data/lib/annotation_security/manager/policy_factory.rb +29 -29
- data/lib/annotation_security/manager/policy_manager.rb +87 -79
- data/lib/annotation_security/manager/relation_loader.rb +272 -272
- data/lib/annotation_security/manager/resource_manager.rb +36 -36
- data/lib/annotation_security/manager/right_loader.rb +87 -87
- data/lib/annotation_security/policy/abstract_policy.rb +344 -344
- data/lib/annotation_security/policy/abstract_static_policy.rb +75 -75
- data/lib/annotation_security/policy/all_resources_policy.rb +20 -20
- data/lib/annotation_security/policy/rule.rb +340 -340
- data/lib/annotation_security/policy/rule_set.rb +138 -138
- data/lib/annotation_security/rails.rb +22 -39
- data/lib/{extensions → annotation_security/rails/2/extensions}/filter.rb +131 -133
- data/lib/annotation_security/rails/2/includes/action_controller.rb +144 -0
- data/lib/annotation_security/rails/2/includes/active_record.rb +28 -0
- data/lib/annotation_security/rails/2/initializer.rb +35 -0
- data/lib/annotation_security/{model_observer.rb → rails/2/model_observer.rb} +61 -61
- data/lib/annotation_security/rails/3/extensions/filter.rb +28 -0
- data/lib/annotation_security/{includes → rails/3/includes}/action_controller.rb +143 -144
- data/lib/annotation_security/{includes → rails/3/includes}/active_record.rb +27 -27
- data/lib/annotation_security/rails/3/initializer.rb +40 -0
- data/lib/annotation_security/rails/3/model_observer.rb +61 -0
- data/lib/annotation_security/rails/extensions.rb +21 -0
- data/lib/{extensions → annotation_security/rails/extensions}/action_controller.rb +31 -32
- data/lib/{extensions → annotation_security/rails/extensions}/active_record.rb +33 -34
- data/lib/{extensions → annotation_security/rails/extensions}/object.rb +10 -10
- data/lib/annotation_security/{filters.rb → rails/filters.rb} +37 -37
- data/lib/annotation_security/user_wrapper.rb +73 -73
- data/lib/annotation_security/utils.rb +141 -141
- data/lib/security_context.rb +588 -589
- data/spec/annotation_security/exceptions_spec.rb +16 -16
- data/spec/annotation_security/includes/helper_spec.rb +82 -82
- data/spec/annotation_security/manager/policy_manager_spec.rb +15 -15
- data/spec/annotation_security/manager/resource_manager_spec.rb +17 -17
- data/spec/annotation_security/manager/right_loader_spec.rb +17 -17
- data/spec/annotation_security/policy/abstract_policy_spec.rb +16 -16
- data/spec/annotation_security/policy/all_resources_policy_spec.rb +24 -24
- data/spec/annotation_security/policy/rule_set_spec.rb +112 -112
- data/spec/annotation_security/policy/rule_spec.rb +77 -77
- data/spec/annotation_security/policy/test_policy_spec.rb +80 -80
- data/spec/annotation_security/security_context_spec.rb +129 -78
- data/spec/annotation_security/utils_spec.rb +73 -73
- data/spec/helper/test_controller.rb +65 -65
- data/spec/helper/test_helper.rb +5 -5
- data/spec/helper/test_relations.rb +6 -6
- data/spec/helper/test_resource.rb +38 -38
- data/spec/helper/test_role.rb +21 -21
- data/spec/helper/test_user.rb +31 -31
- data/spec/rails_stub.rb +44 -37
- metadata +110 -96
- data/CHANGELOG.md +0 -14
- data/HOW-TO.md +0 -275
- data/README.md +0 -39
- data/lib/annotation_security/version.rb +0 -10
@@ -1,142 +1,142 @@
|
|
1
|
-
#
|
2
|
-
# = lib/annotation_security/utils.rb
|
3
|
-
#
|
4
|
-
# Provides some methods that are needed at several locations in the plug-in.
|
5
|
-
#
|
6
|
-
|
7
|
-
class AnnotationSecurity::Utils # :nodoc:
|
8
|
-
|
9
|
-
PREFIXES = /\A(may|is|can|has)_/
|
10
|
-
SUFFIXES = /(_(for|in|of|to)|\?)\Z/
|
11
|
-
|
12
|
-
# Removes pre- and suffixes from +method+,
|
13
|
-
# returns +nil+ if no change was made.
|
14
|
-
#
|
15
|
-
def self.method_body(method)
|
16
|
-
body = method.to_s.gsub(PREFIXES,'').gsub(SUFFIXES,'')
|
17
|
-
method.to_s == body ? nil : body
|
18
|
-
end
|
19
|
-
|
20
|
-
# Parses a description string
|
21
|
-
# * +description+ description of a controller action
|
22
|
-
# * +allow_binding+ if false, an exception is raised if the description
|
23
|
-
# contains a variable
|
24
|
-
# Returns right, resource and binding
|
25
|
-
#
|
26
|
-
def self.parse_description(description,allow_binding=false)
|
27
|
-
ActionAnnotation::Utils.parse_description(description,allow_binding)
|
28
|
-
end
|
29
|
-
|
30
|
-
# Parses arguments provided to #apply_policy or #allowed? and returns
|
31
|
-
# [ [:action, :resource_type, resource || nil], ... ]
|
32
|
-
#
|
33
|
-
# See SecurityContext#allowed? for details.
|
34
|
-
#
|
35
|
-
# Each element of the result can be send to a policy using
|
36
|
-
# policy_of_res_type.allowed?(rule, resource)
|
37
|
-
# or
|
38
|
-
# policy_of_res_type.static_policy.allowed?(rule, nil)
|
39
|
-
#
|
40
|
-
# Raises ArgumentError if args could not be parsed.
|
41
|
-
#
|
42
|
-
def self.parse_policy_arguments(args)
|
43
|
-
if args.first.is_a? String
|
44
|
-
hash = AnnotationSecurity::Utils.parse_description(args.first)
|
45
|
-
elsif args.first.is_a? Hash
|
46
|
-
hash = args.first
|
47
|
-
end
|
48
|
-
if hash
|
49
|
-
action = hash.delete(:action) || hash.delete('action')
|
50
|
-
resource = hash.delete(:resource) || hash.delete('resource')
|
51
|
-
unless resource.__is_resource?
|
52
|
-
resource_type = resource
|
53
|
-
resource = nil
|
54
|
-
end
|
55
|
-
resource_type ||= hash.delete(:resource_type)
|
56
|
-
resource_type ||= resource ? resource.resource_type : nil
|
57
|
-
a = [action, resource_type]
|
58
|
-
a << resource if resource
|
59
|
-
args = a + args[1..-1]
|
60
|
-
end
|
61
|
-
|
62
|
-
args << :all_resources unless args.size > 1
|
63
|
-
|
64
|
-
action, resource = args
|
65
|
-
|
66
|
-
if resource.__is_resource?
|
67
|
-
args = [action, resource.resource_type] + args[1..-1]
|
68
|
-
end
|
69
|
-
# if args.size > 2 && args.third == nil
|
70
|
-
# raise ArgumentError, "Did not expect nil as resource"
|
71
|
-
# end
|
72
|
-
args
|
73
|
-
end
|
74
|
-
|
75
|
-
# returns resource type and resource object without action
|
76
|
-
# expects [resource object], [resource type], or both
|
77
|
-
def self.parse_resource_arguments(args)
|
78
|
-
parse_policy_arguments([:r]+args)[1..2]
|
79
|
-
end
|
80
|
-
|
81
|
-
# Returns controller, action, objects and parameters
|
82
|
-
def self.parse_action_args(args)
|
83
|
-
controller = parse_controller(args.first)
|
84
|
-
action = args.second.to_sym
|
85
|
-
|
86
|
-
objects = args.third || []
|
87
|
-
objects = [objects] unless objects.is_a? Array
|
88
|
-
prepare_objects_resources(controller, objects)
|
89
|
-
|
90
|
-
params = args.fourth || {}
|
91
|
-
prepare_params_resources(controller, params)
|
92
|
-
|
93
|
-
objects += params.values
|
94
|
-
|
95
|
-
objects = objects.select { |o| o and o.__is_resource? }
|
96
|
-
return [controller, action, objects, params]
|
97
|
-
end
|
98
|
-
|
99
|
-
# Try to find the controller class from a name.
|
100
|
-
# Looks for [name](s)Controller.
|
101
|
-
#
|
102
|
-
# parse_controller :welcome #=> WelcomeController
|
103
|
-
# parse_controller :user # => UsersController
|
104
|
-
#
|
105
|
-
def self.parse_controller(controller) # :nodoc:
|
106
|
-
begin
|
107
|
-
"#{controller.to_s.camelize}Controller".constantize
|
108
|
-
rescue NameError
|
109
|
-
"#{controller.to_s.pluralize.camelize}Controller".constantize
|
110
|
-
end
|
111
|
-
rescue NameError
|
112
|
-
raise NameError, "Controller '#{controller}' was not found"
|
113
|
-
end
|
114
|
-
|
115
|
-
# if there are non-resources in objects, use the values to get resources
|
116
|
-
# from the controllers default resource type
|
117
|
-
#
|
118
|
-
def self.prepare_objects_resources(controller, objects)
|
119
|
-
res_type = controller.default_resource
|
120
|
-
objects.collect! do |o|
|
121
|
-
if o.__is_resource?
|
122
|
-
o
|
123
|
-
else
|
124
|
-
AnnotationSecurity::ResourceManager.get_resource(res_type, o)
|
125
|
-
end
|
126
|
-
end
|
127
|
-
end
|
128
|
-
|
129
|
-
# if there are non-resources in objects, use the values to get resources
|
130
|
-
# assuming the keys are the resource types (:id is defalut resource)
|
131
|
-
#
|
132
|
-
def self.prepare_params_resources(controller, params)
|
133
|
-
params.each do |k, v|
|
134
|
-
unless v.__is_resource?
|
135
|
-
res_type = k == :id ? controller.default_resource : k
|
136
|
-
v = AnnotationSecurity::ResourceManager.get_resource(res_type, v)
|
137
|
-
params[k] = v
|
138
|
-
end
|
139
|
-
end
|
140
|
-
end
|
141
|
-
|
1
|
+
#
|
2
|
+
# = lib/annotation_security/utils.rb
|
3
|
+
#
|
4
|
+
# Provides some methods that are needed at several locations in the plug-in.
|
5
|
+
#
|
6
|
+
|
7
|
+
class AnnotationSecurity::Utils # :nodoc:
|
8
|
+
|
9
|
+
PREFIXES = /\A(may|is|can|has)_/
|
10
|
+
SUFFIXES = /(_(for|in|of|to)|\?)\Z/
|
11
|
+
|
12
|
+
# Removes pre- and suffixes from +method+,
|
13
|
+
# returns +nil+ if no change was made.
|
14
|
+
#
|
15
|
+
def self.method_body(method)
|
16
|
+
body = method.to_s.gsub(PREFIXES,'').gsub(SUFFIXES,'')
|
17
|
+
method.to_s == body ? nil : body
|
18
|
+
end
|
19
|
+
|
20
|
+
# Parses a description string
|
21
|
+
# * +description+ description of a controller action
|
22
|
+
# * +allow_binding+ if false, an exception is raised if the description
|
23
|
+
# contains a variable
|
24
|
+
# Returns right, resource and binding
|
25
|
+
#
|
26
|
+
def self.parse_description(description,allow_binding=false)
|
27
|
+
ActionAnnotation::Utils.parse_description(description,allow_binding)
|
28
|
+
end
|
29
|
+
|
30
|
+
# Parses arguments provided to #apply_policy or #allowed? and returns
|
31
|
+
# [ [:action, :resource_type, resource || nil], ... ]
|
32
|
+
#
|
33
|
+
# See SecurityContext#allowed? for details.
|
34
|
+
#
|
35
|
+
# Each element of the result can be send to a policy using
|
36
|
+
# policy_of_res_type.allowed?(rule, resource)
|
37
|
+
# or
|
38
|
+
# policy_of_res_type.static_policy.allowed?(rule, nil)
|
39
|
+
#
|
40
|
+
# Raises ArgumentError if args could not be parsed.
|
41
|
+
#
|
42
|
+
def self.parse_policy_arguments(args)
|
43
|
+
if args.first.is_a? String
|
44
|
+
hash = AnnotationSecurity::Utils.parse_description(args.first)
|
45
|
+
elsif args.first.is_a? Hash
|
46
|
+
hash = args.first
|
47
|
+
end
|
48
|
+
if hash
|
49
|
+
action = hash.delete(:action) || hash.delete('action')
|
50
|
+
resource = hash.delete(:resource) || hash.delete('resource') || args.delete_at(1)
|
51
|
+
unless resource.__is_resource?
|
52
|
+
resource_type = resource
|
53
|
+
resource = nil
|
54
|
+
end
|
55
|
+
resource_type ||= hash.delete(:resource_type)
|
56
|
+
resource_type ||= resource ? resource.resource_type : nil
|
57
|
+
a = [action, resource_type]
|
58
|
+
a << resource if resource
|
59
|
+
args = a + args[1..-1]
|
60
|
+
end
|
61
|
+
|
62
|
+
args << :all_resources unless args.size > 1
|
63
|
+
|
64
|
+
action, resource = args
|
65
|
+
|
66
|
+
if resource.__is_resource?
|
67
|
+
args = [action, resource.resource_type] + args[1..-1]
|
68
|
+
end
|
69
|
+
# if args.size > 2 && args.third == nil
|
70
|
+
# raise ArgumentError, "Did not expect nil as resource"
|
71
|
+
# end
|
72
|
+
args
|
73
|
+
end
|
74
|
+
|
75
|
+
# returns resource type and resource object without action
|
76
|
+
# expects [resource object], [resource type], or both
|
77
|
+
def self.parse_resource_arguments(args)
|
78
|
+
parse_policy_arguments([:r]+args)[1..2]
|
79
|
+
end
|
80
|
+
|
81
|
+
# Returns controller, action, objects and parameters
|
82
|
+
def self.parse_action_args(args)
|
83
|
+
controller = parse_controller(args.first)
|
84
|
+
action = args.second.to_sym
|
85
|
+
|
86
|
+
objects = args.third || []
|
87
|
+
objects = [objects] unless objects.is_a? Array
|
88
|
+
prepare_objects_resources(controller, objects)
|
89
|
+
|
90
|
+
params = args.fourth || {}
|
91
|
+
prepare_params_resources(controller, params)
|
92
|
+
|
93
|
+
objects += params.values
|
94
|
+
|
95
|
+
objects = objects.select { |o| o and o.__is_resource? }
|
96
|
+
return [controller, action, objects, params]
|
97
|
+
end
|
98
|
+
|
99
|
+
# Try to find the controller class from a name.
|
100
|
+
# Looks for [name](s)Controller.
|
101
|
+
#
|
102
|
+
# parse_controller :welcome #=> WelcomeController
|
103
|
+
# parse_controller :user # => UsersController
|
104
|
+
#
|
105
|
+
def self.parse_controller(controller) # :nodoc:
|
106
|
+
begin
|
107
|
+
"#{controller.to_s.camelize}Controller".constantize
|
108
|
+
rescue NameError
|
109
|
+
"#{controller.to_s.pluralize.camelize}Controller".constantize
|
110
|
+
end
|
111
|
+
rescue NameError
|
112
|
+
raise NameError, "Controller '#{controller}' was not found"
|
113
|
+
end
|
114
|
+
|
115
|
+
# if there are non-resources in objects, use the values to get resources
|
116
|
+
# from the controllers default resource type
|
117
|
+
#
|
118
|
+
def self.prepare_objects_resources(controller, objects)
|
119
|
+
res_type = controller.default_resource
|
120
|
+
objects.collect! do |o|
|
121
|
+
if o.__is_resource?
|
122
|
+
o
|
123
|
+
else
|
124
|
+
AnnotationSecurity::ResourceManager.get_resource(res_type, o)
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
# if there are non-resources in objects, use the values to get resources
|
130
|
+
# assuming the keys are the resource types (:id is defalut resource)
|
131
|
+
#
|
132
|
+
def self.prepare_params_resources(controller, params)
|
133
|
+
params.each do |k, v|
|
134
|
+
unless v.__is_resource?
|
135
|
+
res_type = k == :id ? controller.default_resource : k
|
136
|
+
v = AnnotationSecurity::ResourceManager.get_resource(res_type, v)
|
137
|
+
params[k] = v
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
142
|
end
|
data/lib/security_context.rb
CHANGED
@@ -1,589 +1,588 @@
|
|
1
|
-
#
|
2
|
-
# = lib/security_context.rb
|
3
|
-
#
|
4
|
-
# Contains the SecurityContext
|
5
|
-
require 'active_support'
|
6
|
-
|
7
|
-
# = SecurityContext
|
8
|
-
#
|
9
|
-
# The SecurityContext provides methods for all security concerns of
|
10
|
-
# the current request.
|
11
|
-
#
|
12
|
-
# For every request, it has to be initialized using #current_user=. It is
|
13
|
-
# recommended to do this in a security filter, which can be used to catch
|
14
|
-
# AnnotationSecurityExceptions as well.
|
15
|
-
#
|
16
|
-
# The SecurityContext is implemented as a singleton for the current thread.
|
17
|
-
# Thus, all instance methods can be send to the class as well.
|
18
|
-
#
|
19
|
-
class SecurityContext
|
20
|
-
|
21
|
-
# Returns current security context
|
22
|
-
#
|
23
|
-
def self.current
|
24
|
-
Thread.current[:security_context]
|
25
|
-
end
|
26
|
-
|
27
|
-
# At the begin of a request, the security context will be initialized for the
|
28
|
-
# current controller.
|
29
|
-
#
|
30
|
-
def self.initialize(controller) # :nodoc:
|
31
|
-
load(new(controller))
|
32
|
-
end
|
33
|
-
|
34
|
-
# As the security context is a singleton bound to the current thread,
|
35
|
-
# it will not be available in other threads. The following example shows
|
36
|
-
# how to use the security context inside of a spawn block
|
37
|
-
#
|
38
|
-
# copy = SecurityContext.copy
|
39
|
-
# spawn do
|
40
|
-
# SecurityContext.load(copy)
|
41
|
-
# begin
|
42
|
-
# # ...
|
43
|
-
# rescue SecurityViolationError
|
44
|
-
# puts 'Security was violated'
|
45
|
-
# end
|
46
|
-
# end
|
47
|
-
#
|
48
|
-
def self.load(sec_context)
|
49
|
-
Thread.current[:security_context] = sec_context
|
50
|
-
end
|
51
|
-
|
52
|
-
if Rails.env == 'development'
|
53
|
-
# Disables all security checkings.
|
54
|
-
# Is only available in development mode.
|
55
|
-
def self.ignore_security!
|
56
|
-
security_methods.each do |method|
|
57
|
-
class_eval "def self.#{method}(*args); true; end"
|
58
|
-
end
|
59
|
-
end
|
60
|
-
end
|
61
|
-
|
62
|
-
## ===========================================================================
|
63
|
-
## Instance
|
64
|
-
|
65
|
-
# Initialize context for the given controller
|
66
|
-
#
|
67
|
-
def initialize(controller) # :nodoc:
|
68
|
-
super()
|
69
|
-
|
70
|
-
@controller = controller
|
71
|
-
|
72
|
-
# initialize rule
|
73
|
-
|
74
|
-
# rules that are not bound to any source,
|
75
|
-
# will be triggered by model observer
|
76
|
-
@context_rules = new_rules_hash
|
77
|
-
@valid_objects = new_valid_objects_hash
|
78
|
-
|
79
|
-
# rules bound to request param
|
80
|
-
@param_rules = new_bound_rules_hash
|
81
|
-
@param_valid_objects = new_bound_valid_objects_hash
|
82
|
-
|
83
|
-
# rules bound to variable
|
84
|
-
@var_rules = new_bound_rules_hash
|
85
|
-
@var_valid_objects = new_bound_valid_objects_hash
|
86
|
-
|
87
|
-
# Hash with all required policies
|
88
|
-
@policies = new_policy_hash
|
89
|
-
end
|
90
|
-
|
91
|
-
# Sets the current user. This has to be done in a before or around filter,
|
92
|
-
# *before* entering the action. Elsewise, the user will be interpreted as
|
93
|
-
# not being logged in. Once set, the current user cannot be changed.
|
94
|
-
#
|
95
|
-
def credential=(user)
|
96
|
-
if @cred_set
|
97
|
-
raise AnnotationSecurity::AnnotationSecurityError,
|
98
|
-
"Credential already set for this request"
|
99
|
-
end
|
100
|
-
@cred_set = true
|
101
|
-
@credential = user
|
102
|
-
end
|
103
|
-
|
104
|
-
# Get the current credential
|
105
|
-
def credential
|
106
|
-
@credential
|
107
|
-
end
|
108
|
-
|
109
|
-
alias current_credential= credential=
|
110
|
-
alias current_credential credential
|
111
|
-
|
112
|
-
# Creates a copy of the current security context.
|
113
|
-
# See #load for more information.
|
114
|
-
def copy
|
115
|
-
self.class.new(@controller)
|
116
|
-
end
|
117
|
-
|
118
|
-
# Will be set if an security exception was catched by the security filter
|
119
|
-
def security_exception=(ex) # :nodoc:
|
120
|
-
@security_exception = ex
|
121
|
-
@controller.security_exception = ex
|
122
|
-
end
|
123
|
-
|
124
|
-
# If the action was aborted due to a security exception, this returns the
|
125
|
-
# exception that was raised. Returns nil if no exception occurred.
|
126
|
-
#
|
127
|
-
def security_exception
|
128
|
-
@security_exception
|
129
|
-
end
|
130
|
-
|
131
|
-
# See eval_with_security.
|
132
|
-
def send_with_security(rules, obj, msg, *args, &proc)
|
133
|
-
eval_with_security(rules) { obj.send(msg, *args, &proc) }
|
134
|
-
end
|
135
|
-
|
136
|
-
# Evaluates the given block, additionally using the given rules.
|
137
|
-
# rules == [ { :action => action, :resource => res_type, :source => binding }, ...]
|
138
|
-
# action and res_type should be symbols, binding is optional
|
139
|
-
#
|
140
|
-
def eval_with_security(rules)
|
141
|
-
install_rules(rules)
|
142
|
-
|
143
|
-
apply_rules_before_action
|
144
|
-
|
145
|
-
result = yield
|
146
|
-
|
147
|
-
apply_rules_after_action
|
148
|
-
|
149
|
-
result
|
150
|
-
rescue AnnotationSecurity::SecurityError
|
151
|
-
SecurityContext.security_exception = $!
|
152
|
-
raise $!
|
153
|
-
ensure
|
154
|
-
uninstall_rules(rules)
|
155
|
-
result
|
156
|
-
end
|
157
|
-
|
158
|
-
def apply_rules_before_action # :nodoc:
|
159
|
-
# apply static rules before entering the action
|
160
|
-
apply_static_rules
|
161
|
-
# bindings may apply to parameters, try to check them too
|
162
|
-
apply_param_rules
|
163
|
-
apply_var_rules
|
164
|
-
end
|
165
|
-
|
166
|
-
def apply_rules_after_action # :nodoc:
|
167
|
-
# check again, bindings may have been changed
|
168
|
-
apply_var_rules
|
169
|
-
end
|
170
|
-
|
171
|
-
# Returns true iif the operation defined by +policy_args+ is allowed.
|
172
|
-
#
|
173
|
-
# The following calls to \#allowed? are allowed:
|
174
|
-
#
|
175
|
-
# allowed? :show, :resource, @resource
|
176
|
-
# # => true if the current user has the right to show @resource,
|
177
|
-
# # which belongs to the :resource resource-class
|
178
|
-
#
|
179
|
-
# In case of model objects or other classes which implement a #resource_type
|
180
|
-
# method the the second argument may be ommited
|
181
|
-
#
|
182
|
-
# allowed? :show, @resource
|
183
|
-
# # equivalent to the above call if @resource.resource_type == :resource
|
184
|
-
#
|
185
|
-
# A policy description used as a controller annotation may also be to check
|
186
|
-
# a right
|
187
|
-
#
|
188
|
-
# allowed? "show resource", @resource
|
189
|
-
# # => true if the current user has the right "show resource" for @resource
|
190
|
-
#
|
191
|
-
# A policy may also be applied without an object representing the context:
|
192
|
-
#
|
193
|
-
# allowed? :show, :resource
|
194
|
-
# # => true if the current may show resources.
|
195
|
-
#
|
196
|
-
# This will only check system and pretest rules. The result +true+ does not
|
197
|
-
# mean that the user may show all resources. However, a +false+ indicates
|
198
|
-
# that the user is not allowed to show any resources.
|
199
|
-
#
|
200
|
-
# If the resource type is omitted as well, only rules defined for all
|
201
|
-
# resources can be tested. See RelationLoader#all_resources for details.
|
202
|
-
#
|
203
|
-
# allowed? :administrate
|
204
|
-
# # => true if the user is allowed to administrate all resources.
|
205
|
-
#
|
206
|
-
def allowed?(*policy_args)
|
207
|
-
policy_args = AnnotationSecurity::Utils.parse_policy_arguments(policy_args)
|
208
|
-
__allowed?(*policy_args)
|
209
|
-
end
|
210
|
-
|
211
|
-
# Equivalent to allowed?; is? is provided for better readability.
|
212
|
-
#
|
213
|
-
# SecurityContext.allowed? :logged_in
|
214
|
-
# vs
|
215
|
-
# SecurityContext.is? :logged_in
|
216
|
-
#
|
217
|
-
alias is? allowed?
|
218
|
-
|
219
|
-
# Raises a SecurityViolationError if the rule defined by +policy_args+ is not
|
220
|
-
# allowed. See allowed? for details.
|
221
|
-
#
|
222
|
-
def apply_rule(*args)
|
223
|
-
self.class.raise_access_denied(*args) unless allowed?(*args)
|
224
|
-
end
|
225
|
-
|
226
|
-
# Checks the rules of an other action. Note that rules that are bound to a
|
227
|
-
# variable can not be checked.
|
228
|
-
#
|
229
|
-
# ==== Parameters
|
230
|
-
# * +controller+ Symbol representing the controller, like :resource
|
231
|
-
# * +action+ The called action, like :update
|
232
|
-
# * +objects+ (optional) List of objects that will be relevant for that action.
|
233
|
-
# * +params+ (optional) Hash of the passed parameters, like :id => 1.
|
234
|
-
#
|
235
|
-
# ==== Examples
|
236
|
-
#
|
237
|
-
# Checks static and pretest rules.
|
238
|
-
# allow_action? :resource, :create
|
239
|
-
# # => true if the current user may execute ResourcesController#create
|
240
|
-
#
|
241
|
-
# Checks static, pretest and context rules
|
242
|
-
# allow_action? :resource, :edit, [@resource]
|
243
|
-
# # => true if the current user may execute ResourcesController#edit,
|
244
|
-
# # assuming that @resource will be used in that action
|
245
|
-
#
|
246
|
-
# Checks static, pretest and context rules and all rules that are bound
|
247
|
-
# to :id.
|
248
|
-
# allow_action? :resource, :edit, [@resource], {:id => 4}
|
249
|
-
# # => true if the current user may execute ResourcesController#edit,
|
250
|
-
# # assuming that @resource will be used in that action
|
251
|
-
#
|
252
|
-
def allow_action?(*args) # :nodoc:
|
253
|
-
|
254
|
-
controller, action, objects, params =
|
255
|
-
AnnotationSecurity::Utils.parse_action_args(args)
|
256
|
-
|
257
|
-
# var rules are ignored here
|
258
|
-
context_rules, param_rules, _ = get_rule_set(controller, action)
|
259
|
-
|
260
|
-
# check static rules
|
261
|
-
evaluate_statically(context_rules)
|
262
|
-
|
263
|
-
# check context rules for all objects
|
264
|
-
objects.each do |o|
|
265
|
-
res_type = o.resource_type
|
266
|
-
evaluate_context_rules(context_rules, res_type, o)
|
267
|
-
end
|
268
|
-
|
269
|
-
evaluate_bound_rules_for_params(param_rules, params)
|
270
|
-
|
271
|
-
true
|
272
|
-
rescue SecurityViolationError
|
273
|
-
return false
|
274
|
-
end
|
275
|
-
|
276
|
-
# Applies all system and pretest rules of the current action.
|
277
|
-
# Raises a SecurityViolationError if a rule is violated.
|
278
|
-
#
|
279
|
-
def apply_static_rules # :nodoc:
|
280
|
-
evaluate_statically(@context_rules)
|
281
|
-
end
|
282
|
-
|
283
|
-
def apply_param_rules # :nodoc:
|
284
|
-
evaluate_bound_rules(@param_rules, @param_valid_objects)
|
285
|
-
end
|
286
|
-
|
287
|
-
def apply_var_rules # :nodoc:
|
288
|
-
evaluate_bound_rules(@var_rules, @var_valid_objects)
|
289
|
-
end
|
290
|
-
|
291
|
-
# Applies all rules of the current action to the resource defined by
|
292
|
-
# +resource_args+. Raises a SecurityViolationError if a rule is
|
293
|
-
# violated.
|
294
|
-
#
|
295
|
-
def apply_context_rules(*res_args) # :nodoc:
|
296
|
-
restype, res = AnnotationSecurity::Utils.parse_resource_arguments(res_args)
|
297
|
-
evaluate_context_rules(@context_rules, restype, res)
|
298
|
-
end
|
299
|
-
|
300
|
-
alias apply_rules apply_context_rules # :nodoc:
|
301
|
-
|
302
|
-
# Call if a resource object was touched during an action. Will be called
|
303
|
-
# automatically for model objects.
|
304
|
-
#
|
305
|
-
# Applies all rules that are currently active to the resource defined by
|
306
|
-
# +resource_args+. Raises a SecurityViolationError if a rule is
|
307
|
-
# violated.
|
308
|
-
#
|
309
|
-
# ==== Usage
|
310
|
-
# observe :resource, @resource
|
311
|
-
# where <tt>:resource</tt> is the resource type @resource belongs to, or
|
312
|
-
# observe @resource
|
313
|
-
# which is equivalent if <tt>@resource.resource_name == :resource</tt>
|
314
|
-
#
|
315
|
-
def observe(*resource_args)
|
316
|
-
apply_context_rules(*resource_args)
|
317
|
-
end
|
318
|
-
|
319
|
-
# Raise a SecurityViolationError.
|
320
|
-
# See allowed? for details on +policy_args+.
|
321
|
-
#
|
322
|
-
def self.raise_access_denied(*policy_args)
|
323
|
-
log_access_denied(policy_args)
|
324
|
-
raise SecurityViolationError.access_denied(credential,*policy_args)
|
325
|
-
end
|
326
|
-
|
327
|
-
# Activates access logging for the current request.
|
328
|
-
#
|
329
|
-
def log!(&proc)
|
330
|
-
@enable_logging = true
|
331
|
-
@log = proc || Proc.new do |result, action, res_type, resource|
|
332
|
-
result = result ? 'ALLOWED' : 'REFUSED' unless result.is_a? String
|
333
|
-
msg = "%-8s %-10s %-16s %s" % [result, action, res_type, resource]
|
334
|
-
puts msg
|
335
|
-
end
|
336
|
-
end
|
337
|
-
|
338
|
-
def log_access_denied(policy_args) # :nodoc:
|
339
|
-
@log.call('DENIED!', *policy_args) if @enable_logging
|
340
|
-
end
|
341
|
-
|
342
|
-
protected
|
343
|
-
|
344
|
-
def log_access_check(*policy_args)
|
345
|
-
@log.call(*policy_args) if @enable_logging
|
346
|
-
end
|
347
|
-
|
348
|
-
private
|
349
|
-
|
350
|
-
# data =========================================================================
|
351
|
-
|
352
|
-
# { binding => { :res_type1 => [:action1, ...], ... }, ... }
|
353
|
-
def new_bound_rules_hash() # :nodoc:
|
354
|
-
Hash.new { |h,k| h[k] = new_rules_hash }
|
355
|
-
end
|
356
|
-
|
357
|
-
# { :res_type1 => [:action1, ...], ... }
|
358
|
-
def new_rules_hash() # :nodoc:
|
359
|
-
Hash.new { |h,k| h[k] = [] }
|
360
|
-
end
|
361
|
-
|
362
|
-
# { binding => [object1, ...], ...}
|
363
|
-
def new_bound_valid_objects_hash() # :nodoc:
|
364
|
-
Hash.new { |h,k| h[k] = [] }
|
365
|
-
end
|
366
|
-
|
367
|
-
# { :res_type1 => { :action1 => [object1, ...], ...}, ...}
|
368
|
-
def new_valid_objects_hash() # :nodoc:
|
369
|
-
Hash.new { |h,k| h[k] = Hash.new { |h2,k2| h2[k2] = [] } }
|
370
|
-
end
|
371
|
-
|
372
|
-
# {:res_type1 => policy1, ...}
|
373
|
-
def new_policy_hash() # :nodoc:
|
374
|
-
Hash.new { |h,k| h[k] = new_policy(k) }
|
375
|
-
end
|
376
|
-
|
377
|
-
def new_policy(resource_type) # :nodoc:
|
378
|
-
AnnotationSecurity::PolicyManager.create_policy(resource_type, credential)
|
379
|
-
end
|
380
|
-
|
381
|
-
# Get the policy for a resource type from the cache
|
382
|
-
def policy(res_type) # :nodoc:
|
383
|
-
@policies[res_type]
|
384
|
-
end
|
385
|
-
|
386
|
-
# rules management =============================================================
|
387
|
-
|
388
|
-
def install_rules(rules, rule_set=nil, controller=@controller.class)
|
389
|
-
rules.each { |rule| install_rule rule, rule_set, controller }
|
390
|
-
end
|
391
|
-
|
392
|
-
def install_rule(rule, rule_set, controller)
|
393
|
-
rule_list(rule, rule_set, controller) << rule[:action]
|
394
|
-
end
|
395
|
-
|
396
|
-
def uninstall_rules(rules, rule_set=nil, controller=@controller.class)
|
397
|
-
rules.each { |rule| uninstall_rule rule, rule_set, controller }
|
398
|
-
end
|
399
|
-
|
400
|
-
def uninstall_rule(rule, rule_set, controller)
|
401
|
-
list = rule_list(rule, rule_set, controller)
|
402
|
-
i = list.index(rule[:action])
|
403
|
-
list.delete_at(i) if i
|
404
|
-
end
|
405
|
-
|
406
|
-
def rule_list(rule, rule_set, controller)
|
407
|
-
rule_set ||= [@context_rules, @param_rules, @var_rules]
|
408
|
-
resource = rule[:resource] || controller.default_resource
|
409
|
-
source = rule[:source]
|
410
|
-
if source.nil?
|
411
|
-
list = rule_set.first[resource]
|
412
|
-
elsif source.is_a? Symbol
|
413
|
-
list = rule_set.second[source][resource]
|
414
|
-
else
|
415
|
-
list = rule_set.third[source][resource]
|
416
|
-
end
|
417
|
-
list
|
418
|
-
end
|
419
|
-
|
420
|
-
# returns rule set for other controller actions
|
421
|
-
def get_rule_set(controller, action) # :nodoc:
|
422
|
-
@rule_sets ||= Hash.new { |h,k| h[k] = {} }
|
423
|
-
rule_set = @rule_sets[controller][action]
|
424
|
-
unless rule_set
|
425
|
-
rule_set = [new_rules_hash, new_bound_rules_hash, new_bound_rules_hash]
|
426
|
-
rules = controller.descriptions_of action
|
427
|
-
install_rules rules, rule_set, controller
|
428
|
-
@rule_sets[controller][action] = rule_set
|
429
|
-
end
|
430
|
-
rule_set
|
431
|
-
end
|
432
|
-
|
433
|
-
# rule evaluation ==============================================================
|
434
|
-
|
435
|
-
# Evaluate the rules statically, skips all rules that are static only.
|
436
|
-
# * +rules+ a Hash like {:resource_type => [:right1, :right2]}
|
437
|
-
def evaluate_statically(rules) # :nodoc:
|
438
|
-
# rules == { :resource1 => [:right1, ...], ... }
|
439
|
-
rules.each_pair do |resource_type,rights|
|
440
|
-
policy(resource_type).evaluate_statically(rights)
|
441
|
-
end
|
442
|
-
end
|
443
|
-
|
444
|
-
# Checks bound rules. Evaluates the bindings, on success adds objects
|
445
|
-
# to valid objects.
|
446
|
-
# rules == { binding1 => { :res_type1 => [:action1, ...], ... }, ... }
|
447
|
-
# valid_objects == { binding1 => [object1, ...], ... }
|
448
|
-
def evaluate_bound_rules(rules, valid_objects) # :nodoc:
|
449
|
-
evaluate_bound_rules_with_binding(rules, valid_objects) do |binding|
|
450
|
-
@controller.values_of_source(binding)
|
451
|
-
end
|
452
|
-
end
|
453
|
-
|
454
|
-
# Checks bound rules using the values from params
|
455
|
-
# rules == { binding1 => { :res_type1 => [:action1, ...], ... }, ... }
|
456
|
-
# params == { binding1 => object1, ... }
|
457
|
-
def evaluate_bound_rules_for_params(rules, params) # :nodoc:
|
458
|
-
valid_objects = new_bound_valid_objects_hash
|
459
|
-
evaluate_bound_rules_with_binding(rules, valid_objects) do |binding|
|
460
|
-
values = params[binding]
|
461
|
-
values.is_a?(Array) ? values : [values]
|
462
|
-
end
|
463
|
-
end
|
464
|
-
|
465
|
-
def evaluate_bound_rules_with_binding(rules, valid_objects, &proc) # :nodoc:
|
466
|
-
rules.each_key do |binding|
|
467
|
-
value_ids = proc.call(binding)
|
468
|
-
rules[binding].each_key do |res_type|
|
469
|
-
values = value_ids.collect do |id|
|
470
|
-
AnnotationSecurity::ResourceManager.get_resource res_type, id
|
471
|
-
end
|
472
|
-
values.compact!
|
473
|
-
values_of_res_type = values - valid_objects[binding]
|
474
|
-
values_of_res_type.each do |resource|
|
475
|
-
evaluate_rules(rules[binding][res_type],
|
476
|
-
res_type,
|
477
|
-
resource)
|
478
|
-
valid_objects[binding] << resource
|
479
|
-
end
|
480
|
-
end
|
481
|
-
end
|
482
|
-
end
|
483
|
-
|
484
|
-
# Checks context rules for given resource
|
485
|
-
# rules == { :res_type1 => [:action1, ...], ... }
|
486
|
-
def evaluate_context_rules(rules, res_type, res) # :nodoc:
|
487
|
-
evaluate_rules(rules[res_type], res_type, res)
|
488
|
-
end
|
489
|
-
|
490
|
-
# Checks if actions on resource are allowed. If true, adds to valid objects.
|
491
|
-
# Returns true
|
492
|
-
# actions == [:action1, ...]
|
493
|
-
# valid_objects == { :action1 => [object1, ...], ... }
|
494
|
-
def evaluate_rules(actions, res_type, resource) # :nodoc:
|
495
|
-
valid_objects = @valid_objects[res_type]
|
496
|
-
actions.each do |action|
|
497
|
-
unless valid_objects[action].index(resource)
|
498
|
-
__apply_rule(action, res_type, resource)
|
499
|
-
valid_objects[action] << resource
|
500
|
-
end
|
501
|
-
end
|
502
|
-
true
|
503
|
-
end
|
504
|
-
|
505
|
-
# Usage:
|
506
|
-
# __allowed? :show, :assignment, an_assignment
|
507
|
-
def __allowed?(action, res_type, resource=nil) # :nodoc:
|
508
|
-
|
509
|
-
block = lambda do
|
510
|
-
if resource
|
511
|
-
policy(res_type).allowed?(action, resource)
|
512
|
-
else
|
513
|
-
policy(res_type).static_policy.allowed?(action, nil)
|
514
|
-
end
|
515
|
-
end
|
516
|
-
|
517
|
-
block.call
|
518
|
-
log_access_check r, action, res_type, resource
|
519
|
-
end
|
520
|
-
end
|
521
|
-
|
522
|
-
# Raises a SecurityViolationError if the rule defined by +policy_args+ is not
|
523
|
-
# allowed. See __allowed? for details.
|
524
|
-
#
|
525
|
-
def __apply_rule(*args) # :nodoc:
|
526
|
-
self.class.raise_access_denied(*args) unless __allowed?(*args)
|
527
|
-
end
|
528
|
-
|
529
|
-
#=============================================================================
|
530
|
-
# Singleton
|
531
|
-
|
532
|
-
def self.security_methods
|
533
|
-
instance_methods(false).delete_if { |m| [:enabled?].member? m.to_sym }
|
534
|
-
end
|
535
|
-
|
536
|
-
#=============================================================================
|
537
|
-
# Without security block implementation
|
538
|
-
|
539
|
-
class SecurityContextDummy
|
540
|
-
attr_accessor :credential
|
541
|
-
|
542
|
-
def initialize(credential)
|
543
|
-
self.credential = credential
|
544
|
-
end
|
545
|
-
|
546
|
-
def method_missing(symbol, *args)
|
547
|
-
|
548
|
-
end
|
549
|
-
|
550
|
-
def enabled?
|
551
|
-
false
|
552
|
-
end
|
553
|
-
end
|
554
|
-
|
555
|
-
public
|
556
|
-
|
557
|
-
def enabled?
|
558
|
-
true
|
559
|
-
end
|
560
|
-
|
561
|
-
# Runs a given block with security disabled. Inside the block, the context
|
562
|
-
# will be disabled for the current thread.
|
563
|
-
#
|
564
|
-
def self.without_security!(&block)
|
565
|
-
old_current = current
|
566
|
-
|
567
|
-
|
568
|
-
|
569
|
-
|
570
|
-
|
571
|
-
|
572
|
-
|
573
|
-
|
574
|
-
|
575
|
-
|
576
|
-
|
577
|
-
|
578
|
-
|
579
|
-
|
580
|
-
|
581
|
-
|
582
|
-
|
583
|
-
|
584
|
-
|
585
|
-
|
586
|
-
|
587
|
-
|
588
|
-
|
589
|
-
end
|
1
|
+
#
|
2
|
+
# = lib/security_context.rb
|
3
|
+
#
|
4
|
+
# Contains the SecurityContext
|
5
|
+
require 'active_support'
|
6
|
+
|
7
|
+
# = SecurityContext
|
8
|
+
#
|
9
|
+
# The SecurityContext provides methods for all security concerns of
|
10
|
+
# the current request.
|
11
|
+
#
|
12
|
+
# For every request, it has to be initialized using #current_user=. It is
|
13
|
+
# recommended to do this in a security filter, which can be used to catch
|
14
|
+
# AnnotationSecurityExceptions as well.
|
15
|
+
#
|
16
|
+
# The SecurityContext is implemented as a singleton for the current thread.
|
17
|
+
# Thus, all instance methods can be send to the class as well.
|
18
|
+
#
|
19
|
+
class SecurityContext
|
20
|
+
|
21
|
+
# Returns current security context
|
22
|
+
#
|
23
|
+
def self.current
|
24
|
+
Thread.current[:security_context]
|
25
|
+
end
|
26
|
+
|
27
|
+
# At the begin of a request, the security context will be initialized for the
|
28
|
+
# current controller.
|
29
|
+
#
|
30
|
+
def self.initialize(controller) # :nodoc:
|
31
|
+
load(new(controller))
|
32
|
+
end
|
33
|
+
|
34
|
+
# As the security context is a singleton bound to the current thread,
|
35
|
+
# it will not be available in other threads. The following example shows
|
36
|
+
# how to use the security context inside of a spawn block
|
37
|
+
#
|
38
|
+
# copy = SecurityContext.copy
|
39
|
+
# spawn do
|
40
|
+
# SecurityContext.load(copy)
|
41
|
+
# begin
|
42
|
+
# # ...
|
43
|
+
# rescue SecurityViolationError
|
44
|
+
# puts 'Security was violated'
|
45
|
+
# end
|
46
|
+
# end
|
47
|
+
#
|
48
|
+
def self.load(sec_context)
|
49
|
+
Thread.current[:security_context] = sec_context
|
50
|
+
end
|
51
|
+
|
52
|
+
if Rails.env == 'development'
|
53
|
+
# Disables all security checkings.
|
54
|
+
# Is only available in development mode.
|
55
|
+
def self.ignore_security!
|
56
|
+
security_methods.each do |method|
|
57
|
+
class_eval "def self.#{method}(*args); true; end"
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
## ===========================================================================
|
63
|
+
## Instance
|
64
|
+
|
65
|
+
# Initialize context for the given controller
|
66
|
+
#
|
67
|
+
def initialize(controller) # :nodoc:
|
68
|
+
super()
|
69
|
+
|
70
|
+
@controller = controller
|
71
|
+
|
72
|
+
# initialize rule
|
73
|
+
|
74
|
+
# rules that are not bound to any source,
|
75
|
+
# will be triggered by model observer
|
76
|
+
@context_rules = new_rules_hash
|
77
|
+
@valid_objects = new_valid_objects_hash
|
78
|
+
|
79
|
+
# rules bound to request param
|
80
|
+
@param_rules = new_bound_rules_hash
|
81
|
+
@param_valid_objects = new_bound_valid_objects_hash
|
82
|
+
|
83
|
+
# rules bound to variable
|
84
|
+
@var_rules = new_bound_rules_hash
|
85
|
+
@var_valid_objects = new_bound_valid_objects_hash
|
86
|
+
|
87
|
+
# Hash with all required policies
|
88
|
+
@policies = new_policy_hash
|
89
|
+
end
|
90
|
+
|
91
|
+
# Sets the current user. This has to be done in a before or around filter,
|
92
|
+
# *before* entering the action. Elsewise, the user will be interpreted as
|
93
|
+
# not being logged in. Once set, the current user cannot be changed.
|
94
|
+
#
|
95
|
+
def credential=(user)
|
96
|
+
if @cred_set
|
97
|
+
raise AnnotationSecurity::AnnotationSecurityError,
|
98
|
+
"Credential already set for this request"
|
99
|
+
end
|
100
|
+
@cred_set = true
|
101
|
+
@credential = user
|
102
|
+
end
|
103
|
+
|
104
|
+
# Get the current credential
|
105
|
+
def credential
|
106
|
+
@credential
|
107
|
+
end
|
108
|
+
|
109
|
+
alias current_credential= credential=
|
110
|
+
alias current_credential credential
|
111
|
+
|
112
|
+
# Creates a copy of the current security context.
|
113
|
+
# See #load for more information.
|
114
|
+
def copy
|
115
|
+
returning self.class.new(@controller) { |sc| sc.credential = credential }
|
116
|
+
end
|
117
|
+
|
118
|
+
# Will be set if an security exception was catched by the security filter
|
119
|
+
def security_exception=(ex) # :nodoc:
|
120
|
+
@security_exception = ex
|
121
|
+
@controller.security_exception = ex
|
122
|
+
end
|
123
|
+
|
124
|
+
# If the action was aborted due to a security exception, this returns the
|
125
|
+
# exception that was raised. Returns nil if no exception occurred.
|
126
|
+
#
|
127
|
+
def security_exception
|
128
|
+
@security_exception
|
129
|
+
end
|
130
|
+
|
131
|
+
# See eval_with_security.
|
132
|
+
def send_with_security(rules, obj, msg, *args, &proc)
|
133
|
+
eval_with_security(rules) { obj.send(msg, *args, &proc) }
|
134
|
+
end
|
135
|
+
|
136
|
+
# Evaluates the given block, additionally using the given rules.
|
137
|
+
# rules == [ { :action => action, :resource => res_type, :source => binding }, ...]
|
138
|
+
# action and res_type should be symbols, binding is optional
|
139
|
+
#
|
140
|
+
def eval_with_security(rules)
|
141
|
+
install_rules(rules)
|
142
|
+
|
143
|
+
apply_rules_before_action
|
144
|
+
|
145
|
+
result = yield
|
146
|
+
|
147
|
+
apply_rules_after_action
|
148
|
+
|
149
|
+
result
|
150
|
+
rescue AnnotationSecurity::SecurityError
|
151
|
+
SecurityContext.security_exception = $!
|
152
|
+
raise $!
|
153
|
+
ensure
|
154
|
+
uninstall_rules(rules)
|
155
|
+
result
|
156
|
+
end
|
157
|
+
|
158
|
+
def apply_rules_before_action # :nodoc:
|
159
|
+
# apply static rules before entering the action
|
160
|
+
apply_static_rules
|
161
|
+
# bindings may apply to parameters, try to check them too
|
162
|
+
apply_param_rules
|
163
|
+
apply_var_rules
|
164
|
+
end
|
165
|
+
|
166
|
+
def apply_rules_after_action # :nodoc:
|
167
|
+
# check again, bindings may have been changed
|
168
|
+
apply_var_rules
|
169
|
+
end
|
170
|
+
|
171
|
+
# Returns true iif the operation defined by +policy_args+ is allowed.
|
172
|
+
#
|
173
|
+
# The following calls to \#allowed? are allowed:
|
174
|
+
#
|
175
|
+
# allowed? :show, :resource, @resource
|
176
|
+
# # => true if the current user has the right to show @resource,
|
177
|
+
# # which belongs to the :resource resource-class
|
178
|
+
#
|
179
|
+
# In case of model objects or other classes which implement a #resource_type
|
180
|
+
# method the the second argument may be ommited
|
181
|
+
#
|
182
|
+
# allowed? :show, @resource
|
183
|
+
# # equivalent to the above call if @resource.resource_type == :resource
|
184
|
+
#
|
185
|
+
# A policy description used as a controller annotation may also be to check
|
186
|
+
# a right
|
187
|
+
#
|
188
|
+
# allowed? "show resource", @resource
|
189
|
+
# # => true if the current user has the right "show resource" for @resource
|
190
|
+
#
|
191
|
+
# A policy may also be applied without an object representing the context:
|
192
|
+
#
|
193
|
+
# allowed? :show, :resource
|
194
|
+
# # => true if the current may show resources.
|
195
|
+
#
|
196
|
+
# This will only check system and pretest rules. The result +true+ does not
|
197
|
+
# mean that the user may show all resources. However, a +false+ indicates
|
198
|
+
# that the user is not allowed to show any resources.
|
199
|
+
#
|
200
|
+
# If the resource type is omitted as well, only rules defined for all
|
201
|
+
# resources can be tested. See RelationLoader#all_resources for details.
|
202
|
+
#
|
203
|
+
# allowed? :administrate
|
204
|
+
# # => true if the user is allowed to administrate all resources.
|
205
|
+
#
|
206
|
+
def allowed?(*policy_args)
|
207
|
+
policy_args = AnnotationSecurity::Utils.parse_policy_arguments(policy_args)
|
208
|
+
__allowed?(*policy_args)
|
209
|
+
end
|
210
|
+
|
211
|
+
# Equivalent to allowed?; is? is provided for better readability.
|
212
|
+
#
|
213
|
+
# SecurityContext.allowed? :logged_in
|
214
|
+
# vs
|
215
|
+
# SecurityContext.is? :logged_in
|
216
|
+
#
|
217
|
+
alias is? allowed?
|
218
|
+
|
219
|
+
# Raises a SecurityViolationError if the rule defined by +policy_args+ is not
|
220
|
+
# allowed. See allowed? for details.
|
221
|
+
#
|
222
|
+
def apply_rule(*args)
|
223
|
+
self.class.raise_access_denied(*args) unless allowed?(*args)
|
224
|
+
end
|
225
|
+
|
226
|
+
# Checks the rules of an other action. Note that rules that are bound to a
|
227
|
+
# variable can not be checked.
|
228
|
+
#
|
229
|
+
# ==== Parameters
|
230
|
+
# * +controller+ Symbol representing the controller, like :resource
|
231
|
+
# * +action+ The called action, like :update
|
232
|
+
# * +objects+ (optional) List of objects that will be relevant for that action.
|
233
|
+
# * +params+ (optional) Hash of the passed parameters, like :id => 1.
|
234
|
+
#
|
235
|
+
# ==== Examples
|
236
|
+
#
|
237
|
+
# Checks static and pretest rules.
|
238
|
+
# allow_action? :resource, :create
|
239
|
+
# # => true if the current user may execute ResourcesController#create
|
240
|
+
#
|
241
|
+
# Checks static, pretest and context rules
|
242
|
+
# allow_action? :resource, :edit, [@resource]
|
243
|
+
# # => true if the current user may execute ResourcesController#edit,
|
244
|
+
# # assuming that @resource will be used in that action
|
245
|
+
#
|
246
|
+
# Checks static, pretest and context rules and all rules that are bound
|
247
|
+
# to :id.
|
248
|
+
# allow_action? :resource, :edit, [@resource], {:id => 4}
|
249
|
+
# # => true if the current user may execute ResourcesController#edit,
|
250
|
+
# # assuming that @resource will be used in that action
|
251
|
+
#
|
252
|
+
def allow_action?(*args) # :nodoc:
|
253
|
+
|
254
|
+
controller, action, objects, params =
|
255
|
+
AnnotationSecurity::Utils.parse_action_args(args)
|
256
|
+
|
257
|
+
# var rules are ignored here
|
258
|
+
context_rules, param_rules, _ = get_rule_set(controller, action)
|
259
|
+
|
260
|
+
# check static rules
|
261
|
+
evaluate_statically(context_rules)
|
262
|
+
|
263
|
+
# check context rules for all objects
|
264
|
+
objects.each do |o|
|
265
|
+
res_type = o.resource_type
|
266
|
+
evaluate_context_rules(context_rules, res_type, o)
|
267
|
+
end
|
268
|
+
|
269
|
+
evaluate_bound_rules_for_params(param_rules, params)
|
270
|
+
|
271
|
+
true
|
272
|
+
rescue SecurityViolationError
|
273
|
+
return false
|
274
|
+
end
|
275
|
+
|
276
|
+
# Applies all system and pretest rules of the current action.
|
277
|
+
# Raises a SecurityViolationError if a rule is violated.
|
278
|
+
#
|
279
|
+
def apply_static_rules # :nodoc:
|
280
|
+
evaluate_statically(@context_rules)
|
281
|
+
end
|
282
|
+
|
283
|
+
def apply_param_rules # :nodoc:
|
284
|
+
evaluate_bound_rules(@param_rules, @param_valid_objects)
|
285
|
+
end
|
286
|
+
|
287
|
+
def apply_var_rules # :nodoc:
|
288
|
+
evaluate_bound_rules(@var_rules, @var_valid_objects)
|
289
|
+
end
|
290
|
+
|
291
|
+
# Applies all rules of the current action to the resource defined by
|
292
|
+
# +resource_args+. Raises a SecurityViolationError if a rule is
|
293
|
+
# violated.
|
294
|
+
#
|
295
|
+
def apply_context_rules(*res_args) # :nodoc:
|
296
|
+
restype, res = AnnotationSecurity::Utils.parse_resource_arguments(res_args)
|
297
|
+
evaluate_context_rules(@context_rules, restype, res)
|
298
|
+
end
|
299
|
+
|
300
|
+
alias apply_rules apply_context_rules # :nodoc:
|
301
|
+
|
302
|
+
# Call if a resource object was touched during an action. Will be called
|
303
|
+
# automatically for model objects.
|
304
|
+
#
|
305
|
+
# Applies all rules that are currently active to the resource defined by
|
306
|
+
# +resource_args+. Raises a SecurityViolationError if a rule is
|
307
|
+
# violated.
|
308
|
+
#
|
309
|
+
# ==== Usage
|
310
|
+
# observe :resource, @resource
|
311
|
+
# where <tt>:resource</tt> is the resource type @resource belongs to, or
|
312
|
+
# observe @resource
|
313
|
+
# which is equivalent if <tt>@resource.resource_name == :resource</tt>
|
314
|
+
#
|
315
|
+
def observe(*resource_args)
|
316
|
+
apply_context_rules(*resource_args)
|
317
|
+
end
|
318
|
+
|
319
|
+
# Raise a SecurityViolationError.
|
320
|
+
# See allowed? for details on +policy_args+.
|
321
|
+
#
|
322
|
+
def self.raise_access_denied(*policy_args)
|
323
|
+
log_access_denied(policy_args)
|
324
|
+
raise SecurityViolationError.access_denied(credential,*policy_args)
|
325
|
+
end
|
326
|
+
|
327
|
+
# Activates access logging for the current request.
|
328
|
+
#
|
329
|
+
def log!(&proc)
|
330
|
+
@enable_logging = true
|
331
|
+
@log = proc || Proc.new do |result, action, res_type, resource|
|
332
|
+
result = result ? 'ALLOWED' : 'REFUSED' unless result.is_a? String
|
333
|
+
msg = "%-8s %-10s %-16s %s" % [result, action, res_type, resource]
|
334
|
+
puts msg
|
335
|
+
end
|
336
|
+
end
|
337
|
+
|
338
|
+
def log_access_denied(policy_args) # :nodoc:
|
339
|
+
@log.call('DENIED!', *policy_args) if @enable_logging
|
340
|
+
end
|
341
|
+
|
342
|
+
protected
|
343
|
+
|
344
|
+
def log_access_check(*policy_args)
|
345
|
+
@log.call(*policy_args) if @enable_logging
|
346
|
+
end
|
347
|
+
|
348
|
+
private
|
349
|
+
|
350
|
+
# data =========================================================================
|
351
|
+
|
352
|
+
# { binding => { :res_type1 => [:action1, ...], ... }, ... }
|
353
|
+
def new_bound_rules_hash() # :nodoc:
|
354
|
+
Hash.new { |h,k| h[k] = new_rules_hash }
|
355
|
+
end
|
356
|
+
|
357
|
+
# { :res_type1 => [:action1, ...], ... }
|
358
|
+
def new_rules_hash() # :nodoc:
|
359
|
+
Hash.new { |h,k| h[k] = [] }
|
360
|
+
end
|
361
|
+
|
362
|
+
# { binding => [object1, ...], ...}
|
363
|
+
def new_bound_valid_objects_hash() # :nodoc:
|
364
|
+
Hash.new { |h,k| h[k] = [] }
|
365
|
+
end
|
366
|
+
|
367
|
+
# { :res_type1 => { :action1 => [object1, ...], ...}, ...}
|
368
|
+
def new_valid_objects_hash() # :nodoc:
|
369
|
+
Hash.new { |h,k| h[k] = Hash.new { |h2,k2| h2[k2] = [] } }
|
370
|
+
end
|
371
|
+
|
372
|
+
# {:res_type1 => policy1, ...}
|
373
|
+
def new_policy_hash() # :nodoc:
|
374
|
+
Hash.new { |h,k| h[k] = new_policy(k) }
|
375
|
+
end
|
376
|
+
|
377
|
+
def new_policy(resource_type) # :nodoc:
|
378
|
+
AnnotationSecurity::PolicyManager.create_policy(resource_type, credential)
|
379
|
+
end
|
380
|
+
|
381
|
+
# Get the policy for a resource type from the cache
|
382
|
+
def policy(res_type) # :nodoc:
|
383
|
+
@policies[res_type]
|
384
|
+
end
|
385
|
+
|
386
|
+
# rules management =============================================================
|
387
|
+
|
388
|
+
def install_rules(rules, rule_set=nil, controller=@controller.class)
|
389
|
+
rules.each { |rule| install_rule rule, rule_set, controller }
|
390
|
+
end
|
391
|
+
|
392
|
+
def install_rule(rule, rule_set, controller)
|
393
|
+
rule_list(rule, rule_set, controller) << rule[:action]
|
394
|
+
end
|
395
|
+
|
396
|
+
def uninstall_rules(rules, rule_set=nil, controller=@controller.class)
|
397
|
+
rules.each { |rule| uninstall_rule rule, rule_set, controller }
|
398
|
+
end
|
399
|
+
|
400
|
+
def uninstall_rule(rule, rule_set, controller)
|
401
|
+
list = rule_list(rule, rule_set, controller)
|
402
|
+
i = list.index(rule[:action])
|
403
|
+
list.delete_at(i) if i
|
404
|
+
end
|
405
|
+
|
406
|
+
def rule_list(rule, rule_set, controller)
|
407
|
+
rule_set ||= [@context_rules, @param_rules, @var_rules]
|
408
|
+
resource = rule[:resource] || controller.default_resource
|
409
|
+
source = rule[:source]
|
410
|
+
if source.nil?
|
411
|
+
list = rule_set.first[resource]
|
412
|
+
elsif source.is_a? Symbol
|
413
|
+
list = rule_set.second[source][resource]
|
414
|
+
else
|
415
|
+
list = rule_set.third[source][resource]
|
416
|
+
end
|
417
|
+
list
|
418
|
+
end
|
419
|
+
|
420
|
+
# returns rule set for other controller actions
|
421
|
+
def get_rule_set(controller, action) # :nodoc:
|
422
|
+
@rule_sets ||= Hash.new { |h,k| h[k] = {} }
|
423
|
+
rule_set = @rule_sets[controller][action]
|
424
|
+
unless rule_set
|
425
|
+
rule_set = [new_rules_hash, new_bound_rules_hash, new_bound_rules_hash]
|
426
|
+
rules = controller.descriptions_of action
|
427
|
+
install_rules rules, rule_set, controller
|
428
|
+
@rule_sets[controller][action] = rule_set
|
429
|
+
end
|
430
|
+
rule_set
|
431
|
+
end
|
432
|
+
|
433
|
+
# rule evaluation ==============================================================
|
434
|
+
|
435
|
+
# Evaluate the rules statically, skips all rules that are static only.
|
436
|
+
# * +rules+ a Hash like {:resource_type => [:right1, :right2]}
|
437
|
+
def evaluate_statically(rules) # :nodoc:
|
438
|
+
# rules == { :resource1 => [:right1, ...], ... }
|
439
|
+
rules.each_pair do |resource_type,rights|
|
440
|
+
policy(resource_type).evaluate_statically(rights)
|
441
|
+
end
|
442
|
+
end
|
443
|
+
|
444
|
+
# Checks bound rules. Evaluates the bindings, on success adds objects
|
445
|
+
# to valid objects.
|
446
|
+
# rules == { binding1 => { :res_type1 => [:action1, ...], ... }, ... }
|
447
|
+
# valid_objects == { binding1 => [object1, ...], ... }
|
448
|
+
def evaluate_bound_rules(rules, valid_objects) # :nodoc:
|
449
|
+
evaluate_bound_rules_with_binding(rules, valid_objects) do |binding|
|
450
|
+
@controller.values_of_source(binding)
|
451
|
+
end
|
452
|
+
end
|
453
|
+
|
454
|
+
# Checks bound rules using the values from params
|
455
|
+
# rules == { binding1 => { :res_type1 => [:action1, ...], ... }, ... }
|
456
|
+
# params == { binding1 => object1, ... }
|
457
|
+
def evaluate_bound_rules_for_params(rules, params) # :nodoc:
|
458
|
+
valid_objects = new_bound_valid_objects_hash
|
459
|
+
evaluate_bound_rules_with_binding(rules, valid_objects) do |binding|
|
460
|
+
values = params[binding]
|
461
|
+
values.is_a?(Array) ? values : [values]
|
462
|
+
end
|
463
|
+
end
|
464
|
+
|
465
|
+
def evaluate_bound_rules_with_binding(rules, valid_objects, &proc) # :nodoc:
|
466
|
+
rules.each_key do |binding|
|
467
|
+
value_ids = proc.call(binding)
|
468
|
+
rules[binding].each_key do |res_type|
|
469
|
+
values = value_ids.collect do |id|
|
470
|
+
AnnotationSecurity::ResourceManager.get_resource res_type, id
|
471
|
+
end
|
472
|
+
values.compact!
|
473
|
+
values_of_res_type = values - valid_objects[binding]
|
474
|
+
values_of_res_type.each do |resource|
|
475
|
+
evaluate_rules(rules[binding][res_type],
|
476
|
+
res_type,
|
477
|
+
resource)
|
478
|
+
valid_objects[binding] << resource
|
479
|
+
end
|
480
|
+
end
|
481
|
+
end
|
482
|
+
end
|
483
|
+
|
484
|
+
# Checks context rules for given resource
|
485
|
+
# rules == { :res_type1 => [:action1, ...], ... }
|
486
|
+
def evaluate_context_rules(rules, res_type, res) # :nodoc:
|
487
|
+
evaluate_rules(rules[res_type], res_type, res)
|
488
|
+
end
|
489
|
+
|
490
|
+
# Checks if actions on resource are allowed. If true, adds to valid objects.
|
491
|
+
# Returns true
|
492
|
+
# actions == [:action1, ...]
|
493
|
+
# valid_objects == { :action1 => [object1, ...], ... }
|
494
|
+
def evaluate_rules(actions, res_type, resource) # :nodoc:
|
495
|
+
valid_objects = @valid_objects[res_type]
|
496
|
+
actions.each do |action|
|
497
|
+
unless valid_objects[action].index(resource)
|
498
|
+
__apply_rule(action, res_type, resource)
|
499
|
+
valid_objects[action] << resource
|
500
|
+
end
|
501
|
+
end
|
502
|
+
true
|
503
|
+
end
|
504
|
+
|
505
|
+
# Usage:
|
506
|
+
# __allowed? :show, :assignment, an_assignment
|
507
|
+
def __allowed?(action, res_type, resource=nil) # :nodoc:
|
508
|
+
|
509
|
+
block = lambda do
|
510
|
+
if resource
|
511
|
+
policy(res_type).allowed?(action, resource)
|
512
|
+
else
|
513
|
+
policy(res_type).static_policy.allowed?(action, nil)
|
514
|
+
end
|
515
|
+
end
|
516
|
+
|
517
|
+
returning block.call do |r|
|
518
|
+
log_access_check r, action, res_type, resource
|
519
|
+
end
|
520
|
+
end
|
521
|
+
|
522
|
+
# Raises a SecurityViolationError if the rule defined by +policy_args+ is not
|
523
|
+
# allowed. See __allowed? for details.
|
524
|
+
#
|
525
|
+
def __apply_rule(*args) # :nodoc:
|
526
|
+
self.class.raise_access_denied(*args) unless __allowed?(*args)
|
527
|
+
end
|
528
|
+
|
529
|
+
#=============================================================================
|
530
|
+
# Singleton
|
531
|
+
|
532
|
+
def self.security_methods
|
533
|
+
instance_methods(false).delete_if { |m| [:enabled?].member? m.to_sym }
|
534
|
+
end
|
535
|
+
|
536
|
+
#=============================================================================
|
537
|
+
# Without security block implementation
|
538
|
+
|
539
|
+
class SecurityContextDummy
|
540
|
+
attr_accessor :credential
|
541
|
+
|
542
|
+
def initialize(credential)
|
543
|
+
self.credential = credential
|
544
|
+
end
|
545
|
+
|
546
|
+
def method_missing(symbol, *args)
|
547
|
+
puts "#{self.class}##{symbol}(#{args})"
|
548
|
+
end
|
549
|
+
|
550
|
+
def enabled?
|
551
|
+
false
|
552
|
+
end
|
553
|
+
end
|
554
|
+
|
555
|
+
public
|
556
|
+
|
557
|
+
def enabled?
|
558
|
+
true
|
559
|
+
end
|
560
|
+
|
561
|
+
# Runs a given block with security disabled. Inside the block, the context
|
562
|
+
# will be disabled for the current thread.
|
563
|
+
#
|
564
|
+
def self.without_security!(&block)
|
565
|
+
old_current = current
|
566
|
+
|
567
|
+
load(SecurityContextDummy.new(old_current.credential))
|
568
|
+
return_value = yield
|
569
|
+
load old_current
|
570
|
+
return_value
|
571
|
+
end
|
572
|
+
|
573
|
+
# create singleton methods
|
574
|
+
security_methods.each do |method|
|
575
|
+
if method.to_s.end_with? '='
|
576
|
+
# setters need a different handling
|
577
|
+
class_eval %{
|
578
|
+
def self.#{method}(value)
|
579
|
+
current.#{method}(value) if current
|
580
|
+
end }
|
581
|
+
else
|
582
|
+
class_eval %{
|
583
|
+
def self.#{method}(*args,&proc)
|
584
|
+
current.#{method}(*args,&proc) if current
|
585
|
+
end }
|
586
|
+
end
|
587
|
+
end
|
588
|
+
end
|