authorization-rails 1.0.12

Sign up to get free protection for your applications and to get access to all the features.
@@ -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