Chris McDonough
2012-10-14 6c4a1e7bdc4239e3c8fa3cfe6673a6e800ce113d
Merge branch 'feature-basic-auth'
5 files modified
221 ■■■■ changed files
.gitignore 2 ●●●●● patch | view | raw | blame | history
CHANGES.txt 5 ●●●● patch | view | raw | blame | history
docs/api/authentication.rst 6 ●●●●● patch | view | raw | blame | history
pyramid/authentication.py 108 ●●●●● patch | view | raw | blame | history
pyramid/tests/test_authentication.py 100 ●●●● patch | view | raw | blame | history
.gitignore
@@ -5,6 +5,7 @@
*.pt.py
*.txt.py
*~
.*.swp
.coverage
.tox/
nosetests.xml
@@ -21,3 +22,4 @@
jyenv/
pypyenv/
env*/
venv/
CHANGES.txt
@@ -11,6 +11,9 @@
- Comments with references to documentation sections placed in scaffold
  ``.ini`` files.
- Added an HTTP Basic authentication policy
  at ``pyramid.authentication.BasicAuthAuthenticationPolicy``.
Bug Fixes
---------
@@ -30,7 +33,7 @@
- When registering a view configuration that named a Chameleon ZPT renderer
  with a macro name in it (e.g. ``renderer='some/template#somemacro.pt``) as
  well as a view configuration without a macro name it it that pointed to the
  same template (e.g. ``renderer='some/template.pt'), internal caching could
  same template (e.g. ``renderer='some/template.pt'``), internal caching could
  confuse the two, and your code might have rendered one instead of the
  other.
docs/api/authentication.rst
@@ -10,12 +10,14 @@
  .. autoclass:: AuthTktAuthenticationPolicy
  .. autoclass:: RepozeWho1AuthenticationPolicy
  .. autoclass:: RemoteUserAuthenticationPolicy
  .. autoclass:: SessionAuthenticationPolicy
  .. autoclass:: BasicAuthAuthenticationPolicy
  .. autoclass:: RepozeWho1AuthenticationPolicy
Helper Classes
~~~~~~~~~~~~~~
pyramid/authentication.py
@@ -1,3 +1,4 @@
import binascii
from codecs import utf_8_decode
from codecs import utf_8_encode
from hashlib import md5
@@ -330,13 +331,13 @@
       Optional.
    ``path``
       Default: ``/``. The path for which the auth_tkt cookie is valid.
       May be desirable if the application only serves part of a domain.
       Optional.
    ``http_only``
       Default: ``False``. Hide cookie from JavaScript by setting the
       HttpOnly flag. Not honored by all browsers.
       Optional.
@@ -553,7 +554,7 @@
        text_type: ('b64unicode', lambda x: b64encode(utf_8_encode(x)[0])),
        binary_type: ('b64str', lambda x: b64encode(x)),
        }
    def __init__(self, secret, cookie_name='auth_tkt', secure=False,
                 include_ip=False, timeout=None, reissue_time=None,
                 max_age=None, http_only=False, path="/", wild_domain=True):
@@ -632,7 +633,7 @@
            remote_addr = environ['REMOTE_ADDR']
        else:
            remote_addr = '0.0.0.0'
        try:
            timestamp, userid, tokens, user_data = self.parse_ticket(
                self.secret, cookie, remote_addr)
@@ -641,7 +642,7 @@
        now = self.now # service tests
        if now is None:
        if now is None:
            now = time_mod.time()
        if self.timeout and ( (timestamp + self.timeout) < now ):
@@ -689,7 +690,7 @@
        environ = request.environ
        request._authtkt_reissue_revoked = True
        return self._get_cookies(environ, '', max_age=EXPIRE)
    def remember(self, request, userid, max_age=None, tokens=()):
        """ Return a set of Set-Cookie headers; when set into a response,
        these headers will represent a valid authentication ticket.
@@ -783,7 +784,7 @@
        Pyramid debug logger about the results of various authentication
        steps.  The output from debugging is useful for reporting to maillist
        or IRC channels when asking for support.
    """
    def __init__(self, prefix='auth.', callback=None, debug=False):
@@ -806,3 +807,94 @@
    def unauthenticated_userid(self, request):
        return request.session.get(self.userid_key)
