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