diff options
| -rw-r--r-- | CHANGES.txt | 9 | ||||
| -rw-r--r-- | docs/whatsnew-1.5.rst | 2 | ||||
| -rw-r--r-- | pyramid/authentication.py | 86 | ||||
| -rw-r--r-- | pyramid/testing.py | 1 | ||||
| -rw-r--r-- | pyramid/tests/test_authentication.py | 129 | ||||
| -rw-r--r-- | setup.py | 2 |
6 files changed, 126 insertions, 103 deletions
diff --git a/CHANGES.txt b/CHANGES.txt index 98784f3d7..0508abc61 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -66,6 +66,9 @@ Features 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 @@ Deprecations 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) ================== diff --git a/docs/whatsnew-1.5.rst b/docs/whatsnew-1.5.rst index 23613896a..bd1bdd66c 100644 --- a/docs/whatsnew-1.5.rst +++ b/docs/whatsnew-1.5.rst @@ -504,3 +504,5 @@ Dependency Changes - Pyramid no longer depends upon ``Mako`` or ``Chameleon``. +- Pyramid now depends on WebOb>=1.3 (it uses ``webob.cookies.CookieProfile`` + from 1.3+). diff --git a/pyramid/authentication.py b/pyramid/authentication.py index 2c301bd29..ba7b864f9 100644 --- a/pyramid/authentication.py +++ b/pyramid/authentication.py @@ -10,6 +10,8 @@ import warnings from zope.interface import implementer +from webob.cookies import CookieProfile + from pyramid.compat import ( long, text_type, @@ -18,6 +20,7 @@ from pyramid.compat import ( url_quote, bytes_, ascii_native_, + native_, ) from pyramid.interfaces import ( @@ -798,8 +801,6 @@ def encode_ip_timestamp(ip, timestamp): 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 @@ class AuthTktCookieHelper(object): 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 @@ class AuthTktCookieHelper(object): 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 @@ class AuthTktCookieHelper(object): 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 @@ class AuthTktCookieHelper(object): ) 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 @@ class BasicAuthAuthenticationPolicy(CallbackAuthenticationPolicy): 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) + diff --git a/pyramid/testing.py b/pyramid/testing.py index b3460d8aa..91dc41dd5 100644 --- a/pyramid/testing.py +++ b/pyramid/testing.py @@ -320,6 +320,7 @@ class DummyRequest( method = 'GET' application_url = 'http://example.com' host = 'example.com:80' + domain = 'example.com' content_length = 0 query_string = '' charset = 'UTF-8' diff --git a/pyramid/tests/test_authentication.py b/pyramid/tests/test_authentication.py index 3ac8f2d61..79d2a5923 100644 --- a/pyramid/tests/test_authentication.py +++ b/pyramid/tests/test_authentication.py @@ -572,7 +572,12 @@ class TestAuthTktCookieHelper(unittest.TestCase): 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 @@ class TestAuthTktCookieHelper(unittest.TestCase): 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 @@ class TestAuthTktCookieHelper(unittest.TestCase): 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 @@ class TestAuthTktCookieHelper(unittest.TestCase): 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 @@ class TestAuthTktCookieHelper(unittest.TestCase): 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 @@ class TestAuthTktCookieHelper(unittest.TestCase): 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 @@ class TestAuthTktCookieHelper(unittest.TestCase): 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 @@ class TestAuthTktCookieHelper(unittest.TestCase): 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 @@ class TestAuthTktCookieHelper(unittest.TestCase): 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 @@ class TestAuthTktCookieHelper(unittest.TestCase): 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 @@ class TestAuthTktCookieHelper(unittest.TestCase): 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 @@ class TestAuthTktCookieHelper(unittest.TestCase): 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 @@ class TestBasicAuthAuthenticationPolicy(unittest.TestCase): 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 @@ class DummyCookies(object): 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 @@ class DummyAuthTktModule(object): 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 @@ -39,7 +39,7 @@ except IOError: 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 |
