summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGES.txt20
-rw-r--r--CONTRIBUTORS.txt2
-rw-r--r--docs/api/authentication.rst5
-rw-r--r--pyramid/authentication.py100
-rw-r--r--pyramid/scripts/proutes.py2
-rw-r--r--pyramid/tests/test_authentication.py78
6 files changed, 170 insertions, 37 deletions
diff --git a/CHANGES.txt b/CHANGES.txt
index 02e3271ce..3eb23b9ec 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -14,9 +14,17 @@ Backward Incompatibilities
To run your server as a daemon you should use a process manager instead of
pserve.
+ See https://github.com/Pylons/pyramid/pull/2615
+
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 a ``request`` object, and returns them as a named tuple.
+ See https://github.com/Pylons/pyramid/pull/2662
+
Bug Fixes
---------
@@ -24,6 +32,18 @@ Bug Fixes
and `attr` is involved.
See: https://github.com/Pylons/pyramid/pull/2687
+- Fix a ``FutureWarning`` in Python 3.5 when using ``re.split`` on the
+ ``format`` setting to the ``proutes`` script.
+ See https://github.com/Pylons/pyramid/pull/2714
+
+- Fix a ``RuntimeWarning`` emitted by WebOb when using arbitrary objects
+ as the ``userid`` in the ``AuthTktAuthenticationPolicy``. This is now caught
+ by the policy and the object is serialized as a base64 string to avoid
+ the cryptic warning. Since the userid will be read back as a string on
+ subsequent requests a more useful warning is emitted encouraging you to
+ use a primitive type instead.
+ See https://github.com/Pylons/pyramid/pull/2715
+
Deprecations
------------
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..57f32327a 100644
--- a/docs/api/authentication.rst
+++ b/docs/api/authentication.rst
@@ -34,5 +34,10 @@ Helper Classes
.. autoclass:: AuthTktCookieHelper
:members:
+ .. autoclass:: HTTPBasicCredentials
+ :members:
+Helper Functions
+~~~~~~~~~~~~~~~~
+ .. autofunction:: extract_http_basic_credentials
diff --git a/pyramid/authentication.py b/pyramid/authentication.py
index e6b888db2..2ee5576d9 100644
--- a/pyramid/authentication.py
+++ b/pyramid/authentication.py
@@ -1,10 +1,12 @@
import binascii
from codecs import utf_8_decode
from codecs import utf_8_encode
+from collections import namedtuple
import hashlib
import base64
import re
import time as time_mod
+import warnings
from zope.interface import implementer
@@ -947,8 +949,19 @@ class AuthTktCookieHelper(object):
if encoding_data:
encoding, encoder = encoding_data
- userid = encoder(userid)
- user_data = 'userid_type:%s' % encoding
+ else:
+ warnings.warn(
+ "userid is of type {}, and is not supported by the "
+ "AuthTktAuthenticationPolicy. Explicitly converting to string "
+ "and storing as base64. Subsequent requests will receive a "
+ "string as the userid, it will not be decoded back to the type "
+ "provided.".format(type(userid)), RuntimeWarning
+ )
+ encoding, encoder = self.userid_type_encoders.get(text_type)
+ userid = str(userid)
+
+ userid = encoder(userid)
+ user_data = 'userid_type:%s' % encoding
new_tokens = []
for token in tokens:
@@ -1083,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]
@@ -1100,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):
@@ -1143,3 +1129,47 @@ class _SimpleSerializer(object):
def dumps(self, appstruct):
return bytes_(appstruct)
+
+
+HTTPBasicCredentials = namedtuple(
+ 'HTTPBasicCredentials', ['username', 'password'])
+
+
+def extract_http_basic_credentials(request):
+ """ A helper function for extraction of HTTP Basic credentials
+ from a given :term:`request`.
+
+ Returns a :class:`.HTTPBasicCredentials` 2-tuple with ``username`` and
+ ``password`` attributes or ``None`` if no credentials could be found.
+
+ """
+ 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 HTTPBasicCredentials(username, password)
diff --git a/pyramid/scripts/proutes.py b/pyramid/scripts/proutes.py
index 19d91cc72..f75810c06 100644
--- a/pyramid/scripts/proutes.py
+++ b/pyramid/scripts/proutes.py
@@ -296,7 +296,7 @@ class PRoutesCommand(object):
items = config.items('proutes')
for k, v in items:
if 'format' == k:
- cols = re.split(r'[,|\s|\n]*', v)
+ cols = re.split(r'[,|\s\n]+', v)
self.column_format = [x.strip() for x in cols]
except configparser.NoSectionError:
diff --git a/pyramid/tests/test_authentication.py b/pyramid/tests/test_authentication.py
index 0a22e5965..b9a4c6be4 100644
--- a/pyramid/tests/test_authentication.py
+++ b/pyramid/tests/test_authentication.py
@@ -1089,7 +1089,10 @@ class TestAuthTktCookieHelper(unittest.TestCase):
helper = self._makeOne('secret')
request = self._makeRequest()
userid = object()
- result = helper.remember(request, userid)
+ with warnings.catch_warnings(record=True) as w:
+ warnings.simplefilter('always', RuntimeWarning)
+ result = helper.remember(request, userid)
+ self.assertTrue(str(w[-1].message).startswith('userid is of type'))
values = self._parseHeaders(result)
self.assertEqual(len(result), 3)
value = values[0]
@@ -1476,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