permit_yo 2.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,4 @@
1
+ en:
2
+ permit_yo:
3
+ permission_denied: "Permission denied. You cannot access the requested page."
4
+ require_user: "Login is required to access the requested page."
data/lib/permit_yo.rb ADDED
@@ -0,0 +1 @@
1
+ require 'permit_yo/engine' if defined?(Rails)
@@ -0,0 +1,160 @@
1
+ require 'permit_yo/exceptions'
2
+ require 'permit_yo/parser'
3
+
4
+ module PermitYo
5
+ module Base
6
+ def self.included(recipient)
7
+ recipient.extend ControllerClassMethods
8
+ recipient.class_eval do
9
+ include ControllerInstanceMethods
10
+ end
11
+ end
12
+
13
+ module ControllerClassMethods
14
+
15
+ # Allow class-level authorization check.
16
+ # permit is used in a before_filter fashion and passes arguments to the before_filter.
17
+ def permit(authorization_expression, *args)
18
+ filter_keys = [:only, :except]
19
+ filter_args, eval_args = {}, {}
20
+ if args.last.is_a? Hash
21
+ filter_args.merge!(args.last.reject {|k,v| not filter_keys.include? k })
22
+ eval_args.merge!(args.last.reject {|k,v| filter_keys.include? k })
23
+ end
24
+ before_filter(filter_args) do |controller|
25
+ controller.permit(authorization_expression, eval_args)
26
+ end
27
+ end
28
+ end
29
+
30
+ module ControllerInstanceMethods
31
+ include PermitYo::Base::EvalParser # RecursiveDescentParser is another option
32
+
33
+ # Permit? turns off redirection by default and takes no blocks
34
+ def permit?(authorization_expression, *args)
35
+ @options = { :allow_guests => false, :redirect => false }
36
+ @options.merge!(args.last.is_a?(Hash) ? args.last : {})
37
+
38
+ has_permission?(authorization_expression)
39
+ end
40
+
41
+ # Allow method-level authorization checks.
42
+ # permit (without a question mark ending) calls redirect on denial by default.
43
+ # Specify :redirect => false to turn off redirection.
44
+ def permit(authorization_expression, *args)
45
+ @options = { :allow_guests => false, :redirect => true }
46
+ @options.merge!(args.last.is_a?(Hash) ? args.last : {})
47
+
48
+ if has_permission?(authorization_expression)
49
+ yield if block_given?
50
+ elsif @options[:redirect]
51
+ handle_redirection
52
+ end
53
+ end
54
+
55
+ private
56
+
57
+ def has_permission?(authorization_expression)
58
+ @current_user = get_user
59
+ if not @options[:allow_guests]
60
+ # We aren't logged in, or an exception has already been raised.
61
+ # Test for both nil and :false symbol as restful_authentication plugin
62
+ # will set current user to ':false' on a failed login (patch by Ho-Sheng Hsiao).
63
+ # Latest incarnations of restful_authentication plugin set current user to false.
64
+ if @current_user.nil? || @current_user == :false || @current_user == false
65
+ return false
66
+ elsif not @current_user.respond_to? :id
67
+ raise(UserDoesntImplementID, "User doesn't implement #id")
68
+ elsif not @current_user.respond_to? :has_role?
69
+ raise(UserDoesntImplementRoles, "User doesn't implement #has_role?")
70
+ end
71
+ end
72
+ parse_authorization_expression(authorization_expression)
73
+ end
74
+
75
+ # Handle redirection within permit if authorization is denied.
76
+ def handle_redirection
77
+ return if not self.respond_to?(:redirect_to)
78
+ # Store url in session for return if this is available from authentication
79
+ send(Rails.application.config.permit_yo.store_location_method) if respond_to?(Rails.application.config.permit_yo.store_location_method)
80
+ if @current_user && @current_user != :false
81
+ respond_to do |format|
82
+ format.html do
83
+ if self.respond_to? :handle_permission_denied_redirection_for_html
84
+ handle_permission_denied_redirection_for_html
85
+ else
86
+ flash[Rails.application.config.permit_yo.permission_denied_flash] = @options[:permission_denied_message] || t('permit_yo.permission_denied')
87
+ redirect_to @options[:permission_denied_redirection] || (self.respond_to?(:permission_denied_redirection) ? permission_denied_redirection : Rails.application.config.permit_yo.permission_denied_redirection)
88
+ end
89
+ end
90
+ format.all do
91
+ if self.respond_to? :"handle_permission_denied_redirection_for_#{params[:format]}"
92
+ self.send :"handle_permission_denied_redirection_for_#{params[:format]}"
93
+ else
94
+ render :text => nil, :status => :forbidden
95
+ end
96
+ end
97
+ end
98
+ else
99
+ respond_to do |format|
100
+ format.html do
101
+ if self.respond_to? :handle_require_user_redirection_for_html
102
+ handle_require_user_redirection_for_html
103
+ else
104
+ flash[Rails.application.config.permit_yo.require_user_flash] = @options[:require_user_message] || t('permit_yo.require_user')
105
+ redirect_to @options[:require_user_redirection] || (self.respond_to?(:require_user_redirection) ? require_user_redirection : Rails.application.config.permit_yo.require_user_redirection)
106
+ end
107
+ end
108
+ format.all do
109
+ if self.respond_to? :"handle_require_user_redirection_for_#{params[:format]}"
110
+ self.send :"handle_require_user_redirection_for_#{params[:format]}"
111
+ else
112
+ render :text => nil, :status => :unauthorized
113
+ end
114
+ end
115
+ end
116
+ end
117
+ false # Want to short-circuit the filters
118
+ end
119
+
120
+ # Try to find current user by checking options hash and instance method in that order.
121
+ def get_user
122
+ if @options[:user]
123
+ @options[:user]
124
+ elsif @options[:get_user_method]
125
+ send(@options[:get_user_method])
126
+ elsif self.respond_to? Rails.application.config.permit_yo.current_user_method
127
+ self.send Rails.application.config.permit_yo.current_user_method
128
+ elsif not @options[:allow_guests]
129
+ raise(CannotObtainUserObject, "Couldn't find ##{Rails.application.config.permit_yo.current_user_method} or @user, and nothing appropriate found in hash")
130
+ end
131
+ end
132
+
133
+ # Try to find a model to query for permissions
134
+ def get_model(str)
135
+ if str =~ /\s*([A-Z]+\w*)\s*/
136
+ # Handle model class
137
+ begin
138
+ Module.const_get(str)
139
+ rescue
140
+ raise CannotObtainModelClass, "Couldn't find model class: #{str}"
141
+ end
142
+ elsif str =~ /\s*:*(\w+)\s*/
143
+ # Handle model instances
144
+ model_name = $1
145
+ model_symbol = model_name.to_sym
146
+ if @options[model_symbol]
147
+ @options[model_symbol]
148
+ elsif instance_variables.include?('@'+model_name)
149
+ instance_variable_get('@'+model_name)
150
+ elsif respond_to?(model_symbol)
151
+ send(model_symbol)
152
+ else
153
+ raise CannotObtainModelObject, "Couldn't find model (#{str}) in hash or as an instance variable"
154
+ end
155
+ end
156
+ end
157
+ end
158
+
159
+ end
160
+ end
@@ -0,0 +1,45 @@
1
+ module PermitYo
2
+ module Default
3
+ module UserExtensions
4
+ def self.included(recipient)
5
+ recipient.extend ClassMethods
6
+ end
7
+
8
+ module ClassMethods
9
+ def acts_as_authorized_user
10
+ include PermitYo::Default::UserExtensions::InstanceMethods
11
+ end
12
+ end
13
+
14
+ module InstanceMethods
15
+ def has_role?(role)
16
+ self.send(:"#{role}?") if self.respond_to?(:"#{role}?")
17
+ end
18
+ end
19
+ end
20
+
21
+ module ModelExtensions
22
+ def self.included(recipient)
23
+ recipient.extend ClassMethods
24
+ end
25
+
26
+ module ClassMethods
27
+ def acts_as_authorizable
28
+ include PermitYo::Default::ModelExtensions::InstanceMethods
29
+ end
30
+ end
31
+
32
+ module InstanceMethods
33
+ def accepts_role?(role, user)
34
+ if self.respond_to? role
35
+ self.send(role) == user
36
+ elsif self.respond_to? role.pluralize
37
+ self.send(role.pluralize).include?(user)
38
+ else
39
+ false
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,29 @@
1
+ require 'permit_yo'
2
+ require 'rails'
3
+ require 'action_controller'
4
+ require 'action_view'
5
+
6
+ module PermitYo
7
+ class Engine < Rails::Engine
8
+ config.permit_yo = ActiveSupport::OrderedOptions.new
9
+ config.permit_yo.root = __FILE__.gsub('/lib/permit_yo/engine.rb', '')
10
+ config.permit_yo.implementation = :default
11
+ config.permit_yo.require_user_redirection = { :controller => 'user_sessions', :action => 'new' }
12
+ config.permit_yo.permission_denied_redirection = ''
13
+ config.permit_yo.store_location_method = :store_location
14
+ config.permit_yo.current_user_method = :current_user
15
+ config.permit_yo.require_user_flash = :notice
16
+ config.permit_yo.permission_denied_flash = :notice
17
+
18
+ initializer "permit_yo.default" do |app|
19
+ ActionController::Base.send :include, PermitYo::Base
20
+ ActionView::Base.send :include, PermitYo::Base::ControllerInstanceMethods
21
+ if app.config.permit_yo.implementation == :default
22
+ require 'active_record'
23
+ ActiveRecord::Base.send :include,
24
+ PermitYo::Default::UserExtensions,
25
+ PermitYo::Default::ModelExtensions
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,43 @@
1
+ module PermitYo #: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
+ class CannotGetAuthorizables < AuthorizationError
42
+ end
43
+ end
@@ -0,0 +1,210 @@
1
+ module PermitYo
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 Exception => error
43
+ raise AuthorizationExpressionInvalid, "Cannot parse authorization (#{str}): #{error.message}"
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
metadata ADDED
@@ -0,0 +1,70 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: permit_yo
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: false
5
+ segments:
6
+ - 2
7
+ - 1
8
+ version: "2.1"
9
+ platform: ruby
10
+ authors:
11
+ - Bill Katz
12
+ - Ian Terrell
13
+ autorequire:
14
+ bindir: bin
15
+ cert_chain: []
16
+
17
+ date: 2010-07-25 00:00:00 -07:00
18
+ default_executable:
19
+ dependencies: []
20
+
21
+ description: An engine that provides authorization for Rails 3 apps.
22
+ email: ian.terrell@gmail.com
23
+ executables: []
24
+
25
+ extensions: []
26
+
27
+ extra_rdoc_files: []
28
+
29
+ files:
30
+ - lib/permit_yo/base.rb
31
+ - lib/permit_yo/default.rb
32
+ - lib/permit_yo/engine.rb
33
+ - lib/permit_yo/exceptions.rb
34
+ - lib/permit_yo/parser.rb
35
+ - lib/permit_yo.rb
36
+ - config/locales/en.yml
37
+ has_rdoc: true
38
+ homepage: http://github.com/ianterrell/permityo
39
+ licenses: []
40
+
41
+ post_install_message:
42
+ rdoc_options: []
43
+
44
+ require_paths:
45
+ - lib
46
+ required_ruby_version: !ruby/object:Gem::Requirement
47
+ none: false
48
+ requirements:
49
+ - - ">="
50
+ - !ruby/object:Gem::Version
51
+ segments:
52
+ - 0
53
+ version: "0"
54
+ required_rubygems_version: !ruby/object:Gem::Requirement
55
+ none: false
56
+ requirements:
57
+ - - ">="
58
+ - !ruby/object:Gem::Version
59
+ segments:
60
+ - 0
61
+ version: "0"
62
+ requirements: []
63
+
64
+ rubyforge_project:
65
+ rubygems_version: 1.3.7
66
+ signing_key:
67
+ specification_version: 3
68
+ summary: A Rails 3 engine for managing authorization.
69
+ test_files: []
70
+