loyal_warden 0.0.5

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.
Files changed (42) hide show
  1. data/Gemfile +11 -0
  2. data/History.rdoc +150 -0
  3. data/LICENSE +20 -0
  4. data/README.textile +9 -0
  5. data/Rakefile +12 -0
  6. data/lib/loyal_warden.rb +2 -0
  7. data/lib/warden.rb +45 -0
  8. data/lib/warden/config.rb +112 -0
  9. data/lib/warden/errors.rb +66 -0
  10. data/lib/warden/hooks.rb +211 -0
  11. data/lib/warden/manager.rb +136 -0
  12. data/lib/warden/mixins/common.rb +44 -0
  13. data/lib/warden/proxy.rb +371 -0
  14. data/lib/warden/session_serializer.rb +52 -0
  15. data/lib/warden/strategies.rb +47 -0
  16. data/lib/warden/strategies/base.rb +175 -0
  17. data/lib/warden/test/helpers.rb +36 -0
  18. data/lib/warden/test/warden_helpers.rb +43 -0
  19. data/lib/warden/version.rb +4 -0
  20. data/loyal_warden.gemspec +26 -0
  21. data/spec/helpers/request_helper.rb +51 -0
  22. data/spec/helpers/strategies/failz.rb +8 -0
  23. data/spec/helpers/strategies/invalid.rb +8 -0
  24. data/spec/helpers/strategies/pass.rb +8 -0
  25. data/spec/helpers/strategies/pass_with_message.rb +8 -0
  26. data/spec/helpers/strategies/password.rb +13 -0
  27. data/spec/helpers/strategies/single.rb +12 -0
  28. data/spec/spec_helper.rb +24 -0
  29. data/spec/warden/authenticated_data_store_spec.rb +114 -0
  30. data/spec/warden/config_spec.rb +48 -0
  31. data/spec/warden/errors_spec.rb +47 -0
  32. data/spec/warden/hooks_spec.rb +373 -0
  33. data/spec/warden/manager_spec.rb +316 -0
  34. data/spec/warden/proxy_spec.rb +1041 -0
  35. data/spec/warden/scoped_session_serializer.rb +123 -0
  36. data/spec/warden/session_serializer_spec.rb +53 -0
  37. data/spec/warden/strategies/base_spec.rb +313 -0
  38. data/spec/warden/strategies_spec.rb +93 -0
  39. data/spec/warden/test/helpers_spec.rb +93 -0
  40. data/spec/warden/test/test_mode_spec.rb +76 -0
  41. data/warden.gemspec +24 -0
  42. metadata +105 -0
