Chris McDonough
2013-12-10 767e44f2fe7c238d0c67308e2e94241236a522e4
use CookieProfile from webob in authentication module, add support for new domain attribute on dummy request, depend on webob 1.3 or better
6 files modified
229 ■■■■■ changed files
CHANGES.txt 9 ●●●●● patch | view | raw | blame | history
docs/whatsnew-1.5.rst 2 ●●●●● patch | view | raw | blame | history
pyramid/authentication.py 86 ●●●●● patch | view | raw | blame | history
pyramid/testing.py 1 ●●●● patch | view | raw | blame | history
pyramid/tests/test_authentication.py 129 ●●●●● patch | view | raw | blame | history
setup.py 2 ●●● patch | view | raw | blame | history
CHANGES.txt
@@ -66,6 +66,9 @@
  to use a different query string format than ``x-www-form-urlencoded``.  See
  https://github.com/Pylons/pyramid/pull/1183
- ``pyramid.testing.DummyRequest`` now has a ``domain`` attribute to match the
  new WebOb 1.3 API.  Its value is ``example.com``.
Bug Fixes
---------
@@ -149,6 +152,12 @@
  Instead, use the newly-added ``unauthenticated_userid`` attribute of the
  request object.
Dependencies
------------
- Pyramid now depends on WebOb>=1.3 (it uses ``webob.cookies.CookieProfile``
  from 1.3+).
1.5a2 (2013-09-22)
==================
docs/whatsnew-1.5.rst
@@ -504,3 +504,5 @@
- Pyramid no longer depends upon ``Mako`` or ``Chameleon``.
- Pyramid now depends on WebOb>=1.3 (it uses ``webob.cookies.CookieProfile``
  from 1.3+).
pyramid/authentication.py
@@ -10,6 +10,8 @@
from zope.interface import implementer
from webob.cookies import CookieProfile
from pyramid.compat import (
    long,
    text_type,
@@ -18,6 +20,7 @@
    url_quote,
    bytes_,
    ascii_native_,
    native_,
    )
