authoreyes 0.1.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.
@@ -0,0 +1,133 @@
1
+ module Authoreyes
2
+ module Authorization
3
+ # An attribute condition that uses existing rules to decide validation
4
+ # and create obligations.
5
+ class AttributeWithPermission < Attribute
6
+ # E.g. privilege :read, attr_or_hash either :attribute or
7
+ # { :attribute => :deeper_attribute }
8
+ def initialize (privilege, attr_or_hash, context = nil)
9
+ @privilege = privilege
10
+ @context = context
11
+ @attr_hash = attr_or_hash
12
+ end
13
+
14
+ def initialize_copy (from)
15
+ @attr_hash = deep_hash_clone(@attr_hash) if @attr_hash.is_a?(Hash)
16
+ end
17
+
18
+ def validate? (attr_validator, object = nil, hash_or_attr = nil)
19
+ object ||= attr_validator.object
20
+ hash_or_attr ||= @attr_hash
21
+ return false unless object
22
+
23
+ case hash_or_attr
24
+ when Symbol
25
+ attr_value = object_attribute_value(object, hash_or_attr)
26
+ case attr_value
27
+ when nil
28
+ raise NilAttributeValueError, "Attribute #{hash_or_attr.inspect} is nil in #{object.inspect}."
29
+ when Enumerable
30
+ attr_value.any? do |inner_value|
31
+ attr_validator.engine.permit? @privilege, :object => inner_value, :user => attr_validator.user
32
+ end
33
+ else
34
+ attr_validator.engine.permit? @privilege, :object => attr_value, :user => attr_validator.user
35
+ end
36
+ when Hash
37
+ hash_or_attr.all? do |attr, sub_hash|
38
+ attr_value = object_attribute_value(object, attr)
39
+ if attr_value == nil
40
+ raise NilAttributeValueError, "Attribute #{attr.inspect} is nil in #{object.inspect}."
41
+ elsif attr_value.is_a?(Enumerable)
42
+ attr_value.any? do |inner_value|
43
+ validate?(attr_validator, inner_value, sub_hash)
44
+ end
45
+ else
46
+ validate?(attr_validator, attr_value, sub_hash)
47
+ end
48
+ end
49
+ when NilClass
50
+ attr_validator.engine.permit? @privilege, :object => object, :user => attr_validator.user
51
+ else
52
+ raise AuthorizationError, "Wrong conditions hash format: #{hash_or_attr.inspect}"
53
+ end
54
+ end
55
+
56
+ # may return an array of obligations to be OR'ed
57
+ def obligation (attr_validator, hash_or_attr = nil, path = [])
58
+ hash_or_attr ||= @attr_hash
59
+ case hash_or_attr
60
+ when Symbol
61
+ @context ||= begin
62
+ rule_model = attr_validator.context.to_s.classify.constantize
63
+ context_reflection = self.class.reflection_for_path(rule_model, path + [hash_or_attr])
64
+ if context_reflection.klass.respond_to?(:decl_auth_context)
65
+ context_reflection.klass.decl_auth_context
66
+ else
67
+ context_reflection.klass.name.tableize.to_sym
68
+ end
69
+ rescue # missing model, reflections
70
+ hash_or_attr.to_s.pluralize.to_sym
71
+ end
72
+
73
+ obligations = attr_validator.engine.obligations(@privilege,
74
+ :context => @context,
75
+ :user => attr_validator.user)
76
+
77
+ obligations.collect {|obl| {hash_or_attr => obl} }
78
+ when Hash
79
+ obligations_array_attrs = []
80
+ obligations =
81
+ hash_or_attr.inject({}) do |all, pair|
82
+ attr, sub_hash = pair
83
+ all[attr] = obligation(attr_validator, sub_hash, path + [attr])
84
+ if all[attr].length > 1
85
+ obligations_array_attrs << attr
86
+ else
87
+ all[attr] = all[attr].first
88
+ end
89
+ all
90
+ end
91
+ obligations = [obligations]
92
+ obligations_array_attrs.each do |attr|
93
+ next_array_size = obligations.first[attr].length
94
+ obligations = obligations.collect do |obls|
95
+ (0...next_array_size).collect do |idx|
96
+ obls_wo_array = obls.clone
97
+ obls_wo_array[attr] = obls_wo_array[attr][idx]
98
+ obls_wo_array
99
+ end
100
+ end.flatten
101
+ end
102
+ obligations
103
+ when NilClass
104
+ attr_validator.engine.obligations(@privilege,
105
+ :context => attr_validator.context,
106
+ :user => attr_validator.user)
107
+ else
108
+ raise AuthorizationError, "Wrong conditions hash format: #{hash_or_attr.inspect}"
109
+ end
110
+ end
111
+
112
+ def to_long_s
113
+ "if_permitted_to #{@privilege.inspect}, #{@attr_hash.inspect}"
114
+ end
115
+
116
+ private
117
+ def self.reflection_for_path (parent_model, path)
118
+ reflection = path.empty? ? parent_model : begin
119
+ parent = reflection_for_path(parent_model, path[0..-2])
120
+ if !parent.respond_to?(:proxy_reflection) and parent.respond_to?(:klass)
121
+ parent.klass.reflect_on_association(path.last)
122
+ else
123
+ parent.reflect_on_association(path.last)
124
+ end
125
+ rescue
126
+ parent.reflect_on_association(path.last)
127
+ end
128
+ raise "invalid path #{path.inspect}" if reflection.nil?
129
+ reflection
130
+ end
131
+ end
132
+ end
133
+ end
@@ -0,0 +1,89 @@
1
+ module Authoreyes
2
+ module Authorization
3
+ class AuthorizationRule
4
+ attr_reader :attributes, :contexts, :role, :privileges, :join_operator,
5
+ :source_file, :source_line
6
+
7
+ def initialize (role, privileges = [], contexts = nil, join_operator = :or,
8
+ options = {})
9
+ @role = role
10
+ @privileges = Set.new(privileges)
11
+ @contexts = Set.new((contexts && !contexts.is_a?(Array) ? [contexts] : contexts))
12
+ @join_operator = join_operator
13
+ @attributes = []
14
+ @source_file = options[:source_file]
15
+ @source_line = options[:source_line]
16
+ end
17
+
18
+ def initialize_copy (from)
19
+ @privileges = @privileges.clone
20
+ @contexts = @contexts.clone
21
+ @attributes = @attributes.collect {|attribute| attribute.clone }
22
+ end
23
+
24
+ def append_privileges (privs)
25
+ @privileges.merge(privs)
26
+ end
27
+
28
+ def append_attribute (attribute)
29
+ @attributes << attribute
30
+ end
31
+
32
+ def matches? (roles, privs, context = nil)
33
+ roles = [roles] unless roles.is_a?(Array)
34
+ @contexts.include?(context) and roles.include?(@role) and
35
+ not (@privileges & privs).empty?
36
+ end
37
+
38
+ def validate? (attr_validator, skip_attribute = false)
39
+ skip_attribute or @attributes.empty? or
40
+ @attributes.send(@join_operator == :and ? :all? : :any?) do |attr|
41
+ begin
42
+ attr.validate?(attr_validator)
43
+ rescue NilAttributeValueError => e
44
+ nil # Bumping up against a nil attribute value flunks the rule.
45
+ end
46
+ end
47
+ end
48
+
49
+ def obligations (attr_validator)
50
+ exceptions = []
51
+ obligations = @attributes.collect do |attr|
52
+ begin
53
+ attr.obligation(attr_validator)
54
+ rescue NotAuthorized => e
55
+ exceptions << e
56
+ nil
57
+ end
58
+ end
59
+
60
+ if exceptions.length > 0 and (@join_operator == :and or exceptions.length == @attributes.length)
61
+ raise NotAuthorized, "Missing authorization in collecting obligations: #{exceptions.map(&:to_s) * ", "}"
62
+ end
63
+
64
+ if @join_operator == :and and !obligations.empty?
65
+ # cross product of OR'ed obligations in arrays
66
+ arrayed_obligations = obligations.map {|obligation| obligation.is_a?(Hash) ? [obligation] : obligation}
67
+ merged_obligations = arrayed_obligations.first
68
+ arrayed_obligations[1..-1].each do |inner_obligations|
69
+ previous_merged_obligations = merged_obligations
70
+ merged_obligations = inner_obligations.collect do |inner_obligation|
71
+ previous_merged_obligations.collect do |merged_obligation|
72
+ merged_obligation.deep_merge(inner_obligation)
73
+ end
74
+ end.flatten
75
+ end
76
+ obligations = merged_obligations
77
+ else
78
+ obligations = obligations.flatten.compact
79
+ end
80
+ obligations.empty? ? [{}] : obligations
81
+ end
82
+
83
+ def to_long_s
84
+ attributes.collect {|attr| attr.to_long_s } * "; "
85
+ end
86
+ end
87
+
88
+ end
89
+ end
@@ -0,0 +1,58 @@
1
+ module Authoreyes
2
+ module Authorization
3
+ class AuthorizationRuleSet
4
+ include Enumerable
5
+ extend Forwardable
6
+ def_delegators :@rules, :each, :length, :[]
7
+
8
+ def initialize(rules = [])
9
+ @rules = rules.clone
10
+ reset!
11
+ end
12
+
13
+ def initialize_copy(source)
14
+ @rules = @rules.collect {|rule| rule.clone}
15
+ reset!
16
+ end
17
+
18
+ def matching(roles, privileges, context)
19
+ roles = [roles] unless roles.is_a?(Array)
20
+ rules = cached_auth_rules[context] || []
21
+ rules.select do |rule|
22
+ rule.matches? roles, privileges, context
23
+ end
24
+ end
25
+
26
+ def delete(rule)
27
+ @rules.delete rule
28
+ reset!
29
+ end
30
+
31
+ def << rule
32
+ @rules << rule
33
+ reset!
34
+ end
35
+
36
+ def each(&block)
37
+ @rules.each &block
38
+ end
39
+
40
+ private
41
+ def reset!
42
+ @cached_auth_rules =nil
43
+ end
44
+
45
+ def cached_auth_rules
46
+ return @cached_auth_rules if @cached_auth_rules
47
+ @cached_auth_rules = {}
48
+ @rules.each do |rule|
49
+ rule.contexts.each do |context|
50
+ @cached_auth_rules[context] ||= []
51
+ @cached_auth_rules[context] << rule
52
+ end
53
+ end
54
+ @cached_auth_rules
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,296 @@
1
+ module Authoreyes
2
+ module Authorization
3
+ # Authorization::Engine implements the reference monitor. It may be used
4
+ # for querying the permission and retrieving obligations under which
5
+ # a certain privilege is granted for the current user.
6
+ class Engine
7
+ extend Forwardable
8
+ attr_reader :reader
9
+
10
+ def_delegators :@reader, :auth_rules_reader, :privileges_reader, :load, :load!
11
+ def_delegators :auth_rules_reader, :auth_rules, :roles, :omnipotent_roles, :role_hierarchy, :role_titles, :role_descriptions
12
+ def_delegators :privileges_reader, :privileges, :privilege_hierarchy
13
+
14
+ # If +reader+ is not given, a new one is created with the default
15
+ # authorization configuration of +AUTH_DSL_FILES+. If given, may be either
16
+ # a Reader object or a path to a configuration file.
17
+ def initialize(options)
18
+ options = {
19
+ reader: nil
20
+ }.merge(options)
21
+ #@auth_rules = AuthorizationRuleSet.new reader.auth_rules_reader.auth_rules
22
+ @reader = ::Authoreyes::Parser::DSLParser.factory(options[:reader] || AUTH_DSL_FILES)
23
+ end
24
+
25
+ def initialize_copy(from) # :nodoc:
26
+ @reader = from.reader.clone
27
+ end
28
+
29
+ # {[priv, ctx] => [priv, ...]}
30
+ def rev_priv_hierarchy
31
+ if @rev_priv_hierarchy.nil?
32
+ @rev_priv_hierarchy = {}
33
+ privilege_hierarchy.each do |key, value|
34
+ value.each do |val|
35
+ @rev_priv_hierarchy[val] ||= []
36
+ @rev_priv_hierarchy[val] << key
37
+ end
38
+ end
39
+ end
40
+ @rev_priv_hierarchy
41
+ end
42
+
43
+ # {[priv, ctx] => [priv, ...]}
44
+ def rev_role_hierarchy
45
+ if @rev_role_hierarchy.nil?
46
+ @rev_role_hierarchy = {}
47
+ role_hierarchy.each do |higher_role, lower_roles|
48
+ lower_roles.each do |role|
49
+ (@rev_role_hierarchy[role] ||= []) << higher_role
50
+ end
51
+ end
52
+ end
53
+ @rev_role_hierarchy
54
+ end
55
+
56
+ # Returns true if privilege is met by the current user. Raises
57
+ # AuthorizationError otherwise. +privilege+ may be given with or
58
+ # without context. In the latter case, the :+context+ option is
59
+ # required.
60
+ #
61
+ # Options:
62
+ # [:+context+]
63
+ # The context part of the privilege.
64
+ # Defaults either to the tableized +class_name+ of the given :+object+, if given.
65
+ # That is, :+users+ for :+object+ of type User.
66
+ # Raises AuthorizationUsageError if context is missing and not to be inferred.
67
+ # [:+object+] An context object to test attribute checks against.
68
+ # [:+skip_attribute_test+]
69
+ # Skips those attribute checks in the
70
+ # authorization rules. Defaults to false.
71
+ # [:+user+]
72
+ # The user to check the authorization for.
73
+ # Defaults to Authorization#current_user.
74
+ # [:+bang+]
75
+ # Should NotAuthorized exceptions be raised
76
+ # Defaults to true.
77
+ #
78
+ def permit! (privilege, options = {})
79
+ return true if Authorization.ignore_access_control
80
+ options = {
81
+ :object => nil,
82
+ :skip_attribute_test => false,
83
+ :context => nil,
84
+ :bang => true
85
+ }.merge(options)
86
+
87
+ # Make sure we're handling all privileges as symbols.
88
+ privilege = privilege.is_a?( Array ) ?
89
+ privilege.flatten.collect { |priv| priv.to_sym } :
90
+ privilege.to_sym
91
+
92
+ #
93
+ # If the object responds to :proxy_reflection, we're probably working with
94
+ # an association proxy. Use 'new' to leverage ActiveRecord's builder
95
+ # functionality to obtain an object against which we can check permissions.
96
+ #
97
+ # Example: permit!( :edit, :object => user.posts )
98
+ #
99
+ if Authorization.is_a_association_proxy?(options[:object]) && options[:object].respond_to?(:new)
100
+ options[:object] = (Rails.version < "3.0" ? options[:object] : options[:object].where(nil)).new
101
+ end
102
+
103
+ options[:context] ||= options[:object] && (
104
+ options[:object].class.respond_to?(:decl_auth_context) ?
105
+ options[:object].class.decl_auth_context :
106
+ options[:object].class.name.tableize.to_sym
107
+ ) rescue NoMethodError
108
+
109
+ user, roles, privileges = user_roles_privleges_from_options(privilege, options)
110
+
111
+ return true if roles.is_a?(Array) and not (roles & omnipotent_roles).empty?
112
+
113
+ # find a authorization rule that matches for at least one of the roles and
114
+ # at least one of the given privileges
115
+ attr_validator = AttributeValidator.new(self, user, options[:object], privilege, options[:context])
116
+ rules = matching_auth_rules(roles, privileges, options[:context])
117
+
118
+ # Test each rule in turn to see whether any one of them is satisfied.
119
+ rules.each do |rule|
120
+ return true if rule.validate?(attr_validator, options[:skip_attribute_test])
121
+ end
122
+
123
+ if options[:bang]
124
+ if rules.empty?
125
+ raise NotAuthorized, "No matching rules found for #{privilege} for #{user.inspect} " +
126
+ "(roles #{roles.inspect}, privileges #{privileges.inspect}, " +
127
+ "context #{options[:context].inspect})."
128
+ else
129
+ raise AttributeAuthorizationError, "#{privilege} not allowed for #{user.inspect} on #{(options[:object] || options[:context]).inspect}."
130
+ end
131
+ else
132
+ false
133
+ end
134
+ end
135
+
136
+ # Calls permit! but doesn't raise authorization errors. If no exception is
137
+ # raised, permit? returns true and yields to the optional block.
138
+ def permit? (privilege, options = {}) # :yields:
139
+ if permit!(privilege, options.merge(:bang=> false))
140
+ yield if block_given?
141
+ true
142
+ else
143
+ false
144
+ end
145
+ end
146
+
147
+ # Returns the obligations to be met by the current user for the given
148
+ # privilege as an array of obligation hashes in form of
149
+ # [{:object_attribute => obligation_value, ...}, ...]
150
+ # where +obligation_value+ is either (recursively) another obligation hash
151
+ # or a value spec, such as
152
+ # [operator, literal_value]
153
+ # The obligation hashes in the array should be OR'ed, conditions inside
154
+ # the hashes AND'ed.
155
+ #
156
+ # Example
157
+ # {:branch => {:company => [:is, 24]}, :active => [:is, true]}
158
+ #
159
+ # Options
160
+ # [:+context+] See permit!
161
+ # [:+user+] See permit!
162
+ #
163
+ def obligations (privilege, options = {})
164
+ options = {:context => nil}.merge(options)
165
+ user, roles, privileges = user_roles_privleges_from_options(privilege, options)
166
+
167
+ permit!(privilege, :skip_attribute_test => true, :user => user, :context => options[:context])
168
+
169
+ return [] if roles.is_a?(Array) and not (roles & omnipotent_roles).empty?
170
+
171
+ attr_validator = AttributeValidator.new(self, user, nil, privilege, options[:context])
172
+ matching_auth_rules(roles, privileges, options[:context]).collect do |rule|
173
+ rule.obligations(attr_validator)
174
+ end.flatten
175
+ end
176
+
177
+ # Returns the description for the given role. The description may be
178
+ # specified with the authorization rules. Returns +nil+ if none was
179
+ # given.
180
+ def description_for (role)
181
+ role_descriptions[role]
182
+ end
183
+
184
+ # Returns the title for the given role. The title may be
185
+ # specified with the authorization rules. Returns +nil+ if none was
186
+ # given.
187
+ def title_for (role)
188
+ role_titles[role]
189
+ end
190
+
191
+ # Returns the role symbols of the given user.
192
+ def roles_for (user)
193
+ user ||= Authorization.current_user
194
+ raise AuthorizationUsageError, "User object doesn't respond to roles (#{user.inspect})" \
195
+ if !user.respond_to?(:role_symbols) and !user.respond_to?(:roles)
196
+
197
+ Rails.logger.info("The use of user.roles is deprecated. Please add a method " +
198
+ "role_symbols to your User model.") if defined?(Rails) and Rails.respond_to?(:logger) and !user.respond_to?(:role_symbols)
199
+
200
+ roles = user.respond_to?(:role_symbols) ? user.role_symbols : user.roles
201
+
202
+ raise AuthorizationUsageError, "User.#{user.respond_to?(:role_symbols) ? 'role_symbols' : 'roles'} " +
203
+ "doesn't return an Array of Symbols (#{roles.inspect})" \
204
+ if !roles.is_a?(Array) or (!roles.empty? and !roles[0].is_a?(Symbol))
205
+
206
+ (roles.empty? ? [Authorization.default_role] : roles)
207
+ end
208
+
209
+ # Returns the role symbols and inherritted role symbols for the given user
210
+ def roles_with_hierarchy_for(user)
211
+ flatten_roles(roles_for(user))
212
+ end
213
+
214
+ def self.development_reload?
215
+ if Rails.env.development?
216
+ mod_time = AUTH_DSL_FILES.map { |m| File.mtime(m) rescue Time.at(0) }.flatten.max
217
+ @@auth_dsl_last_modified ||= mod_time
218
+ if mod_time > @@auth_dsl_last_modified
219
+ @@auth_dsl_last_modified = mod_time
220
+ return true
221
+ end
222
+ end
223
+ end
224
+
225
+ # Returns an instance of Engine, which is created if there isn't one
226
+ # yet. If +dsl_file+ is given, it is passed on to Engine.new and
227
+ # a new instance is always created.
228
+ def self.instance (dsl_file = nil)
229
+ if dsl_file or development_reload?
230
+ @@instance = new(dsl_file)
231
+ else
232
+ @@instance ||= new
233
+ end
234
+ end
235
+
236
+ class AttributeValidator # :nodoc:
237
+ attr_reader :user, :object, :engine, :context, :privilege
238
+ def initialize (engine, user, object = nil, privilege = nil, context = nil)
239
+ @engine = engine
240
+ @user = user
241
+ @object = object
242
+ @privilege = privilege
243
+ @context = context
244
+ end
245
+
246
+ def evaluate (value_block)
247
+ # TODO cache?
248
+ instance_eval(&value_block)
249
+ end
250
+ end
251
+
252
+ private
253
+ def user_roles_privleges_from_options(privilege, options)
254
+ options = {
255
+ :user => nil,
256
+ :context => nil,
257
+ :user_roles => nil
258
+ }.merge(options)
259
+ user = options[:user] || Authorization.current_user
260
+ privileges = privilege.is_a?(Array) ? privilege : [privilege]
261
+
262
+ raise AuthorizationUsageError, "No user object given (#{user.inspect}) or " +
263
+ "set through Authorization.current_user" unless user
264
+
265
+ roles = options[:user_roles] || flatten_roles(roles_for(user))
266
+ privileges = flatten_privileges privileges, options[:context]
267
+ [user, roles, privileges]
268
+ end
269
+
270
+ def flatten_roles (roles, flattened_roles = Set.new)
271
+ # TODO caching?
272
+ roles.reject {|role| flattened_roles.include?(role)}.each do |role|
273
+ flattened_roles << role
274
+ flatten_roles(role_hierarchy[role], flattened_roles) if role_hierarchy[role]
275
+ end
276
+ flattened_roles.to_a
277
+ end
278
+
279
+ # Returns the privilege hierarchy flattened for given privileges in context.
280
+ def flatten_privileges (privileges, context = nil, flattened_privileges = Set.new)
281
+ # TODO caching?
282
+ raise AuthorizationUsageError, "No context given or inferable from object" unless context
283
+ privileges.reject {|priv| flattened_privileges.include?(priv)}.each do |priv|
284
+ flattened_privileges << priv
285
+ flatten_privileges(rev_priv_hierarchy[[priv, nil]], context, flattened_privileges) if rev_priv_hierarchy[[priv, nil]]
286
+ flatten_privileges(rev_priv_hierarchy[[priv, context]], context, flattened_privileges) if rev_priv_hierarchy[[priv, context]]
287
+ end
288
+ flattened_privileges.to_a
289
+ end
290
+
291
+ def matching_auth_rules (roles, privileges, context)
292
+ auth_rules.matching(roles, privileges, context)
293
+ end
294
+ end
295
+ end
296
+ end