constellation-authorization 2.0.0

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