from pyramid.interfaces import (
@@ -798,8 +801,6 @@
    ts_chars = ''.join(map(chr, ts))
    return bytes_(ip_chars + ts_chars)
EXPIRE = object()
class AuthTktCookieHelper(object):
    """
    A helper class for use in third-party authentication policy
@@ -830,55 +831,32 @@
                 include_ip=False, timeout=None, reissue_time=None,
                 max_age=None, http_only=False, path="/", wild_domain=True,
                 hashalg='md5', parent_domain=False, domain=None):
        serializer = _SimpleSerializer()
        self.cookie_profile = CookieProfile(
            cookie_name = cookie_name,
            secure = secure,
            max_age = max_age,
            httponly = http_only,
            path = path,
            serializer=serializer
            )
        self.secret = secret
        self.cookie_name = cookie_name
        self.include_ip = include_ip
        self.secure = secure
        self.include_ip = include_ip
        self.timeout = timeout
        self.reissue_time = reissue_time
        self.max_age = max_age
        self.http_only = http_only
        self.path = path
        self.wild_domain = wild_domain
        self.parent_domain = parent_domain
        self.domain = domain
        self.hashalg = hashalg
        static_flags = []
        if self.secure:
            static_flags.append('; Secure')
        if self.http_only:
            static_flags.append('; HttpOnly')
        self.static_flags = "".join(static_flags)
    def _get_cookies(self, environ, value, max_age=None):
        if max_age is EXPIRE:
            max_age = "; Max-Age=0; Expires=Wed, 31-Dec-97 23:59:59 GMT"
        elif max_age is not None:
            later = datetime.datetime.utcnow() + datetime.timedelta(
                seconds=int(max_age))
            # Wdy, DD-Mon-YY HH:MM:SS GMT
            expires = later.strftime('%a, %d %b %Y %H:%M:%S GMT')
            # the Expires header is *required* at least for IE7 (IE7 does
            # not respect Max-Age)
            max_age = "; Max-Age=%s; Expires=%s" % (max_age, expires)
        else:
            max_age = ''
        cur_domain = environ.get('HTTP_HOST', environ.get('SERVER_NAME'))
        # While Chrome, IE, and Firefox can cope, Opera (at least) cannot
        # cope with a port number in the cookie domain when the URL it
        # receives the cookie from does not also have that port number in it
        # (e.g via a proxy).  In the meantime, HTTP_HOST is sent with port
        # number, and neither Firefox nor Chrome do anything with the
        # information when it's provided in a cookie domain except strip it
        # out.  So we strip out any port number from the cookie domain
        # aggressively to avoid problems.  See also
        # https://github.com/Pylons/pyramid/issues/131
        if ':' in cur_domain:
            cur_domain = cur_domain.split(':', 1)[0]
    def _get_cookies(self, request, value, max_age=None):
        cur_domain = request.domain
        domains = []
        if self.domain:
@@ -892,14 +870,15 @@
                if self.wild_domain:
                    domains.append('.' + cur_domain)
        cookies = []
        base_cookie = '%s="%s"; Path=%s%s%s' % (self.cookie_name, value,
                self.path, max_age, self.static_flags)
        for domain in domains:
            domain = '; Domain=%s' % domain if domain is not None else ''
            cookies.append(('Set-Cookie', '%s%s' % (base_cookie, domain)))
        profile = self.cookie_profile(request)
        return cookies
        kw = {}
        kw['domains'] = domains
        if max_age is not None:
            kw['max_age'] = max_age
        headers = profile.get_headers(value, **kw)
        return headers
    def identify(self, request):
        """ Return a dictionary with authentication information, or ``None``
@@ -968,9 +947,8 @@
    def forget(self, request):
        """ Return a set of expires Set-Cookie headers, which will destroy
        any existing auth_tkt cookie when attached to a response"""
        environ = request.environ
        request._authtkt_reissue_revoked = True
        return self._get_cookies(environ, '', max_age=EXPIRE)
        return self._get_cookies(request, None)
    def remember(self, request, userid, max_age=None, tokens=()):
        """ Return a set of Set-Cookie headers; when set into a response,
@@ -1037,7 +1015,7 @@
            )
        cookie_value = ticket.cookie_value()
        return self._get_cookies(environ, cookie_value, max_age)
        return self._get_cookies(request, cookie_value, max_age)
@implementer(IAuthenticationPolicy)
class SessionAuthenticationPolicy(CallbackAuthenticationPolicy):
@@ -1196,3 +1174,11 @@
        except ValueError: # not enough values to unpack
            return None
        return username, password
class _SimpleSerializer(object):
    def loads(self, bstruct):
        return native_(bstruct)
    def dumps(self, appstruct):
        return bytes_(appstruct)
pyramid/testing.py
@@ -320,6 +320,7 @@
    method = 'GET'
    application_url = 'http://example.com'
    host = 'example.com:80'
    domain = 'example.com'
    content_length = 0
    query_string = ''
    charset = 'UTF-8'
pyramid/tests/test_authentication.py
@@ -572,7 +572,12 @@
        return DummyRequest(environ, cookie=cookie)
    def _cookieValue(self, cookie):
        return eval(cookie.value)
        items = cookie.value.split('/')
        D = {}
        for item in items:
            k, v = item.split('=', 1)
            D[k] = v
        return D
    def _parseHeaders(self, headers):
        return [ self._parseHeader(header) for header in headers ]
@@ -838,7 +843,7 @@
        request.callbacks[0](None, response)
        self.assertEqual(len(response.headerlist), 3)
        self.assertEqual(response.headerlist[0][0], 'Set-Cookie')
        self.assertTrue("'tokens': ()" in response.headerlist[0][1])
        self.assertTrue("/tokens=/" in response.headerlist[0][1])
    def test_remember(self):
        helper = self._makeOne('secret')
@@ -851,11 +856,11 @@
        self.assertTrue(result[0][1].startswith('auth_tkt='))
        self.assertEqual(result[1][0], 'Set-Cookie')
        self.assertTrue(result[1][1].endswith('; Path=/; Domain=localhost'))
        self.assertTrue(result[1][1].endswith('; Domain=localhost; Path=/'))
        self.assertTrue(result[1][1].startswith('auth_tkt='))
        self.assertEqual(result[2][0], 'Set-Cookie')
        self.assertTrue(result[2][1].endswith('; Path=/; Domain=.localhost'))
        self.assertTrue(result[2][1].endswith('; Domain=.localhost; Path=/'))
        self.assertTrue(result[2][1].startswith('auth_tkt='))
    def test_remember_include_ip(self):
@@ -869,11 +874,11 @@
        self.assertTrue(result[0][1].startswith('auth_tkt='))
        self.assertEqual(result[1][0], 'Set-Cookie')
        self.assertTrue(result[1][1].endswith('; Path=/; Domain=localhost'))
        self.assertTrue(result[1][1].endswith('; Domain=localhost; Path=/'))
        self.assertTrue(result[1][1].startswith('auth_tkt='))
        self.assertEqual(result[2][0], 'Set-Cookie')
        self.assertTrue(result[2][1].endswith('; Path=/; Domain=.localhost'))
        self.assertTrue(result[2][1].endswith('; Domain=.localhost; Path=/'))
        self.assertTrue(result[2][1].startswith('auth_tkt='))
    def test_remember_path(self):
@@ -889,12 +894,12 @@
        self.assertEqual(result[1][0], 'Set-Cookie')
        self.assertTrue(result[1][1].endswith(
            '; Path=/cgi-bin/app.cgi/; Domain=localhost'))
            '; Domain=localhost; Path=/cgi-bin/app.cgi/'))
        self.assertTrue(result[1][1].startswith('auth_tkt='))
        self.assertEqual(result[2][0], 'Set-Cookie')
        self.assertTrue(result[2][1].endswith(
            '; Path=/cgi-bin/app.cgi/; Domain=.localhost'))
            '; Domain=.localhost; Path=/cgi-bin/app.cgi/'))
        self.assertTrue(result[2][1].startswith('auth_tkt='))
    def test_remember_http_only(self):
@@ -922,15 +927,15 @@
        self.assertEqual(len(result), 3)
        self.assertEqual(result[0][0], 'Set-Cookie')
        self.assertTrue('; Secure' in result[0][1])
        self.assertTrue('; secure' in result[0][1])
        self.assertTrue(result[0][1].startswith('auth_tkt='))
        self.assertEqual(result[1][0], 'Set-Cookie')
        self.assertTrue('; Secure' in result[1][1])
        self.assertTrue('; secure' in result[1][1])
        self.assertTrue(result[1][1].startswith('auth_tkt='))
        self.assertEqual(result[2][0], 'Set-Cookie')
        self.assertTrue('; Secure' in result[2][1])
        self.assertTrue('; secure' in result[2][1])
        self.assertTrue(result[2][1].startswith('auth_tkt='))
    def test_remember_wild_domain_disabled(self):
@@ -944,62 +949,49 @@
        self.assertTrue(result[0][1].startswith('auth_tkt='))
        self.assertEqual(result[1][0], 'Set-Cookie')
        self.assertTrue(result[1][1].endswith('; Path=/; Domain=localhost'))
        self.assertTrue(result[1][1].endswith('; Domain=localhost; Path=/'))
        self.assertTrue(result[1][1].startswith('auth_tkt='))
    def test_remember_parent_domain(self):
        helper = self._makeOne('secret', parent_domain=True)
        request = self._makeRequest()
        request.environ['HTTP_HOST'] = 'www.example.com'
        request.domain = 'www.example.com'
        result = helper.remember(request, 'other')
        self.assertEqual(len(result), 1)
        self.assertEqual(result[0][0], 'Set-Cookie')
        self.assertTrue(result[0][1].endswith('; Path=/; Domain=.example.com'))
        self.assertTrue(result[0][1].endswith('; Domain=.example.com; Path=/'))
        self.assertTrue(result[0][1].startswith('auth_tkt='))
    def test_remember_parent_domain_supercedes_wild_domain(self):
        helper = self._makeOne('secret', parent_domain=True, wild_domain=True)
        request = self._makeRequest()
        request.environ['HTTP_HOST'] = 'www.example.com'
        request.domain = 'www.example.com'
        result = helper.remember(request, 'other')
        self.assertEqual(len(result), 1)
        self.assertTrue(result[0][1].endswith('; Domain=.example.com'))
        self.assertTrue(result[0][1].endswith('; Domain=.example.com; Path=/'))
    def test_remember_explicit_domain(self):
        helper = self._makeOne('secret', domain='pyramid.bazinga')
        request = self._makeRequest()
        request.environ['HTTP_HOST'] = 'www.example.com'
        request.domain = 'www.example.com'
        result = helper.remember(request, 'other')
        self.assertEqual(len(result), 1)
        self.assertEqual(result[0][0], 'Set-Cookie')
        self.assertTrue(result[0][1].endswith('; Path=/; Domain=pyramid.bazinga'))
        self.assertTrue(result[0][1].endswith(
                '; Domain=pyramid.bazinga; Path=/'))
        self.assertTrue(result[0][1].startswith('auth_tkt='))
    def test_remember_domain_supercedes_parent_and_wild_domain(self):
        helper = self._makeOne('secret', domain='pyramid.bazinga',
                parent_domain=True, wild_domain=True)
        request = self._makeRequest()
        request.environ['HTTP_HOST'] = 'www.example.com'
        request.domain = 'www.example.com'
        result = helper.remember(request, 'other')
        self.assertEqual(len(result), 1)
        self.assertTrue(result[0][1].endswith('; Path=/; Domain=pyramid.bazinga'))
    def test_remember_domain_has_port(self):
        helper = self._makeOne('secret', wild_domain=False)
        request = self._makeRequest()
        request.environ['HTTP_HOST'] = 'example.com:80'
        result = helper.remember(request, 'other')
        self.assertEqual(len(result), 2)
        self.assertEqual(result[0][0], 'Set-Cookie')
        self.assertTrue(result[0][1].endswith('; Path=/'))
        self.assertTrue(result[0][1].startswith('auth_tkt='))
        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='))
        self.assertTrue(result[0][1].endswith(
                '; Domain=pyramid.bazinga; Path=/'))
    def test_remember_binary_userid(self):
        import base64
@@ -1010,7 +1002,7 @@
        self.assertEqual(len(result), 3)
        val = self._cookieValue(values[0])
        self.assertEqual(val['userid'],
                         bytes_(base64.b64encode(b'userid').strip()))
                         text_(base64.b64encode(b'userid').strip()))
        self.assertEqual(val['user_data'], 'userid_type:b64str')
    def test_remember_int_userid(self):
@@ -1044,7 +1036,7 @@
        self.assertEqual(len(result), 3)
        val = self._cookieValue(values[0])
        self.assertEqual(val['userid'],
                         base64.b64encode(userid.encode('utf-8')))
                         text_(base64.b64encode(userid.encode('utf-8'))))
        self.assertEqual(val['user_data'], 'userid_type:b64unicode')
    def test_remember_insane_userid(self):
@@ -1074,13 +1066,13 @@
        self.assertEqual(len(result), 3)
        self.assertEqual(result[0][0], 'Set-Cookie')
        self.assertTrue("'tokens': ('foo', 'bar')" in result[0][1])
        self.assertTrue("/tokens=foo|bar/" in result[0][1])
        self.assertEqual(result[1][0], 'Set-Cookie')
        self.assertTrue("'tokens': ('foo', 'bar')" in result[1][1])
        self.assertTrue("/tokens=foo|bar/" in result[1][1])
        self.assertEqual(result[2][0], 'Set-Cookie')
        self.assertTrue("'tokens': ('foo', 'bar')" in result[2][1])
        self.assertTrue("/tokens=foo|bar/" in result[2][1])
    def test_remember_unicode_but_ascii_token(self):
        helper = self._makeOne('secret')
@@ -1088,7 +1080,7 @@
        la = text_(b'foo', 'utf-8')
        result = helper.remember(request, 'other', tokens=(la,))
        # tokens must be str type on both Python 2 and 3
        self.assertTrue("'tokens': ('foo',)" in result[0][1])
        self.assertTrue("/tokens=foo/" in result[0][1])
    def test_remember_nonascii_token(self):
        helper = self._makeOne('secret')
@@ -1112,18 +1104,25 @@
        self.assertEqual(len(headers), 3)
        name, value = headers[0]
        self.assertEqual(name, 'Set-Cookie')
        self.assertEqual(value,
          'auth_tkt=""; Path=/; Max-Age=0; Expires=Wed, 31-Dec-97 23:59:59 GMT')
        self.assertEqual(
            value,
            'auth_tkt=; Max-Age=0; Path=/; '
            'expires=Wed, 31-Dec-97 23:59:59 GMT'
            )
        name, value = headers[1]
        self.assertEqual(name, 'Set-Cookie')
        self.assertEqual(value,
                         'auth_tkt=""; Path=/; Max-Age=0; '
                         'Expires=Wed, 31-Dec-97 23:59:59 GMT; Domain=localhost')
        self.assertEqual(
            value,
            'auth_tkt=; Domain=localhost; Max-Age=0; Path=/; '
            'expires=Wed, 31-Dec-97 23:59:59 GMT'
            )
        name, value = headers[2]
        self.assertEqual(name, 'Set-Cookie')
        self.assertEqual(value,
                         'auth_tkt=""; Path=/; Max-Age=0; '
                         'Expires=Wed, 31-Dec-97 23:59:59 GMT; Domain=.localhost')
        self.assertEqual(
            value,
            'auth_tkt=; Domain=.localhost; Max-Age=0; Path=/; '
            'expires=Wed, 31-Dec-97 23:59:59 GMT'
            )
class TestAuthTicket(unittest.TestCase):
    def _makeOne(self, *arg, **kw):
@@ -1417,7 +1416,19 @@
        self.assertEqual(policy.forget(None), [
            ('WWW-Authenticate', 'Basic realm="SomeRealm"')])
class TestSimpleSerializer(unittest.TestCase):
    def _makeOne(self):
        from pyramid.authentication import _SimpleSerializer
        return _SimpleSerializer()
    def test_loads(self):
        inst = self._makeOne()
        self.assertEqual(inst.loads(b'abc'), text_('abc'))
    def test_dumps(self):
        inst = self._makeOne()
        self.assertEqual(inst.dumps('abc'), bytes_('abc'))
class DummyContext:
    pass
@@ -1429,6 +1440,7 @@
        return self.cookie
class DummyRequest:
    domain = 'localhost'
    def __init__(self, environ=None, session=None, registry=None, cookie=None):
        self.environ = environ or {}
        self.session = session or {}
@@ -1486,10 +1498,23 @@
                self.kw = kw
            def cookie_value(self):
                result = {'secret':self.secret, 'userid':self.userid,
                          'remote_addr':self.remote_addr}
                result = {
                    'secret':self.secret,
                    'userid':self.userid,
                    'remote_addr':self.remote_addr
                    }
                result.update(self.kw)
                result = repr(result)
                tokens = result.pop('tokens', None)
                if tokens is not None:
                    tokens = '|'.join(tokens)
                    result['tokens'] = tokens
                items = sorted(result.items())
                new_items = []
                for k, v in items:
                    if isinstance(v, bytes):
                        v = text_(v)
                    new_items.append((k,v))
                result = '/'.join(['%s=%s' % (k, v) for k,v in new_items ])
                return result
        self.AuthTicket = AuthTicket
setup.py
@@ -39,7 +39,7 @@
install_requires=[
    'setuptools',
    'WebOb >= 1.2b3', # request.path_info is unicode
    'WebOb >= 1.3', # request.domain and CookieProfile
    'repoze.lru >= 0.4', # py3 compat
    'zope.interface >= 3.8.0',  # has zope.interface.registry
    'zope.deprecation >= 3.5.0', # py3 compat