permit_yo 2.1

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