@@ -0,0 +1,136 @@
1
+ # encoding: utf-8
2
+ require 'warden/hooks'
3
+ require 'warden/config'
4
+
5
+ module Warden
6
+ # The middleware for Rack Authentication
7
+ # The middlware requires that there is a session upstream
8
+ # The middleware injects an authentication object into
9
+ # the rack environment hash
10
+ class Manager
11
+ extend Warden::Hooks
12
+
13
+ attr_accessor :config
14
+
15
+ # Initialize the middleware. If a block is given, a Warden::Config is yielded so you can properly
16
+ # configure the Warden::Manager.
17
+ # :api: public
18
+ def initialize(app, options={})
19
+ default_strategies = options.delete(:default_strategies)
20
+
21
+ @app, @config = app, Warden::Config.new(options)
22
+ @config.default_strategies *default_strategies if default_strategies
23
+ yield @config if block_given?
24
+ self
25
+ end
26
+
27
+ # Invoke the application guarding for throw :warden.
28
+ # If this is downstream from another warden instance, don't do anything.
29
+ # :api: private
30
+ def call(env) # :nodoc:
31
+ return @app.call(env) if env['warden'] && env['warden'].manager != self
32
+
33
+ env['warden'] = Proxy.new(env, self)
34
+ result = catch(:warden) do
35
+ @app.call(env)
36
+ end
37
+
38
+ result ||= {}
39
+ case result
40
+ when Array
41
+ if result.first == 401 && intercept_401?(env)
42
+ process_unauthenticated(env)
43
+ else
44
+ result
45
+ end
46
+ when Hash
47
+ process_unauthenticated(env, result)
48
+ end
49
+ end
50
+
51
+ # :api: private
52
+ def _run_callbacks(*args) #:nodoc:
53
+ self.class._run_callbacks(*args)
54
+ end
55
+
56
+ class << self
57
+ # Prepares the user to serialize into the session.
58
+ # Any object that can be serialized into the session in some way can be used as a "user" object
59
+ # Generally however complex object should not be stored in the session.
60
+ # If possible store only a "key" of the user object that will allow you to reconstitute it.
61
+ #
62
+ # You can supply different methods of serialization for different scopes by passing a scope symbol
63
+ #
64
+ # Example:
65
+ # Warden::Manager.serialize_into_session{ |user| user.id }
66
+ # # With Scope:
67
+ # Warden::Manager.serialize_into_session(:admin) { |user| user.id }
68
+ #
69
+ # :api: public
70
+ def serialize_into_session(scope = nil, &block)
71
+ method_name = scope.nil? ? :serialize : "#{scope}_serialize"
72
+ Warden::SessionSerializer.send :define_method, method_name, &block
73
+ end
74
+
75
+ # Reconstitues the user from the session.
76
+ # Use the results of user_session_key to reconstitue the user from the session on requests after the initial login
77
+ # You can supply different methods of de-serialization for different scopes by passing a scope symbol
78
+ #
79
+ # Example:
80
+ # Warden::Manager.serialize_from_session{ |id| User.get(id) }
81
+ # # With Scope:
82
+ # Warden::Manager.serialize_from_session(:admin) { |id| AdminUser.get(id) }
83
+ #
84
+ # :api: public
85
+ def serialize_from_session(scope = nil, &block)
86
+ method_name = scope.nil? ? :deserialize : "#{scope}_deserialize"
87
+ Warden::SessionSerializer.send :define_method, method_name, &block
88
+ end
89
+ end
90
+
91
+ private
92
+
93
+ def intercept_401?(env)
94
+ config[:intercept_401] && !env['warden'].custom_failure?
95
+ end
96
+
97
+ # When a request is unauthenticated, here's where the processing occurs.
98
+ # It looks at the result of the proxy to see if it's been executed and what action to take.
99
+ # :api: private
100
+ def process_unauthenticated(env, options={})
101
+ options[:action] ||= begin
102
+ opts = config[:scope_defaults][config.default_scope] || {}
103
+ opts[:action] || 'unauthenticated'
104
+ end
105
+
106
+ proxy = env['warden']
107
+ result = options[:result] || proxy.result
108
+
109
+ case result
110
+ when :redirect
111
+ body = proxy.message || "You are being redirected to #{proxy.headers['Location']}"
112
+ [proxy.status, proxy.headers, [body]]
113
+ when :custom
114
+ proxy.custom_response
115
+ else
116
+ call_failure_app(env, options)
117
+ end
118
+ end
119
+
120
+ # Calls the failure app.
121
+ # The before_failure hooks are run on each failure
122
+ # :api: private
123
+ def call_failure_app(env, options = {})
124
+ if config.failure_app
125
+ options.merge!(:attempted_path => ::Rack::Request.new(env).fullpath)
126
+ env["PATH_INFO"] = "/#{options[:action]}"
127
+ env["warden.options"] = options
128
+
129
+ _run_callbacks(:before_failure, env, options)
130
+ config.failure_app.call(env).to_a
131
+ else
132
+ raise "No Failure App provided"
133
+ end
134
+ end # call_failure_app
135
+ end
136
+ end # Warden
@@ -0,0 +1,44 @@
1
+ # encoding: utf-8
2
+ module Warden
3
+ module Mixins
4
+ module Common
5
+
6
+ # Convinience method to access the session
7
+ # :api: public
8
+ def session
9
+ env['rack.session']
10
+ end # session
11
+
12
+ # Alias :session to :raw_session since the former will be user API for storing scoped data.
13
+ alias :raw_session :session
14
+
15
+ # Convenience method to access the rack request.
16
+ # :api: public
17
+ def request
18
+ @request ||= Rack::Request.new(@env)
19
+ end # request
20
+
21
+ # Provides a warden repository for cookies. Those are sent to the client
22
+ # when the response is streamed back from the app.
23
+ # :api: public
24
+ def warden_cookies
25
+ warn "warden_cookies was never functional and is going to be removed in next versions"
26
+ env['warden.cookies'] ||= {}
27
+ end # warden_cookies
28
+
29
+ # Convenience method to access the rack request params
30
+ # :api: public
31
+ def params
32
+ request.params
33
+ end # params
34
+
35
+ # Resets the session. By using this non-hash like sessions can
36
+ # be cleared by overwriting this method in a plugin
37
+ # @api overwritable
38
+ def reset_session!
39
+ raw_session.clear
40
+ end # reset_session!
41
+
42
+ end # Common
43
+ end # Mixins
44
+ end # Warden
@@ -0,0 +1,371 @@
1
+ # encoding: utf-8
2
+
3
+ module Warden
4
+ class UserNotSet < RuntimeError; end
5
+
6
+ class Proxy
7
+ # An accessor to the winning strategy
8
+ # :api: private
9
+ attr_accessor :winning_strategy
10
+
11
+ # An accessor to the rack env hash, the proxy owner and its config
12
+ # :api: public
13
+ attr_reader :env, :manager, :config, :winning_strategies
14
+
15
+ extend ::Forwardable
16
+ include ::Warden::Mixins::Common
17
+
18
+ ENV_WARDEN_ERRORS = 'warden.errors'.freeze
19
+ ENV_SESSION_OPTIONS = 'rack.session.options'.freeze
20
+
21
+ # :api: private
22
+ def_delegators :winning_strategy, :headers, :status, :custom_response
23
+
24
+ # :api: public
25
+ def_delegators :config, :default_strategies
26
+
27
+ def initialize(env, manager) #:nodoc:
28
+ @env, @users, @winning_strategies, @locked = env, {}, {}, false
29
+ @manager, @config = manager, manager.config.dup
30
+ @strategies = Hash.new { |h,k| h[k] = {} }
31
+ manager._run_callbacks(:on_request, self)
32
+ end
33
+
34
+ # Lazily initiate errors object in session.
35
+ # :api: public
36
+ def errors
37
+ @env[ENV_WARDEN_ERRORS] ||= Errors.new
38
+ end
39
+
40
+ # Points to a SessionSerializer instance responsible for handling
41
+ # everything related with storing, fetching and removing the user
42
+ # session.
43
+ # :api: public
44
+ def session_serializer
45
+ @session_serializer ||= Warden::SessionSerializer.new(@env)
46
+ end
47
+
48
+ # Clear the cache of performed strategies so far. Warden runs each
49
+ # strategy just once during the request lifecycle. You can clear the
50
+ # strategies cache if you want to allow a strategy to be run more than
51
+ # once.
52
+ #
53
+ # This method has the same API as authenticate, allowing you to clear
54
+ # specific strategies for given scope:
55
+ #
56
+ # Parameters:
57
+ # args - a list of symbols (labels) that name the strategies to attempt
58
+ # opts - an options hash that contains the :scope of the user to check
59
+ #
60
+ # Example:
61
+ # # Clear all strategies for the configured default_scope
62
+ # env['warden'].clear_strategies_cache!
63
+ #
64
+ # # Clear all strategies for the :admin scope
65
+ # env['warden'].clear_strategies_cache!(:scope => :admin)
66
+ #
67
+ # # Clear password strategy for the :admin scope
68
+ # env['warden'].clear_strategies_cache!(:password, :scope => :admin)
69
+ #
70
+ # :api: public
71
+ def clear_strategies_cache!(*args)
72
+ scope, opts = _retrieve_scope_and_opts(args)
73
+
74
+ @winning_strategies.delete(scope)
75
+ @strategies[scope].each do |k, v|
76
+ v.clear! if args.empty? || args.include?(k)
77
+ end
78
+ end
79
+
80
+ # Locks the proxy so new users cannot authenticate during the
81
+ # request lifecycle. This is useful when the request cannot
82
+ # be verified (for example, using a CSRF verification token).
83
+ # Notice that already authenticated users are kept as so.
84
+ #
85
+ # :api: public
86
+ def lock!
87
+ @locked = true
88
+ end
89
+
90
+ # Run the authentiation strategies for the given strategies.
91
+ # If there is already a user logged in for a given scope, the strategies are not run
92
+ # This does not halt the flow of control and is a passive attempt to authenticate only
93
+ # When scope is not specified, the default_scope is assumed.
94
+ #
95
+ # Parameters:
96
+ # args - a list of symbols (labels) that name the strategies to attempt
97
+ # opts - an options hash that contains the :scope of the user to check
98
+ #
99
+ # Example:
100
+ # env['warden'].authenticate(:password, :basic, :scope => :sudo)
101
+ #
102
+ # :api: public
103
+ def authenticate(*args)
104
+ user, opts = _perform_authentication(*args)
105
+ user
106
+ end
107
+
108
+ # Same API as authenticated, but returns a boolean instead of a user.
109
+ # The difference between this method (authenticate?) and authenticated?
110
+ # is that the former will run strategies if the user has not yet been
111
+ # authenticated, and the second relies on already performed ones.
112
+ # :api: public
113
+ def authenticate?(*args)
114
+ result = !!authenticate(*args)
115
+ yield if result && block_given?
116
+ result
117
+ end
118
+
119
+ # The same as +authenticate+ except on failure it will throw an :warden symbol causing the request to be halted
120
+ # and rendered through the +failure_app+
121
+ #
122
+ # Example
123
+ # env['warden'].authenticate!(:password, :scope => :publisher) # throws if it cannot authenticate
124
+ #
125
+ # :api: public
126
+ def authenticate!(*args)
127
+ user, opts = _perform_authentication(*args)
128
+ throw(:warden, opts) unless user
129
+ user
130
+ end
131
+
132
+ # Check to see if there is an authenticated user for the given scope.
133
+ # This brings the user from the session, but does not run strategies before doing so.
134
+ # If you want strategies to be run, please check authenticate?.
135
+ #
136
+ # Parameters:
137
+ # scope - the scope to check for authentication. Defaults to default_scope
138
+ #
139
+ # Example:
140
+ # env['warden'].authenticated?(:admin)
141
+ #
142
+ # :api: public
143
+ def authenticated?(scope = @config.default_scope)
144
+ result = !!user(scope)
145
+ yield if block_given? && result
146
+ result
147
+ end
148
+
149
+ # Same API as authenticated?, but returns false when authenticated.
150
+ # :api: public
151
+ def unauthenticated?(scope = @config.default_scope)
152
+ result = !authenticated?(scope)
153
+ yield if block_given? && result
154
+ result
155
+ end
156
+
157
+ # Manually set the user into the session and auth proxy
158
+ #
159
+ # Parameters:
160
+ # user - An object that has been setup to serialize into and out of the session.
161
+ # opts - An options hash. Use the :scope option to set the scope of the user, set the :store option to false to skip serializing into the session, set the :run_callbacks to false to skip running the callbacks (the default is true).
162
+ #
163
+ # :api: public
164
+ def set_user(user, opts = {})
165
+ scope = (opts[:scope] ||= @config.default_scope)
166
+
167
+ # Get the default options from the master configuration for the given scope
168
+ opts = (@config[:scope_defaults][scope] || {}).merge(opts)
169
+ opts[:event] ||= :set_user
170
+ @users[scope] = user
171
+
172
+ if opts[:store] != false && opts[:event] != :fetch
173
+ options = env[ENV_SESSION_OPTIONS]
174
+ options[:renew] = true if options
175
+ session_serializer.store(user, scope)
176
+ end
177
+
178
+ run_callbacks = opts.fetch(:run_callbacks, true)
179
+ manager._run_callbacks(:after_set_user, user, self, opts) if run_callbacks
180
+
181
+ @users[scope]
182
+ end
183
+
184
+ # Provides acccess to the user object in a given scope for a request.
185
+ # Will be nil if not logged in. Please notice that this method does not
186
+ # perform strategies.
187
+ #
188
+ # Example:
189
+ # # without scope (default user)
190
+ # env['warden'].user
191
+ #
192
+ # # with scope
193
+ # env['warden'].user(:admin)
194
+ #
195
+ # # as a Hash
196
+ # env['warden'].user(:scope => :admin)
197
+ #
198
+ # # with default scope and run_callbacks option
199
+ # env['warden'].user(:run_callbacks => false)
200
+ #
201
+ # # with a scope and run_callbacks option
202
+ # env['warden'].user(:scope => :admin, :run_callbacks => true)
203
+ #
204
+ # :api: public
205
+ def user(argument = {})
206
+ opts = argument.is_a?(Hash) ? argument : { :scope => argument }
207
+ scope = (opts[:scope] ||= @config.default_scope)
208
+
209
+ if @users.has_key?(scope)
210
+ @users[scope]
211
+ else
212
+ unless user = session_serializer.fetch(scope)
213
+ run_callbacks = opts.fetch(:run_callbacks, true)
214
+ manager._run_callbacks(:after_failed_fetch, user, self, :scope => scope) if run_callbacks
215
+ end
216
+
217
+ @users[scope] = user ? set_user(user, opts.merge(:event => :fetch)) : nil
218
+ end
219
+ end
220
+
221
+ # Provides a scoped session data for authenticated users.
222
+ # Warden manages clearing out this data when a user logs out
223
+ #
224
+ # Example
225
+ # # default scope
226
+ # env['warden'].session[:foo] = "bar"
227
+ #
228
+ # # :sudo scope
229
+ # env['warden'].session(:sudo)[:foo] = "bar"
230
+ #
231
+ # :api: public
232
+ def session(scope = @config.default_scope)
233
+ raise NotAuthenticated, "#{scope.inspect} user is not logged in" unless authenticated?(scope)
234
+ raw_session["warden.user.#{scope}.session"] ||= {}
235
+ end
236
+
237
+ # Provides logout functionality.
238
+ # The logout also manages any authenticated data storage and clears it when a user logs out.
239
+ #
240
+ # Parameters:
241
+ # scopes - a list of scopes to logout
242
+ #
243
+ # Example:
244
+ # # Logout everyone and clear the session
245
+ # env['warden'].logout
246
+ #
247
+ # # Logout the default user but leave the rest of the session alone
248
+ # env['warden'].logout(:default)
249
+ #
250
+ # # Logout the :publisher and :admin user
251
+ # env['warden'].logout(:publisher, :admin)
252
+ #
253
+ # :api: public
254
+ def logout(*scopes)
255
+ if scopes.empty?
256
+ scopes = @users.keys
257
+ reset_session = true
258
+ end
259
+
260
+ scopes.each do |scope|
261
+ user = @users.delete(scope)
262
+ manager._run_callbacks(:before_logout, user, self, :scope => scope)
263
+
264
+ raw_session.delete("warden.user.#{scope}.session") unless raw_session.nil?
265
+ session_serializer.delete(scope, user)
266
+ end
267
+
268
+ reset_session! if reset_session
269
+ end
270
+
271
+ # proxy methods through to the winning strategy
272
+ # :api: private
273
+ def result # :nodoc:
274
+ winning_strategy && winning_strategy.result
275
+ end
276
+
277
+ # Proxy through to the authentication strategy to find out the message that was generated.
278
+ # :api: public
279
+ def message
280
+ winning_strategy && winning_strategy.message
281
+ end
282
+
283
+ # Provides a way to return a 401 without warden defering to the failure app
284
+ # The result is a direct passthrough of your own response
285
+ # :api: public
286
+ def custom_failure!
287
+ @custom_failure = true
288
+ end
289
+
290
+ # Check to see if the custom failure flag has been set
291
+ # :api: public
292
+ def custom_failure?
293
+ !!@custom_failure
294
+ end
295
+
296
+ # Check to see if this is an asset request
297
+ # :api: public
298
+ def asset_request?
299
+ ::Warden::asset_paths.any? { |r| env['PATH_INFO'].to_s.match(r) }
300
+ end
301
+
302
+ def inspect(*args)
303
+ "Warden::Proxy:#{object_id} @config=#{@config.inspect}"
304
+ end
305
+
306
+ def to_s(*args)
307
+ inspect(*args)
308
+ end
309
+
310
+ private
311
+
312
+ def _perform_authentication(*args)
313
+ scope, opts = _retrieve_scope_and_opts(args)
314
+ user = nil
315
+
316
+ # Look for an existing user in the session for this scope.
317
+ # If there was no user in the session. See if we can get one from the request.
318
+ return user, opts if user = user(opts.merge(:scope => scope))
319
+ _run_strategies_for(scope, args)
320
+
321
+ if winning_strategy && winning_strategy.user
322
+ opts[:store] = opts.fetch(:store, winning_strategy.store?)
323
+ set_user(winning_strategy.user, opts.merge!(:event => :authentication))
324
+ end
325
+
326
+ [@users[scope], opts]
327
+ end
328
+
329
+ def _retrieve_scope_and_opts(args) #:nodoc:
330
+ opts = args.last.is_a?(Hash) ? args.pop : {}
331
+ scope = opts[:scope] || @config.default_scope
332
+ opts = (@config[:scope_defaults][scope] || {}).merge(opts)
333
+ [scope, opts]
334
+ end
335
+
336
+ # Run the strategies for a given scope
337
+ def _run_strategies_for(scope, args) #:nodoc:
338
+ self.winning_strategy = @winning_strategies[scope]
339
+ return if winning_strategy && winning_strategy.halted?
340
+
341
+ # Do not run any strategy if locked
342
+ return if @locked
343
+
344
+ if args.empty?
345
+ defaults = @config[:default_strategies]
346
+ strategies = defaults[scope] || defaults[:_all]
347
+ end
348
+
349
+ (strategies || args).each do |name|
350
+ strategy = _fetch_strategy(name, scope)
351
+ next unless strategy && !strategy.performed? && strategy.valid?
352
+
353
+ self.winning_strategy = @winning_strategies[scope] = strategy
354
+ strategy._run!
355
+ break if strategy.halted?
356
+ end
357
+ end
358
+
359
+ # Fetchs strategies and keep them in a hash cache.
360
+ def _fetch_strategy(name, scope)
361
+ @strategies[scope][name] ||= if klass = Warden::Strategies[name]
362
+ klass.new(@env, scope)
363
+ elsif @config.silence_missing_strategies?
364
+ nil
365
+ else
366
+ raise "Invalid strategy #{name}"
367
+ end
368
+ end
369
+ end # Proxy
370
+
371
+ end # Warden