authorization-rails 1.0.12

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,41 @@
1
+ module Authorization #:nodoc:
2
+
3
+ # Base error class for Authorization module
4
+ class AuthorizationError < StandardError
5
+ end
6
+
7
+ # Raised when the authorization expression is invalid (cannot be parsed)
8
+ class AuthorizationExpressionInvalid < AuthorizationError
9
+ end
10
+
11
+ # Raised when we can't find the current user
12
+ class CannotObtainUserObject < AuthorizationError
13
+ end
14
+
15
+ # Raised when an authorization expression contains a model class that doesn't exist
16
+ class CannotObtainModelClass < AuthorizationError
17
+ end
18
+
19
+ # Raised when an authorization expression contains a model reference that doesn't exist
20
+ class CannotObtainModelObject < AuthorizationError
21
+ end
22
+
23
+ # Raised when the obtained user object doesn't implement #id
24
+ class UserDoesntImplementID < AuthorizationError
25
+ end
26
+
27
+ # Raised when the obtained user object doesn't implement #has_role?
28
+ class UserDoesntImplementRoles < AuthorizationError
29
+ end
30
+
31
+ # Raised when the obtained model doesn't implement #accepts_role?
32
+ class ModelDoesntImplementRoles < AuthorizationError
33
+ end
34
+
35
+ class CannotSetRoleWhenHardwired < AuthorizationError
36
+ end
37
+
38
+ class CannotSetObjectRoleWhenSimpleRoleTable < AuthorizationError
39
+ end
40
+
41
+ end
@@ -0,0 +1,82 @@
1
+ require File.dirname(__FILE__) + '/exceptions'
2
+
3
+ # In order to use this mixin, you'll need to define roles by overriding the
4
+ # following functions:
5
+ #
6
+ # User#has_role?(role)
7
+ # Return true or false depending on the roles (strings) passed in.
8
+ #
9
+ # Model#accepts_role?(role, user)
10
+ # Return true or false depending on the roles (strings) this particular user has for
11
+ # this particular model object.
12
+ #
13
+ # See http://www.writertopia.com/developers/authorization
14
+
15
+ module Authorization
16
+ module HardwiredRoles
17
+
18
+ module UserExtensions
19
+ def self.included( recipient )
20
+ recipient.extend( ClassMethods )
21
+ end
22
+
23
+ module ClassMethods
24
+ def acts_as_authorized_user
25
+ include Authorization::HardwiredRoles::UserExtensions::InstanceMethods
26
+ end
27
+ end
28
+
29
+ module InstanceMethods
30
+ # If roles aren't explicitly defined in user class then return false
31
+ def has_role?( role, authorizable_object = nil )
32
+ false
33
+ end
34
+
35
+ def has_role( role, authorizable_object = nil )
36
+ raise( CannotSetRoleWhenHardwired,
37
+ "Hardwired mixin: Cannot set user to role #{role}. Don't use #has_role, use code in models."
38
+ )
39
+ end
40
+
41
+ def has_no_role( role, authorizable_object = nil )
42
+ raise( CannotSetRoleWhenHardwired,
43
+ "Hardwired mixin: Cannot remove user role #{role}. Don't use #has_no_role, use code in models."
44
+ )
45
+ end
46
+ end
47
+ end
48
+
49
+ module ModelExtensions
50
+ def self.included( recipient )
51
+ recipient.extend( ClassMethods )
52
+ end
53
+
54
+ module ClassMethods
55
+ def acts_as_authorizable
56
+ include Authorization::HardwiredRoles::ModelExtensions::InstanceMethods
57
+ end
58
+ end
59
+
60
+ module InstanceMethods
61
+ def accepts_role?( role, user )
62
+ return false
63
+ end
64
+
65
+ def accepts_role( role, user )
66
+ raise( CannotSetRoleWhenHardwired,
67
+ "Hardwired mixin: Cannot set user to role #{role}. Don't use #accepts_role, use code in models."
68
+ )
69
+ end
70
+
71
+ def accepts_no_role( role, user )
72
+ raise( CannotSetRoleWhenHardwired,
73
+ "Hardwired mixin: Cannot set user to role #{role}. Don't use #accepts_no_role, use code in models."
74
+ )
75
+ end
76
+ end
77
+ end
78
+
79
+ end
80
+
81
+ end
82
+
@@ -0,0 +1,119 @@
1
+ require File.dirname(__FILE__) + '/exceptions'
2
+
3
+ # Provides the appearance of dynamically generated methods on the roles database.
4
+ #
5
+ # Examples:
6
+ # user.is_member? --> Returns true if user has any role of "member"
7
+ # user.is_member_of? this_workshop --> Returns true/false. Must have authorizable object after query.
8
+ # user.is_eligible_for [this_award] --> Gives user the role "eligible" for "this_award"
9
+ # user.is_moderator --> Gives user the general role "moderator" (not tied to any class or object)
10
+ # user.is_candidate_of_what --> Returns array of objects for which this user is a "candidate" (any type)
11
+ # user.is_candidate_of_what(Party) --> Returns array of objects for which this user is a "candidate" (only 'Party' type)
12
+ #
13
+ # model.has_members --> Returns array of users which have role "member" on that model
14
+ # model.has_members? --> Returns true/false
15
+ #
16
+ module Authorization
17
+ module Identity
18
+
19
+ module UserExtensions
20
+ module InstanceMethods
21
+
22
+ def method_missing( method_sym, *args )
23
+ method_name = method_sym.to_s
24
+ authorizable_object = args.empty? ? nil : args[0]
25
+
26
+ base_regex = "is_(\\w+)"
27
+ fancy_regex = base_regex + "_(#{Authorization::Base::VALID_PREPOSITIONS_PATTERN})"
28
+ is_either_regex = '^((' + fancy_regex + ')|(' + base_regex + '))'
29
+ base_not_regex = "is_no[t]?_(\\w+)"
30
+ fancy_not_regex = base_not_regex + "_(#{Authorization::Base::VALID_PREPOSITIONS_PATTERN})"
31
+ is_not_either_regex = '^((' + fancy_not_regex + ')|(' + base_not_regex + '))'
32
+
33
+ if method_name =~ Regexp.new(is_either_regex + '_what$')
34
+ role_name = $3 || $6
35
+ has_role_for_objects(role_name, authorizable_object)
36
+ elsif method_name =~ Regexp.new(is_not_either_regex + '\?$')
37
+ role_name = $3 || $6
38
+ not is_role?( role_name, authorizable_object )
39
+ elsif method_name =~ Regexp.new(is_either_regex + '\?$')
40
+ role_name = $3 || $6
41
+ is_role?( role_name, authorizable_object )
42
+ elsif method_name =~ Regexp.new(is_not_either_regex + '$')
43
+ role_name = $3 || $6
44
+ is_no_role( role_name, authorizable_object )
45
+ elsif method_name =~ Regexp.new(is_either_regex + '$')
46
+ role_name = $3 || $6
47
+ is_role( role_name, authorizable_object )
48
+ else
49
+ super
50
+ end
51
+ end
52
+
53
+ private
54
+
55
+ def is_role?( role_name, authorizable_object )
56
+ if authorizable_object.nil?
57
+ return self.has_role?(role_name)
58
+ elsif authorizable_object.respond_to?(:accepts_role?)
59
+ return self.has_role?(role_name, authorizable_object)
60
+ end
61
+ false
62
+ end
63
+
64
+ def is_no_role( role_name, authorizable_object = nil )
65
+ if authorizable_object.nil?
66
+ self.has_no_role role_name
67
+ else
68
+ self.has_no_role role_name, authorizable_object
69
+ end
70
+ end
71
+
72
+ def is_role( role_name, authorizable_object = nil )
73
+ if authorizable_object.nil?
74
+ self.has_role role_name
75
+ else
76
+ self.has_role role_name, authorizable_object
77
+ end
78
+ end
79
+
80
+ def has_role_for_objects(role_name, type)
81
+ if type.nil?
82
+ roles = self.roles.find_all_by_name( role_name )
83
+ else
84
+ roles = self.roles.find_all_by_authorizable_type_and_name( type.name, role_name )
85
+ end
86
+ roles.collect do |role|
87
+ if role.authorizable_id.nil?
88
+ role.authorizable_type.nil? ?
89
+ nil : Module.const_get( role.authorizable_type ) # Returns class
90
+ else
91
+ role.authorizable
92
+ end
93
+ end
94
+ end
95
+ end
96
+ end
97
+
98
+ module ModelExtensions
99
+ module InstanceMethods
100
+
101
+ def method_missing( method_sym, *args )
102
+ method_name = method_sym.to_s
103
+ if method_name =~ /^has_(\w+)\?$/
104
+ role_name = $1.singularize
105
+ self.accepted_roles.find_all_by_name(role_name).any? { |role| role.users.any? }
106
+ elsif method_name =~ /^has_(\w+)$/
107
+ role_name = $1.singularize
108
+ users = self.accepted_roles.find_all_by_name(role_name).collect { |role| role.users }
109
+ users.flatten.uniq if users
110
+ else
111
+ super
112
+ end
113
+ end
114
+
115
+ end
116
+ end
117
+
118
+ end
119
+ end
@@ -0,0 +1,119 @@
1
+ require File.dirname(__FILE__) + '/exceptions'
2
+ require File.dirname(__FILE__) + '/identity'
3
+
4
+ module Authorization
5
+ module ObjectRolesTable
6
+
7
+ module UserExtensions
8
+ def self.included( recipient )
9
+ recipient.extend( ClassMethods )
10
+ end
11
+
12
+ module ClassMethods
13
+ def acts_as_authorized_user(roles_relationship_opts = {})
14
+ has_and_belongs_to_many :roles, roles_relationship_opts
15
+ include Authorization::ObjectRolesTable::UserExtensions::InstanceMethods
16
+ include Authorization::Identity::UserExtensions::InstanceMethods # Provides all kinds of dynamic sugar via method_missing
17
+ end
18
+ end
19
+
20
+ module InstanceMethods
21
+ def has_role?( role_name, authorizable_obj = nil )
22
+ if authorizable_obj.nil?
23
+ case role_name
24
+ when String then self.roles.detect { |role| role.name == role_name } ? true : false
25
+ when Array then role_name.inject(false) { |memo,role| memo ? memo : has_role?(role) }
26
+ else false
27
+ end
28
+ else
29
+ role = get_role( role_name, authorizable_obj )
30
+ role ? self.roles.exists?( role.id ) : false
31
+ end
32
+ end
33
+
34
+ def has_role( role_name, authorizable_obj = nil )
35
+ role = get_role( role_name, authorizable_obj )
36
+ if role.nil?
37
+ if authorizable_obj.is_a? Class
38
+ role = Role.create( :name => role_name, :authorizable_type => authorizable_obj.to_s )
39
+ elsif authorizable_obj
40
+ role = Role.create( :name => role_name, :authorizable => authorizable_obj )
41
+ else
42
+ role = Role.create( :name => role_name )
43
+ end
44
+ end
45
+ self.roles << role if role and not self.roles.exists?( role.id )
46
+ end
47
+
48
+ def has_no_role( role_name, authorizable_obj = nil )
49
+ role = get_role( role_name, authorizable_obj )
50
+ if role
51
+ self.roles.delete( role )
52
+ role.destroy if role.users.empty?
53
+ end
54
+ end
55
+
56
+ private
57
+
58
+ def get_role( role_name, authorizable_obj )
59
+ if authorizable_obj.is_a? Class
60
+ Role.find( :first,
61
+ :conditions => [ 'name = ? and authorizable_type = ? and authorizable_id IS NULL', role_name, authorizable_obj.to_s ] )
62
+ elsif authorizable_obj
63
+ Role.find( :first,
64
+ :conditions => [ 'name = ? and authorizable_type = ? and authorizable_id = ?',
65
+ role_name, authorizable_obj.class.base_class.to_s, authorizable_obj.id ] )
66
+ else
67
+ Role.find( :first,
68
+ :conditions => [ 'name = ? and authorizable_type IS NULL and authorizable_id IS NULL', role_name ] )
69
+ end
70
+ end
71
+
72
+ end
73
+ end
74
+
75
+ module ModelExtensions
76
+ def self.included( recipient )
77
+ recipient.extend( ClassMethods )
78
+ end
79
+
80
+ module ClassMethods
81
+ def acts_as_authorizable
82
+ has_many :accepted_roles, :as => :authorizable, :class_name => 'Role'
83
+
84
+ def accepts_role?( role_name, user )
85
+ user.has_role? role_name, self
86
+ end
87
+
88
+ def accepts_role( role_name, user )
89
+ user.has_role role_name, self
90
+ end
91
+
92
+ def accepts_no_role( role_name, user )
93
+ user.has_no_role role_name, self
94
+ end
95
+
96
+ include Authorization::ObjectRolesTable::ModelExtensions::InstanceMethods
97
+ include Authorization::Identity::ModelExtensions::InstanceMethods # Provides all kinds of dynamic sugar via method_missing
98
+ end
99
+ end
100
+
101
+ module InstanceMethods
102
+ # If roles aren't overriden in model then check roles table
103
+ def accepts_role?( role_name, user )
104
+ user.has_role? role_name, self
105
+ end
106
+
107
+ def accepts_role( role_name, user )
108
+ user.has_role role_name, self
109
+ end
110
+
111
+ def accepts_no_role( role_name, user )
112
+ user.has_no_role role_name, self
113
+ end
114
+ end
115
+ end
116
+
117
+ end
118
+ end
119
+
@@ -0,0 +1,210 @@
1
+ module Authorization
2
+ module Base
3
+
4
+ VALID_PREPOSITIONS = ['of', 'for', 'in', 'on', 'to', 'at', 'by']
5
+ BOOLEAN_OPS = ['not', 'or', 'and']
6
+ VALID_PREPOSITIONS_PATTERN = VALID_PREPOSITIONS.join('|')
7
+
8
+ module EvalParser
9
+ # Parses and evaluates an authorization expression and returns <tt>true</tt> or <tt>false</tt>.
10
+ #
11
+ # The authorization expression is defined by the following grammar:
12
+ # <expr> ::= (<expr>) | not <expr> | <term> or <expr> | <term> and <expr> | <term>
13
+ # <term> ::= <role> | <role> <preposition> <model>
14
+ # <preposition> ::= of | for | in | on | to | at | by
15
+ # <model> ::= /:*\w+/
16
+ # <role> ::= /\w+/ | /'.*'/
17
+ #
18
+ # Instead of doing recursive descent parsing (not so fun when we support nested parentheses, etc),
19
+ # we let Ruby do the work for us by inserting the appropriate permission calls and using eval.
20
+ # This would not be a good idea if you were getting authorization expressions from the outside,
21
+ # so in that case (e.g. somehow letting users literally type in permission expressions) you'd
22
+ # be better off using the recursive descent parser in Module RecursiveDescentParser.
23
+ #
24
+ # We search for parts of our authorization evaluation that match <role> or <role> <preposition> <model>
25
+ # and we ignore anything terminal in our grammar.
26
+ #
27
+ # 1) Replace all <role> <preposition> <model> matches.
28
+ # 2) Replace all <role> matches that aren't one of our other terminals ('not', 'or', 'and', or preposition)
29
+ # 3) Eval
30
+
31
+ def parse_authorization_expression( str )
32
+ if str =~ /[^A-Za-z0-9_:'\(\)\s]/
33
+ raise AuthorizationExpressionInvalid, "Invalid authorization expression (#{str})"
34
+ return false
35
+ end
36
+ @replacements = []
37
+ expr = replace_temporarily_role_of_model( str )
38
+ expr = replace_role( expr )
39
+ expr = replace_role_of_model( expr )
40
+ begin
41
+ instance_eval( expr )
42
+ rescue
43
+ raise AuthorizationExpressionInvalid, "Cannot parse authorization (#{str})"
44
+ end
45
+ end
46
+
47
+ def replace_temporarily_role_of_model( str )
48
+ role_regex = '\s*(\'\s*(.+?)\s*\'|(\w+))\s+'
49
+ model_regex = '\s+(:*\w+)'
50
+ parse_regex = Regexp.new(role_regex + '(' + VALID_PREPOSITIONS.join('|') + ')' + model_regex)
51
+ str.gsub(parse_regex) do |match|
52
+ @replacements.push " process_role_of_model('#{$2 || $3}', '#{$5}') "
53
+ " <#{@replacements.length-1}> "
54
+ end
55
+ end
56
+
57
+ def replace_role( str )
58
+ role_regex = '\s*(\'\s*(.+?)\s*\'|([A-Za-z]\w*))\s*'
59
+ parse_regex = Regexp.new(role_regex)
60
+ str.gsub(parse_regex) do |match|
61
+ if BOOLEAN_OPS.include?($3)
62
+ " #{match} "
63
+ else
64
+ " process_role('#{$2 || $3}') "
65
+ end
66
+ end
67
+ end
68
+
69
+ def replace_role_of_model( str )
70
+ str.gsub(/<(\d+)>/) do |match|
71
+ @replacements[$1.to_i]
72
+ end
73
+ end
74
+
75
+ def process_role_of_model( role_name, model_name )
76
+ model = get_model( model_name )
77
+ raise( ModelDoesntImplementRoles, "Model (#{model_name}) doesn't implement #accepts_role?" ) if not model.respond_to? :accepts_role?
78
+ model.send( :accepts_role?, role_name, @current_user )
79
+ end
80
+
81
+ def process_role( role_name )
82
+ return false if @current_user.nil? || @current_user == :false
83
+ raise( UserDoesntImplementRoles, "User doesn't implement #has_role?" ) if not @current_user.respond_to? :has_role?
84
+ @current_user.has_role?( role_name )
85
+ end
86
+
87
+ end
88
+
89
+ # Parses and evaluates an authorization expression and returns <tt>true</tt> or <tt>false</tt>.
90
+ # This recursive descent parses uses two instance variables:
91
+ # @stack --> a stack with the top holding the boolean expression resulting from the parsing
92
+ #
93
+ # The authorization expression is defined by the following grammar:
94
+ # <expr> ::= (<expr>) | not <expr> | <term> or <expr> | <term> and <expr> | <term>
95
+ # <term> ::= <role> | <role> <preposition> <model>
96
+ # <preposition> ::= of | for | in | on | to | at | by
97
+ # <model> ::= /:*\w+/
98
+ # <role> ::= /\w+/ | /'.*'/
99
+ #
100
+ # There are really two values we must track:
101
+ # (1) whether the expression is valid according to the grammar
102
+ # (2) the evaluated results --> true/false on the permission queries
103
+ # The first is embedded in the control logic because we want short-circuiting. If an expression
104
+ # has been parsed and the permission is false, we don't want to try different ways of parsing.
105
+ # Note that this implementation of a recursive descent parser is meant to be simple
106
+ # and doesn't allow arbitrary nesting of parentheses. It supports up to 5 levels of nesting.
107
+ # It also won't handle some types of expressions (A or B) and C, which has to be rewritten as
108
+ # C and (A or B) so the parenthetical expressions are in the tail.
109
+ module RecursiveDescentParser
110
+
111
+ OPT_PARENTHESES_PATTERN = '(([^()]|\(([^()]|\(([^()]|\(([^()]|\(([^()]|\(([^()])*\))*\))*\))*\))*\))*)'
112
+ PARENTHESES_PATTERN = '\(' + OPT_PARENTHESES_PATTERN + '\)'
113
+ NOT_PATTERN = '^\s*not\s+' + OPT_PARENTHESES_PATTERN + '$'
114
+ AND_PATTERN = '^\s*' + OPT_PARENTHESES_PATTERN + '\s+and\s+' + OPT_PARENTHESES_PATTERN + '\s*$'
115
+ OR_PATTERN = '^\s*' + OPT_PARENTHESES_PATTERN + '\s+or\s+' + OPT_PARENTHESES_PATTERN + '\s*$'
116
+ ROLE_PATTERN = '(\'\s*(.+)\s*\'|(\w+))'
117
+ MODEL_PATTERN = '(:*\w+)'
118
+
119
+ PARENTHESES_REGEX = Regexp.new('^\s*' + PARENTHESES_PATTERN + '\s*$')
120
+ NOT_REGEX = Regexp.new(NOT_PATTERN)
121
+ AND_REGEX = Regexp.new(AND_PATTERN)
122
+ OR_REGEX = Regexp.new(OR_PATTERN)
123
+ ROLE_REGEX = Regexp.new('^\s*' + ROLE_PATTERN + '\s*$')
124
+ ROLE_OF_MODEL_REGEX = Regexp.new('^\s*' + ROLE_PATTERN + '\s+(' + VALID_PREPOSITIONS_PATTERN + ')\s+' + MODEL_PATTERN + '\s*$')
125
+
126
+ def parse_authorization_expression( str )
127
+ @stack = []
128
+ raise AuthorizationExpressionInvalid, "Cannot parse authorization (#{str})" if not parse_expr( str )
129
+ return @stack.pop
130
+ end
131
+
132
+ def parse_expr( str )
133
+ parse_parenthesis( str ) or
134
+ parse_not( str ) or
135
+ parse_or( str ) or
136
+ parse_and( str ) or
137
+ parse_term( str )
138
+ end
139
+
140
+ def parse_not( str )
141
+ if str =~ NOT_REGEX
142
+ can_parse = parse_expr( $1 )
143
+ @stack.push( !@stack.pop ) if can_parse
144
+ end
145
+ false
146
+ end
147
+
148
+ def parse_or( str )
149
+ if str =~ OR_REGEX
150
+ can_parse = parse_expr( $1 ) and parse_expr( $8 )
151
+ @stack.push( @stack.pop | @stack.pop ) if can_parse
152
+ return can_parse
153
+ end
154
+ false
155
+ end
156
+
157
+ def parse_and( str )
158
+ if str =~ AND_REGEX
159
+ can_parse = parse_expr( $1 ) and parse_expr( $8 )
160
+ @stack.push(@stack.pop & @stack.pop) if can_parse
161
+ return can_parse
162
+ end
163
+ false
164
+ end
165
+
166
+ # Descend down parenthesis (allow up to 5 levels of nesting)
167
+ def parse_parenthesis( str )
168
+ str =~ PARENTHESES_REGEX ? parse_expr( $1 ) : false
169
+ end
170
+
171
+ def parse_term( str )
172
+ parse_role_of_model( str ) or
173
+ parse_role( str )
174
+ end
175
+
176
+ # Parse <role> of <model>
177
+ def parse_role_of_model( str )
178
+ if str =~ ROLE_OF_MODEL_REGEX
179
+ role_name = $2 || $3
180
+ model_name = $5
181
+ model_obj = get_model( model_name )
182
+ raise( ModelDoesntImplementRoles, "Model (#{model_name}) doesn't implement #accepts_role?" ) if not model_obj.respond_to? :accepts_role?
183
+
184
+ has_permission = model_obj.send( :accepts_role?, role_name, @current_user )
185
+ @stack.push( has_permission )
186
+ true
187
+ else
188
+ false
189
+ end
190
+ end
191
+
192
+ # Parse <role> of the User-like object
193
+ def parse_role( str )
194
+ if str =~ ROLE_REGEX
195
+ role_name = $1
196
+ if @current_user.nil? || @current_user == :false
197
+ @stack.push(false)
198
+ else
199
+ raise( UserDoesntImplementRoles, "User doesn't implement #has_role?" ) if not @current_user.respond_to? :has_role?
200
+ @stack.push( @current_user.has_role?(role_name) )
201
+ end
202
+ true
203
+ else
204
+ false
205
+ end
206
+ end
207
+
208
+ end
209
+ end
210
+ end