proxy.rb 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371
  1. # encoding: utf-8
  2. module Warden
  3. class UserNotSet < RuntimeError; end
  4. class Proxy
  5. # An accessor to the winning strategy
  6. # :api: private
  7. attr_accessor :winning_strategy
  8. # An accessor to the rack env hash, the proxy owner and its config
  9. # :api: public
  10. attr_reader :env, :manager, :config, :winning_strategies
  11. extend ::Forwardable
  12. include ::Warden::Mixins::Common
  13. ENV_WARDEN_ERRORS = 'warden.errors'.freeze
  14. ENV_SESSION_OPTIONS = 'rack.session.options'.freeze
  15. # :api: private
  16. def_delegators :winning_strategy, :headers, :status, :custom_response
  17. # :api: public
  18. def_delegators :config, :default_strategies
  19. def initialize(env, manager) #:nodoc:
  20. @env, @users, @winning_strategies, @locked = env, {}, {}, false
  21. @manager, @config = manager, manager.config.dup
  22. @strategies = Hash.new { |h,k| h[k] = {} }
  23. manager._run_callbacks(:on_request, self)
  24. end
  25. # Lazily initiate errors object in session.
  26. # :api: public
  27. def errors
  28. @env[ENV_WARDEN_ERRORS] ||= Errors.new
  29. end
  30. # Points to a SessionSerializer instance responsible for handling
  31. # everything related with storing, fetching and removing the user
  32. # session.
  33. # :api: public
  34. def session_serializer
  35. @session_serializer ||= Warden::SessionSerializer.new(@env)
  36. end
  37. # Clear the cache of performed strategies so far. Warden runs each
  38. # strategy just once during the request lifecycle. You can clear the
  39. # strategies cache if you want to allow a strategy to be run more than
  40. # once.
  41. #
  42. # This method has the same API as authenticate, allowing you to clear
  43. # specific strategies for given scope:
  44. #
  45. # Parameters:
  46. # args - a list of symbols (labels) that name the strategies to attempt
  47. # opts - an options hash that contains the :scope of the user to check
  48. #
  49. # Example:
  50. # # Clear all strategies for the configured default_scope
  51. # env['warden'].clear_strategies_cache!
  52. #
  53. # # Clear all strategies for the :admin scope
  54. # env['warden'].clear_strategies_cache!(:scope => :admin)
  55. #
  56. # # Clear password strategy for the :admin scope
  57. # env['warden'].clear_strategies_cache!(:password, :scope => :admin)
  58. #
  59. # :api: public
  60. def clear_strategies_cache!(*args)
  61. scope, opts = _retrieve_scope_and_opts(args)
  62. @winning_strategies.delete(scope)
  63. @strategies[scope].each do |k, v|
  64. v.clear! if args.empty? || args.include?(k)
  65. end
  66. end
  67. # Locks the proxy so new users cannot authenticate during the
  68. # request lifecycle. This is useful when the request cannot
  69. # be verified (for example, using a CSRF verification token).
  70. # Notice that already authenticated users are kept as so.
  71. #
  72. # :api: public
  73. def lock!
  74. @locked = true
  75. end
  76. # Run the authentiation strategies for the given strategies.
  77. # If there is already a user logged in for a given scope, the strategies are not run
  78. # This does not halt the flow of control and is a passive attempt to authenticate only
  79. # When scope is not specified, the default_scope is assumed.
  80. #
  81. # Parameters:
  82. # args - a list of symbols (labels) that name the strategies to attempt
  83. # opts - an options hash that contains the :scope of the user to check
  84. #
  85. # Example:
  86. # env['warden'].authenticate(:password, :basic, :scope => :sudo)
  87. #
  88. # :api: public
  89. def authenticate(*args)
  90. user, opts = _perform_authentication(*args)
  91. user
  92. end
  93. # Same API as authenticated, but returns a boolean instead of a user.
  94. # The difference between this method (authenticate?) and authenticated?
  95. # is that the former will run strategies if the user has not yet been
  96. # authenticated, and the second relies on already performed ones.
  97. # :api: public
  98. def authenticate?(*args)
  99. result = !!authenticate(*args)
  100. yield if result && block_given?
  101. result
  102. end
  103. # The same as +authenticate+ except on failure it will throw an :warden symbol causing the request to be halted
  104. # and rendered through the +failure_app+
  105. #
  106. # Example
  107. # env['warden'].authenticate!(:password, :scope => :publisher) # throws if it cannot authenticate
  108. #
  109. # :api: public
  110. def authenticate!(*args)
  111. user, opts = _perform_authentication(*args)
  112. throw(:warden, opts) unless user
  113. user
  114. end
  115. # Check to see if there is an authenticated user for the given scope.
  116. # This brings the user from the session, but does not run strategies before doing so.
  117. # If you want strategies to be run, please check authenticate?.
  118. #
  119. # Parameters:
  120. # scope - the scope to check for authentication. Defaults to default_scope
  121. #
  122. # Example:
  123. # env['warden'].authenticated?(:admin)
  124. #
  125. # :api: public
  126. def authenticated?(scope = @config.default_scope)
  127. result = !!user(scope)
  128. yield if block_given? && result
  129. result
  130. end
  131. # Same API as authenticated?, but returns false when authenticated.
  132. # :api: public
  133. def unauthenticated?(scope = @config.default_scope)
  134. result = !authenticated?(scope)
  135. yield if block_given? && result
  136. result
  137. end
  138. # Manually set the user into the session and auth proxy
  139. #
  140. # Parameters:
  141. # user - An object that has been setup to serialize into and out of the session.
  142. # 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).
  143. #
  144. # :api: public
  145. def set_user(user, opts = {})
  146. scope = (opts[:scope] ||= @config.default_scope)
  147. # Get the default options from the master configuration for the given scope
  148. opts = (@config[:scope_defaults][scope] || {}).merge(opts)
  149. opts[:event] ||= :set_user
  150. @users[scope] = user
  151. if opts[:store] != false && opts[:event] != :fetch
  152. options = env[ENV_SESSION_OPTIONS]
  153. options[:renew] = true if options
  154. session_serializer.store(user, scope)
  155. end
  156. run_callbacks = opts.fetch(:run_callbacks, true)
  157. manager._run_callbacks(:after_set_user, user, self, opts) if run_callbacks
  158. @users[scope]
  159. end
  160. # Provides acccess to the user object in a given scope for a request.
  161. # Will be nil if not logged in. Please notice that this method does not
  162. # perform strategies.
  163. #
  164. # Example:
  165. # # without scope (default user)
  166. # env['warden'].user
  167. #
  168. # # with scope
  169. # env['warden'].user(:admin)
  170. #
  171. # # as a Hash
  172. # env['warden'].user(:scope => :admin)
  173. #
  174. # # with default scope and run_callbacks option
  175. # env['warden'].user(:run_callbacks => false)
  176. #
  177. # # with a scope and run_callbacks option
  178. # env['warden'].user(:scope => :admin, :run_callbacks => true)
  179. #
  180. # :api: public
  181. def user(argument = {})
  182. opts = argument.is_a?(Hash) ? argument : { :scope => argument }
  183. scope = (opts[:scope] ||= @config.default_scope)
  184. if @users.has_key?(scope)
  185. @users[scope]
  186. else
  187. unless user = session_serializer.fetch(scope)
  188. run_callbacks = opts.fetch(:run_callbacks, true)
  189. manager._run_callbacks(:after_failed_fetch, user, self, :scope => scope) if run_callbacks
  190. end
  191. @users[scope] = user ? set_user(user, opts.merge(:event => :fetch)) : nil
  192. end
  193. end
  194. # Provides a scoped session data for authenticated users.
  195. # Warden manages clearing out this data when a user logs out
  196. #
  197. # Example
  198. # # default scope
  199. # env['warden'].session[:foo] = "bar"
  200. #
  201. # # :sudo scope
  202. # env['warden'].session(:sudo)[:foo] = "bar"
  203. #
  204. # :api: public
  205. def session(scope = @config.default_scope)
  206. raise NotAuthenticated, "#{scope.inspect} user is not logged in" unless authenticated?(scope)
  207. raw_session["warden.user.#{scope}.session"] ||= {}
  208. end
  209. # Provides logout functionality.
  210. # The logout also manages any authenticated data storage and clears it when a user logs out.
  211. #
  212. # Parameters:
  213. # scopes - a list of scopes to logout
  214. #
  215. # Example:
  216. # # Logout everyone and clear the session
  217. # env['warden'].logout
  218. #
  219. # # Logout the default user but leave the rest of the session alone
  220. # env['warden'].logout(:default)
  221. #
  222. # # Logout the :publisher and :admin user
  223. # env['warden'].logout(:publisher, :admin)
  224. #
  225. # :api: public
  226. def logout(*scopes)
  227. if scopes.empty?
  228. scopes = @users.keys
  229. reset_session = true
  230. end
  231. scopes.each do |scope|
  232. user = @users.delete(scope)
  233. manager._run_callbacks(:before_logout, user, self, :scope => scope)
  234. raw_session.delete("warden.user.#{scope}.session")
  235. session_serializer.delete(scope, user)
  236. end
  237. reset_session! if reset_session
  238. end
  239. # proxy methods through to the winning strategy
  240. # :api: private
  241. def result # :nodoc:
  242. winning_strategy && winning_strategy.result
  243. end
  244. # Proxy through to the authentication strategy to find out the message that was generated.
  245. # :api: public
  246. def message
  247. winning_strategy && winning_strategy.message
  248. end
  249. # Provides a way to return a 401 without warden defering to the failure app
  250. # The result is a direct passthrough of your own response
  251. # :api: public
  252. def custom_failure!
  253. @custom_failure = true
  254. end
  255. # Check to see if the custom failure flag has been set
  256. # :api: public
  257. def custom_failure?
  258. !!@custom_failure
  259. end
  260. # Check to see if this is an asset request
  261. # :api: public
  262. def asset_request?
  263. ::Warden::asset_paths.any? { |r| env['PATH_INFO'].to_s.match(r) }
  264. end
  265. def inspect(*args)
  266. "Warden::Proxy:#{object_id} @config=#{@config.inspect}"
  267. end
  268. def to_s(*args)
  269. inspect(*args)
  270. end
  271. private
  272. def _perform_authentication(*args)
  273. scope, opts = _retrieve_scope_and_opts(args)
  274. user = nil
  275. # Look for an existing user in the session for this scope.
  276. # If there was no user in the session. See if we can get one from the request.
  277. return user, opts if user = user(opts.merge(:scope => scope))
  278. _run_strategies_for(scope, args)
  279. if winning_strategy && winning_strategy.user
  280. opts[:store] = opts.fetch(:store, winning_strategy.store?)
  281. set_user(winning_strategy.user, opts.merge!(:event => :authentication))
  282. end
  283. [@users[scope], opts]
  284. end
  285. def _retrieve_scope_and_opts(args) #:nodoc:
  286. opts = args.last.is_a?(Hash) ? args.pop : {}
  287. scope = opts[:scope] || @config.default_scope
  288. opts = (@config[:scope_defaults][scope] || {}).merge(opts)
  289. [scope, opts]
  290. end
  291. # Run the strategies for a given scope
  292. def _run_strategies_for(scope, args) #:nodoc:
  293. self.winning_strategy = @winning_strategies[scope]
  294. return if winning_strategy && winning_strategy.halted?
  295. # Do not run any strategy if locked
  296. return if @locked
  297. if args.empty?
  298. defaults = @config[:default_strategies]
  299. strategies = defaults[scope] || defaults[:_all]
  300. end
  301. (strategies || args).each do |name|
  302. strategy = _fetch_strategy(name, scope)
  303. next unless strategy && !strategy.performed? && strategy.valid?
  304. self.winning_strategy = @winning_strategies[scope] = strategy
  305. strategy._run!
  306. break if strategy.halted?
  307. end
  308. end
  309. # Fetchs strategies and keep them in a hash cache.
  310. def _fetch_strategy(name, scope)
  311. @strategies[scope][name] ||= if klass = Warden::Strategies[name]
  312. klass.new(@env, scope)
  313. elsif @config.silence_missing_strategies?
  314. nil
  315. else
  316. raise "Invalid strategy #{name}"
  317. end
  318. end
  319. end # Proxy
  320. end # Warden