.gitignore | ●●●●● patch | view | raw | blame | history | |
CHANGES.txt | ●●●●● patch | view | raw | blame | history | |
docs/api/authentication.rst | ●●●●● patch | view | raw | blame | history | |
pyramid/authentication.py | ●●●●● patch | view | raw | blame | history | |
pyramid/tests/test_authentication.py | ●●●●● 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 = []