loyal_warden 0.0.5

Sign up to get free protection for your applications and to get access to all the features.
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