diff options
| author | Michael Merickel <michael@merickel.org> | 2016-08-31 23:40:39 -0500 |
|---|---|---|
| committer | Michael Merickel <michael@merickel.org> | 2016-08-31 23:40:39 -0500 |
| commit | f9a5c5ba01066dfc81805def6aac8dab9dde2d67 (patch) | |
| tree | 8c25b01203992222b7fe0989cf45d641dd4bd99c | |
| parent | dd0a1589a3d6cbf4557ba4987fae48b715bf1714 (diff) | |
| parent | 693cb098a7bc8fbff5fb97c1ac031d0b6e397060 (diff) | |
| download | pyramid-f9a5c5ba01066dfc81805def6aac8dab9dde2d67.tar.gz pyramid-f9a5c5ba01066dfc81805def6aac8dab9dde2d67.tar.bz2 pyramid-f9a5c5ba01066dfc81805def6aac8dab9dde2d67.zip | |
Merge branch 'extract_http_basic' of canni/pyramid into canni-extract_http_basic
| -rw-r--r-- | CHANGES.txt | 5 | ||||
| -rw-r--r-- | CONTRIBUTORS.txt | 2 | ||||
| -rw-r--r-- | docs/api/authentication.rst | 3 | ||||
| -rw-r--r-- | pyramid/authentication.py | 86 | ||||
| -rw-r--r-- | pyramid/tests/test_authentication.py | 73 |
5 files changed, 136 insertions, 33 deletions
diff --git a/CHANGES.txt b/CHANGES.txt index f679f9993..b485ae59e 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -19,6 +19,11 @@ Backward Incompatibilities Features -------- +- The `_get_credentials` private method of `BasicAuthAuthenticationPolicy` + has been extracted into standalone function `extract_http_basic_credentials` + in `pyramid.authentication` module, this function extracts HTTP Basic + credentials from `request` object, and returns them as a named tuple. + Bug Fixes --------- diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index 12b6fedcf..bb21337e2 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -279,6 +279,8 @@ Contributors - Jean-Christophe Bohin, 2016/06/13 +- Dariusz Gorecki, 2016/07/15 + - Jon Davidson, 2016/07/18 - Keith Yang, 2016/07/22 diff --git a/docs/api/authentication.rst b/docs/api/authentication.rst index 19d08618b..de2c73491 100644 --- a/docs/api/authentication.rst +++ b/docs/api/authentication.rst @@ -35,4 +35,7 @@ Helper Classes :members: +Helper Functions +~~~~~~~~~~~~~~~~ + .. autofunction:: extract_http_basic_credentials diff --git a/pyramid/authentication.py b/pyramid/authentication.py index 8d0adfa3d..7d766fd06 100644 --- a/pyramid/authentication.py +++ b/pyramid/authentication.py @@ -1,6 +1,7 @@ import binascii from codecs import utf_8_decode from codecs import utf_8_encode +from collections import namedtuple import hashlib import base64 import re @@ -1095,7 +1096,7 @@ class BasicAuthAuthenticationPolicy(CallbackAuthenticationPolicy): def unauthenticated_userid(self, request): """ The userid parsed from the ``Authorization`` request header.""" - credentials = self._get_credentials(request) + credentials = extract_http_basic_credentials(request) if credentials: return credentials[0] @@ -1112,42 +1113,15 @@ class BasicAuthAuthenticationPolicy(CallbackAuthenticationPolicy): 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) + # Username arg is ignored. Unfortunately + # extract_http_basic_credentials winds up getting called twice when + # authenticated_userid is called. Avoiding that, however, + # winds up duplicating logic from the superclass. + credentials = extract_http_basic_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: - authbytes = b64decode(auth.strip()) - except (TypeError, binascii.Error): # can't decode - return None - - # try utf-8 first, then latin-1; see discussion in - # https://github.com/Pylons/pyramid/issues/898 - try: - auth = authbytes.decode('utf-8') - except UnicodeDecodeError: - auth = authbytes.decode('latin-1') - - try: - username, password = auth.split(':', 1) - except ValueError: # not enough values to unpack - return None - return username, password class _SimpleSerializer(object): def loads(self, bstruct): @@ -1155,3 +1129,49 @@ class _SimpleSerializer(object): def dumps(self, appstruct): return bytes_(appstruct) + + +http_basic_credentials = namedtuple('http_basic_credentials', + ['username', 'password']) + + +def extract_http_basic_credentials(request): + """ A helper function for extraction of HTTP Basic credentials + from a given :term:`request`. Returned values: + + - ``None`` - when credentials couldn't be extracted + - ``namedtuple`` with extracted ``username`` and ``password`` attributes + + ``request`` + The :term:`request` object + """ + 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: + authbytes = b64decode(auth.strip()) + except (TypeError, binascii.Error): # can't decode + return None + + # try utf-8 first, then latin-1; see discussion in + # https://github.com/Pylons/pyramid/issues/898 + try: + auth = authbytes.decode('utf-8') + except UnicodeDecodeError: + auth = authbytes.decode('latin-1') + + try: + username, password = auth.split(':', 1) + except ValueError: # not enough values to unpack + return None + + return http_basic_credentials(username, password) diff --git a/pyramid/tests/test_authentication.py b/pyramid/tests/test_authentication.py index ce9e50719..b9a4c6be4 100644 --- a/pyramid/tests/test_authentication.py +++ b/pyramid/tests/test_authentication.py @@ -1479,6 +1479,79 @@ class TestBasicAuthAuthenticationPolicy(unittest.TestCase): self.assertEqual(policy.forget(None), [ ('WWW-Authenticate', 'Basic realm="SomeRealm"')]) + +class TestExtractHTTPBasicCredentials(unittest.TestCase): + def _get_func(self): + from pyramid.authentication import extract_http_basic_credentials + return extract_http_basic_credentials + + def test_no_auth_header(self): + request = testing.DummyRequest() + fn = self._get_func() + + self.assertIsNone(fn(request)) + + def test_invalid_payload(self): + import base64 + request = testing.DummyRequest() + request.headers['Authorization'] = 'Basic %s' % base64.b64encode( + bytes_('chrisrpassword')).decode('ascii') + fn = self._get_func() + self.assertIsNone(fn(request)) + + def test_not_a_basic_auth_scheme(self): + import base64 + request = testing.DummyRequest() + request.headers['Authorization'] = 'OtherScheme %s' % base64.b64encode( + bytes_('chrisr:password')).decode('ascii') + fn = self._get_func() + self.assertIsNone(fn(request)) + + def test_no_base64_encoding(self): + request = testing.DummyRequest() + request.headers['Authorization'] = 'Basic ...' + fn = self._get_func() + self.assertIsNone(fn(request)) + + def test_latin1_payload(self): + import base64 + request = testing.DummyRequest() + inputs = (b'm\xc3\xb6rk\xc3\xb6:' + b'm\xc3\xb6rk\xc3\xb6password').decode('utf-8') + request.headers['Authorization'] = 'Basic %s' % ( + base64.b64encode(inputs.encode('latin-1')).decode('latin-1')) + fn = self._get_func() + self.assertEqual(fn(request), ( + b'm\xc3\xb6rk\xc3\xb6'.decode('utf-8'), + b'm\xc3\xb6rk\xc3\xb6password'.decode('utf-8') + )) + + def test_utf8_payload(self): + import base64 + request = testing.DummyRequest() + inputs = (b'm\xc3\xb6rk\xc3\xb6:' + b'm\xc3\xb6rk\xc3\xb6password').decode('utf-8') + request.headers['Authorization'] = 'Basic %s' % ( + base64.b64encode(inputs.encode('utf-8')).decode('latin-1')) + fn = self._get_func() + self.assertEqual(fn(request), ( + b'm\xc3\xb6rk\xc3\xb6'.decode('utf-8'), + b'm\xc3\xb6rk\xc3\xb6password'.decode('utf-8') + )) + + def test_namedtuple_return(self): + import base64 + request = testing.DummyRequest() + request.headers['Authorization'] = 'Basic %s' % base64.b64encode( + bytes_('chrisr:pass')).decode('ascii') + fn = self._get_func() + result = fn(request) + + self.assertEqual(result.username, 'chrisr') + self.assertEqual(result.password, 'pass') + + + class TestSimpleSerializer(unittest.TestCase): def _makeOne(self): from pyramid.authentication import _SimpleSerializer |