@implementer(IAuthenticationPolicy)
class BasicAuthAuthenticationPolicy(CallbackAuthenticationPolicy):
    """ A :app:`Pyramid` authentication policy which uses HTTP standard basic
    authentication protocol to authenticate users.  To use this policy you will
    need to provide a callback which checks the supplied user credentials
    against your source of login data.
    Constructor Arguments
    ``check``
       A callback function passed a username, password and request, in that
       order as positional arguments.  Expected to return ``None`` if the
       userid doesn't exist or a sequence of principal identifiers (possibly
       empty) if the user does exist.
    ``realm``
       Default: ``"Realm"``.  The Basic Auth Realm string.  Usually displayed to
       the user by the browser in the login dialog.
    ``debug``
        Default: ``False``.  If ``debug`` is ``True``, log messages to the
        Pyramid debug logger about the results of various authentication
        steps.  The output from debugging is useful for reporting to maillist
        or IRC channels when asking for support.
    **Issuing a challenge**
    Regular browsers will not send username/password credentials unless they
    first receive a challenge from the server.  The following recipe will
    register a view that will send a Basic Auth challenge to the user whenever
    there is an attempt to call a view which results in a Forbidden response::
        from pyramid.httpexceptions import HTTPForbidden
        from pyramid.httpexceptions import HTTPUnauthorized
        from pyramid.security import forget
        from pyramid.view import view_config
        @view_config(context=HTTPForbidden)
        def basic_challenge(request):
            response = HTTPUnauthorized()
            response.headers.update(forget(request))
            return response
    """
    def __init__(self, check, realm='Realm', debug=False):
        self.check = check
        self.realm = realm
        self.debug = debug
    def unauthenticated_userid(self, request):
        credentials = self._get_credentials(request)
        if credentials:
            return credentials[0]
    def remember(self, request, principal, **kw):
        return []
    def forget(self, request):
        return [('WWW-Authenticate', 'Basic realm="%s"' % self.realm)]
    def callback(self, username, request):
        # Username arg is ignored.  Unfortunately _get_credentials winds up
        # getting called twice when authenticated_userid is called.  Avoiding
        # that, however, winds up duplicating logic from the superclass.
        credentials = self._get_credentials(request)
        if credentials:
            username, password = credentials
            return self.check(username, password, request)
    def _get_credentials(self, request):
        authorization = request.headers.get('Authorization')
        if not authorization:
            return None
        try:
            authmeth, auth = authorization.split(' ', 1)
        except ValueError: # not enough values to unpack
            return None
        if authmeth.lower() != 'basic':
            return None
        try:
            auth = b64decode(auth.strip()).decode('ascii')
        except (TypeError, binascii.Error): # can't decode
            return None
        try:
            username, password = auth.split(':', 1)
        except ValueError: # not enough values to unpack
            return None
        return username, password
pyramid/tests/test_authentication.py
@@ -14,7 +14,7 @@
    def tearDown(self):
        del self.config
    def debug(self, msg):
        self.messages.append(msg)
@@ -151,7 +151,7 @@
    def _makeOne(self, identifier_name='auth_tkt', callback=None):
        return self._getTargetClass()(identifier_name, callback)
    def test_class_implements_IAuthenticationPolicy(self):
        from zope.interface.verify import verifyClass
        from pyramid.interfaces import IAuthenticationPolicy
@@ -251,7 +251,7 @@
        result = policy.remember(request, 'fred')
        self.assertEqual(result[0], request.environ)
        self.assertEqual(result[1], {'repoze.who.userid':'fred'})
    def test_forget_no_plugins(self):
        request = DummyRequest({})
        policy = self._makeOne()
@@ -276,7 +276,7 @@
    def _makeOne(self, environ_key='REMOTE_USER', callback=None):
        return self._getTargetClass()(environ_key, callback)
    def test_class_implements_IAuthenticationPolicy(self):
        from zope.interface.verify import verifyClass
        from pyramid.interfaces import IAuthenticationPolicy
@@ -301,7 +301,7 @@
        request = DummyRequest({})
        policy = self._makeOne()
        self.assertEqual(policy.authenticated_userid(request), None)
    def test_authenticated_userid(self):
        request = DummyRequest({'REMOTE_USER':'fred'})
        policy = self._makeOne()
@@ -326,7 +326,7 @@
        policy = self._makeOne()
        result = policy.remember(request, 'fred')
        self.assertEqual(result, [])
    def test_forget(self):
        request = DummyRequest({'REMOTE_USER':'fred'})
        policy = self._makeOne()
@@ -375,7 +375,7 @@
        request = DummyRequest({})
        policy = self._makeOne(None, None)
        self.assertEqual(policy.authenticated_userid(request), None)
    def test_authenticated_userid_callback_returns_None(self):
        request = DummyRequest({})
        def callback(userid, request):
@@ -426,7 +426,7 @@
        result = policy.remember(request, 'fred', a=1, b=2)
        self.assertEqual(policy.cookie.kw, {'a':1, 'b':2})
        self.assertEqual(result, [])
    def test_forget(self):
        request = DummyRequest({})
        policy = self._makeOne(None, None)
