authoreyes 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,18 @@
1
+ # Authoreyes::Parser
2
+ require 'authoreyes/parser/priveleges_reader'
3
+ require 'authoreyes/parser/authorization_rules_parser'
4
+ require 'authoreyes/parser/dsl_parser'
5
+ require 'authoreyes/authorization'
6
+
7
+ module Authoreyes
8
+ # Parses an authorization configuration file in the authorization DSL and
9
+ # constructs a data model of its contents.
10
+ module Parser
11
+ # Signals that the specified file to load was not found.
12
+ class DSLFileNotFoundError < Exception; end
13
+ # Signals errors that occur while reading and parsing an authorization DSL
14
+ class DSLError < Exception; end
15
+ # Signals errors in the syntax of an authorization DSL.
16
+ class DSLSyntaxError < DSLError; end
17
+ end
18
+ end
@@ -0,0 +1,399 @@
1
+ module Authoreyes
2
+ module Parser
3
+ # For examples and the modeled data model, see the
4
+ # README[link:files/README_rdoc.html].
5
+ #
6
+ # Also, see role definition methods
7
+ # * AuthorizationRulesReader#role,
8
+ # * AuthorizationRulesReader#includes,
9
+ # * AuthorizationRulesReader#title,
10
+ # * AuthorizationRulesReader#description
11
+ #
12
+ # Methods for rule definition in roles
13
+ # * AuthorizationRulesReader#has_permission_on,
14
+ # * AuthorizationRulesReader#to,
15
+ # * AuthorizationRulesReader#if_attribute,
16
+ # * AuthorizationRulesReader#if_permitted_to
17
+ #
18
+ # Methods to be used in if_attribute statements
19
+ # * AuthorizationRulesReader#contains,
20
+ # * AuthorizationRulesReader#does_not_contain,
21
+ # * AuthorizationRulesReader#intersects_with,
22
+ # * AuthorizationRulesReader#is,
23
+ # * AuthorizationRulesReader#is_not,
24
+ # * AuthorizationRulesReader#is_in,
25
+ # * AuthorizationRulesReader#is_not_in,
26
+ # * AuthorizationRulesReader#lt,
27
+ # * AuthorizationRulesReader#lte,
28
+ # * AuthorizationRulesReader#gt,
29
+ # * AuthorizationRulesReader#gte
30
+ #
31
+ # And privilege definition methods
32
+ # * PrivilegesReader#privilege,
33
+ # * PrivilegesReader#includes
34
+ #
35
+ class AuthorizationRulesParser
36
+ attr_reader :roles, :role_hierarchy, :auth_rules,
37
+ :role_descriptions, :role_titles, :omnipotent_roles # :nodoc:
38
+
39
+ def initialize # :nodoc:
40
+ @current_role = nil
41
+ @current_rule = nil
42
+ @roles = []
43
+ @omnipotent_roles = []
44
+ # higher_role => [lower_roles]
45
+ @role_hierarchy = {}
46
+ @role_titles = {}
47
+ @role_descriptions = {}
48
+ @auth_rules = ::Authoreyes::Authorization::AuthorizationRuleSet.new
49
+ end
50
+
51
+ def initialize_copy (from) # :nodoc:
52
+ [
53
+ :roles,
54
+ :role_hierarchy,
55
+ :auth_rules,
56
+ :role_descriptions,
57
+ :role_titles,
58
+ :omnipotent_roles
59
+ ].each do |attribute|
60
+ instance_variable_set(:"@#{attribute}", from.send(attribute).clone)
61
+ end
62
+ end
63
+
64
+ def append_role (role, options = {}) # :nodoc:
65
+ @roles << role unless @roles.include? role
66
+ @role_titles[role] = options[:title] if options[:title]
67
+ @role_descriptions[role] =
68
+ options[:description] if options[:description]
69
+ end
70
+
71
+ # Defines the authorization rules for the given +role+ in the
72
+ # following block.
73
+ # role :admin do
74
+ # has_permissions_on ...
75
+ # end
76
+ #
77
+ def role (role, options = {}, &block)
78
+ append_role role, options
79
+ @current_role = role
80
+ yield
81
+ ensure
82
+ @current_role = nil
83
+ end
84
+
85
+ # Roles may inherit all the rights from subroles. The given +roles+
86
+ # become subroles of the current block's role.
87
+ # role :admin do
88
+ # includes :user
89
+ # has_permission_on :employees, :to => [:update, :create]
90
+ # end
91
+ # role :user do
92
+ # has_permission_on :employees, :to => :read
93
+ # end
94
+ #
95
+ def includes (*roles)
96
+ raise DSLError, "includes only in role blocks" if @current_role.nil?
97
+ @role_hierarchy[@current_role] ||= []
98
+ @role_hierarchy[@current_role] += roles.flatten
99
+ end
100
+
101
+ # Allows the definition of privileges to be allowed for the current role,
102
+ # either in a has_permission_on block or directly in one call.
103
+ # role :admin
104
+ # has_permission_on :employees, :to => :read
105
+ # has_permission_on [:employees, :orders], :to => :read
106
+ # has_permission_on :employees do
107
+ # to :create
108
+ # if_attribute ...
109
+ # end
110
+ # has_permission_on :employees, :to => :delete do
111
+ # if_attribute ...
112
+ # end
113
+ # end
114
+ # The block form allows to describe restrictions on the permissions
115
+ # using if_attribute. Multiple has_permission_on statements are
116
+ # OR'ed when evaluating the permissions. Also, multiple if_attribute
117
+ # statements in one block are OR'ed if no :+join_by+ option is given
118
+ # (see below). To AND conditions, either set :+join_by+ to :and or place
119
+ # them in one if_attribute statement.
120
+ #
121
+ # Available options
122
+ # [:+to+]
123
+ # A symbol or an array of symbols representing the privileges that
124
+ # should be granted in this statement.
125
+ # [:+join_by+]
126
+ # Join operator to logically connect the constraint statements inside
127
+ # of the has_permission_on block. May be :+and+ or :+or+.
128
+ # Defaults to :+or+.
129
+ #
130
+ def has_permission_on (*args, &block)
131
+ options = args.extract_options!
132
+ context = args.flatten
133
+
134
+ raise DSLError, "has_permission_on only allowed in role blocks" if @current_role.nil?
135
+ options = {:to => [], :join_by => :or}.merge(options)
136
+
137
+ privs = options[:to]
138
+ privs = [privs] unless privs.is_a?(Array)
139
+ raise DSLError, "has_permission_on either needs a block or :to option" if !block_given? and privs.empty?
140
+
141
+ file, line = file_and_line_number_from_call_stack
142
+ rule = ::Authoreyes::Authorization::AuthorizationRule.new(@current_role, privs, context, options[:join_by],
143
+ :source_file => file, :source_line => line)
144
+ @auth_rules << rule
145
+ if block_given?
146
+ @current_rule = rule
147
+ yield
148
+ raise DSLError, "has_permission_on block
149
+ content specifies no privileges" if rule.privileges.empty?
150
+ # TODO ensure?
151
+ @current_rule = nil
152
+ end
153
+ end
154
+
155
+ # Removes any permission checks for the current role.
156
+ # role :admin
157
+ # has_omnipotence
158
+ # end
159
+ def has_omnipotence
160
+ raise DSLError, "has_omnipotence only allowed in role blocks" if @current_role.nil?
161
+ @omnipotent_roles << @current_role
162
+ end
163
+
164
+ # Sets a description for the current role. E.g.
165
+ # role :admin
166
+ # description "To be assigned to administrative personnel"
167
+ # has_permission_on ...
168
+ # end
169
+ def description (text)
170
+ raise DSLError, "description only allowed in role blocks" if @current_role.nil?
171
+ role_descriptions[@current_role] = text
172
+ end
173
+
174
+ # Sets a human-readable title for the current role. E.g.
175
+ # role :admin
176
+ # title "Administrator"
177
+ # has_permission_on ...
178
+ # end
179
+ def title (text)
180
+ raise DSLError, "title only allowed in role blocks" if @current_role.nil?
181
+ role_titles[@current_role] = text
182
+ end
183
+
184
+ # Used in a has_permission_on block, to may be used to specify privileges
185
+ # to be assigned to the current role under the conditions specified in
186
+ # the current block.
187
+ # role :admin
188
+ # has_permission_on :employees do
189
+ # to :create, :read, :update, :delete
190
+ # end
191
+ # end
192
+ def to (*privs)
193
+ raise DSLError, "to only allowed in has_permission_on blocks" if @current_rule.nil?
194
+ @current_rule.append_privileges(privs.flatten)
195
+ end
196
+
197
+ # In a has_permission_on block, if_attribute specifies conditions
198
+ # of dynamic parameters that have to be met for the user to meet the
199
+ # privileges in this block. Conditions are evaluated on the context
200
+ # object. Thus, the following allows CRUD for branch admins only on
201
+ # employees that belong to the same branch as the current user.
202
+ # role :branch_admin
203
+ # has_permission_on :employees do
204
+ # to :create, :read, :update, :delete
205
+ # if_attribute :branch => is { user.branch }
206
+ # end
207
+ # end
208
+ # In this case, is is the operator for evaluating the condition. Another
209
+ # operator is contains for collections. In the block supplied to the
210
+ # operator, +user+ specifies the current user for whom the condition
211
+ # is evaluated.
212
+ #
213
+ # Conditions may be nested:
214
+ # role :company_admin
215
+ # has_permission_on :employees do
216
+ # to :create, :read, :update, :delete
217
+ # if_attribute :branch => { :company => is {user.branch.company} }
218
+ # end
219
+ # end
220
+ #
221
+ # has_many and has_many through associations may also be nested.
222
+ # Then, at least one item in the association needs to fulfill the
223
+ # subsequent condition:
224
+ # if_attribute :company => { :branches => { :manager => { :last_name => is { user.last_name } } }
225
+ # Beware of possible performance issues when using has_many associations in
226
+ # permitted_to? checks. For
227
+ # permitted_to? :read, object
228
+ # a check like
229
+ # object.company.branches.any? { |branch| branch.manager ... }
230
+ # will be executed. with_permission_to scopes construct efficient SQL
231
+ # joins, though.
232
+ #
233
+ # Multiple attributes in one :if_attribute statement are AND'ed.
234
+ # Multiple if_attribute statements are OR'ed if the join operator for the
235
+ # has_permission_on block isn't explicitly set. Thus, the following would
236
+ # require the current user either to be of the same branch AND the employee
237
+ # to be "changeable_by_coworker". OR the current user has to be the
238
+ # employee in question.
239
+ # has_permission_on :employees, :to => :manage do
240
+ # if_attribute :branch => is {user.branch}, :changeable_by_coworker => true
241
+ # if_attribute :id => is {user.id}
242
+ # end
243
+ # The join operator for if_attribute rules can explicitly set to AND, though.
244
+ # See has_permission_on for details.
245
+ #
246
+ # Arrays and fixed values may be used directly as hash values:
247
+ # if_attribute :id => 1
248
+ # if_attribute :type => "special"
249
+ # if_attribute :id => [1,2]
250
+ #
251
+ def if_attribute (attr_conditions_hash)
252
+ raise DSLError, "if_attribute only in has_permission blocks" if @current_rule.nil?
253
+ parse_attribute_conditions_hash!(attr_conditions_hash)
254
+ @current_rule.append_attribute ::Authoreyes::Authorization::Attribute.new(attr_conditions_hash)
255
+ end
256
+
257
+ # if_permitted_to allows the has_permission_on block to depend on
258
+ # permissions on associated objects. By using it, the authorization
259
+ # rules may be a lot DRYer. E.g.:
260
+ #
261
+ # role :branch_manager
262
+ # has_permission_on :branches, :to => :manage do
263
+ # if_attribute :employees => contains { user }
264
+ # end
265
+ # has_permission_on :employees, :to => :read do
266
+ # if_permitted_to :read, :branch
267
+ # # instead of
268
+ # # if_attribute :branch => { :employees => contains { user } }
269
+ # end
270
+ # end
271
+ #
272
+ # if_permitted_to associations may be nested as well:
273
+ # if_permitted_to :read, :branch => :company
274
+ #
275
+ # You can even use has_many associations as target. Then, it is checked
276
+ # if the current user has the required privilege on *any* of the target objects.
277
+ # if_permitted_to :read, :branch => :employees
278
+ # Beware of performance issues with permission checks. In the current implementation,
279
+ # all employees are checked until the first permitted is found.
280
+ # with_permissions_to, on the other hand, constructs more efficient SQL
281
+ # instead.
282
+ #
283
+ # To check permissions based on the current object, the attribute has to
284
+ # be left out:
285
+ # has_permission_on :branches, :to => :manage do
286
+ # if_attribute :employees => contains { user }
287
+ # end
288
+ # has_permission_on :branches, :to => :paint_green do
289
+ # if_permitted_to :update
290
+ # end
291
+ # Normally, one would merge those rules into one. Dividing makes sense
292
+ # if additional if_attribute are used in the second rule or those rules
293
+ # are applied to different roles.
294
+ #
295
+ # Options:
296
+ # [:+context+]
297
+ # When using with_permissions_to, the target context of the if_permitted_to
298
+ # statement is inferred from the last reflections target class. Still,
299
+ # you may override this algorithm by setting the context explicitly.
300
+ # if_permitted_to :read, :home_branch, :context => :branches
301
+ # if_permitted_to :read, :branch => :main_company, :context => :companies
302
+ #
303
+ def if_permitted_to (privilege, attr_or_hash = nil, options = {})
304
+ raise DSLError, "if_permitted_to only in has_permission blocks" if @current_rule.nil?
305
+ options[:context] ||= attr_or_hash.delete(:context) if attr_or_hash.is_a?(Hash)
306
+ # only :context option in attr_or_hash:
307
+ attr_or_hash = nil if attr_or_hash.is_a?(Hash) and attr_or_hash.empty?
308
+ @current_rule.append_attribute ::Authoreyes::Authorization::AttributeWithPermission.new(privilege,
309
+ attr_or_hash, options[:context])
310
+ end
311
+
312
+ # In an if_attribute statement, is says that the value has to be
313
+ # met exactly by the if_attribute attribute. For information on the block
314
+ # argument, see if_attribute.
315
+ def is (&block)
316
+ [:is, block]
317
+ end
318
+
319
+ # The negation of is.
320
+ def is_not (&block)
321
+ [:is_not, block]
322
+ end
323
+
324
+ # In an if_attribute statement, contains says that the value has to be
325
+ # part of the collection specified by the if_attribute attribute.
326
+ # For information on the block argument, see if_attribute.
327
+ def contains (&block)
328
+ [:contains, block]
329
+ end
330
+
331
+ # The negation of contains. Currently, query rewriting is disabled
332
+ # for does_not_contain.
333
+ def does_not_contain (&block)
334
+ [:does_not_contain, block]
335
+ end
336
+
337
+ # In an if_attribute statement, intersects_with requires that at least
338
+ # one of the values has to be part of the collection specified by the
339
+ # if_attribute attribute. The value block needs to evaluate to an
340
+ # Enumerable. For information on the block argument, see if_attribute.
341
+ def intersects_with (&block)
342
+ [:intersects_with, block]
343
+ end
344
+
345
+ # In an if_attribute statement, is_in says that the value has to
346
+ # contain the attribute value.
347
+ # For information on the block argument, see if_attribute.
348
+ def is_in (&block)
349
+ [:is_in, block]
350
+ end
351
+
352
+ # The negation of is_in.
353
+ def is_not_in (&block)
354
+ [:is_not_in, block]
355
+ end
356
+
357
+ # Less than
358
+ def lt (&block)
359
+ [:lt, block]
360
+ end
361
+
362
+ # Less than or equal to
363
+ def lte (&block)
364
+ [:lte, block]
365
+ end
366
+
367
+ # Greater than
368
+ def gt (&block)
369
+ [:gt, block]
370
+ end
371
+
372
+ # Greater than or equal to
373
+ def gte (&block)
374
+ [:gte, block]
375
+ end
376
+
377
+ private
378
+ def parse_attribute_conditions_hash! (hash)
379
+ merge_hash = {}
380
+ hash.each do |key, value|
381
+ if value.is_a?(Hash)
382
+ parse_attribute_conditions_hash!(value)
383
+ elsif !value.is_a?(Array)
384
+ merge_hash[key] = [:is, proc { value }]
385
+ elsif value.is_a?(Array) and !value[0].is_a?(Symbol)
386
+ merge_hash[key] = [:is_in, proc { value }]
387
+ end
388
+ end
389
+ hash.merge!(merge_hash)
390
+ end
391
+
392
+ def file_and_line_number_from_call_stack
393
+ caller_parts = caller(2).first.split(':')
394
+ [caller_parts[0] == "(eval)" ? nil : caller_parts[0],
395
+ caller_parts[1] && caller_parts[1].to_i]
396
+ end
397
+ end
398
+ end
399
+ end
@@ -0,0 +1,91 @@
1
+ module Authoreyes
2
+ module Parser
3
+ # Top-level reader, parses the methods +privileges+ and +authorization+.
4
+ # +authorization+ takes a block with authorization rules as described in
5
+ # AuthorizationRulesReader. The block to +privileges+ defines privilege
6
+ # hierarchies, as described in PrivilegesReader.
7
+ #
8
+ class DSLParser
9
+ attr_reader :privileges_reader, :auth_rules_reader # :nodoc:
10
+
11
+ def initialize
12
+ @privileges_reader = PrivilegesReader.new
13
+ @auth_rules_reader = AuthorizationRulesParser.new
14
+ end
15
+
16
+ def initialize_copy (from) # :nodoc:
17
+ @privileges_reader = from.privileges_reader.clone
18
+ @auth_rules_reader = from.auth_rules_reader.clone
19
+ end
20
+
21
+ # ensures you get back a DSLReader
22
+ # if you provide a:
23
+ # DSLReader - you will get it back.
24
+ # String or Array - it will treat it as if you have passed a path
25
+ # or an array of paths and attempt to load those.
26
+ def self.factory(obj)
27
+ case obj
28
+ when Parser::DSLParser
29
+ obj
30
+ when String, Array
31
+ load(obj)
32
+ end
33
+ end
34
+
35
+ # Parses an authorization DSL specification from the string given
36
+ # in +dsl_data+. Raises DSLSyntaxError if errors occur on parsing.
37
+ def parse(dsl_data, file_name = nil)
38
+ if file_name
39
+ DSLMethods.new(self).instance_eval(dsl_data, file_name)
40
+ else
41
+ DSLMethods.new(self).instance_eval(dsl_data)
42
+ end
43
+ rescue SyntaxError, NoMethodError, NameError => e
44
+ raise DSLSyntaxError, "Illegal DSL syntax: #{e}"
45
+ end
46
+
47
+ # Load and parse a DSL from the given file name.
48
+ def load(dsl_file)
49
+ parse(File.read(dsl_file), dsl_file) if File.exist?(dsl_file)
50
+ end
51
+
52
+ # Load and parse a DSL from the given file name. Raises
53
+ # Authorization::Reader::DSLFileNotFoundError
54
+ # if the file cannot be found.
55
+ def load!(dsl_file)
56
+ raise ::Authoreyes::Parser::DSLFileNotFoundError, "Error reading authorization rules file with path '#{dsl_file}'! Please ensure it exists and that it is accessible." unless File.exist?(dsl_file)
57
+ load(dsl_file)
58
+ end
59
+
60
+ # Loads and parses DSL files and returns a new reader
61
+ def self.load(dsl_files)
62
+ # TODO cache reader in production mode?
63
+ reader = new
64
+ dsl_files = [dsl_files].flatten
65
+ dsl_files.each do |file|
66
+ reader.load(file)
67
+ end
68
+ reader
69
+ end
70
+
71
+ # DSL methods
72
+ class DSLMethods # :nodoc:
73
+ def initialize(parent)
74
+ @parent = parent
75
+ end
76
+
77
+ def privileges(&block)
78
+ @parent.privileges_reader.instance_eval(&block)
79
+ end
80
+
81
+ def contexts(&block)
82
+ # Not implemented
83
+ end
84
+
85
+ def authorization(&block)
86
+ @parent.auth_rules_reader.instance_eval(&block)
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end