# -*- coding: utf-8 -*- ############################################################################## # # Copyright (c) 2009, Gustavo Narea . # All Rights Reserved. # # This software is subject to the provisions of the BSD-like license at # http://www.repoze.org/LICENSE.txt. A copy of the license should accompany # this distribution. THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL # EXPRESS OR IMPLIED WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, # THE IMPLIED WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND # FITNESS FOR A PARTICULAR PURPOSE. # ############################################################################## """Test utilities for repoze.who-powered applications.""" import sys from logging import INFO from re import compile as compile_regex from zope.interface import implements from paste.httpexceptions import HTTPUnauthorized from paste.deploy.converters import asbool from repoze.who.middleware import PluggableAuthenticationMiddleware from repoze.who.config import WhoConfig, \ make_middleware_with_config as mk_mw_cfg from repoze.who.interfaces import IIdentifier, IAuthenticator, IChallenger __all__ = ['AuthenticationForgerPlugin', 'AuthenticationForgerMiddleware', 'make_middleware', 'make_middleware_with_config'] _HTTP_STATUS_PATTERN = compile_regex(r'^(?P[0-9]{3}) (?P.*)$') class AuthenticationForgerPlugin(object): """ :mod:`repoze.who` plugin to forge authentication easily and bypass :mod:`repoze.who` challenges. This plugin enables you to write identifier and challenger-independent tests. As a result, your protected areas will be easier to test: #. To forge authentication, without bypassing identification (i.e., running MD providers), you can use the following WebTest-powered test:: def test_authorization_granted(self): '''The right subject must get what she requested''' environ = {'REMOTE_USER': 'manager'} resp = self.app.get('/admin/', extra_environ=environ, status=200) assert 'some text' in resp.body As you can see, this is an identifier-independent way to forge authentication. #. To check that authorization was denied, in a challenger-independent way, you can use:: def test_authorization_denied_anonymous(self): '''Anonymous users must get a 401 page''' self.app.get('/admin/', status=401) def test_authorization_denied_authenticated(self): '''Authenticated users must get a 403 page''' environ = {'REMOTE_USER': 'editor'} self.app.get('/admin/', extra_environ=environ, status=403) """ implements(IIdentifier, IAuthenticator, IChallenger) def __init__(self, fake_user_key='REMOTE_USER', remote_user_key='repoze.who.testutil.userid'): """ :param fake_user_key: The key for the item in the ``environ`` which will contain the forged user Id. :type fake_user_key: str :param remote_user_key: The actual "external" ``remote_user_key`` used by :mod:`repoze.who`. :type remote_user_key: str """ self.fake_user_key = fake_user_key self.remote_user_key = remote_user_key # IIdentifier def identify(self, environ): """ Pre-authenticate using the user Id found in the relevant ``environ`` item, if any. The user Id. found will be put into ``identity['fake-userid']``, for :meth:`authenticate`. """ if self.fake_user_key in environ: identity = {'fake-userid': environ[self.fake_user_key]} return identity # IIdentifier def remember(self, environ, identity): """Do nothing""" pass # IIdentifier def forget(self, environ, identity): """Do nothing""" pass # IAuthenticator def authenticate(self, environ, identity): """ Turn the value in ``identity['fake-userid']`` into the remote user's name. Finally, it removes ``identity['fake-userid']`` so that it won't reach the WSGI application. """ if 'fake-userid' in identity: environ[self.remote_user_key] = identity.pop('fake-userid') return environ[self.remote_user_key] # IChallenger def challenge(self, environ, status, app_headers, forget_headers): """Return a 401 page unconditionally.""" headers = app_headers + forget_headers # The HTTP status code and reason may not be the default ones: status_parts = _HTTP_STATUS_PATTERN.search(status) if status_parts: reason = status_parts.group('reason') code = int(status_parts.group('code')) else: reason = 'HTTP Unauthorized' code = 401 # Building the response: response = HTTPUnauthorized(headers=headers) response.title = reason response.code = code return response class AuthenticationForgerMiddleware(PluggableAuthenticationMiddleware): """ :class:`PluggableAuthenticationMiddleware ` proxy to forge authentication, without bypassing identification. """ def __init__(self, app, identifiers, authenticators, challengers, mdproviders, classifier, challenge_decider, log_stream=None, log_level=INFO, remote_user_key='REMOTE_USER'): """ Setup authentication in an easy to forge way. All the arguments received will be passed as is to :class:`repoze.who.middleware.PluggableAuthenticationMiddleware`, with one instance of :class:`AuthenticationForgerPlugin` in: * ``identifiers``. This instance will be inserted in the first position of the list. * ``authenticators``. Any authenticator passed will be ignored; such an instance will be the only authenticator defined. * ``challengers``. Any challenger passed will be ignored; such an instance will be the only challenger defined. Internally, it will also set ``remote_user_key`` to ``'repoze.who.testutil.userid'``, so that you can use the standard ``'REMOTE_USER'`` in your tests. The metadata providers won't be modified. """ self.actual_remote_user_key = remote_user_key forger = AuthenticationForgerPlugin(fake_user_key=remote_user_key) forger = ('auth_forger', forger) identifiers.insert(0, forger) authenticators = [forger] challengers = [forger] # Calling the parent's constructor: init = super(AuthenticationForgerMiddleware, self).__init__ init(app, identifiers, authenticators, challengers, mdproviders, classifier, challenge_decider, log_stream, log_level, 'repoze.who.testutil.userid') #{ Middleware makers: def make_middleware(skip_authentication=False, *args, **kwargs): """ Return the requested authentication middleware. :param skip_authentication: If ``True``, an instance of :class:`AuthenticationForgerMiddleware` will be returned instead of :class:`repoze.who.middleware.PluggableAuthenticationMiddleware` :type skip_authentication: bool ``args`` and ``kwargs`` are the positional and named arguments, respectively, to be passed to the relevant authentication middleware. """ if asbool(skip_authentication): # We must replace the middleware: return AuthenticationForgerMiddleware(*args, **kwargs) else: return PluggableAuthenticationMiddleware(*args, **kwargs) def make_middleware_with_config(app, global_conf, config_file, log_file=None, log_level=None, skip_authentication=False): """ Proxy :func:`repoze.who.config.make_middleware_with_config` to skip authentication when required. If ``skip_authentication`` evaluates to ``True``, then the returned middleware will be an instance of :class:`AuthenticationForgerMiddleware`. """ if not asbool(skip_authentication): # We must not replace the middleware return mk_mw_cfg(app, global_conf, config_file, log_file, log_level) # We must replace the middleware: parser = WhoConfig(global_conf['here']) parser.parse(open(config_file)) return AuthenticationForgerMiddleware( app, parser.identifiers, parser.authenticators, parser.challengers, parser.mdproviders, parser.request_classifier, parser.challenge_decider, remote_user_key=parser.remote_user_key, ) #}