From 8134a723b12221663babc855ccc941daf88ba5c3 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Sat, 7 Dec 2013 01:07:24 -0500 Subject: use a single serializer instead of serialize/deserialize in session.py, use SignedSerializer from (yet to be released) webob 1.3 instead of local logic in session.py --- pyramid/session.py | 104 +++++++++++++++++++----------------------- pyramid/tests/test_session.py | 27 ++++++----- 2 files changed, 63 insertions(+), 68 deletions(-) diff --git a/pyramid/session.py b/pyramid/session.py index d3a4113b9..c9d738b9c 100644 --- a/pyramid/session.py +++ b/pyramid/session.py @@ -8,6 +8,8 @@ import time from zope.deprecation import deprecated from zope.interface import implementer +from webob.cookies import SignedSerializer + from pyramid.compat import ( pickle, PY3, @@ -119,9 +121,17 @@ def check_csrf_token(request, return False return True +class PickleSerializer(object): + """ A Webob cookie serializer that uses the pickle protocol to dump Python + data to bytes.""" + def loads(self, bstruct): + return pickle.loads(bstruct) + + def dumps(self, appstruct): + return pickle.dumps(appstruct, pickle.HIGHEST_PROTOCOL) + def BaseCookieSessionFactory( - serialize, - deserialize, + serializer, cookie_name='session', max_age=None, path='/', @@ -154,13 +164,11 @@ def BaseCookieSessionFactory( Parameters: - ``serialize`` - A callable accepting a Python object and returning a bytestring. A - ``ValueError`` should be raised for malformed inputs. - - ``deserialize`` - A callable accepting a bytestring and returning a Python object. A - ``ValueError`` should be raised for malformed inputs. + ``serializer`` + An object with two methods: `loads`` and ``dumps``. The ``loads`` method + should accept bytes and return a Python object. The ``dumps`` method + should accept a Python object and return bytes. A ``ValueError`` should + be raised for malformed inputs. ``cookie_name`` The name of the cookie used for sessioning. Default: ``'session'``. @@ -238,7 +246,7 @@ def BaseCookieSessionFactory( cookieval = request.cookies.get(self._cookie_name) if cookieval is not None: try: - value = deserialize(bytes_(cookieval)) + value = serializer.loads(bytes_(cookieval)) except ValueError: # the cookie failed to deserialize, dropped value = None @@ -336,7 +344,7 @@ def BaseCookieSessionFactory( exception = getattr(self.request, 'exception', None) if exception is not None: # dont set a cookie during exceptions return False - cookieval = native_(serialize( + cookieval = native_(serializer.dumps( (self.accessed, self.created, dict(self)) )) if len(cookieval) > 4064: @@ -430,9 +438,20 @@ def UnencryptedCookieSessionFactoryConfig( is valid. Default: ``signed_deserialize`` (using pickle). """ + class SerializerWrapper(object): + def __init__(self, secret): + self.secret = secret + + def loads(self, bstruct): + return signed_deserialize(bstruct, secret) + + def dumps(self, appstruct): + return signed_serialize(appstruct, secret) + + serializer = SerializerWrapper(secret) + return BaseCookieSessionFactory( - lambda v: signed_serialize(v, secret), - lambda v: signed_deserialize(v, secret), + serializer, cookie_name=cookie_name, max_age=cookie_max_age, path=cookie_path, @@ -463,8 +482,7 @@ def SignedCookieSessionFactory( reissue_time=0, hashalg='sha512', salt='pyramid.session.', - serialize=None, - deserialize=None, + serializer=None, ): """ .. versionadded:: 1.5 @@ -546,53 +564,27 @@ def SignedCookieSessionFactory( If ``True``, set a session cookie even if an exception occurs while rendering a view. Default: ``True``. - ``serialize`` - A callable accepting a Python object and returning a bytestring. A - ``ValueError`` should be raised for malformed inputs. - Default: :func:`pickle.dumps`. - - ``deserialize`` - A callable accepting a bytestring and returning a Python object. A - ``ValueError`` should be raised for malformed inputs. - Default: :func:`pickle.loads`. + ``serializer`` + An object with two methods: `loads`` and ``dumps``. The ``loads`` method + should accept bytes and return a Python object. The ``dumps`` method + should accept a Python object and return bytes. A ``ValueError`` should + be raised for malformed inputs. If a serializer is not passed, the + :class:`pyramid.session.PickleSerializer` serializer will be used. .. versionadded: 1.5a3 """ + if serializer is None: + serializer = PickleSerializer() - if serialize is None: - serialize = lambda v: pickle.dumps(v, pickle.HIGHEST_PROTOCOL) - - if deserialize is None: - deserialize = pickle.loads - - digestmod = lambda string=b'': hashlib.new(hashalg, string) - digest_size = digestmod().digest_size - - salted_secret = bytes_(salt or '') + bytes_(secret) - - def signed_serialize(appstruct): - cstruct = serialize(appstruct) - sig = hmac.new(salted_secret, cstruct, digestmod).digest() - return base64.b64encode(cstruct + sig) - - def signed_deserialize(bstruct): - try: - fstruct = base64.b64decode(bstruct) - except (binascii.Error, TypeError) as e: - raise ValueError('Badly formed base64 data: %s' % e) - - cstruct = fstruct[:-digest_size] - expected_sig = fstruct[-digest_size:] - - sig = hmac.new(salted_secret, cstruct, digestmod).digest() - if strings_differ(sig, expected_sig): - raise ValueError('Invalid signature') - - return deserialize(cstruct) + signed_serializer = SignedSerializer( + secret, + salt, + hashalg, + serializer=serializer, + ) return BaseCookieSessionFactory( - signed_serialize, - signed_deserialize, + signed_serializer, cookie_name=cookie_name, max_age=max_age, path=path, diff --git a/pyramid/tests/test_session.py b/pyramid/tests/test_session.py index a9f70d6a0..1ad0729b3 100644 --- a/pyramid/tests/test_session.py +++ b/pyramid/tests/test_session.py @@ -264,8 +264,8 @@ class SharedCookieSessionTests(object): class TestBaseCookieSession(SharedCookieSessionTests, unittest.TestCase): def _makeOne(self, request, **kw): from pyramid.session import BaseCookieSessionFactory - return BaseCookieSessionFactory( - dummy_serialize, dummy_deserialize, **kw)(request) + serializer = DummySerializer() + return BaseCookieSessionFactory(serializer, **kw)(request) def _serialize(self, value): return json.dumps(value) @@ -294,7 +294,7 @@ class TestSignedCookieSession(SharedCookieSessionTests, unittest.TestCase): digestmod = lambda: hashlib.new(hashalg) cstruct = pickle.dumps(value, pickle.HIGHEST_PROTOCOL) sig = hmac.new(salt + b'secret', cstruct, digestmod).digest() - return base64.b64encode(cstruct + sig) + return base64.urlsafe_b64encode(sig + cstruct).rstrip(b'=') def test_reissue_not_triggered(self): import time @@ -353,11 +353,12 @@ class TestSignedCookieSession(SharedCookieSessionTests, unittest.TestCase): import hmac import time request = testing.DummyRequest() - cstruct = dummy_serialize((time.time(), 0, {'state': 1})) + serializer = DummySerializer() + cstruct = serializer.dumps((time.time(), 0, {'state': 1})) sig = hmac.new(b'pyramid.session.secret', cstruct, sha512).digest() - cookieval = base64.b64encode(cstruct + sig) + cookieval = base64.urlsafe_b64encode(sig + cstruct).rstrip(b'=') request.cookies['session'] = cookieval - session = self._makeOne(request, deserialize=dummy_deserialize) + session = self._makeOne(request, serializer=serializer) self.assertEqual(session['state'], 1) def test_invalid_data_size(self): @@ -382,7 +383,7 @@ class TestSignedCookieSession(SharedCookieSessionTests, unittest.TestCase): try: result = callbacks[0](request, response) - except TypeError as e: # pragma: no cover + except TypeError: # pragma: no cover self.fail('HMAC failed to initialize due to key length.') self.assertEqual(result, None) @@ -413,8 +414,9 @@ class TestUnencryptedCookieSession(SharedCookieSessionTests, unittest.TestCase): kw.setdefault(dest, kw.pop(src)) def _serialize(self, value): + from pyramid.compat import bytes_ from pyramid.session import signed_serialize - return signed_serialize(value, 'secret') + return bytes_(signed_serialize(value, 'secret')) def test_serialize_option(self): from pyramid.response import Response @@ -596,11 +598,12 @@ class Test_check_csrf_token(unittest.TestCase): result = self._callFUT(request, 'csrf_token', raises=False) self.assertEqual(result, False) -def dummy_serialize(value): - return json.dumps(value).encode('utf-8') +class DummySerializer(object): + def dumps(self, value): + return json.dumps(value).encode('utf-8') -def dummy_deserialize(value): - return json.loads(value.decode('utf-8')) + def loads(self, value): + return json.loads(value.decode('utf-8')) class DummySessionFactory(dict): _dirty = False -- cgit v1.2.3 From 767e44f2fe7c238d0c67308e2e94241236a522e4 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Tue, 10 Dec 2013 05:19:41 -0500 Subject: use CookieProfile from webob in authentication module, add support for new domain attribute on dummy request, depend on webob 1.3 or better --- CHANGES.txt | 9 +++ docs/whatsnew-1.5.rst | 2 + pyramid/authentication.py | 86 ++++++++++------------- pyramid/testing.py | 1 + pyramid/tests/test_authentication.py | 129 +++++++++++++++++++++-------------- 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 diff --git a/setup.py b/setup.py index 2d49717b7..d27240664 100644 --- a/setup.py +++ b/setup.py @@ -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 -- cgit v1.2.3 From ab579e223f7a719acd4dd2c5ddeeb70953bec0e7 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Tue, 10 Dec 2013 14:51:20 -0500 Subject: add note about non-bw-compat between SignedCookieSessionFactory and UnencryptedCookieSessionFactory. Ref #1200. --- CHANGES.txt | 12 +++++++----- pyramid/session.py | 9 ++++++++- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 0508abc61..7e044bf0d 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -35,11 +35,13 @@ Features See https://github.com/Pylons/pyramid/pull/1149 - Added a new ``SignedCookieSessionFactory`` which is very similar to the - ``UnencryptedCookieSessionFactoryConfig`` but with a clearer focus on - signing content. The custom serializer arguments to this function should - only focus on serializing, unlike its predecessor which required the - serializer to also perform signing. - See https://github.com/Pylons/pyramid/pull/1142 + ``UnencryptedCookieSessionFactoryConfig`` but with a clearer focus on signing + content. The custom serializer arguments to this function should only focus + on serializing, unlike its predecessor which required the serializer to also + perform signing. See https://github.com/Pylons/pyramid/pull/1142 . Note + that cookies generated using ``SignedCookieSessionFactory`` are not + compatible with cookies generated using ``UnencryptedCookieSessionFactory``, + so existing user session data will be destroyed if you switch to it. - Added a new ``BaseCookieSessionFactory`` which acts as a generic cookie factory that can be used by framework implementors to create their own diff --git a/pyramid/session.py b/pyramid/session.py index c9d738b9c..8c9900975 100644 --- a/pyramid/session.py +++ b/pyramid/session.py @@ -382,6 +382,10 @@ def UnencryptedCookieSessionFactoryConfig( """ .. deprecated:: 1.5 Use :func:`pyramid.session.SignedCookieSessionFactory` instead. + Caveat: Cookies generated using ``SignedCookieSessionFactory`` are not + compatible with cookies generated using + ``UnencryptedCookieSessionFactory``, so existing user session data will + be destroyed if you switch to it. Configure a :term:`session factory` which will provide unencrypted (but signed) cookie-based sessions. The return value of this @@ -466,7 +470,10 @@ def UnencryptedCookieSessionFactoryConfig( deprecated( 'UnencryptedCookieSessionFactoryConfig', 'The UnencryptedCookieSessionFactoryConfig callable is deprecated as of ' - 'Pyramid 1.5. Use ``pyramid.session.SignedCookieSessionFactory`` instead.' + 'Pyramid 1.5. Use ``pyramid.session.SignedCookieSessionFactory`` instead. ' + 'Caveat: Cookies generated using SignedCookieSessionFactory are not ' + 'compatible with cookies generated using UnencryptedCookieSessionFactory, ' + 'so existing user session data will be destroyed if you switch to it.' ) def SignedCookieSessionFactory( -- cgit v1.2.3 From 5c4318abe647b65869ac6dbb5b57cf1088712061 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Tue, 10 Dec 2013 15:10:08 -0500 Subject: prep for 1.5a3 --- CHANGES.txt | 4 ++-- docs/whatsnew-1.5.rst | 5 ++++- setup.py | 2 +- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 7e044bf0d..8ca6e7e9b 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,5 +1,5 @@ -Unreleased -========== +1.5a3 (2013-12-10) +================== Features -------- diff --git a/docs/whatsnew-1.5.rst b/docs/whatsnew-1.5.rst index bd1bdd66c..9ccf097a8 100644 --- a/docs/whatsnew-1.5.rst +++ b/docs/whatsnew-1.5.rst @@ -348,7 +348,10 @@ The feature additions in Pyramid 1.5 follow. signing content. The custom serializer arguments to this function should only focus on serializing, unlike its predecessor which required the serializer to also perform signing. - See https://github.com/Pylons/pyramid/pull/1142 + See https://github.com/Pylons/pyramid/pull/1142 . Note + that cookies generated using ``SignedCookieSessionFactory`` are not + compatible with cookies generated using ``UnencryptedCookieSessionFactory``, + so existing user session data will be destroyed if you switch to it. - Added a new ``BaseCookieSessionFactory`` which acts as a generic cookie factory that can be used by framework implementors to create their own diff --git a/setup.py b/setup.py index d27240664..e6c9a490a 100644 --- a/setup.py +++ b/setup.py @@ -69,7 +69,7 @@ testing_extras = tests_require + [ ] setup(name='pyramid', - version='1.5a2', + version='1.5a3', description='The Pyramid Web Framework, a Pylons project', long_description=README + '\n\n' + CHANGES, classifiers=[ -- cgit v1.2.3 From 5431138583ee9d4382c1fe6dd0fb706ed514a4bf Mon Sep 17 00:00:00 2001 From: Matthew Wilkes Date: Tue, 10 Dec 2013 20:29:28 +0000 Subject: Use new pluralize calling convention recognised by Lingua 1.7 --- docs/narr/i18n.rst | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/docs/narr/i18n.rst b/docs/narr/i18n.rst index c9b782c08..5f50ca212 100644 --- a/docs/narr/i18n.rst +++ b/docs/narr/i18n.rst @@ -607,10 +607,8 @@ object, but the domain and mapping information attached is ignored. def aview(request): localizer = request.localizer num = 1 - translated = localizer.pluralize( - _('item_plural', default="${number} items"), - None, num, 'mydomain', mapping={'number':num} - ) + translated = localizer.pluralize('item_plural', '${number} items', + num, 'mydomain', mapping={'number':num}) The corresponding message catalog must have language plural definitions and plural alternatives set. -- cgit v1.2.3 From 2da03fedcdebd9499d13eadf1500f80d04f4d796 Mon Sep 17 00:00:00 2001 From: Matthew Wilkes Date: Tue, 10 Dec 2013 20:36:34 +0000 Subject: Expand on docstring --- pyramid/i18n.py | 33 +++++++++++++++++++++++---------- 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/pyramid/i18n.py b/pyramid/i18n.py index 6ffd93e8f..aaba769c6 100644 --- a/pyramid/i18n.py +++ b/pyramid/i18n.py @@ -75,16 +75,16 @@ class Localizer(object): :term:`message identifier` objects as a singular/plural pair and an ``n`` value representing the number that appears in the message using gettext plural forms support. The ``singular`` - and ``plural`` objects passed may be translation strings or - unicode strings. ``n`` represents the number of elements. - ``domain`` is the translation domain to use to do the - pluralization, and ``mapping`` is the interpolation mapping - that should be used on the result. Note that if the objects - passed are translation strings, their domains and mappings are - ignored. The domain and mapping arguments must be used - instead. If the ``domain`` is not supplied, a default domain - is used (usually ``messages``). - + and ``plural`` objects should be unicode strings. There is no + reason to use translation string objects as arguments as all + metadata is ignored. + + ``n`` represents the number of elements. ``domain`` is the + translation domain to use to do the pluralization, and ``mapping`` + is the interpolation mapping that should be used on the result. If + the ``domain`` is not supplied, a default domain is used (usually + ``messages``). + Example:: num = 1 @@ -93,6 +93,19 @@ class Localizer(object): num, mapping={'num':num}) + If using the gettext plural support, which is required for + languages that have pluralisation rules other than n != 1, the + ``singular`` argument must be the message_id defined in the + translation file. The plural argument is not used in this case. + + Example:: + + num = 1 + translated = localizer.pluralize('item_plural', + '', + num, + mapping={'num':num}) + """ if self.pluralizer is None: -- cgit v1.2.3 From 8856e23b9aa6d3d12da8d74c751c2a7191ff687b Mon Sep 17 00:00:00 2001 From: Ira Date: Thu, 12 Dec 2013 00:38:58 +0000 Subject: Fix a typo. --- pyramid/url.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyramid/url.py b/pyramid/url.py index 484ee775f..78dd297d5 100644 --- a/pyramid/url.py +++ b/pyramid/url.py @@ -359,7 +359,7 @@ class URLMethodsMixin(object): .. warning:: if no ``elements`` arguments are specified, the resource URL will end with a trailing slash. If any ``elements`` are used, the generated URL will *not* - end in trailing a slash. + end in a trailing slash. If a keyword argument ``query`` is present, it will be used to compose a query string that will be tacked on to the end of the URL. The value -- cgit v1.2.3