@@ -482,7 +482,7 @@
        request = self._makeRequest(None)
        result = helper.identify(request)
        self.assertEqual(result, None)
    def test_identify_good_cookie_include_ip(self):
        helper = self._makeOne('secret', include_ip=True)
        request = self._makeRequest('ticket')
@@ -605,7 +605,7 @@
        request = self._makeRequest('ticket')
        result = helper.identify(request)
        self.assertEqual(result, None)
    def test_identify_cookie_timed_out(self):
        helper = self._makeOne('secret', timeout=1)
        request = self._makeRequest({'HTTP_COOKIE':'auth_tkt=bogus'})
@@ -828,7 +828,7 @@
        self.assertEqual(result[1][0], 'Set-Cookie')
        self.assertTrue(result[1][1].endswith('; Path=/; Domain=example.com'))
        self.assertTrue(result[1][1].startswith('auth_tkt='))
    def test_remember_binary_userid(self):
        import base64
        helper = self._makeOne('secret')
@@ -1106,6 +1106,78 @@
        self.assertEqual(request.session.get('userid'), None)
        self.assertEqual(result, [])
class TestBasicAuthAuthenticationPolicy(unittest.TestCase):
    def _getTargetClass(self):
        from pyramid.authentication import BasicAuthAuthenticationPolicy as cls
        return cls
    def _makeOne(self, check):
        return self._getTargetClass()(check, realm='SomeRealm')
    def test_class_implements_IAuthenticationPolicy(self):
        from zope.interface.verify import verifyClass
        from pyramid.interfaces import IAuthenticationPolicy
        verifyClass(IAuthenticationPolicy, self._getTargetClass())
    def test_unauthenticated_userid(self):
        import base64
        request = testing.DummyRequest()
        request.headers['Authorization'] = 'Basic %s' % base64.b64encode(
            bytes_('chrisr:password')).decode('ascii')
        policy = self._makeOne(None)
        self.assertEqual(policy.unauthenticated_userid(request), 'chrisr')
    def test_unauthenticated_userid_no_credentials(self):
        request = testing.DummyRequest()
        policy = self._makeOne(None)
        self.assertEqual(policy.unauthenticated_userid(request), None)
    def test_unauthenticated_bad_header(self):
        request = testing.DummyRequest()
        request.headers['Authorization'] = '...'
        policy = self._makeOne(None)
        self.assertEqual(policy.unauthenticated_userid(request), None)
    def test_unauthenticated_userid_not_basic(self):
        request = testing.DummyRequest()
        request.headers['Authorization'] = 'Complicated things'
        policy = self._makeOne(None)
        self.assertEqual(policy.unauthenticated_userid(request), None)
    def test_unauthenticated_userid_corrupt_base64(self):
        request = testing.DummyRequest()
        request.headers['Authorization'] = 'Basic chrisr:password'
        policy = self._makeOne(None)
        self.assertEqual(policy.unauthenticated_userid(request), None)
    def test_authenticated_userid(self):
        import base64
        request = testing.DummyRequest()
        request.headers['Authorization'] = 'Basic %s' % base64.b64encode(
            bytes_('chrisr:password')).decode('ascii')
        def check(username, password, request):
            return []
        policy = self._makeOne(check)
        self.assertEqual(policy.authenticated_userid(request), 'chrisr')
    def test_unauthenticated_userid_invalid_payload(self):
        import base64
        request = testing.DummyRequest()
        request.headers['Authorization'] = 'Basic %s' % base64.b64encode(
            bytes_('chrisrpassword')).decode('ascii')
        policy = self._makeOne(None)
        self.assertEqual(policy.unauthenticated_userid(request), None)
    def test_remember(self):
        policy = self._makeOne(None)
        self.assertEqual(policy.remember(None, None), [])
    def test_forget(self):
        policy = self._makeOne(None)
        self.assertEqual(policy.forget(None), [
            ('WWW-Authenticate', 'Basic realm="SomeRealm"')])
class DummyContext:
    pass
@@ -1130,7 +1202,7 @@
class DummyWhoPlugin:
    def remember(self, environ, identity):
        return environ, identity
    def forget(self, environ, identity):
        return environ, identity
@@ -1164,7 +1236,7 @@
                raise self.BadTicket()
            return self.timestamp, self.userid, self.tokens, self.user_data
        self.parse_ticket = parse_ticket
        class AuthTicket(object):
            def __init__(self, secret, userid, remote_addr, **kw):
                self.secret = secret
@@ -1186,4 +1258,4 @@
class DummyResponse:
    def __init__(self):
        self.headerlist = []