constellation-authorization 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,5 @@
1
+ module Constellation
2
+ module Authorization
3
+ require 'constellation/authorization/engine' if defined?(Rails)
4
+ end
5
+ end
@@ -0,0 +1,134 @@
1
+ require 'constellation/authorization/exceptions'
2
+ require 'constellation/authorization/parser'
3
+
4
+ module Constellation
5
+ module Authorization
6
+ module Base
7
+ def self.included(recipient)
8
+ recipient.extend ControllerClassMethods
9
+ recipient.class_eval do
10
+ include ControllerInstanceMethods
11
+ end
12
+ end
13
+
14
+ module ControllerClassMethods
15
+
16
+ # Allow class-level authorization check.
17
+ # permit is used in a before_filter fashion and passes arguments to the before_filter.
18
+ def permit(authorization_expression, *args)
19
+ filter_keys = [:only, :except]
20
+ filter_args, eval_args = {}, {}
21
+ if args.last.is_a? Hash
22
+ filter_args.merge!(args.last.reject {|k,v| not filter_keys.include? k })
23
+ eval_args.merge!(args.last.reject {|k,v| filter_keys.include? k })
24
+ end
25
+ before_filter(filter_args) do |controller|
26
+ controller.permit(authorization_expression, eval_args)
27
+ end
28
+ end
29
+ end
30
+
31
+ module ControllerInstanceMethods
32
+ include Constellation::Authorization::Base::EvalParser # RecursiveDescentParser is another option
33
+
34
+ # Permit? turns off redirection by default and takes no blocks
35
+ def permit?(authorization_expression, *args)
36
+ @options = { :allow_guests => false, :redirect => false }
37
+ @options.merge!(args.last.is_a?(Hash) ? args.last : {})
38
+
39
+ has_permission?(authorization_expression)
40
+ end
41
+
42
+ # Allow method-level authorization checks.
43
+ # permit (without a question mark ending) calls redirect on denial by default.
44
+ # Specify :redirect => false to turn off redirection.
45
+ def permit(authorization_expression, *args)
46
+ @options = { :allow_guests => false, :redirect => true }
47
+ @options.merge!(args.last.is_a?(Hash) ? args.last : {})
48
+
49
+ if has_permission?(authorization_expression)
50
+ yield if block_given?
51
+ elsif @options[:redirect]
52
+ handle_redirection
53
+ end
54
+ end
55
+
56
+ private
57
+
58
+ def has_permission?(authorization_expression)
59
+ @current_user = get_user
60
+ if not @options[:allow_guests]
61
+ # We aren't logged in, or an exception has already been raised.
62
+ # Test for both nil and :false symbol as restful_authentication plugin
63
+ # will set current user to ':false' on a failed login (patch by Ho-Sheng Hsiao).
64
+ # Latest incarnations of restful_authentication plugin set current user to false.
65
+ if @current_user.nil? || @current_user == :false || @current_user == false
66
+ return false
67
+ elsif not @current_user.respond_to? :id
68
+ raise(UserDoesntImplementID, "User doesn't implement #id")
69
+ elsif not @current_user.respond_to? :has_role?
70
+ raise(UserDoesntImplementRoles, "User doesn't implement #has_role?")
71
+ end
72
+ end
73
+ parse_authorization_expression(authorization_expression)
74
+ end
75
+
76
+ # Handle redirection within permit if authorization is denied.
77
+ def handle_redirection
78
+ return if not self.respond_to?(:redirect_to)
79
+
80
+ # Store url in session for return if this is available from
81
+ # authentication
82
+ send(Rails.application.config.constellation.authorization.store_location_method) if respond_to? Rails.application.config.constellation.authorization.store_location_method
83
+ if @current_user && @current_user != :false
84
+ flash[:notice] = @options[:permission_denied_message] || "Permission denied. You cannot access the requested page."
85
+ redirect_to @options[:permission_denied_redirection] || Rails.application.config.constellation.authorization.permission_denied_redirection
86
+ else
87
+ flash[:notice] = @options[:login_required_message] || "Login is required to access the requested page."
88
+ redirect_to @options[:login_required_redirection] || Rails.application.config.constellation.authorization.login_required_redirection
89
+ end
90
+ false # Want to short-circuit the filters
91
+ end
92
+
93
+ # Try to find current user by checking options hash and instance method in that order.
94
+ def get_user
95
+ if @options[:user]
96
+ @options[:user]
97
+ elsif @options[:get_user_method]
98
+ send(@options[:get_user_method])
99
+ elsif self.respond_to? Rails.application.config.constellation.authorization.current_user_method
100
+ self.send Rails.application.config.constellation.authorization.current_user_method
101
+ elsif not @options[:allow_guests]
102
+ raise(CannotObtainUserObject, "Couldn't find ##{Rails.application.config.constellation.authorization.current_user_method} or @user, and nothing appropriate found in hash")
103
+ end
104
+ end
105
+
106
+ # Try to find a model to query for permissions
107
+ def get_model(str)
108
+ if str =~ /\s*([A-Z]+\w*)\s*/
109
+ # Handle model class
110
+ begin
111
+ Module.const_get(str)
112
+ rescue
113
+ raise CannotObtainModelClass, "Couldn't find model class: #{str}"
114
+ end
115
+ elsif str =~ /\s*:*(\w+)\s*/
116
+ # Handle model instances
117
+ model_name = $1
118
+ model_symbol = model_name.to_sym
119
+ if @options[model_symbol]
120
+ @options[model_symbol]
121
+ elsif instance_variables.include?('@'+model_name)
122
+ instance_variable_get('@'+model_name)
123
+ elsif respond_to?(model_symbol)
124
+ send(model_symbol)
125
+ else
126
+ raise CannotObtainModelObject, "Couldn't find model (#{str}) in hash or as an instance variable"
127
+ end
128
+ end
129
+ end
130
+ end
131
+
132
+ end
133
+ end
134
+ end
@@ -0,0 +1,47 @@
1
+ module Constellation
2
+ module Authorization
3
+ module Default
4
+ module UserExtensions
5
+ def self.included(recipient)
6
+ recipient.extend ClassMethods
7
+ end
8
+
9
+ module ClassMethods
10
+ def acts_as_authorized_user
11
+ include Constellation::Authorization::Default::UserExtensions::InstanceMethods
12
+ end
13
+ end
14
+
15
+ module InstanceMethods
16
+ def has_role?(role)
17
+ self.send(:"#{role}?") if self.respond_to?(:"#{role}?")
18
+ end
19
+ end
20
+ end
21
+
22
+ module ModelExtensions
23
+ def self.included(recipient)
24
+ recipient.extend ClassMethods
25
+ end
26
+
27
+ module ClassMethods
28
+ def acts_as_authorizable
29
+ include Constellation::Authorization::Default::ModelExtensions::InstanceMethods
30
+ end
31
+ end
32
+
33
+ module InstanceMethods
34
+ def accepts_role?(role, user)
35
+ if self.respond_to? role
36
+ self.send(role) == user
37
+ elsif self.respond_to? role.pluralize
38
+ self.send(role.pluralize).include?(user)
39
+ else
40
+ false
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,27 @@
1
+ require 'constellation/authorization'
2
+ require 'rails'
3
+
4
+ module Constellation
5
+ module Authorization
6
+ class Engine < Rails::Engine
7
+ config.constellation = ActiveSupport::OrderedOptions.new unless config.respond_to? :constellation
8
+ config.constellation.authorization = ActiveSupport::OrderedOptions.new
9
+ config.constellation.authorization.mixin = :default
10
+ config.constellation.authorization.login_required_redirection = { :controller => 'session', :action => 'new' }
11
+ config.constellation.authorization.permission_denied_redirection = ''
12
+ config.constellation.authorization.store_location_method = :store_location
13
+ config.constellation.authorization.current_user_method = :current_user
14
+
15
+
16
+ initializer "constellation.authorization.default" do |app|
17
+ ActionController::Base.send :include, Constellation::Authorization::Base
18
+ ActionView::Base.send :include, Constellation::Authorization::Base::ControllerInstanceMethods
19
+ if app.config.constellation.authorization.mixin == :default
20
+ ActiveRecord::Base.send :include,
21
+ Constellation::Authorization::Default::UserExtensions,
22
+ Constellation::Authorization::Default::ModelExtensions
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,45 @@
1
+ module Constellation
2
+ module Authorization #:nodoc:
3
+
4
+ # Base error class for Authorization module
5
+ class AuthorizationError < StandardError
6
+ end
7
+
8
+ # Raised when the authorization expression is invalid (cannot be parsed)
9
+ class AuthorizationExpressionInvalid < AuthorizationError
10
+ end
11
+
12
+ # Raised when we can't find the current user
13
+ class CannotObtainUserObject < AuthorizationError
14
+ end
15
+
16
+ # Raised when an authorization expression contains a model class that doesn't exist
17
+ class CannotObtainModelClass < AuthorizationError
18
+ end
19
+
20
+ # Raised when an authorization expression contains a model reference that doesn't exist
21
+ class CannotObtainModelObject < AuthorizationError
22
+ end
23
+
24
+ # Raised when the obtained user object doesn't implement #id
25
+ class UserDoesntImplementID < AuthorizationError
26
+ end
27
+
28
+ # Raised when the obtained user object doesn't implement #has_role?
29
+ class UserDoesntImplementRoles < AuthorizationError
30
+ end
31
+
32
+ # Raised when the obtained model doesn't implement #accepts_role?
33
+ class ModelDoesntImplementRoles < AuthorizationError
34
+ end
35
+
36
+ class CannotSetRoleWhenHardwired < AuthorizationError
37
+ end
38
+
39
+ class CannotSetObjectRoleWhenSimpleRoleTable < AuthorizationError
40
+ end
41
+
42
+ class CannotGetAuthorizables < AuthorizationError
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,212 @@
1
+ module Constellation
2
+ module Authorization
3
+ module Base
4
+
5
+ VALID_PREPOSITIONS = ['of', 'for', 'in', 'on', 'to', 'at', 'by']
6
+ BOOLEAN_OPS = ['not', 'or', 'and']
7
+ VALID_PREPOSITIONS_PATTERN = VALID_PREPOSITIONS.join('|')
8
+
9
+ module EvalParser
10
+ # Parses and evaluates an authorization expression and returns <tt>true</tt> or <tt>false</tt>.
11
+ #
12
+ # The authorization expression is defined by the following grammar:
13
+ # <expr> ::= (<expr>) | not <expr> | <term> or <expr> | <term> and <expr> | <term>
14
+ # <term> ::= <role> | <role> <preposition> <model>
15
+ # <preposition> ::= of | for | in | on | to | at | by
16
+ # <model> ::= /:*\w+/
17
+ # <role> ::= /\w+/ | /'.*'/
18
+ #
19
+ # Instead of doing recursive descent parsing (not so fun when we support nested parentheses, etc),
20
+ # we let Ruby do the work for us by inserting the appropriate permission calls and using eval.
21
+ # This would not be a good idea if you were getting authorization expressions from the outside,
22
+ # so in that case (e.g. somehow letting users literally type in permission expressions) you'd
23
+ # be better off using the recursive descent parser in Module RecursiveDescentParser.
24
+ #
25
+ # We search for parts of our authorization evaluation that match <role> or <role> <preposition> <model>
26
+ # and we ignore anything terminal in our grammar.
27
+ #
28
+ # 1) Replace all <role> <preposition> <model> matches.
29
+ # 2) Replace all <role> matches that aren't one of our other terminals ('not', 'or', 'and', or preposition)
30
+ # 3) Eval
31
+
32
+ def parse_authorization_expression( str )
33
+ if str =~ /[^A-Za-z0-9_:'\(\)\s]/
34
+ raise AuthorizationExpressionInvalid, "Invalid authorization expression (#{str})"
35
+ return false
36
+ end
37
+ @replacements = []
38
+ expr = replace_temporarily_role_of_model( str )
39
+ expr = replace_role( expr )
40
+ expr = replace_role_of_model( expr )
41
+ begin
42
+ instance_eval( expr )
43
+ rescue Exception => error
44
+ raise AuthorizationExpressionInvalid, "Cannot parse authorization (#{str}): #{error.message}"
45
+ end
46
+ end
47
+
48
+ def replace_temporarily_role_of_model( str )
49
+ role_regex = '\s*(\'\s*(.+?)\s*\'|(\w+))\s+'
50
+ model_regex = '\s+(:*\w+)'
51
+ parse_regex = Regexp.new(role_regex + '(' + VALID_PREPOSITIONS.join('|') + ')' + model_regex)
52
+ str.gsub(parse_regex) do |match|
53
+ @replacements.push " process_role_of_model('#{$2 || $3}', '#{$5}') "
54
+ " <#{@replacements.length-1}> "
55
+ end
56
+ end
57
+
58
+ def replace_role( str )
59
+ role_regex = '\s*(\'\s*(.+?)\s*\'|([A-Za-z]\w*))\s*'
60
+ parse_regex = Regexp.new(role_regex)
61
+ str.gsub(parse_regex) do |match|
62
+ if BOOLEAN_OPS.include?($3)
63
+ " #{match} "
64
+ else
65
+ " process_role('#{$2 || $3}') "
66
+ end
67
+ end
68
+ end
69
+
70
+ def replace_role_of_model( str )
71
+ str.gsub(/<(\d+)>/) do |match|
72
+ @replacements[$1.to_i]
73
+ end
74
+ end
75
+
76
+ def process_role_of_model( role_name, model_name )
77
+ model = get_model( model_name )
78
+ raise( ModelDoesntImplementRoles, "Model (#{model_name}) doesn't implement #accepts_role?" ) if not model.respond_to? :accepts_role?
79
+ model.send( :accepts_role?, role_name, @current_user )
80
+ end
81
+
82
+ def process_role( role_name )
83
+ return false if @current_user.nil? || @current_user == :false
84
+ raise( UserDoesntImplementRoles, "User doesn't implement #has_role?" ) if not @current_user.respond_to? :has_role?
85
+ @current_user.has_role?( role_name )
86
+ end
87
+
88
+ end
89
+
90
+ # Parses and evaluates an authorization expression and returns <tt>true</tt> or <tt>false</tt>.
91
+ # This recursive descent parses uses two instance variables:
92
+ # @stack --> a stack with the top holding the boolean expression resulting from the parsing
93
+ #
94
+ # The authorization expression is defined by the following grammar:
95
+ # <expr> ::= (<expr>) | not <expr> | <term> or <expr> | <term> and <expr> | <term>
96
+ # <term> ::= <role> | <role> <preposition> <model>
97
+ # <preposition> ::= of | for | in | on | to | at | by
98
+ # <model> ::= /:*\w+/
99
+ # <role> ::= /\w+/ | /'.*'/
100
+ #
101
+ # There are really two values we must track:
102
+ # (1) whether the expression is valid according to the grammar
103
+ # (2) the evaluated results --> true/false on the permission queries
104
+ # The first is embedded in the control logic because we want short-circuiting. If an expression
105
+ # has been parsed and the permission is false, we don't want to try different ways of parsing.
106
+ # Note that this implementation of a recursive descent parser is meant to be simple
107
+ # and doesn't allow arbitrary nesting of parentheses. It supports up to 5 levels of nesting.
108
+ # It also won't handle some types of expressions (A or B) and C, which has to be rewritten as
109
+ # C and (A or B) so the parenthetical expressions are in the tail.
110
+ module RecursiveDescentParser
111
+
112
+ OPT_PARENTHESES_PATTERN = '(([^()]|\(([^()]|\(([^()]|\(([^()]|\(([^()]|\(([^()])*\))*\))*\))*\))*\))*)'
113
+ PARENTHESES_PATTERN = '\(' + OPT_PARENTHESES_PATTERN + '\)'
114
+ NOT_PATTERN = '^\s*not\s+' + OPT_PARENTHESES_PATTERN + '$'
115
+ AND_PATTERN = '^\s*' + OPT_PARENTHESES_PATTERN + '\s+and\s+' + OPT_PARENTHESES_PATTERN + '\s*$'
116
+ OR_PATTERN = '^\s*' + OPT_PARENTHESES_PATTERN + '\s+or\s+' + OPT_PARENTHESES_PATTERN + '\s*$'
117
+ ROLE_PATTERN = '(\'\s*(.+)\s*\'|(\w+))'
118
+ MODEL_PATTERN = '(:*\w+)'
119
+
120
+ PARENTHESES_REGEX = Regexp.new('^\s*' + PARENTHESES_PATTERN + '\s*$')
121
+ NOT_REGEX = Regexp.new(NOT_PATTERN)
122
+ AND_REGEX = Regexp.new(AND_PATTERN)
123
+ OR_REGEX = Regexp.new(OR_PATTERN)
124
+ ROLE_REGEX = Regexp.new('^\s*' + ROLE_PATTERN + '\s*$')
125
+ ROLE_OF_MODEL_REGEX = Regexp.new('^\s*' + ROLE_PATTERN + '\s+(' + VALID_PREPOSITIONS_PATTERN + ')\s+' + MODEL_PATTERN + '\s*$')
126
+
127
+ def parse_authorization_expression( str )
128
+ @stack = []
129
+ raise AuthorizationExpressionInvalid, "Cannot parse authorization (#{str})" if not parse_expr( str )
130
+ return @stack.pop
131
+ end
132
+
133
+ def parse_expr( str )
134
+ parse_parenthesis( str ) or
135
+ parse_not( str ) or
136
+ parse_or( str ) or
137
+ parse_and( str ) or
138
+ parse_term( str )
139
+ end
140
+
141
+ def parse_not( str )
142
+ if str =~ NOT_REGEX
143
+ can_parse = parse_expr( $1 )
144
+ @stack.push( !@stack.pop ) if can_parse
145
+ end
146
+ false
147
+ end
148
+
149
+ def parse_or( str )
150
+ if str =~ OR_REGEX
151
+ can_parse = parse_expr( $1 ) and parse_expr( $8 )
152
+ @stack.push( @stack.pop | @stack.pop ) if can_parse
153
+ return can_parse
154
+ end
155
+ false
156
+ end
157
+
158
+ def parse_and( str )
159
+ if str =~ AND_REGEX
160
+ can_parse = parse_expr( $1 ) and parse_expr( $8 )
161
+ @stack.push(@stack.pop & @stack.pop) if can_parse
162
+ return can_parse
163
+ end
164
+ false
165
+ end
166
+
167
+ # Descend down parenthesis (allow up to 5 levels of nesting)
168
+ def parse_parenthesis( str )
169
+ str =~ PARENTHESES_REGEX ? parse_expr( $1 ) : false
170
+ end
171
+
172
+ def parse_term( str )
173
+ parse_role_of_model( str ) or
174
+ parse_role( str )
175
+ end
176
+
177
+ # Parse <role> of <model>
178
+ def parse_role_of_model( str )
179
+ if str =~ ROLE_OF_MODEL_REGEX
180
+ role_name = $2 || $3
181
+ model_name = $5
182
+ model_obj = get_model( model_name )
183
+ raise( ModelDoesntImplementRoles, "Model (#{model_name}) doesn't implement #accepts_role?" ) if not model_obj.respond_to? :accepts_role?
184
+
185
+ has_permission = model_obj.send( :accepts_role?, role_name, @current_user )
186
+ @stack.push( has_permission )
187
+ true
188
+ else
189
+ false
190
+ end
191
+ end
192
+
193
+ # Parse <role> of the User-like object
194
+ def parse_role( str )
195
+ if str =~ ROLE_REGEX
196
+ role_name = $1
197
+ if @current_user.nil? || @current_user == :false
198
+ @stack.push(false)
199
+ else
200
+ raise( UserDoesntImplementRoles, "User doesn't implement #has_role?" ) if not @current_user.respond_to? :has_role?
201
+ @stack.push( @current_user.has_role?(role_name) )
202
+ end
203
+ true
204
+ else
205
+ false
206
+ end
207
+ end
208
+
209
+ end
210
+ end
211
+ end
212
+ end
metadata ADDED
@@ -0,0 +1,69 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: constellation-authorization
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: false
5
+ segments:
6
+ - 2
7
+ - 0
8
+ - 0
9
+ version: 2.0.0
10
+ platform: ruby
11
+ authors:
12
+ - Bill Katz
13
+ - Ian Terrell
14
+ - Jeff Bozek
15
+ autorequire:
16
+ bindir: bin
17
+ cert_chain: []
18
+
19
+ date: 2010-04-28 00:00:00 -07:00
20
+ default_executable:
21
+ dependencies: []
22
+
23
+ description: A module for managing authorization.
24
+ email: ian@constellationsoft.com;jeff@constellationsoft.com
25
+ executables: []
26
+
27
+ extensions: []
28
+
29
+ extra_rdoc_files: []
30
+
31
+ files:
32
+ - lib/constellation/authorization/base.rb
33
+ - lib/constellation/authorization/default.rb
34
+ - lib/constellation/authorization/engine.rb
35
+ - lib/constellation/authorization/exceptions.rb
36
+ - lib/constellation/authorization/parser.rb
37
+ - lib/constellation/authorization.rb
38
+ has_rdoc: true
39
+ homepage: http://github.com/constellationsoft/authorization
40
+ licenses: []
41
+
42
+ post_install_message:
43
+ rdoc_options: []
44
+
45
+ require_paths:
46
+ - lib
47
+ required_ruby_version: !ruby/object:Gem::Requirement
48
+ requirements:
49
+ - - ">="
50
+ - !ruby/object:Gem::Version
51
+ segments:
52
+ - 0
53
+ version: "0"
54
+ required_rubygems_version: !ruby/object:Gem::Requirement
55
+ requirements:
56
+ - - ">="
57
+ - !ruby/object:Gem::Version
58
+ segments:
59
+ - 0
60
+ version: "0"
61
+ requirements: []
62
+
63
+ rubyforge_project:
64
+ rubygems_version: 1.3.6
65
+ signing_key:
66
+ specification_version: 3
67
+ summary: A module for managing authorization.
68
+ test_files: []
69
+