# -*- 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,
)
#}