summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMichael Merickel <michael@merickel.org>2016-08-31 23:40:39 -0500
committerMichael Merickel <michael@merickel.org>2016-08-31 23:40:39 -0500
commitf9a5c5ba01066dfc81805def6aac8dab9dde2d67 (patch)
tree8c25b01203992222b7fe0989cf45d641dd4bd99c
parentdd0a1589a3d6cbf4557ba4987fae48b715bf1714 (diff)
parent693cb098a7bc8fbff5fb97c1ac031d0b6e397060 (diff)
downloadpyramid-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.txt5
-rw-r--r--CONTRIBUTORS.txt2
-rw-r--r--docs/api/authentication.rst3
-rw-r--r--pyramid/authentication.py86
-rw-r--r--pyramid/tests/test_authentication.py73
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