From 6454a73a9ea0b00f3881e003efda3048e8407a5f Mon Sep 17 00:00:00 2001 From: rforkel Date: Tue, 26 Jun 2012 08:40:34 +0200 Subject: Added support for passing multiple decorators to add_view. --- pyramid/config/views.py | 20 ++++++++++++++++---- pyramid/scaffolds/copydir.py | 2 +- pyramid/tests/test_config/test_views.py | 22 ++++++++++++++++++++++ pyramid/tests/test_view.py | 4 ++++ 4 files changed, 43 insertions(+), 5 deletions(-) diff --git a/pyramid/config/views.py b/pyramid/config/views.py index 9e9b5321b..b00c4b9f2 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -807,11 +807,12 @@ class ViewsConfiguratorMixin(object): decorator - A :term:`dotted Python name` to function (or the function itself) + A :term:`dotted Python name` to function (or the function itself, + or a list or tuple of the aforementioned) which will be used to decorate the registered :term:`view - callable`. The decorator function will be called with the view + callable`. The decorator function(s) will be called with the view callable as a single argument. The view callable it is passed will - accept ``(context, request)``. The decorator must return a + accept ``(context, request)``. The decorator(s) must return a replacement view callable which also accepts ``(context, request)``. @@ -979,7 +980,18 @@ class ViewsConfiguratorMixin(object): for_ = self.maybe_dotted(for_) containment = self.maybe_dotted(containment) mapper = self.maybe_dotted(mapper) - decorator = self.maybe_dotted(decorator) + + def combine(*decorators): + def decorated(view_callable): + for decorator in decorators: + view_callable = decorator(view_callable) + return view_callable + return decorated + + if isinstance(decorator, (tuple, list)): + decorator = combine(*map(self.maybe_dotted, decorator)) + else: + decorator = self.maybe_dotted(decorator) if not view: if renderer: diff --git a/pyramid/scaffolds/copydir.py b/pyramid/scaffolds/copydir.py index d55ea165a..ba0988523 100644 --- a/pyramid/scaffolds/copydir.py +++ b/pyramid/scaffolds/copydir.py @@ -245,7 +245,7 @@ Responses: def makedirs(dir, verbosity, pad): parent = os.path.dirname(os.path.abspath(dir)) if not os.path.exists(parent): - makedirs(parent, verbosity, pad) + makedirs(parent, verbosity, pad) # pragma: no cover os.mkdir(dir) def substitute_filename(fn, vars): diff --git a/pyramid/tests/test_config/test_views.py b/pyramid/tests/test_config/test_views.py index 9b46f83c9..4435d341e 100644 --- a/pyramid/tests/test_config/test_views.py +++ b/pyramid/tests/test_config/test_views.py @@ -184,6 +184,28 @@ class TestViewsConfigurationMixin(unittest.TestCase): result = wrapper(None, None) self.assertEqual(result, 'OK') + def test_add_view_with_decorator_tuple(self): + from pyramid.renderers import null_renderer + def view(request): + """ ABC """ + return 'OK' + def view_wrapper1(fn): + def inner(context, request): + return 'wrapped1' + fn(context, request) + return inner + def view_wrapper2(fn): + def inner(context, request): + return 'wrapped2' + fn(context, request) + return inner + config = self._makeOne(autocommit=True) + config.add_view(view=view, decorator=(view_wrapper1, view_wrapper2), + renderer=null_renderer) + wrapper = self._getViewCallable(config) + self.assertFalse(wrapper is view) + self.assertEqual(wrapper.__doc__, view.__doc__) + result = wrapper(None, None) + self.assertEqual(result, 'wrapped2wrapped1OK') + def test_add_view_with_http_cache(self): import datetime from pyramid.response import Response diff --git a/pyramid/tests/test_view.py b/pyramid/tests/test_view.py index a105adb70..35fa132a8 100644 --- a/pyramid/tests/test_view.py +++ b/pyramid/tests/test_view.py @@ -371,6 +371,10 @@ class TestViewConfigDecorator(unittest.TestCase): self.assertEqual(decorator.mapper, 'mapper') self.assertEqual(decorator.decorator, 'decorator') self.assertEqual(decorator.match_param, 'match_param') + + def test_create_decorator_tuple(self): + decorator = self._makeOne(decorator=('decorator1', 'decorator2')) + self.assertEqual(decorator.decorator, ('decorator1', 'decorator2')) def test_call_function(self): decorator = self._makeOne() -- cgit v1.2.3 From 6b2c15e8187b072343cb162d731b5026cc0f940a Mon Sep 17 00:00:00 2001 From: rforkel Date: Tue, 26 Jun 2012 10:07:59 +0200 Subject: Added explanations to docstring. --- pyramid/config/views.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/pyramid/config/views.py b/pyramid/config/views.py index b00c4b9f2..e8169e958 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -816,6 +816,27 @@ class ViewsConfiguratorMixin(object): replacement view callable which also accepts ``(context, request)``. + If decorator is a tuple or list of callables, the callables will be + combined and used in the order provided as a decorator. + For example:: + + @view_config(..., decorator=[decorator1, decorator2]) + def myview(request): + .... + + Is similar to doing:: + + @view_config(...) + @decorator2 + @decorator1 + def myview(request): + ... + + Except with the existing benefits of ``decorator=`` (having a common + decorator syntax for all view calling conventions and not having to + think about preserving function attributes such as ``__name__`` and + ``__module__`` within decorator logic). + mapper A Python object or :term:`dotted Python name` which refers to a -- cgit v1.2.3 From 801adfb060911b92f9787ec6517250436b1373be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Domen=20Ko=C5=BEar?= Date: Sun, 23 Sep 2012 18:01:46 +0200 Subject: Add SHA512AuthTktAuthenticationPolicy and deprecate AuthTktAuthenticationPolicy --- docs/api/authentication.rst | 2 + pyramid/authentication.py | 73 +++++++++++++++++++++-------- pyramid/tests/test_authentication.py | 90 ++++++++++++++++++++++++++++++------ 3 files changed, 134 insertions(+), 31 deletions(-) diff --git a/docs/api/authentication.rst b/docs/api/authentication.rst index 5d4dbd9e3..ed96e9c98 100644 --- a/docs/api/authentication.rst +++ b/docs/api/authentication.rst @@ -8,6 +8,8 @@ Authentication Policies .. automodule:: pyramid.authentication + .. autoclass:: SHA512AuthTktAuthenticationPolicy + .. autoclass:: AuthTktAuthenticationPolicy .. autoclass:: RepozeWho1AuthenticationPolicy diff --git a/pyramid/authentication.py b/pyramid/authentication.py index 83bdb13d1..730312144 100644 --- a/pyramid/authentication.py +++ b/pyramid/authentication.py @@ -1,10 +1,11 @@ from codecs import utf_8_decode from codecs import utf_8_encode -from hashlib import md5 +import hashlib import base64 import datetime import re import time as time_mod +import warnings from zope.interface import implementer @@ -254,8 +255,7 @@ class RemoteUserAuthenticationPolicy(CallbackAuthenticationPolicy): def forget(self, request): return [] -@implementer(IAuthenticationPolicy) -class AuthTktAuthenticationPolicy(CallbackAuthenticationPolicy): +class BaseAuthTktAuthenticationPolicy(CallbackAuthenticationPolicy): """ A :app:`Pyramid` :term:`authentication policy` which obtains data from a Pyramid "auth ticket" cookie. @@ -357,6 +357,8 @@ class AuthTktAuthenticationPolicy(CallbackAuthenticationPolicy): Objects of this class implement the interface described by :class:`pyramid.interfaces.IAuthenticationPolicy`. """ + hashalg = '' + def __init__(self, secret, callback=None, @@ -382,6 +384,7 @@ class AuthTktAuthenticationPolicy(CallbackAuthenticationPolicy): http_only=http_only, path=path, wild_domain=wild_domain, + hashalg=self.hashalg, ) self.callback = callback self.debug = debug @@ -399,6 +402,32 @@ class AuthTktAuthenticationPolicy(CallbackAuthenticationPolicy): def forget(self, request): return self.cookie.forget(request) +@implementer(IAuthenticationPolicy) +class SHA512AuthTktAuthenticationPolicy(BaseAuthTktAuthenticationPolicy): + __doc__ = """ + .. versionadded:: 1.4 + """ + BaseAuthTktAuthenticationPolicy.__doc__ + hashalg = 'sha512' + +@implementer(IAuthenticationPolicy) +class AuthTktAuthenticationPolicy(BaseAuthTktAuthenticationPolicy): + __doc__ = """ + .. warning:: + + Deprecated in 1.4 due to security concerns, + use :class:`SHA512AuthTktAuthenticationPolicy` instead. + + """ + BaseAuthTktAuthenticationPolicy.__doc__ + hashalg = 'md5' + + def __init__(self, *a, **kw): + warnings.warn('Deprecated due to the usage of md5, ' + 'hash function known to have collisions. ' + 'Use SHA512AuthTktAuthenticationPolicy instead.', + DeprecationWarning, + stacklevel=2) + super(AuthTktAuthenticationPolicy, self).__init__(*a, **kw) + def b64encode(v): return base64.b64encode(bytes_(v)).strip().replace(b'\n', b'') @@ -427,7 +456,8 @@ class AuthTicket(object): """ def __init__(self, secret, userid, ip, tokens=(), user_data='', - time=None, cookie_name='auth_tkt', secure=False): + time=None, cookie_name='auth_tkt', secure=False, + hashalg='md5'): self.secret = secret self.userid = userid self.ip = ip @@ -439,11 +469,12 @@ class AuthTicket(object): self.time = time self.cookie_name = cookie_name self.secure = secure + self.hashalg = hashalg def digest(self): return calculate_digest( self.ip, self.time, self.secret, self.userid, self.tokens, - self.user_data) + self.user_data, self.hashalg) def cookie_value(self): v = '%s%08x%s!' % (self.digest(), int(self.time), @@ -465,7 +496,7 @@ class BadTicket(Exception): Exception.__init__(self, msg) # this function licensed under the MIT license (stolen from Paste) -def parse_ticket(secret, ticket, ip): +def parse_ticket(secret, ticket, ip, hashalg): """ Parse the ticket, returning (timestamp, userid, tokens, user_data). @@ -473,13 +504,14 @@ def parse_ticket(secret, ticket, ip): with an explanation. """ ticket = ticket.strip('"') - digest = ticket[:32] + digest_size = hashlib.new(hashalg).digest_size * 2 + digest = ticket[:digest_size] try: - timestamp = int(ticket[32:40], 16) + timestamp = int(ticket[digest_size:digest_size + 8], 16) except ValueError as e: raise BadTicket('Timestamp is not a hex integer: %s' % e) try: - userid, data = ticket[40:].split('!', 1) + userid, data = ticket[digest_size + 8:].split('!', 1) except ValueError: raise BadTicket('userid is not followed by !') userid = url_unquote(userid) @@ -491,7 +523,7 @@ def parse_ticket(secret, ticket, ip): user_data = data expected = calculate_digest(ip, timestamp, secret, - userid, tokens, user_data) + userid, tokens, user_data, hashalg) # Avoid timing attacks (see # http://seb.dbzteam.org/crypto/python-oauth-timing-hmac.pdf) @@ -504,16 +536,19 @@ def parse_ticket(secret, ticket, ip): return (timestamp, userid, tokens, user_data) # this function licensed under the MIT license (stolen from Paste) -def calculate_digest(ip, timestamp, secret, userid, tokens, user_data): +def calculate_digest(ip, timestamp, secret, userid, tokens, user_data, hashalg): secret = bytes_(secret, 'utf-8') userid = bytes_(userid, 'utf-8') tokens = bytes_(tokens, 'utf-8') user_data = bytes_(user_data, 'utf-8') - digest0 = md5( + hash_obj = hashlib.new(hashalg) + hash_obj.update( encode_ip_timestamp(ip, timestamp) + secret + userid + b'\0' - + tokens + b'\0' + user_data).hexdigest() - digest = md5(bytes_(digest0) + secret).hexdigest() - return digest + + tokens + b'\0' + user_data) + digest = hash_obj.hexdigest() + hash_obj2 = hashlib.new(hashalg) + hash_obj2.update(bytes_(digest) + secret) + return hash_obj2.hexdigest() # this function licensed under the MIT license (stolen from Paste) def encode_ip_timestamp(ip, timestamp): @@ -556,7 +591,7 @@ class AuthTktCookieHelper(object): def __init__(self, secret, cookie_name='auth_tkt', secure=False, include_ip=False, timeout=None, reissue_time=None, - max_age=None, http_only=False, path="/", wild_domain=True): + max_age=None, http_only=False, path="/", wild_domain=True, hashalg='md5'): self.secret = secret self.cookie_name = cookie_name self.include_ip = include_ip @@ -567,6 +602,7 @@ class AuthTktCookieHelper(object): self.http_only = http_only self.path = path self.wild_domain = wild_domain + self.hashalg = hashalg static_flags = [] if self.secure: @@ -635,7 +671,7 @@ class AuthTktCookieHelper(object): try: timestamp, userid, tokens, user_data = self.parse_ticket( - self.secret, cookie, remote_addr) + self.secret, cookie, remote_addr, self.hashalg) except self.BadTicket: return None @@ -750,7 +786,8 @@ class AuthTktCookieHelper(object): tokens=tokens, user_data=user_data, cookie_name=self.cookie_name, - secure=self.secure) + secure=self.secure, + hashalg=self.hashalg) cookie_value = ticket.cookie_value() return self._get_cookies(environ, cookie_value, max_age) diff --git a/pyramid/tests/test_authentication.py b/pyramid/tests/test_authentication.py index e513b9a48..ae517fc40 100644 --- a/pyramid/tests/test_authentication.py +++ b/pyramid/tests/test_authentication.py @@ -343,13 +343,43 @@ class TestAutkTktAuthenticationPolicy(unittest.TestCase): inst.cookie = DummyCookieHelper(cookieidentity) return inst - def test_allargs(self): - # pass all known args - inst = self._getTargetClass()( - 'secret', callback=None, cookie_name=None, secure=False, - include_ip=False, timeout=None, reissue_time=None, - ) - self.assertEqual(inst.callback, None) + def test_is_subclass(self): + from pyramid.authentication import BaseAuthTktAuthenticationPolicy + inst = self._makeOne(None, None) + self.assertTrue(isinstance(inst, BaseAuthTktAuthenticationPolicy)) + + def test_md5(self): + inst = self._makeOne(None, None) + self.assertEqual(inst.hashalg, 'md5') + + def test_class_implements_IAuthenticationPolicy(self): + from zope.interface.verify import verifyClass + from pyramid.interfaces import IAuthenticationPolicy + verifyClass(IAuthenticationPolicy, self._getTargetClass()) + + def test_instance_implements_IAuthenticationPolicy(self): + from zope.interface.verify import verifyObject + from pyramid.interfaces import IAuthenticationPolicy + verifyObject(IAuthenticationPolicy, self._makeOne(None, None)) + +class TestSHA512AutkTktAuthenticationPolicy(unittest.TestCase): + def _getTargetClass(self): + from pyramid.authentication import SHA512AuthTktAuthenticationPolicy + return SHA512AuthTktAuthenticationPolicy + + def _makeOne(self, callback, cookieidentity, **kw): + inst = self._getTargetClass()('secret', callback, **kw) + inst.cookie = DummyCookieHelper(cookieidentity) + return inst + + def test_is_subclass(self): + from pyramid.authentication import BaseAuthTktAuthenticationPolicy + inst = self._makeOne(None, None) + self.assertTrue(isinstance(inst, BaseAuthTktAuthenticationPolicy)) + + def test_sha512(self): + inst = self._makeOne(None, None) + self.assertEqual(inst.hashalg, 'sha512') def test_class_implements_IAuthenticationPolicy(self): from zope.interface.verify import verifyClass @@ -361,6 +391,24 @@ class TestAutkTktAuthenticationPolicy(unittest.TestCase): from pyramid.interfaces import IAuthenticationPolicy verifyObject(IAuthenticationPolicy, self._makeOne(None, None)) +class TestBaseAutkTktAuthenticationPolicy(unittest.TestCase): + def _getTargetClass(self): + from pyramid.authentication import BaseAuthTktAuthenticationPolicy + return BaseAuthTktAuthenticationPolicy + + def _makeOne(self, callback, cookieidentity, **kw): + inst = self._getTargetClass()('secret', callback, **kw) + inst.cookie = DummyCookieHelper(cookieidentity) + return inst + + def test_allargs(self): + # pass all known args + inst = self._getTargetClass()( + 'secret', callback=None, cookie_name=None, secure=False, + include_ip=False, timeout=None, reissue_time=None, + ) + self.assertEqual(inst.callback, None) + def test_unauthenticated_userid_returns_None(self): request = DummyRequest({}) policy = self._makeOne(None, None) @@ -971,6 +1019,14 @@ class TestAuthTicket(unittest.TestCase): result = ticket.digest() self.assertEqual(result, '126fd6224912187ee9ffa80e0b81420c') + def test_digest_sha512(self): + ticket = self._makeOne('secret', 'userid', '0.0.0.0', + time=10, hashalg='sha512') + result = ticket.digest() + self.assertEqual(result, '74770b2e0d5b1a54c2a466ec567a40f7d7823576aa49'\ + '3c65fc3445e9b44097f4a80410319ef8cb256a2e60b9'\ + 'c2002e48a9e33a3e8ee4379352c04ef96d2cb278') + def test_cookie_value(self): ticket = self._makeOne('secret', 'userid', '0.0.0.0', time=10, tokens=('a', 'b')) @@ -989,13 +1045,13 @@ class TestBadTicket(unittest.TestCase): self.assertTrue(isinstance(exc, Exception)) class Test_parse_ticket(unittest.TestCase): - def _callFUT(self, secret, ticket, ip): + def _callFUT(self, secret, ticket, ip, hashalg='md5'): from pyramid.authentication import parse_ticket - return parse_ticket(secret, ticket, ip) + return parse_ticket(secret, ticket, ip, hashalg) - def _assertRaisesBadTicket(self, secret, ticket, ip): + def _assertRaisesBadTicket(self, secret, ticket, ip, hashalg='md5'): from pyramid.authentication import BadTicket - self.assertRaises(BadTicket,self._callFUT, secret, ticket, ip) + self.assertRaises(BadTicket,self._callFUT, secret, ticket, ip, hashalg) def test_bad_timestamp(self): ticket = 'x' * 64 @@ -1014,6 +1070,13 @@ class Test_parse_ticket(unittest.TestCase): result = self._callFUT('secret', ticket, '0.0.0.0') self.assertEqual(result, (10, 'userid', ['a', 'b'], '')) + def test_correct_with_user_data_sha512(self): + ticket = '7d947cdef99bad55f8e3382a8bd089bb9dd0547f7925b7d189adc1160cab'\ + '0ec0e6888faa41eba641a18522b26f19109f3ffafb769767ba8a26d02aae'\ + 'ae56599a0000000auserid!a,b!' + result = self._callFUT('secret', ticket, '0.0.0.0', 'sha512') + self.assertEqual(result, (10, 'userid', ['a', 'b'], '')) + class TestSessionAuthenticationPolicy(unittest.TestCase): def _getTargetClass(self): from pyramid.authentication import SessionAuthenticationPolicy @@ -1150,13 +1213,14 @@ class DummyCookieHelper: class DummyAuthTktModule(object): def __init__(self, timestamp=0, userid='userid', tokens=(), user_data='', - parse_raise=False): + parse_raise=False, hashalg="md5"): self.timestamp = timestamp self.userid = userid self.tokens = tokens self.user_data = user_data self.parse_raise = parse_raise - def parse_ticket(secret, value, remote_addr): + self.hashalg = hashalg + def parse_ticket(secret, value, remote_addr, hashalg): self.secret = secret self.value = value self.remote_addr = remote_addr -- cgit v1.2.3 From c4e3f4fe37003f9ad30883a31b3946ed78439506 Mon Sep 17 00:00:00 2001 From: Wyatt L Baldwin Date: Tue, 9 Oct 2012 19:19:13 -0700 Subject: Get rid of monkeypatch of Request and Response in pyramid.__init__ Added `request.default_request_factory()`, which is now used as the default request factory in the `Router` constructor instead of using the `Request` class directly as the default request factory. Fixes #702 --- pyramid/__init__.py | 5 ----- pyramid/request.py | 7 +++++++ pyramid/router.py | 5 +++-- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/pyramid/__init__.py b/pyramid/__init__.py index 473d5e1c6..e69de29bb 100644 --- a/pyramid/__init__.py +++ b/pyramid/__init__.py @@ -1,5 +0,0 @@ -from pyramid.request import Request -from pyramid.response import Response -Response.RequestClass = Request -Request.ResponseClass = Response -del Request, Response diff --git a/pyramid/request.py b/pyramid/request.py index af3310829..0abf87dbb 100644 --- a/pyramid/request.py +++ b/pyramid/request.py @@ -9,6 +9,7 @@ from webob import BaseRequest from pyramid.interfaces import ( IRequest, + IRequestFactory, IResponse, ISessionFactory, IResponseFactory, @@ -28,6 +29,12 @@ from pyramid.response import Response from pyramid.url import URLMethodsMixin from pyramid.util import InstancePropertyMixin + +@implementer(IRequestFactory) +def default_request_factory(environ): + return Request(environ, ResponseClass=Response) + + class TemplateContext(object): pass diff --git a/pyramid/router.py b/pyramid/router.py index 0c7f61071..f1318ec3c 100644 --- a/pyramid/router.py +++ b/pyramid/router.py @@ -25,7 +25,8 @@ from pyramid.events import ( ) from pyramid.httpexceptions import HTTPNotFound -from pyramid.request import Request +from pyramid.request import default_request_factory +from pyramid.response import Response from pyramid.threadlocal import manager from pyramid.traversal import ( @@ -48,7 +49,7 @@ class Router(object): self.logger = q(IDebugLogger) self.root_factory = q(IRootFactory, default=DefaultRootFactory) self.routes_mapper = q(IRoutesMapper) - self.request_factory = q(IRequestFactory, default=Request) + self.request_factory = q(IRequestFactory, default=default_request_factory) self.request_extensions = q(IRequestExtensions) tweens = q(ITweens) if tweens is None: -- cgit v1.2.3 From 0a1fb171514f4a41cf8679ef61c06397854dde07 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Sun, 14 Oct 2012 20:12:00 -0500 Subject: pypi support for python 3.3 --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index 9af2f2100..40117cf4c 100644 --- a/setup.py +++ b/setup.py @@ -79,6 +79,7 @@ setup(name='pyramid', "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.2", + "Programming Language :: Python :: 3.3", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Framework :: Pyramid", -- cgit v1.2.3 From 4b552e539a1725356b9982261b73fd88de7d59a1 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Tue, 30 Oct 2012 01:00:55 -0500 Subject: raise exc if view_execution_permitted invoked on non-existant view fix #299 --- pyramid/security.py | 6 ++++++ pyramid/tests/test_security.py | 18 ++++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/pyramid/security.py b/pyramid/security.py index 4b929241e..5d4a8db4a 100644 --- a/pyramid/security.py +++ b/pyramid/security.py @@ -4,6 +4,7 @@ from pyramid.interfaces import ( IAuthenticationPolicy, IAuthorizationPolicy, ISecuredView, + IView, IViewClassifier, ) @@ -140,6 +141,11 @@ def view_execution_permitted(context, request, name=''): provides = [IViewClassifier] + map_(providedBy, (request, context)) view = reg.adapters.lookup(provides, ISecuredView, name=name) if view is None: + view = reg.adapters.lookup(provides, IView, name=name) + if view is None: + raise TypeError('No registered view satisfies the constraints. ' + 'It would not make sense to claim that this view ' + '"is" or "is not" permitted.') return Allowed( 'Allowed: view name %r in context %r (no permission defined)' % (name, context)) diff --git a/pyramid/tests/test_security.py b/pyramid/tests/test_security.py index ba9538b01..e530e33ca 100644 --- a/pyramid/tests/test_security.py +++ b/pyramid/tests/test_security.py @@ -131,19 +131,37 @@ class TestViewExecutionPermitted(unittest.TestCase): return checker def test_no_permission(self): + from zope.interface import Interface from pyramid.threadlocal import get_current_registry from pyramid.interfaces import ISettings + from pyramid.interfaces import IView + from pyramid.interfaces import IViewClassifier settings = dict(debug_authorization=True) reg = get_current_registry() reg.registerUtility(settings, ISettings) context = DummyContext() request = DummyRequest({}) + class DummyView(object): + pass + view = DummyView() + reg.registerAdapter(view, (IViewClassifier, Interface, Interface), + IView, '') result = self._callFUT(context, request, '') msg = result.msg self.assertTrue("Allowed: view name '' in context" in msg) self.assertTrue('(no permission defined)' in msg) self.assertEqual(result, True) + def test_no_view_registered(self): + from pyramid.threadlocal import get_current_registry + from pyramid.interfaces import ISettings + settings = dict(debug_authorization=True) + reg = get_current_registry() + reg.registerUtility(settings, ISettings) + context = DummyContext() + request = DummyRequest({}) + self.assertRaises(TypeError, self._callFUT, context, request, '') + def test_with_permission(self): from zope.interface import Interface from zope.interface import directlyProvides -- cgit v1.2.3 From 6099144cf0b85ecfb9c97e344ef6ca499833725e Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Tue, 30 Oct 2012 01:08:35 -0500 Subject: updated changes --- CHANGES.txt | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGES.txt b/CHANGES.txt index 740de0f17..25d2dc75c 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -6,6 +6,14 @@ Features - Added an ``effective_principals`` route and view predicate. +Bug Fixes +--------- + +- :func:`pyramid.security.view_execution_permitted` would return `True` if + no view could be found. This case now raises an exception as it doesn't + make sense make an assertion about a non-existant view. See + https://github.com/Pylons/pyramid/issues/299. + 1.4a3 (2012-10-26) ================== -- cgit v1.2.3 From 6e96403bbc3069ec876690be976121b759c51cb1 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Tue, 30 Oct 2012 01:09:51 -0500 Subject: updated docs --- pyramid/security.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/pyramid/security.py b/pyramid/security.py index 5d4a8db4a..3e25f9b2f 100644 --- a/pyramid/security.py +++ b/pyramid/security.py @@ -133,7 +133,13 @@ def view_execution_permitted(context, request, name=''): view using the effective authentication/authorization policies and the ``request``. Return a boolean result. If no :term:`authorization policy` is in effect, or if the view is not - protected by a permission, return ``True``.""" + protected by a permission, return ``True``. If no view can view found, + an exception will be raised. + + .. versionchanged:: 1.4a4 + An exception is raised if no view is found. + + """ try: reg = request.registry except AttributeError: -- cgit v1.2.3 From 41b7db829af0700c0b02185067db5b2ffd3e43f1 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Tue, 30 Oct 2012 02:24:43 -0500 Subject: updated authentication policy api documentation --- docs/api/authentication.rst | 10 ++++++ pyramid/authentication.py | 83 ++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 92 insertions(+), 1 deletion(-) diff --git a/docs/api/authentication.rst b/docs/api/authentication.rst index 587026a3b..19d08618b 100644 --- a/docs/api/authentication.rst +++ b/docs/api/authentication.rst @@ -9,14 +9,24 @@ Authentication Policies .. automodule:: pyramid.authentication .. autoclass:: AuthTktAuthenticationPolicy + :members: + :inherited-members: .. autoclass:: RemoteUserAuthenticationPolicy + :members: + :inherited-members: .. autoclass:: SessionAuthenticationPolicy + :members: + :inherited-members: .. autoclass:: BasicAuthAuthenticationPolicy + :members: + :inherited-members: .. autoclass:: RepozeWho1AuthenticationPolicy + :members: + :inherited-members: Helper Classes ~~~~~~~~~~~~~~ diff --git a/pyramid/authentication.py b/pyramid/authentication.py index d4fd7ab8b..1bae20937 100644 --- a/pyramid/authentication.py +++ b/pyramid/authentication.py @@ -48,6 +48,15 @@ class CallbackAuthenticationPolicy(object): logger.debug(methodname + ': ' + msg) def authenticated_userid(self, request): + """ Return the authenticated userid or ``None``. + + If no callback is registered, this will be the same as + ``unauthenticated_userid``. + + If a ``callback`` is registered, this will return the userid if + and only if the callback returns a value that is not ``None``. + + """ debug = self.debug userid = self.unauthenticated_userid(request) if userid is None: @@ -78,6 +87,28 @@ class CallbackAuthenticationPolicy(object): ) def effective_principals(self, request): + """ A list of effective principals derived from request. + + This will return a list of principals including, at least, + :data:`pyramid.security.Everyone`. If there is no authenticated + userid, or the ``callback`` returns ``None``, this will be the + only principal: + + .. code-block:: python + + return [Everyone] + + If the ``callback`` does not return ``None`` and an authenticated + userid is found, then the principals will include + :data:`pyramid.security.Authenticated`, the ``authenticated_userid`` + and the list of principals returned by the ``callback``: + + .. code-block:: python + + extra_principals = callback(userid, request) + return [Everyone, Authenticated, userid] + extra_principals + + """ debug = self.debug effective_principals = [Everyone] userid = self.unauthenticated_userid(request) @@ -163,6 +194,15 @@ class RepozeWho1AuthenticationPolicy(CallbackAuthenticationPolicy): return identifier def authenticated_userid(self, request): + """ Return the authenticated userid or ``None``. + + If no callback is registered, this will be the same as + ``unauthenticated_userid``. + + If a ``callback`` is registered, this will return the userid if + and only if the callback returns a value that is not ``None``. + + """ identity = self._get_identity(request) if identity is None: return None @@ -172,12 +212,25 @@ class RepozeWho1AuthenticationPolicy(CallbackAuthenticationPolicy): return identity['repoze.who.userid'] def unauthenticated_userid(self, request): + """ Return the ``repoze.who.userid`` key from the detected identity.""" identity = self._get_identity(request) if identity is None: return None return identity['repoze.who.userid'] def effective_principals(self, request): + """ A list of effective principals derived from the identity. + + This will return a list of principals including, at least, + :data:`pyramid.security.Everyone`. If there is no identity, or + the ``callback`` returns ``None``, this will be the only principal. + + If the ``callback`` does not return ``None`` and an identity is + found, then the principals will include + :data:`pyramid.security.Authenticated`, the ``authenticated_userid`` + and the list of principals returned by the ``callback``. + + """ effective_principals = [Everyone] identity = self._get_identity(request) if identity is None: @@ -196,6 +249,7 @@ class RepozeWho1AuthenticationPolicy(CallbackAuthenticationPolicy): return effective_principals def remember(self, request, principal, **kw): + """ Store the ``principal`` as ``repoze.who.userid``.""" identifier = self._get_identifier(request) if identifier is None: return [] @@ -204,6 +258,12 @@ class RepozeWho1AuthenticationPolicy(CallbackAuthenticationPolicy): return identifier.remember(environ, identity) def forget(self, request): + """ Forget the current authenticated user. + + Return headers that, if included in a response, will delete the + cookie responsible for tracking the current user. + + """ identifier = self._get_identifier(request) if identifier is None: return [] @@ -247,12 +307,19 @@ class RemoteUserAuthenticationPolicy(CallbackAuthenticationPolicy): self.debug = debug def unauthenticated_userid(self, request): + """ The ``REMOTE_USER`` value found within the ``environ``.""" return request.environ.get(self.environ_key) def remember(self, request, principal, **kw): + """ A no-op. The ``REMOTE_USER`` does not provide a protocol for + remembering the user. This will be application-specific and can + be done somewhere else or in a subclass.""" return [] def forget(self, request): + """ A no-op. The ``REMOTE_USER`` does not provide a protocol for + forgetting the user. This will be application-specific and can + be done somewhere else or in a subclass.""" return [] @implementer(IAuthenticationPolicy) @@ -388,16 +455,23 @@ class AuthTktAuthenticationPolicy(CallbackAuthenticationPolicy): self.debug = debug def unauthenticated_userid(self, request): + """ The userid key within the auth_tkt cookie.""" result = self.cookie.identify(request) if result: return result['userid'] def remember(self, request, principal, **kw): """ Accepts the following kw args: ``max_age=, - ``tokens=``""" + ``tokens=``. + + Return a list of headers which will set appropriate cookies on + the response. + + """ return self.cookie.remember(request, principal, **kw) def forget(self, request): + """ A list of headers which will delete appropriate cookies.""" return self.cookie.forget(request) def b64encode(v): @@ -860,14 +934,21 @@ class BasicAuthAuthenticationPolicy(CallbackAuthenticationPolicy): self.debug = debug def unauthenticated_userid(self, request): + """ The userid parsed from the ``Authorization`` request header.""" credentials = self._get_credentials(request) if credentials: return credentials[0] def remember(self, request, principal, **kw): + """ A no-op. Basic authentication does not provide a protocol for + remembering the user. Credentials are sent on every request. + + """ return [] def forget(self, request): + """ Returns challenge headers. This should be attached to a response + to indicate that credentials are required.""" return [('WWW-Authenticate', 'Basic realm="%s"' % self.realm)] def callback(self, username, request): -- cgit v1.2.3 From a2b3e2847ba6e09bf24c1e455dda96b35a090b50 Mon Sep 17 00:00:00 2001 From: Carlos de la Guardia Date: Fri, 2 Nov 2012 00:06:58 -0600 Subject: typo --- docs/narr/renderers.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/narr/renderers.rst b/docs/narr/renderers.rst index 63287e2cd..1158d2225 100644 --- a/docs/narr/renderers.rst +++ b/docs/narr/renderers.rst @@ -329,7 +329,7 @@ time "by hand". Configure a JSONP renderer using the Once this renderer is registered via :meth:`~pyramid.config.Configurator.add_renderer` as above, you can use ``jsonp`` as the ``renderer=`` parameter to ``@view_config`` or -:meth:`pyramid.config.Configurator.add_view``: +:meth:`pyramid.config.Configurator.add_view`: .. code-block:: python -- cgit v1.2.3 From 76430b07f199cdbdbd19f463367c72eda1b537c3 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Fri, 2 Nov 2012 13:13:26 -0400 Subject: explain csrf token stealing potentiality --- docs/narr/sessions.rst | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/docs/narr/sessions.rst b/docs/narr/sessions.rst index 1aa1b6341..f7da7838e 100644 --- a/docs/narr/sessions.rst +++ b/docs/narr/sessions.rst @@ -63,10 +63,15 @@ application by using the ``session_factory`` argument to the this implementation is, by default, *unencrypted*. You should not use it when you keep sensitive information in the session object, as the information can be easily read by both users of your application and third - parties who have access to your users' network traffic. Use a different - session factory implementation (preferably one which keeps session data on - the server) for anything but the most basic of applications where "session - security doesn't matter". + parties who have access to your users' network traffic. And if you use this + sessioning implementation, and you inadvertently create a cross-site + scripting vulnerability in your application, because the session data is + stored unencrypted in a cookie, it will also be easier for evildoers to + obtain the current user's cross-site scripting token. In short, use a + different session factory implementation (preferably one which keeps session + data on the server) for anything but the most basic of applications where + "session security doesn't matter", and you are sure your application has no + cross-site scripting vulnerabilities. .. index:: single: session object -- cgit v1.2.3 From 07c9ee0ec96eb664974fe314a46389ed59390520 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Fri, 2 Nov 2012 21:05:46 -0400 Subject: - Do not allow the userid returned from the ``authenticated_userid`` or the userid that is one of the list of principals returned by ``effective_principals`` to be either of the strings ``system.Everyone`` or ``system.Authenticated`` when any of the built-in authorization policies that live in ``pyramid.authentication`` are in use. These two strings are reserved for internal usage by Pyramid and they will not be accepted as valid userids. --- CHANGES.txt | 8 +++ pyramid/authentication.py | 89 +++++++++++++++++++++++++++++++-- pyramid/tests/test_authentication.py | 97 ++++++++++++++++++++++++++++++++++++ 3 files changed, 191 insertions(+), 3 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 740de0f17..291795da3 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -6,6 +6,14 @@ Features - Added an ``effective_principals`` route and view predicate. +- Do not allow the userid returned from the ``authenticated_userid`` or the + userid that is one of the list of principals returned by + ``effective_principals`` to be either of the strings ``system.Everyone`` or + ``system.Authenticated`` when any of the built-in authorization policies that + live in ``pyramid.authentication`` are in use. These two strings are + reserved for internal usage by Pyramid and they will not be accepted as valid + userids. + 1.4a3 (2012-10-26) ================== diff --git a/pyramid/authentication.py b/pyramid/authentication.py index 1bae20937..8be34cc0a 100644 --- a/pyramid/authentication.py +++ b/pyramid/authentication.py @@ -47,6 +47,11 @@ class CallbackAuthenticationPolicy(object): methodname = classname + '.' + methodname logger.debug(methodname + ': ' + msg) + def _clean_principal(self, princid): + if princid in (Authenticated, Everyone): + princid = None + return princid + def authenticated_userid(self, request): """ Return the authenticated userid or ``None``. @@ -65,6 +70,14 @@ class CallbackAuthenticationPolicy(object): 'authenticated_userid', request) return None + if self._clean_principal(userid) is None: + debug and self._log( + ('use of userid %r is disallowed by any built-in Pyramid ' + 'security policy, returning None' % userid), + 'authenticated_userid' , + request) + return None + if self.callback is None: debug and self._log( 'there was no groupfinder callback; returning %r' % (userid,), @@ -112,6 +125,7 @@ class CallbackAuthenticationPolicy(object): debug = self.debug effective_principals = [Everyone] userid = self.unauthenticated_userid(request) + if userid is None: debug and self._log( 'unauthenticated_userid returned %r; returning %r' % ( @@ -120,6 +134,16 @@ class CallbackAuthenticationPolicy(object): request ) return effective_principals + + if self._clean_principal(userid) is None: + debug and self._log( + ('unauthenticated_userid returned disallowed %r; returning %r ' + 'as if it was None' % (userid, effective_principals)), + 'effective_principals', + request + ) + return effective_principals + if self.callback is None: debug and self._log( 'groupfinder callback is None, so groups is []', @@ -132,6 +156,7 @@ class CallbackAuthenticationPolicy(object): 'groupfinder callback returned %r as groups' % (groups,), 'effective_principals', request) + if groups is None: # is None! debug and self._log( 'returning effective principals: %r' % ( @@ -204,12 +229,36 @@ class RepozeWho1AuthenticationPolicy(CallbackAuthenticationPolicy): """ identity = self._get_identity(request) + if identity is None: + self.debug and self._log( + 'repoze.who identity is None, returning None', + 'authenticated_userid', + request) return None + + userid = identity['repoze.who.userid'] + + if userid is None: + self.debug and self._log( + 'repoze.who.userid is None, returning None' % userid, + 'authenticated_userid', + request) + return None + + if self._clean_principal(userid) is None: + self.debug and self._log( + ('use of userid %r is disallowed by any built-in Pyramid ' + 'security policy, returning None' % userid), + 'authenticated_userid', + request) + return None + if self.callback is None: - return identity['repoze.who.userid'] + return userid + if self.callback(identity, request) is not None: # is not None! - return identity['repoze.who.userid'] + return userid def unauthenticated_userid(self, request): """ Return the ``repoze.who.userid`` key from the detected identity.""" @@ -233,19 +282,53 @@ class RepozeWho1AuthenticationPolicy(CallbackAuthenticationPolicy): """ effective_principals = [Everyone] identity = self._get_identity(request) + if identity is None: + self.debug and self._log( + ('repoze.who identity was None; returning %r' % + effective_principals), + 'effective_principals', + request + ) return effective_principals + if self.callback is None: groups = [] else: groups = self.callback(identity, request) + if groups is None: # is None! + self.debug and self._log( + ('security policy groups callback returned None; returning %r' % + effective_principals), + 'effective_principals', + request + ) return effective_principals + userid = identity['repoze.who.userid'] + + if userid is None: + self.debug and self._log( + ('repoze.who.userid was None; returning %r' % + effective_principals), + 'effective_principals', + request + ) + return effective_principals + + if self._clean_principal(userid) is None: + self.debug and self._log( + ('unauthenticated_userid returned disallowed %r; returning %r ' + 'as if it was None' % (userid, effective_principals)), + 'effective_principals', + request + ) + return effective_principals + effective_principals.append(Authenticated) effective_principals.append(userid) effective_principals.extend(groups) - return effective_principals def remember(self, request, principal, **kw): diff --git a/pyramid/tests/test_authentication.py b/pyramid/tests/test_authentication.py index dfe3cf0b0..2b7a770c1 100644 --- a/pyramid/tests/test_authentication.py +++ b/pyramid/tests/test_authentication.py @@ -76,6 +76,30 @@ class TestCallbackAuthenticationPolicyDebugging(unittest.TestCase): "authenticated_userid: groupfinder callback returned []; " "returning 'fred'") + def test_authenticated_userid_fails_cleaning_as_Authenticated(self): + request = DummyRequest(registry=self.config.registry) + policy = self._makeOne(userid='system.Authenticated') + self.assertEqual(policy.authenticated_userid(request), None) + self.assertEqual(len(self.messages), 1) + self.assertEqual( + self.messages[0], + "pyramid.tests.test_authentication.MyAuthenticationPolicy." + "authenticated_userid: use of userid 'system.Authenticated' is " + "disallowed by any built-in Pyramid security policy, returning " + "None") + + def test_authenticated_userid_fails_cleaning_as_Everyone(self): + request = DummyRequest(registry=self.config.registry) + policy = self._makeOne(userid='system.Everyone') + self.assertEqual(policy.authenticated_userid(request), None) + self.assertEqual(len(self.messages), 1) + self.assertEqual( + self.messages[0], + "pyramid.tests.test_authentication.MyAuthenticationPolicy." + "authenticated_userid: use of userid 'system.Everyone' is " + "disallowed by any built-in Pyramid security policy, returning " + "None") + def test_effective_principals_no_unauthenticated_userid(self): request = DummyRequest(registry=self.config.registry) policy = self._makeOne() @@ -144,6 +168,34 @@ class TestCallbackAuthenticationPolicyDebugging(unittest.TestCase): "effective_principals: returning effective principals: " "['system.Everyone', 'system.Authenticated', 'fred']") + def test_effective_principals_with_unclean_principal_Authenticated(self): + request = DummyRequest(registry=self.config.registry) + policy = self._makeOne(userid='system.Authenticated') + self.assertEqual( + policy.effective_principals(request), + ['system.Everyone']) + self.assertEqual(len(self.messages), 1) + self.assertEqual( + self.messages[0], + "pyramid.tests.test_authentication.MyAuthenticationPolicy." + "effective_principals: unauthenticated_userid returned disallowed " + "'system.Authenticated'; returning ['system.Everyone'] as if it " + "was None") + + def test_effective_principals_with_unclean_principal_Everyone(self): + request = DummyRequest(registry=self.config.registry) + policy = self._makeOne(userid='system.Everyone') + self.assertEqual( + policy.effective_principals(request), + ['system.Everyone']) + self.assertEqual(len(self.messages), 1) + self.assertEqual( + self.messages[0], + "pyramid.tests.test_authentication.MyAuthenticationPolicy." + "effective_principals: unauthenticated_userid returned disallowed " + "'system.Everyone'; returning ['system.Everyone'] as if it " + "was None") + class TestRepozeWho1AuthenticationPolicy(unittest.TestCase): def _getTargetClass(self): from pyramid.authentication import RepozeWho1AuthenticationPolicy @@ -184,6 +236,12 @@ class TestRepozeWho1AuthenticationPolicy(unittest.TestCase): policy = self._makeOne() self.assertEqual(policy.authenticated_userid(request), 'fred') + def test_authenticated_userid_repoze_who_userid_is_None(self): + request = DummyRequest( + {'repoze.who.identity':{'repoze.who.userid':None}}) + policy = self._makeOne() + self.assertEqual(policy.authenticated_userid(request), None) + def test_authenticated_userid_with_callback_returns_None(self): request = DummyRequest( {'repoze.who.identity':{'repoze.who.userid':'fred'}}) @@ -200,6 +258,20 @@ class TestRepozeWho1AuthenticationPolicy(unittest.TestCase): policy = self._makeOne(callback=callback) self.assertEqual(policy.authenticated_userid(request), 'fred') + def test_authenticated_userid_unclean_principal_Authenticated(self): + request = DummyRequest( + {'repoze.who.identity':{'repoze.who.userid':'system.Authenticated'}} + ) + policy = self._makeOne() + self.assertEqual(policy.authenticated_userid(request), None) + + def test_authenticated_userid_unclean_principal_Everyone(self): + request = DummyRequest( + {'repoze.who.identity':{'repoze.who.userid':'system.Everyone'}} + ) + policy = self._makeOne() + self.assertEqual(policy.authenticated_userid(request), None) + def test_effective_principals_None(self): from pyramid.security import Everyone request = DummyRequest({}) @@ -237,6 +309,31 @@ class TestRepozeWho1AuthenticationPolicy(unittest.TestCase): policy = self._makeOne(callback=callback) self.assertEqual(policy.effective_principals(request), [Everyone]) + def test_effective_principals_repoze_who_userid_is_None(self): + from pyramid.security import Everyone + request = DummyRequest( + {'repoze.who.identity':{'repoze.who.userid':None}} + ) + policy = self._makeOne() + self.assertEqual(policy.effective_principals(request), [Everyone]) + + def test_effective_principals_repoze_who_userid_is_unclean_Everyone(self): + from pyramid.security import Everyone + request = DummyRequest( + {'repoze.who.identity':{'repoze.who.userid':'system.Everyone'}} + ) + policy = self._makeOne() + self.assertEqual(policy.effective_principals(request), [Everyone]) + + def test_effective_principals_repoze_who_userid_is_unclean_Authenticated( + self): + from pyramid.security import Everyone + request = DummyRequest( + {'repoze.who.identity':{'repoze.who.userid':'system.Authenticated'}} + ) + policy = self._makeOne() + self.assertEqual(policy.effective_principals(request), [Everyone]) + def test_remember_no_plugins(self): request = DummyRequest({}) policy = self._makeOne() -- cgit v1.2.3 From 47146eb568aa35f88c567fd785f676a89ce76e12 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Fri, 2 Nov 2012 21:07:15 -0400 Subject: garden --- CHANGES.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGES.txt b/CHANGES.txt index 291795da3..80d4a5dee 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -14,6 +14,8 @@ Features reserved for internal usage by Pyramid and they will not be accepted as valid userids. +- Slightly better debug logging from RepozeWho1AuthenticationPolicy. + 1.4a3 (2012-10-26) ================== -- cgit v1.2.3 From 2c3e04fda933f5faf3f365b22cda8accf79841de Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Fri, 2 Nov 2012 21:26:14 -0400 Subject: garden --- TODO.txt | 3 --- 1 file changed, 3 deletions(-) diff --git a/TODO.txt b/TODO.txt index 6787b8a55..ba0e6a3bc 100644 --- a/TODO.txt +++ b/TODO.txt @@ -13,9 +13,6 @@ Nice-to-Have - Have action methods return their discriminators. -- Add docs about upgrading between Pyramid versions (e.g. how to see - deprecation warnings). - - Fix renderers chapter to better document system values passed to template renderers. -- cgit v1.2.3 From a007a4f5a7a6c81ef9bb15f4ccea35212bc020c0 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Fri, 2 Nov 2012 22:00:35 -0400 Subject: - In the past if a renderer returned ``None``, the body of the resulting response would be set explicitly to the empty string. Instead, now, the body is left unchanged, which allows the renderer to set a body itself by using e.g. ``request.response.body = b'foo'``. The body set by the renderer will be unmolested on the way out. See https://github.com/Pylons/pyramid/issues/709 Closes #709 --- CHANGES.txt | 10 ++++++++++ pyramid/renderers.py | 12 +++++------- pyramid/tests/test_renderers.py | 12 +++++++++++- 3 files changed, 26 insertions(+), 8 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 563851e74..0ef1a0593 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -21,6 +21,16 @@ Features it doesn't make sense to assert that a nonexistent view is execution-permitted. See https://github.com/Pylons/pyramid/issues/299. +Bug Fixes +--------- + +- In the past if a renderer returned ``None``, the body of the resulting + response would be set explicitly to the empty string. Instead, now, the body + is left unchanged, which allows the renderer to set a body itself by using + e.g. ``request.response.body = b'foo'``. The body set by the renderer will + be unmolested on the way out. See + https://github.com/Pylons/pyramid/issues/709 + 1.4a3 (2012-10-26) ================== diff --git a/pyramid/renderers.py b/pyramid/renderers.py index 57a61ebba..6839d72f5 100644 --- a/pyramid/renderers.py +++ b/pyramid/renderers.py @@ -573,13 +573,11 @@ class RendererHelper(object): response = response_factory() - if result is None: - result = '' - - if isinstance(result, text_type): - response.text = result - else: - response.body = result + if result is not None: + if isinstance(result, text_type): + response.text = result + else: + response.body = result if request is not None: # deprecated mechanism to set up request.response_* attrs, see diff --git a/pyramid/tests/test_renderers.py b/pyramid/tests/test_renderers.py index cb6c364a7..befb714bd 100644 --- a/pyramid/tests/test_renderers.py +++ b/pyramid/tests/test_renderers.py @@ -663,13 +663,23 @@ class TestRendererHelper(unittest.TestCase): response = helper._make_response(la.encode('utf-8'), request) self.assertEqual(response.body, la.encode('utf-8')) - def test__make_response_result_is_None(self): + def test__make_response_result_is_None_no_body(self): from pyramid.response import Response request = testing.DummyRequest() request.response = Response() helper = self._makeOne('loo.foo') response = helper._make_response(None, request) self.assertEqual(response.body, b'') + + def test__make_response_result_is_None_existing_body_not_molested(self): + from pyramid.response import Response + request = testing.DummyRequest() + response = Response() + response.body = b'abc' + request.response = response + helper = self._makeOne('loo.foo') + response = helper._make_response(None, request) + self.assertEqual(response.body, b'abc') def test__make_response_with_content_type(self): from pyramid.response import Response -- cgit v1.2.3 From 44d73a4f72d2d822b7772a6596edb6d3720d6598 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Fri, 2 Nov 2012 22:23:03 -0400 Subject: garden --- TODO.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/TODO.txt b/TODO.txt index ba0e6a3bc..ac5738244 100644 --- a/TODO.txt +++ b/TODO.txt @@ -6,8 +6,6 @@ Nice-to-Have - config.set_registry_attr (with conflict detection). -- _fix_registry should dictify the registry being fixed. - - Provide the presumed renderer name to the called view as an attribute of the request. @@ -174,3 +172,5 @@ Probably Bad Ideas with config.partial(introspection=False) as c: c.add_view(..) +- _fix_registry should dictify the registry being fixed. + -- cgit v1.2.3 From c60c0e01d9053fdb676af9f3bcac883cefd671e8 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Fri, 2 Nov 2012 22:45:58 -0400 Subject: dont use a separate request factory; instead just assign ResponseClass attr of the Request class to pyramid.response.Response --- pyramid/__init__.py | 1 + pyramid/request.py | 9 ++------- pyramid/router.py | 5 ++--- pyramid/tests/test_request.py | 6 +++++- 4 files changed, 10 insertions(+), 11 deletions(-) diff --git a/pyramid/__init__.py b/pyramid/__init__.py index e69de29bb..5bb534f79 100644 --- a/pyramid/__init__.py +++ b/pyramid/__init__.py @@ -0,0 +1 @@ +# package diff --git a/pyramid/request.py b/pyramid/request.py index 0abf87dbb..9e275c2c0 100644 --- a/pyramid/request.py +++ b/pyramid/request.py @@ -9,7 +9,6 @@ from webob import BaseRequest from pyramid.interfaces import ( IRequest, - IRequestFactory, IResponse, ISessionFactory, IResponseFactory, @@ -29,12 +28,6 @@ from pyramid.response import Response from pyramid.url import URLMethodsMixin from pyramid.util import InstancePropertyMixin - -@implementer(IRequestFactory) -def default_request_factory(environ): - return Request(environ, ResponseClass=Response) - - class TemplateContext(object): pass @@ -335,6 +328,8 @@ class Request(BaseRequest, DeprecatedRequestMethodsMixin, URLMethodsMixin, matchdict = None matched_route = None + ResponseClass = Response + @reify def tmpl_context(self): # docs-deprecated template context for Pylons-like apps; do not diff --git a/pyramid/router.py b/pyramid/router.py index f1318ec3c..0c7f61071 100644 --- a/pyramid/router.py +++ b/pyramid/router.py @@ -25,8 +25,7 @@ from pyramid.events import ( ) from pyramid.httpexceptions import HTTPNotFound -from pyramid.request import default_request_factory -from pyramid.response import Response +from pyramid.request import Request from pyramid.threadlocal import manager from pyramid.traversal import ( @@ -49,7 +48,7 @@ class Router(object): self.logger = q(IDebugLogger) self.root_factory = q(IRootFactory, default=DefaultRootFactory) self.routes_mapper = q(IRoutesMapper) - self.request_factory = q(IRequestFactory, default=default_request_factory) + self.request_factory = q(IRequestFactory, default=Request) self.request_extensions = q(IRequestExtensions) tweens = q(ITweens) if tweens is None: diff --git a/pyramid/tests/test_request.py b/pyramid/tests/test_request.py index 86cfd8b09..945e36a7f 100644 --- a/pyramid/tests/test_request.py +++ b/pyramid/tests/test_request.py @@ -42,13 +42,17 @@ class TestRequest(unittest.TestCase): from zope.interface.verify import verifyClass from pyramid.interfaces import IRequest verifyClass(IRequest, self._getTargetClass()) - klass = self._getTargetClass() def test_instance_conforms_to_IRequest(self): from zope.interface.verify import verifyObject from pyramid.interfaces import IRequest verifyObject(IRequest, self._makeOne()) + def test_ResponseClass_is_pyramid_Response(self): + from pyramid.response import Response + cls = self._getTargetClass() + self.assertEqual(cls.ResponseClass, Response) + def test_charset_defaults_to_utf8(self): r = self._makeOne({'PATH_INFO':'/'}) self.assertEqual(r.charset, 'UTF-8') -- cgit v1.2.3 From cb745b36a4b3b110728bfd4876e2d65649d22f3d Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Fri, 2 Nov 2012 22:48:30 -0400 Subject: garden; fixes #702 --- CHANGES.txt | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGES.txt b/CHANGES.txt index 0ef1a0593..86a9e8b50 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -21,6 +21,13 @@ Features it doesn't make sense to assert that a nonexistent view is execution-permitted. See https://github.com/Pylons/pyramid/issues/299. +- Get rid of shady monkeypatching of ``pyramid.request.Request`` and + ``pyramid.response.Response`` done within the ``__init__.py`` of Pyramid. + Webob no longer relies on this being done. Instead, the ResponseClass + attribute of the Pyramid Request class is assigned to the Pyramid response + class; that's enough to satisfy WebOb and behave as it did before with the + monkeypatching. + Bug Fixes --------- -- cgit v1.2.3 From 20370c3a7b6355f9a43855af3bccf918f9970435 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Sat, 3 Nov 2012 18:10:23 -0400 Subject: why is this file here --- pyramid/tests/test_config/test_init.py:TestConfigurator_add_directive | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 pyramid/tests/test_config/test_init.py:TestConfigurator_add_directive diff --git a/pyramid/tests/test_config/test_init.py:TestConfigurator_add_directive b/pyramid/tests/test_config/test_init.py:TestConfigurator_add_directive deleted file mode 100644 index e69de29bb..000000000 -- cgit v1.2.3 From 66fe1d05adbbcb07482972b4fd512676d68388ee Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Sat, 3 Nov 2012 19:02:53 -0400 Subject: - Move ``TopologicalSorter`` from ``pyramid.config.util`` to ``pyramid.util``, move ``CyclicDependencyError`` from ``pyramid.config.util`` to ``pyramid.exceptions``, rename ``Singleton`` to ``Sentinel`` and move from ``pyramid.config.util`` to ``pyramid.config.util``; this is in an effort to move that stuff that may be an API one day out of ``pyramid.config.util, because that package should never be imported from non-Pyramid code. TopologicalSorter is still not an API, but may become one. --- CHANGES.txt | 11 ++ pyramid/config/util.py | 152 +----------------- pyramid/exceptions.py | 18 +++ pyramid/tests/test_config/test_tweens.py | 4 +- pyramid/tests/test_config/test_util.py | 268 ------------------------------- pyramid/tests/test_exceptions.py | 12 ++ pyramid/tests/test_util.py | 257 +++++++++++++++++++++++++++++ pyramid/util.py | 165 +++++++++++++++++++ 8 files changed, 466 insertions(+), 421 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 86a9e8b50..298bddf7a 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -38,6 +38,17 @@ Bug Fixes be unmolested on the way out. See https://github.com/Pylons/pyramid/issues/709 +Internals +--------- + +- Move ``TopologicalSorter`` from ``pyramid.config.util`` to ``pyramid.util``, + move ``CyclicDependencyError`` from ``pyramid.config.util`` to + ``pyramid.exceptions``, rename ``Singleton`` to ``Sentinel`` and move from + ``pyramid.config.util`` to ``pyramid.config.util``; this is in an effort to + move that stuff that may be an API one day out of ``pyramid.config.util, + because that package should never be imported from non-Pyramid code. + TopologicalSorter is still not an API, but may become one. + 1.4a3 (2012-10-26) ================== diff --git a/pyramid/config/util.py b/pyramid/config/util.py index a4df44408..1c6e1ca15 100644 --- a/pyramid/config/util.py +++ b/pyramid/config/util.py @@ -12,8 +12,8 @@ from pyramid.compat import ( ) from pyramid.exceptions import ConfigurationError - from pyramid.registry import predvalseq +from pyramid.util import TopologicalSorter from hashlib import md5 @@ -72,156 +72,6 @@ def as_sorted_tuple(val): # under = after # over = before -class Singleton(object): - def __init__(self, repr): - self.repr = repr - - def __repr__(self): - return self.repr - -FIRST = Singleton('FIRST') -LAST = Singleton('LAST') - -class TopologicalSorter(object): - def __init__( - self, - default_before=LAST, - default_after=None, - first=FIRST, - last=LAST, - ): - self.names = [] - self.req_before = set() - self.req_after = set() - self.name2before = {} - self.name2after = {} - self.name2val = {} - self.order = [] - self.default_before = default_before - self.default_after = default_after - self.first = first - self.last = last - - def remove(self, name): - self.names.remove(name) - del self.name2val[name] - after = self.name2after.pop(name, []) - if after: - self.req_after.remove(name) - for u in after: - self.order.remove((u, name)) - before = self.name2before.pop(name, []) - if before: - self.req_before.remove(name) - for u in before: - self.order.remove((name, u)) - - def add(self, name, val, after=None, before=None): - if name in self.names: - self.remove(name) - self.names.append(name) - self.name2val[name] = val - if after is None and before is None: - before = self.default_before - after = self.default_after - if after is not None: - if not is_nonstr_iter(after): - after = (after,) - self.name2after[name] = after - self.order += [(u, name) for u in after] - self.req_after.add(name) - if before is not None: - if not is_nonstr_iter(before): - before = (before,) - self.name2before[name] = before - self.order += [(name, o) for o in before] - self.req_before.add(name) - - def sorted(self): - order = [(self.first, self.last)] - roots = [] - graph = {} - names = [self.first, self.last] - names.extend(self.names) - - for a, b in self.order: - order.append((a, b)) - - def add_node(node): - if not node in graph: - roots.append(node) - graph[node] = [0] # 0 = number of arcs coming into this node - - def add_arc(fromnode, tonode): - graph[fromnode].append(tonode) - graph[tonode][0] += 1 - if tonode in roots: - roots.remove(tonode) - - for name in names: - add_node(name) - - has_before, has_after = set(), set() - for a, b in order: - if a in names and b in names: # deal with missing dependencies - add_arc(a, b) - has_before.add(a) - has_after.add(b) - - if not self.req_before.issubset(has_before): - raise ConfigurationError( - 'Unsatisfied before dependencies: %s' - % (', '.join(sorted(self.req_before - has_before))) - ) - if not self.req_after.issubset(has_after): - raise ConfigurationError( - 'Unsatisfied after dependencies: %s' - % (', '.join(sorted(self.req_after - has_after))) - ) - - sorted_names = [] - - while roots: - root = roots.pop(0) - sorted_names.append(root) - children = graph[root][1:] - for child in children: - arcs = graph[child][0] - arcs -= 1 - graph[child][0] = arcs - if arcs == 0: - roots.insert(0, child) - del graph[root] - - if graph: - # loop in input - cycledeps = {} - for k, v in graph.items(): - cycledeps[k] = v[1:] - raise CyclicDependencyError(cycledeps) - - result = [] - - for name in sorted_names: - if name in self.names: - result.append((name, self.name2val[name])) - - return result - -class CyclicDependencyError(Exception): - def __init__(self, cycles): - self.cycles = cycles - - def __str__(self): - L = [] - cycles = self.cycles - for cycle in cycles: - dependent = cycle - dependees = cycles[cycle] - L.append('%r sorts before %r' % (dependent, dependees)) - msg = 'Implicit ordering cycle:' + '; '.join(L) - return msg - class PredicateList(object): def __init__(self): diff --git a/pyramid/exceptions.py b/pyramid/exceptions.py index 04b6e20b7..1c8f99f62 100644 --- a/pyramid/exceptions.py +++ b/pyramid/exceptions.py @@ -60,3 +60,21 @@ class ConfigurationExecutionError(ConfigurationError): def __str__(self): return "%s: %s\n in:\n %s" % (self.etype, self.evalue, self.info) + +class CyclicDependencyError(Exception): + """ The exception raised when the Pyramid topological sorter detects a + cyclic dependency.""" + def __init__(self, cycles): + self.cycles = cycles + + def __str__(self): + L = [] + cycles = self.cycles + for cycle in cycles: + dependent = cycle + dependees = cycles[cycle] + L.append('%r sorts before %r' % (dependent, dependees)) + msg = 'Implicit ordering cycle:' + '; '.join(L) + return msg + + diff --git a/pyramid/tests/test_config/test_tweens.py b/pyramid/tests/test_config/test_tweens.py index 8853b6899..9c3433468 100644 --- a/pyramid/tests/test_config/test_tweens.py +++ b/pyramid/tests/test_config/test_tweens.py @@ -392,7 +392,7 @@ class TestTweens(unittest.TestCase): self.assertRaises(ConfigurationError, tweens.implicit) def test_implicit_ordering_conflict_direct(self): - from pyramid.config.util import CyclicDependencyError + from pyramid.exceptions import CyclicDependencyError tweens = self._makeOne() add = tweens.add_implicit add('browserid', 'browserid_factory') @@ -400,7 +400,7 @@ class TestTweens(unittest.TestCase): self.assertRaises(CyclicDependencyError, tweens.implicit) def test_implicit_ordering_conflict_indirect(self): - from pyramid.config.util import CyclicDependencyError + from pyramid.exceptions import CyclicDependencyError tweens = self._makeOne() add = tweens.add_implicit add('browserid', 'browserid_factory') diff --git a/pyramid/tests/test_config/test_util.py b/pyramid/tests/test_config/test_util.py index 13cb27526..8c3cd7455 100644 --- a/pyramid/tests/test_config/test_util.py +++ b/pyramid/tests/test_config/test_util.py @@ -396,274 +396,6 @@ class TestActionInfo(unittest.TestCase): self.assertEqual(str(inst), "Line 0 of file filename:\n linerepr ") -class TestTopologicalSorter(unittest.TestCase): - def _makeOne(self, *arg, **kw): - from pyramid.config.util import TopologicalSorter - return TopologicalSorter(*arg, **kw) - - def test_remove(self): - inst = self._makeOne() - inst.names.append('name') - inst.name2val['name'] = 1 - inst.req_after.add('name') - inst.req_before.add('name') - inst.name2after['name'] = ('bob',) - inst.name2before['name'] = ('fred',) - inst.order.append(('bob', 'name')) - inst.order.append(('name', 'fred')) - inst.remove('name') - self.assertFalse(inst.names) - self.assertFalse(inst.req_before) - self.assertFalse(inst.req_after) - self.assertFalse(inst.name2before) - self.assertFalse(inst.name2after) - self.assertFalse(inst.name2val) - self.assertFalse(inst.order) - - def test_add(self): - from pyramid.config.util import LAST - sorter = self._makeOne() - sorter.add('name', 'factory') - self.assertEqual(sorter.names, ['name']) - self.assertEqual(sorter.name2val, - {'name':'factory'}) - self.assertEqual(sorter.order, [('name', LAST)]) - sorter.add('name2', 'factory2') - self.assertEqual(sorter.names, ['name', 'name2']) - self.assertEqual(sorter.name2val, - {'name':'factory', 'name2':'factory2'}) - self.assertEqual(sorter.order, - [('name', LAST), ('name2', LAST)]) - sorter.add('name3', 'factory3', before='name2') - self.assertEqual(sorter.names, - ['name', 'name2', 'name3']) - self.assertEqual(sorter.name2val, - {'name':'factory', 'name2':'factory2', - 'name3':'factory3'}) - self.assertEqual(sorter.order, - [('name', LAST), ('name2', LAST), - ('name3', 'name2')]) - - def test_sorted_ordering_1(self): - sorter = self._makeOne() - sorter.add('name1', 'factory1') - sorter.add('name2', 'factory2') - self.assertEqual(sorter.sorted(), - [ - ('name1', 'factory1'), - ('name2', 'factory2'), - ]) - - def test_sorted_ordering_2(self): - from pyramid.config.util import FIRST - sorter = self._makeOne() - sorter.add('name1', 'factory1') - sorter.add('name2', 'factory2', after=FIRST) - self.assertEqual(sorter.sorted(), - [ - ('name2', 'factory2'), - ('name1', 'factory1'), - ]) - - def test_sorted_ordering_3(self): - from pyramid.config.util import FIRST - sorter = self._makeOne() - add = sorter.add - add('auth', 'auth_factory', after='browserid') - add('dbt', 'dbt_factory') - add('retry', 'retry_factory', before='txnmgr', after='exceptionview') - add('browserid', 'browserid_factory') - add('txnmgr', 'txnmgr_factory', after='exceptionview') - add('exceptionview', 'excview_factory', after=FIRST) - self.assertEqual(sorter.sorted(), - [ - ('exceptionview', 'excview_factory'), - ('retry', 'retry_factory'), - ('txnmgr', 'txnmgr_factory'), - ('dbt', 'dbt_factory'), - ('browserid', 'browserid_factory'), - ('auth', 'auth_factory'), - ]) - - def test_sorted_ordering_4(self): - from pyramid.config.util import FIRST - sorter = self._makeOne() - add = sorter.add - add('exceptionview', 'excview_factory', after=FIRST) - add('auth', 'auth_factory', after='browserid') - add('retry', 'retry_factory', before='txnmgr', after='exceptionview') - add('browserid', 'browserid_factory') - add('txnmgr', 'txnmgr_factory', after='exceptionview') - add('dbt', 'dbt_factory') - self.assertEqual(sorter.sorted(), - [ - ('exceptionview', 'excview_factory'), - ('retry', 'retry_factory'), - ('txnmgr', 'txnmgr_factory'), - ('browserid', 'browserid_factory'), - ('auth', 'auth_factory'), - ('dbt', 'dbt_factory'), - ]) - - def test_sorted_ordering_5(self): - from pyramid.config.util import LAST, FIRST - sorter = self._makeOne() - add = sorter.add - add('exceptionview', 'excview_factory') - add('auth', 'auth_factory', after=FIRST) - add('retry', 'retry_factory', before='txnmgr', after='exceptionview') - add('browserid', 'browserid_factory', after=FIRST) - add('txnmgr', 'txnmgr_factory', after='exceptionview', before=LAST) - add('dbt', 'dbt_factory') - self.assertEqual(sorter.sorted(), - [ - ('browserid', 'browserid_factory'), - ('auth', 'auth_factory'), - ('exceptionview', 'excview_factory'), - ('retry', 'retry_factory'), - ('txnmgr', 'txnmgr_factory'), - ('dbt', 'dbt_factory'), - ]) - - def test_sorted_ordering_missing_before_partial(self): - from pyramid.exceptions import ConfigurationError - sorter = self._makeOne() - add = sorter.add - add('dbt', 'dbt_factory') - add('auth', 'auth_factory', after='browserid') - add('retry', 'retry_factory', before='txnmgr', after='exceptionview') - add('browserid', 'browserid_factory') - self.assertRaises(ConfigurationError, sorter.sorted) - - def test_sorted_ordering_missing_after_partial(self): - from pyramid.exceptions import ConfigurationError - sorter = self._makeOne() - add = sorter.add - add('dbt', 'dbt_factory') - add('auth', 'auth_factory', after='txnmgr') - add('retry', 'retry_factory', before='dbt', after='exceptionview') - add('browserid', 'browserid_factory') - self.assertRaises(ConfigurationError, sorter.sorted) - - def test_sorted_ordering_missing_before_and_after_partials(self): - from pyramid.exceptions import ConfigurationError - sorter = self._makeOne() - add = sorter.add - add('dbt', 'dbt_factory') - add('auth', 'auth_factory', after='browserid') - add('retry', 'retry_factory', before='foo', after='txnmgr') - add('browserid', 'browserid_factory') - self.assertRaises(ConfigurationError, sorter.sorted) - - def test_sorted_ordering_missing_before_partial_with_fallback(self): - from pyramid.config.util import LAST - sorter = self._makeOne() - add = sorter.add - add('exceptionview', 'excview_factory', before=LAST) - add('auth', 'auth_factory', after='browserid') - add('retry', 'retry_factory', before=('txnmgr', LAST), - after='exceptionview') - add('browserid', 'browserid_factory') - add('dbt', 'dbt_factory') - self.assertEqual(sorter.sorted(), - [ - ('exceptionview', 'excview_factory'), - ('retry', 'retry_factory'), - ('browserid', 'browserid_factory'), - ('auth', 'auth_factory'), - ('dbt', 'dbt_factory'), - ]) - - def test_sorted_ordering_missing_after_partial_with_fallback(self): - from pyramid.config.util import FIRST - sorter = self._makeOne() - add = sorter.add - add('exceptionview', 'excview_factory', after=FIRST) - add('auth', 'auth_factory', after=('txnmgr','browserid')) - add('retry', 'retry_factory', after='exceptionview') - add('browserid', 'browserid_factory') - add('dbt', 'dbt_factory') - self.assertEqual(sorter.sorted(), - [ - ('exceptionview', 'excview_factory'), - ('retry', 'retry_factory'), - ('browserid', 'browserid_factory'), - ('auth', 'auth_factory'), - ('dbt', 'dbt_factory'), - ]) - - def test_sorted_ordering_with_partial_fallbacks(self): - from pyramid.config.util import LAST - sorter = self._makeOne() - add = sorter.add - add('exceptionview', 'excview_factory', before=('wontbethere', LAST)) - add('retry', 'retry_factory', after='exceptionview') - add('browserid', 'browserid_factory', before=('wont2', 'exceptionview')) - self.assertEqual(sorter.sorted(), - [ - ('browserid', 'browserid_factory'), - ('exceptionview', 'excview_factory'), - ('retry', 'retry_factory'), - ]) - - def test_sorted_ordering_with_multiple_matching_fallbacks(self): - from pyramid.config.util import LAST - sorter = self._makeOne() - add = sorter.add - add('exceptionview', 'excview_factory', before=LAST) - add('retry', 'retry_factory', after='exceptionview') - add('browserid', 'browserid_factory', before=('retry', 'exceptionview')) - self.assertEqual(sorter.sorted(), - [ - ('browserid', 'browserid_factory'), - ('exceptionview', 'excview_factory'), - ('retry', 'retry_factory'), - ]) - - def test_sorted_ordering_with_missing_fallbacks(self): - from pyramid.exceptions import ConfigurationError - from pyramid.config.util import LAST - sorter = self._makeOne() - add = sorter.add - add('exceptionview', 'excview_factory', before=LAST) - add('retry', 'retry_factory', after='exceptionview') - add('browserid', 'browserid_factory', before=('txnmgr', 'auth')) - self.assertRaises(ConfigurationError, sorter.sorted) - - def test_sorted_ordering_conflict_direct(self): - from pyramid.config.util import CyclicDependencyError - sorter = self._makeOne() - add = sorter.add - add('browserid', 'browserid_factory') - add('auth', 'auth_factory', before='browserid', after='browserid') - self.assertRaises(CyclicDependencyError, sorter.sorted) - - def test_sorted_ordering_conflict_indirect(self): - from pyramid.config.util import CyclicDependencyError - sorter = self._makeOne() - add = sorter.add - add('browserid', 'browserid_factory') - add('auth', 'auth_factory', before='browserid') - add('dbt', 'dbt_factory', after='browserid', before='auth') - self.assertRaises(CyclicDependencyError, sorter.sorted) - -class TestSingleton(unittest.TestCase): - def test_repr(self): - from pyramid.config.util import Singleton - r = repr(Singleton('ABC')) - self.assertEqual(r, 'ABC') - -class TestCyclicDependencyError(unittest.TestCase): - def _makeOne(self, cycles): - from pyramid.config.util import CyclicDependencyError - return CyclicDependencyError(cycles) - - def test___str__(self): - exc = self._makeOne({'a':['c', 'd'], 'c':['a']}) - result = str(exc) - self.assertTrue("'a' sorts before ['c', 'd']" in result) - self.assertTrue("'c' sorts before ['a']" in result) - class DummyCustomPredicate(object): def __init__(self): self.__text__ = 'custom predicate' diff --git a/pyramid/tests/test_exceptions.py b/pyramid/tests/test_exceptions.py index 773767d89..aa5ebb376 100644 --- a/pyramid/tests/test_exceptions.py +++ b/pyramid/tests/test_exceptions.py @@ -74,3 +74,15 @@ class TestConfigurationExecutionError(unittest.TestCase): exc = self._makeOne('etype', 'evalue', 'info') self.assertEqual(str(exc), 'etype: evalue\n in:\n info') +class TestCyclicDependencyError(unittest.TestCase): + def _makeOne(self, cycles): + from pyramid.exceptions import CyclicDependencyError + return CyclicDependencyError(cycles) + + def test___str__(self): + exc = self._makeOne({'a':['c', 'd'], 'c':['a']}) + result = str(exc) + self.assertTrue("'a' sorts before ['c', 'd']" in result) + self.assertTrue("'c' sorts before ['a']" in result) + + diff --git a/pyramid/tests/test_util.py b/pyramid/tests/test_util.py index 3d85e18f5..785950230 100644 --- a/pyramid/tests/test_util.py +++ b/pyramid/tests/test_util.py @@ -288,6 +288,263 @@ class Test_object_description(unittest.TestCase): self._callFUT(inst), str(inst)[:100] + ' ... ]') +class TestTopologicalSorter(unittest.TestCase): + def _makeOne(self, *arg, **kw): + from pyramid.util import TopologicalSorter + return TopologicalSorter(*arg, **kw) + + def test_remove(self): + inst = self._makeOne() + inst.names.append('name') + inst.name2val['name'] = 1 + inst.req_after.add('name') + inst.req_before.add('name') + inst.name2after['name'] = ('bob',) + inst.name2before['name'] = ('fred',) + inst.order.append(('bob', 'name')) + inst.order.append(('name', 'fred')) + inst.remove('name') + self.assertFalse(inst.names) + self.assertFalse(inst.req_before) + self.assertFalse(inst.req_after) + self.assertFalse(inst.name2before) + self.assertFalse(inst.name2after) + self.assertFalse(inst.name2val) + self.assertFalse(inst.order) + + def test_add(self): + from pyramid.util import LAST + sorter = self._makeOne() + sorter.add('name', 'factory') + self.assertEqual(sorter.names, ['name']) + self.assertEqual(sorter.name2val, + {'name':'factory'}) + self.assertEqual(sorter.order, [('name', LAST)]) + sorter.add('name2', 'factory2') + self.assertEqual(sorter.names, ['name', 'name2']) + self.assertEqual(sorter.name2val, + {'name':'factory', 'name2':'factory2'}) + self.assertEqual(sorter.order, + [('name', LAST), ('name2', LAST)]) + sorter.add('name3', 'factory3', before='name2') + self.assertEqual(sorter.names, + ['name', 'name2', 'name3']) + self.assertEqual(sorter.name2val, + {'name':'factory', 'name2':'factory2', + 'name3':'factory3'}) + self.assertEqual(sorter.order, + [('name', LAST), ('name2', LAST), + ('name3', 'name2')]) + + def test_sorted_ordering_1(self): + sorter = self._makeOne() + sorter.add('name1', 'factory1') + sorter.add('name2', 'factory2') + self.assertEqual(sorter.sorted(), + [ + ('name1', 'factory1'), + ('name2', 'factory2'), + ]) + + def test_sorted_ordering_2(self): + from pyramid.util import FIRST + sorter = self._makeOne() + sorter.add('name1', 'factory1') + sorter.add('name2', 'factory2', after=FIRST) + self.assertEqual(sorter.sorted(), + [ + ('name2', 'factory2'), + ('name1', 'factory1'), + ]) + + def test_sorted_ordering_3(self): + from pyramid.util import FIRST + sorter = self._makeOne() + add = sorter.add + add('auth', 'auth_factory', after='browserid') + add('dbt', 'dbt_factory') + add('retry', 'retry_factory', before='txnmgr', after='exceptionview') + add('browserid', 'browserid_factory') + add('txnmgr', 'txnmgr_factory', after='exceptionview') + add('exceptionview', 'excview_factory', after=FIRST) + self.assertEqual(sorter.sorted(), + [ + ('exceptionview', 'excview_factory'), + ('retry', 'retry_factory'), + ('txnmgr', 'txnmgr_factory'), + ('dbt', 'dbt_factory'), + ('browserid', 'browserid_factory'), + ('auth', 'auth_factory'), + ]) + + def test_sorted_ordering_4(self): + from pyramid.util import FIRST + sorter = self._makeOne() + add = sorter.add + add('exceptionview', 'excview_factory', after=FIRST) + add('auth', 'auth_factory', after='browserid') + add('retry', 'retry_factory', before='txnmgr', after='exceptionview') + add('browserid', 'browserid_factory') + add('txnmgr', 'txnmgr_factory', after='exceptionview') + add('dbt', 'dbt_factory') + self.assertEqual(sorter.sorted(), + [ + ('exceptionview', 'excview_factory'), + ('retry', 'retry_factory'), + ('txnmgr', 'txnmgr_factory'), + ('browserid', 'browserid_factory'), + ('auth', 'auth_factory'), + ('dbt', 'dbt_factory'), + ]) + + def test_sorted_ordering_5(self): + from pyramid.util import LAST, FIRST + sorter = self._makeOne() + add = sorter.add + add('exceptionview', 'excview_factory') + add('auth', 'auth_factory', after=FIRST) + add('retry', 'retry_factory', before='txnmgr', after='exceptionview') + add('browserid', 'browserid_factory', after=FIRST) + add('txnmgr', 'txnmgr_factory', after='exceptionview', before=LAST) + add('dbt', 'dbt_factory') + self.assertEqual(sorter.sorted(), + [ + ('browserid', 'browserid_factory'), + ('auth', 'auth_factory'), + ('exceptionview', 'excview_factory'), + ('retry', 'retry_factory'), + ('txnmgr', 'txnmgr_factory'), + ('dbt', 'dbt_factory'), + ]) + + def test_sorted_ordering_missing_before_partial(self): + from pyramid.exceptions import ConfigurationError + sorter = self._makeOne() + add = sorter.add + add('dbt', 'dbt_factory') + add('auth', 'auth_factory', after='browserid') + add('retry', 'retry_factory', before='txnmgr', after='exceptionview') + add('browserid', 'browserid_factory') + self.assertRaises(ConfigurationError, sorter.sorted) + + def test_sorted_ordering_missing_after_partial(self): + from pyramid.exceptions import ConfigurationError + sorter = self._makeOne() + add = sorter.add + add('dbt', 'dbt_factory') + add('auth', 'auth_factory', after='txnmgr') + add('retry', 'retry_factory', before='dbt', after='exceptionview') + add('browserid', 'browserid_factory') + self.assertRaises(ConfigurationError, sorter.sorted) + + def test_sorted_ordering_missing_before_and_after_partials(self): + from pyramid.exceptions import ConfigurationError + sorter = self._makeOne() + add = sorter.add + add('dbt', 'dbt_factory') + add('auth', 'auth_factory', after='browserid') + add('retry', 'retry_factory', before='foo', after='txnmgr') + add('browserid', 'browserid_factory') + self.assertRaises(ConfigurationError, sorter.sorted) + + def test_sorted_ordering_missing_before_partial_with_fallback(self): + from pyramid.util import LAST + sorter = self._makeOne() + add = sorter.add + add('exceptionview', 'excview_factory', before=LAST) + add('auth', 'auth_factory', after='browserid') + add('retry', 'retry_factory', before=('txnmgr', LAST), + after='exceptionview') + add('browserid', 'browserid_factory') + add('dbt', 'dbt_factory') + self.assertEqual(sorter.sorted(), + [ + ('exceptionview', 'excview_factory'), + ('retry', 'retry_factory'), + ('browserid', 'browserid_factory'), + ('auth', 'auth_factory'), + ('dbt', 'dbt_factory'), + ]) + + def test_sorted_ordering_missing_after_partial_with_fallback(self): + from pyramid.util import FIRST + sorter = self._makeOne() + add = sorter.add + add('exceptionview', 'excview_factory', after=FIRST) + add('auth', 'auth_factory', after=('txnmgr','browserid')) + add('retry', 'retry_factory', after='exceptionview') + add('browserid', 'browserid_factory') + add('dbt', 'dbt_factory') + self.assertEqual(sorter.sorted(), + [ + ('exceptionview', 'excview_factory'), + ('retry', 'retry_factory'), + ('browserid', 'browserid_factory'), + ('auth', 'auth_factory'), + ('dbt', 'dbt_factory'), + ]) + + def test_sorted_ordering_with_partial_fallbacks(self): + from pyramid.util import LAST + sorter = self._makeOne() + add = sorter.add + add('exceptionview', 'excview_factory', before=('wontbethere', LAST)) + add('retry', 'retry_factory', after='exceptionview') + add('browserid', 'browserid_factory', before=('wont2', 'exceptionview')) + self.assertEqual(sorter.sorted(), + [ + ('browserid', 'browserid_factory'), + ('exceptionview', 'excview_factory'), + ('retry', 'retry_factory'), + ]) + + def test_sorted_ordering_with_multiple_matching_fallbacks(self): + from pyramid.util import LAST + sorter = self._makeOne() + add = sorter.add + add('exceptionview', 'excview_factory', before=LAST) + add('retry', 'retry_factory', after='exceptionview') + add('browserid', 'browserid_factory', before=('retry', 'exceptionview')) + self.assertEqual(sorter.sorted(), + [ + ('browserid', 'browserid_factory'), + ('exceptionview', 'excview_factory'), + ('retry', 'retry_factory'), + ]) + + def test_sorted_ordering_with_missing_fallbacks(self): + from pyramid.exceptions import ConfigurationError + from pyramid.util import LAST + sorter = self._makeOne() + add = sorter.add + add('exceptionview', 'excview_factory', before=LAST) + add('retry', 'retry_factory', after='exceptionview') + add('browserid', 'browserid_factory', before=('txnmgr', 'auth')) + self.assertRaises(ConfigurationError, sorter.sorted) + + def test_sorted_ordering_conflict_direct(self): + from pyramid.exceptions import CyclicDependencyError + sorter = self._makeOne() + add = sorter.add + add('browserid', 'browserid_factory') + add('auth', 'auth_factory', before='browserid', after='browserid') + self.assertRaises(CyclicDependencyError, sorter.sorted) + + def test_sorted_ordering_conflict_indirect(self): + from pyramid.exceptions import CyclicDependencyError + sorter = self._makeOne() + add = sorter.add + add('browserid', 'browserid_factory') + add('auth', 'auth_factory', before='browserid') + add('dbt', 'dbt_factory', after='browserid', before='auth') + self.assertRaises(CyclicDependencyError, sorter.sorted) + +class TestSentinel(unittest.TestCase): + def test_repr(self): + from pyramid.util import Sentinel + r = repr(Sentinel('ABC')) + self.assertEqual(r, 'ABC') + def dummyfunc(): pass class Dummy(object): diff --git a/pyramid/util.py b/pyramid/util.py index 6190e8156..d83837322 100644 --- a/pyramid/util.py +++ b/pyramid/util.py @@ -1,8 +1,14 @@ import inspect import weakref +from pyramid.exceptions import ( + ConfigurationError, + CyclicDependencyError, + ) + from pyramid.compat import ( iteritems_, + is_nonstr_iter, integer_types, string_types, text_, @@ -288,3 +294,162 @@ def shortrepr(object, closer): r = r[:100] + ' ... %s' % closer return r +class Sentinel(object): + def __init__(self, repr): + self.repr = repr + + def __repr__(self): + return self.repr + +FIRST = Sentinel('FIRST') +LAST = Sentinel('LAST') + +class TopologicalSorter(object): + """ A utility class which can be used to perform topological sorts against + tuple-like data.""" + def __init__( + self, + default_before=LAST, + default_after=None, + first=FIRST, + last=LAST, + ): + self.names = [] + self.req_before = set() + self.req_after = set() + self.name2before = {} + self.name2after = {} + self.name2val = {} + self.order = [] + self.default_before = default_before + self.default_after = default_after + self.first = first + self.last = last + + def remove(self, name): + """ Remove a node from the sort input """ + self.names.remove(name) + del self.name2val[name] + after = self.name2after.pop(name, []) + if after: + self.req_after.remove(name) + for u in after: + self.order.remove((u, name)) + before = self.name2before.pop(name, []) + if before: + self.req_before.remove(name) + for u in before: + self.order.remove((name, u)) + + def add(self, name, val, after=None, before=None): + """ Add a node to the sort input. The ``name`` should be a string or + any other hashable object, the ``val`` should be the sortable (doesn't + need to be hashable). ``after`` and ``before`` represents the name of + one of the other sortables (or a sequence of such named) or one of the + special sentinel values :attr:`pyramid.util.FIRST`` or + :attr:`pyramid.util.LAST` representing the first or last positions + respectively. ``FIRST`` and ``LAST`` can also be part of a sequence + passed as ``before`` or ``after``. A sortable should not be added + after LAST or before FIRST. An example:: + + sorter = TopologicalSorter() + sorter.add('a', {'a':1}, before=LAST, after='b') + sorter.add('b', {'b':2}, before=LAST, after='c') + sorter.add('c', {'c':3}) + + sorter.sorted() # will be {'c':3}, {'b':2}, {'a':1} + + """ + if name in self.names: + self.remove(name) + self.names.append(name) + self.name2val[name] = val + if after is None and before is None: + before = self.default_before + after = self.default_after + if after is not None: + if not is_nonstr_iter(after): + after = (after,) + self.name2after[name] = after + self.order += [(u, name) for u in after] + self.req_after.add(name) + if before is not None: + if not is_nonstr_iter(before): + before = (before,) + self.name2before[name] = before + self.order += [(name, o) for o in before] + self.req_before.add(name) + + + def sorted(self): + """ Returns the sort input values in topologically sorted order""" + order = [(self.first, self.last)] + roots = [] + graph = {} + names = [self.first, self.last] + names.extend(self.names) + + for a, b in self.order: + order.append((a, b)) + + def add_node(node): + if not node in graph: + roots.append(node) + graph[node] = [0] # 0 = number of arcs coming into this node + + def add_arc(fromnode, tonode): + graph[fromnode].append(tonode) + graph[tonode][0] += 1 + if tonode in roots: + roots.remove(tonode) + + for name in names: + add_node(name) + + has_before, has_after = set(), set() + for a, b in order: + if a in names and b in names: # deal with missing dependencies + add_arc(a, b) + has_before.add(a) + has_after.add(b) + + if not self.req_before.issubset(has_before): + raise ConfigurationError( + 'Unsatisfied before dependencies: %s' + % (', '.join(sorted(self.req_before - has_before))) + ) + if not self.req_after.issubset(has_after): + raise ConfigurationError( + 'Unsatisfied after dependencies: %s' + % (', '.join(sorted(self.req_after - has_after))) + ) + + sorted_names = [] + + while roots: + root = roots.pop(0) + sorted_names.append(root) + children = graph[root][1:] + for child in children: + arcs = graph[child][0] + arcs -= 1 + graph[child][0] = arcs + if arcs == 0: + roots.insert(0, child) + del graph[root] + + if graph: + # loop in input + cycledeps = {} + for k, v in graph.items(): + cycledeps[k] = v[1:] + raise CyclicDependencyError(cycledeps) + + result = [] + + for name in sorted_names: + if name in self.names: + result.append((name, self.name2val[name])) + + return result + -- cgit v1.2.3 From 34d4cd0ea38fdbe0ab0e0832fc2114953ef4e94a Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Sun, 4 Nov 2012 01:02:19 -0500 Subject: - In uncommon cases, the ``pyramid_excview_tween_factory`` might have inadvertently raised a ``KeyError`` looking for ``request_iface`` as an attribute of the request. It no longer fails in this case. See https://github.com/Pylons/pyramid/issues/700 Fixes #700 --- CHANGES.txt | 5 +++++ pyramid/tweens.py | 5 ++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGES.txt b/CHANGES.txt index 298bddf7a..8d5a00e77 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -38,6 +38,11 @@ Bug Fixes be unmolested on the way out. See https://github.com/Pylons/pyramid/issues/709 +- In uncommon cases, the ``pyramid_excview_tween_factory`` might have + inadvertently raised a ``KeyError`` looking for ``request_iface`` as an + attribute of the request. It no longer fails in this case. See + https://github.com/Pylons/pyramid/issues/700 + Internals --------- diff --git a/pyramid/tweens.py b/pyramid/tweens.py index 73a95e1b8..cf2238deb 100644 --- a/pyramid/tweens.py +++ b/pyramid/tweens.py @@ -2,6 +2,7 @@ import sys from pyramid.interfaces import ( IExceptionViewClassifier, + IRequest, IView, ) @@ -28,7 +29,9 @@ def excview_tween_factory(handler, registry): # sane (e.g. caching headers) if 'response' in attrs: del attrs['response'] - request_iface = attrs['request_iface'] + # we use .get instead of .__getitem__ below due to + # https://github.com/Pylons/pyramid/issues/700 + request_iface = attrs.get('request_iface', IRequest) provides = providedBy(exc) for_ = (IExceptionViewClassifier, request_iface.combined, provides) view_callable = adapters.lookup(for_, IView, default=None) -- cgit v1.2.3 From 04875452db1da40bd8ed0841869d511b8d86527d Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Sun, 4 Nov 2012 01:51:42 -0500 Subject: fix docs, upgrade tutorials, add change note, deprecate using zope.deprecation instead of a warning, make hashalg arg a kwarg in certain cases in case someone (maybe me) is using nonapi function imports from authentication --- CHANGES.txt | 20 ++++++++++- docs/api/authentication.rst | 2 ++ docs/narr/security.rst | 4 +-- docs/tutorials/wiki/authorization.rst | 18 +++++----- .../wiki/src/authorization/tutorial/__init__.py | 6 ++-- docs/tutorials/wiki/src/tests/tutorial/__init__.py | 6 ++-- docs/tutorials/wiki2/authorization.rst | 18 +++++----- .../wiki2/src/authorization/tutorial/__init__.py | 4 +-- .../tutorials/wiki2/src/tests/tutorial/__init__.py | 4 +-- pyramid/authentication.py | 40 ++++++++++++++-------- pyramid/tests/test_authentication.py | 12 +++++-- 11 files changed, 86 insertions(+), 48 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 8d5a00e77..45fc19762 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -43,6 +43,24 @@ Bug Fixes attribute of the request. It no longer fails in this case. See https://github.com/Pylons/pyramid/issues/700 +Deprecations +------------ + +- The ``pyramid.authentication.AuthTktAuthenticationPolicy`` authentication + policy is deprecated in Pyramid 1.4 due to its use of the MD5 hashing + algorithm, which has known hash collision vulnerabilities. The risk of an + exploit is low. However, for improved authentication security, use the + ``pyramid.authentication.SHA512AuthTktAuthenticationPolicy`` instead. + Cookies generated by the AuthTktAuthenticationPolicy are not compatible with + cookies generated by the SHA512AuthTktAuthenticationPolicy, however, so + switching to the latter will imply that all existing users with a valid + cookie will be required to re-login. The SHA-512 version is not compatible + with Apache's mod_auth_tkt either, so if you are relying on that + compatibility, you'll want to stick with the MD5 version. + + A deprecation warning is now emitted when the AuthTktAuthenticationPolicy is + imported. + Internals --------- @@ -50,7 +68,7 @@ Internals move ``CyclicDependencyError`` from ``pyramid.config.util`` to ``pyramid.exceptions``, rename ``Singleton`` to ``Sentinel`` and move from ``pyramid.config.util`` to ``pyramid.config.util``; this is in an effort to - move that stuff that may be an API one day out of ``pyramid.config.util, + move that stuff that may be an API one day out of ``pyramid.config.util``, because that package should never be imported from non-Pyramid code. TopologicalSorter is still not an API, but may become one. diff --git a/docs/api/authentication.rst b/docs/api/authentication.rst index e6ee5658b..49ee405eb 100644 --- a/docs/api/authentication.rst +++ b/docs/api/authentication.rst @@ -9,6 +9,8 @@ Authentication Policies .. automodule:: pyramid.authentication .. autoclass:: SHA512AuthTktAuthenticationPolicy + :members: + :inherited-members: .. autoclass:: AuthTktAuthenticationPolicy :members: diff --git a/docs/narr/security.rst b/docs/narr/security.rst index 07ec0f21e..3c9759e6c 100644 --- a/docs/narr/security.rst +++ b/docs/narr/security.rst @@ -90,9 +90,9 @@ For example: :linenos: from pyramid.config import Configurator - from pyramid.authentication import AuthTktAuthenticationPolicy + from pyramid.authentication import SHA512AuthTktAuthenticationPolicy from pyramid.authorization import ACLAuthorizationPolicy - authentication_policy = AuthTktAuthenticationPolicy('seekrit') + authentication_policy = SHA512AuthTktAuthenticationPolicy('seekrit') authorization_policy = ACLAuthorizationPolicy() config = Configurator() config.set_authentication_policy(authentication_policy) diff --git a/docs/tutorials/wiki/authorization.rst b/docs/tutorials/wiki/authorization.rst index 9e0bf0f09..e2c4637d7 100644 --- a/docs/tutorials/wiki/authorization.rst +++ b/docs/tutorials/wiki/authorization.rst @@ -134,15 +134,15 @@ Now add those policies to the configuration: (Only the highlighted lines need to be added.) -We are enabling an ``AuthTktAuthenticationPolicy``, it is based in an auth -ticket that may be included in the request, and an ``ACLAuthorizationPolicy`` -that uses an ACL to determine the allow or deny outcome for a view. - -Note that the -:class:`pyramid.authentication.AuthTktAuthenticationPolicy` constructor -accepts two arguments: ``secret`` and ``callback``. ``secret`` is a string -representing an encryption key used by the "authentication ticket" machinery -represented by this policy: it is required. The ``callback`` is the +We are enabling an ``SHA512AuthTktAuthenticationPolicy``, it is based in an +auth ticket that may be included in the request, and an +``ACLAuthorizationPolicy`` that uses an ACL to determine the allow or deny +outcome for a view. + +Note that the :class:`pyramid.authentication.SHA512AuthTktAuthenticationPolicy` +constructor accepts two arguments: ``secret`` and ``callback``. ``secret`` is +a string representing an encryption key used by the "authentication ticket" +machinery represented by this policy: it is required. The ``callback`` is the ``groupfinder()`` function that we created before. Add permission declarations diff --git a/docs/tutorials/wiki/src/authorization/tutorial/__init__.py b/docs/tutorials/wiki/src/authorization/tutorial/__init__.py index 6989145d8..4c766fea2 100644 --- a/docs/tutorials/wiki/src/authorization/tutorial/__init__.py +++ b/docs/tutorials/wiki/src/authorization/tutorial/__init__.py @@ -1,7 +1,7 @@ from pyramid.config import Configurator from pyramid_zodbconn import get_connection -from pyramid.authentication import AuthTktAuthenticationPolicy +from pyramid.authentication import SHA512AuthTktAuthenticationPolicy from pyramid.authorization import ACLAuthorizationPolicy from .models import appmaker @@ -14,8 +14,8 @@ def root_factory(request): def main(global_config, **settings): """ This function returns a WSGI application. """ - authn_policy = AuthTktAuthenticationPolicy(secret='sosecret', - callback=groupfinder) + authn_policy = SHA512AuthTktAuthenticationPolicy(secret='sosecret', + callback=groupfinder) authz_policy = ACLAuthorizationPolicy() config = Configurator(root_factory=root_factory, settings=settings) config.set_authentication_policy(authn_policy) diff --git a/docs/tutorials/wiki/src/tests/tutorial/__init__.py b/docs/tutorials/wiki/src/tests/tutorial/__init__.py index 6989145d8..4c766fea2 100644 --- a/docs/tutorials/wiki/src/tests/tutorial/__init__.py +++ b/docs/tutorials/wiki/src/tests/tutorial/__init__.py @@ -1,7 +1,7 @@ from pyramid.config import Configurator from pyramid_zodbconn import get_connection -from pyramid.authentication import AuthTktAuthenticationPolicy +from pyramid.authentication import SHA512AuthTktAuthenticationPolicy from pyramid.authorization import ACLAuthorizationPolicy from .models import appmaker @@ -14,8 +14,8 @@ def root_factory(request): def main(global_config, **settings): """ This function returns a WSGI application. """ - authn_policy = AuthTktAuthenticationPolicy(secret='sosecret', - callback=groupfinder) + authn_policy = SHA512AuthTktAuthenticationPolicy(secret='sosecret', + callback=groupfinder) authz_policy = ACLAuthorizationPolicy() config = Configurator(root_factory=root_factory, settings=settings) config.set_authentication_policy(authn_policy) diff --git a/docs/tutorials/wiki2/authorization.rst b/docs/tutorials/wiki2/authorization.rst index 6b2d44410..e3087dea5 100644 --- a/docs/tutorials/wiki2/authorization.rst +++ b/docs/tutorials/wiki2/authorization.rst @@ -151,15 +151,15 @@ Now add those policies to the configuration: (Only the highlighted lines need to be added.) -We are enabling an ``AuthTktAuthenticationPolicy``, it is based in an auth -ticket that may be included in the request, and an ``ACLAuthorizationPolicy`` -that uses an ACL to determine the allow or deny outcome for a view. - -Note that the -:class:`pyramid.authentication.AuthTktAuthenticationPolicy` constructor -accepts two arguments: ``secret`` and ``callback``. ``secret`` is a string -representing an encryption key used by the "authentication ticket" machinery -represented by this policy: it is required. The ``callback`` is the +We are enabling an ``SHA512AuthTktAuthenticationPolicy``, it is based in an +auth ticket that may be included in the request, and an +``ACLAuthorizationPolicy`` that uses an ACL to determine the allow or deny +outcome for a view. + +Note that the :class:`pyramid.authentication.SHA512AuthTktAuthenticationPolicy` +constructor accepts two arguments: ``secret`` and ``callback``. ``secret`` is +a string representing an encryption key used by the "authentication ticket" +machinery represented by this policy: it is required. The ``callback`` is the ``groupfinder()`` function that we created before. Add permission declarations diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/__init__.py b/docs/tutorials/wiki2/src/authorization/tutorial/__init__.py index 8922a3cc0..585cdf884 100644 --- a/docs/tutorials/wiki2/src/authorization/tutorial/__init__.py +++ b/docs/tutorials/wiki2/src/authorization/tutorial/__init__.py @@ -1,5 +1,5 @@ from pyramid.config import Configurator -from pyramid.authentication import AuthTktAuthenticationPolicy +from pyramid.authentication import SHA512AuthTktAuthenticationPolicy from pyramid.authorization import ACLAuthorizationPolicy from sqlalchemy import engine_from_config @@ -17,7 +17,7 @@ def main(global_config, **settings): engine = engine_from_config(settings, 'sqlalchemy.') DBSession.configure(bind=engine) Base.metadata.bind = engine - authn_policy = AuthTktAuthenticationPolicy( + authn_policy = SHA512AuthTktAuthenticationPolicy( 'sosecret', callback=groupfinder) authz_policy = ACLAuthorizationPolicy() config = Configurator(settings=settings, diff --git a/docs/tutorials/wiki2/src/tests/tutorial/__init__.py b/docs/tutorials/wiki2/src/tests/tutorial/__init__.py index 8922a3cc0..585cdf884 100644 --- a/docs/tutorials/wiki2/src/tests/tutorial/__init__.py +++ b/docs/tutorials/wiki2/src/tests/tutorial/__init__.py @@ -1,5 +1,5 @@ from pyramid.config import Configurator -from pyramid.authentication import AuthTktAuthenticationPolicy +from pyramid.authentication import SHA512AuthTktAuthenticationPolicy from pyramid.authorization import ACLAuthorizationPolicy from sqlalchemy import engine_from_config @@ -17,7 +17,7 @@ def main(global_config, **settings): engine = engine_from_config(settings, 'sqlalchemy.') DBSession.configure(bind=engine) Base.metadata.bind = engine - authn_policy = AuthTktAuthenticationPolicy( + authn_policy = SHA512AuthTktAuthenticationPolicy( 'sosecret', callback=groupfinder) authz_policy = ACLAuthorizationPolicy() config = Configurator(settings=settings, diff --git a/pyramid/authentication.py b/pyramid/authentication.py index 7e7ee4dfb..2e58c70e7 100644 --- a/pyramid/authentication.py +++ b/pyramid/authentication.py @@ -6,8 +6,8 @@ import base64 import datetime import re import time as time_mod -import warnings +from zope.deprecation import deprecated from zope.interface import implementer from pyramid.compat import ( @@ -407,7 +407,7 @@ class RemoteUserAuthenticationPolicy(CallbackAuthenticationPolicy): return [] class BaseAuthTktAuthenticationPolicy(CallbackAuthenticationPolicy): - """ A :app:`Pyramid` :term:`authentication policy` which + """A :app:`Pyramid` :term:`authentication policy` which obtains data from a Pyramid "auth ticket" cookie. Constructor Arguments @@ -562,8 +562,8 @@ class BaseAuthTktAuthenticationPolicy(CallbackAuthenticationPolicy): @implementer(IAuthenticationPolicy) class SHA512AuthTktAuthenticationPolicy(BaseAuthTktAuthenticationPolicy): - __doc__ = """ - .. versionadded:: 1.4 + __doc__ = """.. versionadded:: 1.4 + """ + BaseAuthTktAuthenticationPolicy.__doc__ hashalg = 'sha512' @@ -578,13 +578,20 @@ class AuthTktAuthenticationPolicy(BaseAuthTktAuthenticationPolicy): """ + BaseAuthTktAuthenticationPolicy.__doc__ hashalg = 'md5' - def __init__(self, *a, **kw): - warnings.warn('Deprecated due to the usage of md5, ' - 'hash function known to have collisions. ' - 'Use SHA512AuthTktAuthenticationPolicy instead.', - DeprecationWarning, - stacklevel=2) - super(AuthTktAuthenticationPolicy, self).__init__(*a, **kw) +deprecated( + 'AuthTktAuthenticationPolicy', + 'The AuthTktAuthenticationPolicy is deprecated in Pyramid 1.4 ' + 'due to its use of the MD5 hashing algorithm, which has known ' + 'hash collision vulnerabilities. The risk of an exploit is low. ' + 'However, for improved authentication security, use the ' + 'pyramid.authentication.SHA512AuthTktAuthenticationPolicy instead. ' + 'Cookies generated by the AuthTktAuthenticationPolicy are *not* ' + 'compatible with cookies generated by the ' + 'SHA512AuthTktAuthenticationPolicy, however, so switching to the ' + 'latter will imply that all existing users with a valid cookie ' + 'will be required to re-login. The SHA-512 version is not compatible ' + 'with Apache\'s mod_auth_tkt either.' + ) def b64encode(v): return base64.b64encode(bytes_(v)).strip().replace(b'\n', b'') @@ -654,7 +661,7 @@ class BadTicket(Exception): Exception.__init__(self, msg) # this function licensed under the MIT license (stolen from Paste) -def parse_ticket(secret, ticket, ip, hashalg): +def parse_ticket(secret, ticket, ip, hashalg='md5'): """ Parse the ticket, returning (timestamp, userid, tokens, user_data). @@ -694,7 +701,8 @@ def parse_ticket(secret, ticket, ip, hashalg): return (timestamp, userid, tokens, user_data) # this function licensed under the MIT license (stolen from Paste) -def calculate_digest(ip, timestamp, secret, userid, tokens, user_data, hashalg): +def calculate_digest(ip, timestamp, secret, userid, tokens, user_data, + hashalg='md5'): secret = bytes_(secret, 'utf-8') userid = bytes_(userid, 'utf-8') tokens = bytes_(tokens, 'utf-8') @@ -749,7 +757,8 @@ class AuthTktCookieHelper(object): def __init__(self, secret, cookie_name='auth_tkt', secure=False, include_ip=False, timeout=None, reissue_time=None, - max_age=None, http_only=False, path="/", wild_domain=True, hashalg='md5'): + max_age=None, http_only=False, path="/", wild_domain=True, + hashalg='md5'): self.secret = secret self.cookie_name = cookie_name self.include_ip = include_ip @@ -945,7 +954,8 @@ class AuthTktCookieHelper(object): user_data=user_data, cookie_name=self.cookie_name, secure=self.secure, - hashalg=self.hashalg) + hashalg=self.hashalg + ) cookie_value = ticket.cookie_value() return self._get_cookies(environ, cookie_value, max_age) diff --git a/pyramid/tests/test_authentication.py b/pyramid/tests/test_authentication.py index b1a88b915..8b640b860 100644 --- a/pyramid/tests/test_authentication.py +++ b/pyramid/tests/test_authentication.py @@ -430,7 +430,15 @@ class TestRemoteUserAuthenticationPolicy(unittest.TestCase): result = policy.forget(request) self.assertEqual(result, []) -class TestAutkTktAuthenticationPolicy(unittest.TestCase): +class TestAuthTktAuthenticationPolicy(unittest.TestCase): + def setUp(self): + from zope.deprecation import __show__ + __show__.off() + + def tearDown(self): + from zope.deprecation import __show__ + __show__.on() + def _getTargetClass(self): from pyramid.authentication import AuthTktAuthenticationPolicy return AuthTktAuthenticationPolicy @@ -459,7 +467,7 @@ class TestAutkTktAuthenticationPolicy(unittest.TestCase): from pyramid.interfaces import IAuthenticationPolicy verifyObject(IAuthenticationPolicy, self._makeOne(None, None)) -class TestSHA512AutkTktAuthenticationPolicy(unittest.TestCase): +class TestSHA512AuthTktAuthenticationPolicy(unittest.TestCase): def _getTargetClass(self): from pyramid.authentication import SHA512AuthTktAuthenticationPolicy return SHA512AuthTktAuthenticationPolicy -- cgit v1.2.3 From 19b8207ff1e959669d296407ed112545364a495d Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Sun, 4 Nov 2012 11:19:41 -0600 Subject: merged SHA512AuthTktAuthenticationPolicy into AuthTktAuthenticationPolicy AuthTktAuthenticationPolicy now accepts a hashalg parameter and is no longer deprecated. Docs recommend overriding hashalg and using 'sha512'. --- CHANGES.txt | 22 ++---- docs/api/authentication.rst | 4 -- docs/narr/security.rst | 10 +-- docs/tutorials/wiki/authorization.rst | 4 +- .../wiki/src/authorization/tutorial/__init__.py | 6 +- docs/tutorials/wiki/src/tests/tutorial/__init__.py | 6 +- docs/tutorials/wiki2/authorization.rst | 4 +- .../wiki2/src/authorization/tutorial/__init__.py | 6 +- .../tutorials/wiki2/src/tests/tutorial/__init__.py | 6 +- pyramid/authentication.py | 68 ++++++++---------- pyramid/tests/test_authentication.py | 82 +++++----------------- 11 files changed, 72 insertions(+), 146 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 45fc19762..390d3c3e3 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -4,6 +4,10 @@ Next release Features -------- +- ``pyramid.authentication.AuthTktAuthenticationPolicy`` has been updated to + support newer hashing algorithms such as ``sha512``. Existing applications + should consider updating if possible. + - Added an ``effective_principals`` route and view predicate. - Do not allow the userid returned from the ``authenticated_userid`` or the @@ -43,24 +47,6 @@ Bug Fixes attribute of the request. It no longer fails in this case. See https://github.com/Pylons/pyramid/issues/700 -Deprecations ------------- - -- The ``pyramid.authentication.AuthTktAuthenticationPolicy`` authentication - policy is deprecated in Pyramid 1.4 due to its use of the MD5 hashing - algorithm, which has known hash collision vulnerabilities. The risk of an - exploit is low. However, for improved authentication security, use the - ``pyramid.authentication.SHA512AuthTktAuthenticationPolicy`` instead. - Cookies generated by the AuthTktAuthenticationPolicy are not compatible with - cookies generated by the SHA512AuthTktAuthenticationPolicy, however, so - switching to the latter will imply that all existing users with a valid - cookie will be required to re-login. The SHA-512 version is not compatible - with Apache's mod_auth_tkt either, so if you are relying on that - compatibility, you'll want to stick with the MD5 version. - - A deprecation warning is now emitted when the AuthTktAuthenticationPolicy is - imported. - Internals --------- diff --git a/docs/api/authentication.rst b/docs/api/authentication.rst index 49ee405eb..19d08618b 100644 --- a/docs/api/authentication.rst +++ b/docs/api/authentication.rst @@ -8,10 +8,6 @@ Authentication Policies .. automodule:: pyramid.authentication - .. autoclass:: SHA512AuthTktAuthenticationPolicy - :members: - :inherited-members: - .. autoclass:: AuthTktAuthenticationPolicy :members: :inherited-members: diff --git a/docs/narr/security.rst b/docs/narr/security.rst index 3c9759e6c..3a94b4f7d 100644 --- a/docs/narr/security.rst +++ b/docs/narr/security.rst @@ -90,13 +90,13 @@ For example: :linenos: from pyramid.config import Configurator - from pyramid.authentication import SHA512AuthTktAuthenticationPolicy + from pyramid.authentication import AuthTktAuthenticationPolicy from pyramid.authorization import ACLAuthorizationPolicy - authentication_policy = SHA512AuthTktAuthenticationPolicy('seekrit') - authorization_policy = ACLAuthorizationPolicy() + authn_policy = AuthTktAuthenticationPolicy('seekrit', hashalg='sha512') + authz_policy = ACLAuthorizationPolicy() config = Configurator() - config.set_authentication_policy(authentication_policy) - config.set_authorization_policy(authorization_policy) + config.set_authentication_policy(authn_policy) + config.set_authorization_policy(authz_policy) .. note:: the ``authentication_policy`` and ``authorization_policy`` arguments may also be passed to their respective methods mentioned above diff --git a/docs/tutorials/wiki/authorization.rst b/docs/tutorials/wiki/authorization.rst index e2c4637d7..24249945a 100644 --- a/docs/tutorials/wiki/authorization.rst +++ b/docs/tutorials/wiki/authorization.rst @@ -134,12 +134,12 @@ Now add those policies to the configuration: (Only the highlighted lines need to be added.) -We are enabling an ``SHA512AuthTktAuthenticationPolicy``, it is based in an +We are enabling an ``AuthTktAuthenticationPolicy``, it is based in an auth ticket that may be included in the request, and an ``ACLAuthorizationPolicy`` that uses an ACL to determine the allow or deny outcome for a view. -Note that the :class:`pyramid.authentication.SHA512AuthTktAuthenticationPolicy` +Note that the :class:`pyramid.authentication.AuthTktAuthenticationPolicy` constructor accepts two arguments: ``secret`` and ``callback``. ``secret`` is a string representing an encryption key used by the "authentication ticket" machinery represented by this policy: it is required. The ``callback`` is the diff --git a/docs/tutorials/wiki/src/authorization/tutorial/__init__.py b/docs/tutorials/wiki/src/authorization/tutorial/__init__.py index 4c766fea2..b42e01d03 100644 --- a/docs/tutorials/wiki/src/authorization/tutorial/__init__.py +++ b/docs/tutorials/wiki/src/authorization/tutorial/__init__.py @@ -1,7 +1,7 @@ from pyramid.config import Configurator from pyramid_zodbconn import get_connection -from pyramid.authentication import SHA512AuthTktAuthenticationPolicy +from pyramid.authentication import AuthTktAuthenticationPolicy from pyramid.authorization import ACLAuthorizationPolicy from .models import appmaker @@ -14,8 +14,8 @@ def root_factory(request): def main(global_config, **settings): """ This function returns a WSGI application. """ - authn_policy = SHA512AuthTktAuthenticationPolicy(secret='sosecret', - callback=groupfinder) + authn_policy = AuthTktAuthenticationPolicy( + 'sosecret', callback=groupfinder, hashalg='sha512') authz_policy = ACLAuthorizationPolicy() config = Configurator(root_factory=root_factory, settings=settings) config.set_authentication_policy(authn_policy) diff --git a/docs/tutorials/wiki/src/tests/tutorial/__init__.py b/docs/tutorials/wiki/src/tests/tutorial/__init__.py index 4c766fea2..b42e01d03 100644 --- a/docs/tutorials/wiki/src/tests/tutorial/__init__.py +++ b/docs/tutorials/wiki/src/tests/tutorial/__init__.py @@ -1,7 +1,7 @@ from pyramid.config import Configurator from pyramid_zodbconn import get_connection -from pyramid.authentication import SHA512AuthTktAuthenticationPolicy +from pyramid.authentication import AuthTktAuthenticationPolicy from pyramid.authorization import ACLAuthorizationPolicy from .models import appmaker @@ -14,8 +14,8 @@ def root_factory(request): def main(global_config, **settings): """ This function returns a WSGI application. """ - authn_policy = SHA512AuthTktAuthenticationPolicy(secret='sosecret', - callback=groupfinder) + authn_policy = AuthTktAuthenticationPolicy( + 'sosecret', callback=groupfinder, hashalg='sha512') authz_policy = ACLAuthorizationPolicy() config = Configurator(root_factory=root_factory, settings=settings) config.set_authentication_policy(authn_policy) diff --git a/docs/tutorials/wiki2/authorization.rst b/docs/tutorials/wiki2/authorization.rst index e3087dea5..1ddf8c82d 100644 --- a/docs/tutorials/wiki2/authorization.rst +++ b/docs/tutorials/wiki2/authorization.rst @@ -151,12 +151,12 @@ Now add those policies to the configuration: (Only the highlighted lines need to be added.) -We are enabling an ``SHA512AuthTktAuthenticationPolicy``, it is based in an +We are enabling an ``AuthTktAuthenticationPolicy``, it is based in an auth ticket that may be included in the request, and an ``ACLAuthorizationPolicy`` that uses an ACL to determine the allow or deny outcome for a view. -Note that the :class:`pyramid.authentication.SHA512AuthTktAuthenticationPolicy` +Note that the :class:`pyramid.authentication.AuthTktAuthenticationPolicy` constructor accepts two arguments: ``secret`` and ``callback``. ``secret`` is a string representing an encryption key used by the "authentication ticket" machinery represented by this policy: it is required. The ``callback`` is the diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/__init__.py b/docs/tutorials/wiki2/src/authorization/tutorial/__init__.py index 585cdf884..76071173a 100644 --- a/docs/tutorials/wiki2/src/authorization/tutorial/__init__.py +++ b/docs/tutorials/wiki2/src/authorization/tutorial/__init__.py @@ -1,5 +1,5 @@ from pyramid.config import Configurator -from pyramid.authentication import SHA512AuthTktAuthenticationPolicy +from pyramid.authentication import AuthTktAuthenticationPolicy from pyramid.authorization import ACLAuthorizationPolicy from sqlalchemy import engine_from_config @@ -17,8 +17,8 @@ def main(global_config, **settings): engine = engine_from_config(settings, 'sqlalchemy.') DBSession.configure(bind=engine) Base.metadata.bind = engine - authn_policy = SHA512AuthTktAuthenticationPolicy( - 'sosecret', callback=groupfinder) + authn_policy = AuthTktAuthenticationPolicy( + 'sosecret', callback=groupfinder, hashalg='sha512') authz_policy = ACLAuthorizationPolicy() config = Configurator(settings=settings, root_factory='tutorial.models.RootFactory') diff --git a/docs/tutorials/wiki2/src/tests/tutorial/__init__.py b/docs/tutorials/wiki2/src/tests/tutorial/__init__.py index 585cdf884..76071173a 100644 --- a/docs/tutorials/wiki2/src/tests/tutorial/__init__.py +++ b/docs/tutorials/wiki2/src/tests/tutorial/__init__.py @@ -1,5 +1,5 @@ from pyramid.config import Configurator -from pyramid.authentication import SHA512AuthTktAuthenticationPolicy +from pyramid.authentication import AuthTktAuthenticationPolicy from pyramid.authorization import ACLAuthorizationPolicy from sqlalchemy import engine_from_config @@ -17,8 +17,8 @@ def main(global_config, **settings): engine = engine_from_config(settings, 'sqlalchemy.') DBSession.configure(bind=engine) Base.metadata.bind = engine - authn_policy = SHA512AuthTktAuthenticationPolicy( - 'sosecret', callback=groupfinder) + authn_policy = AuthTktAuthenticationPolicy( + 'sosecret', callback=groupfinder, hashalg='sha512') authz_policy = ACLAuthorizationPolicy() config = Configurator(settings=settings, root_factory='tutorial.models.RootFactory') diff --git a/pyramid/authentication.py b/pyramid/authentication.py index 2e58c70e7..dbca68a11 100644 --- a/pyramid/authentication.py +++ b/pyramid/authentication.py @@ -7,7 +7,6 @@ import datetime import re import time as time_mod -from zope.deprecation import deprecated from zope.interface import implementer from pyramid.compat import ( @@ -406,10 +405,18 @@ class RemoteUserAuthenticationPolicy(CallbackAuthenticationPolicy): be done somewhere else or in a subclass.""" return [] -class BaseAuthTktAuthenticationPolicy(CallbackAuthenticationPolicy): +@implementer(IAuthenticationPolicy) +class AuthTktAuthenticationPolicy(CallbackAuthenticationPolicy): """A :app:`Pyramid` :term:`authentication policy` which obtains data from a Pyramid "auth ticket" cookie. + .. warning:: + + The default hash algorithm used in this policy is MD5 and has known + hash collision vulnerabilities. The risk of an exploit is low. + However, for improved authentication security, use + ``hashalg='sha512'``. + Constructor Arguments ``secret`` @@ -498,6 +505,26 @@ class BaseAuthTktAuthenticationPolicy(CallbackAuthenticationPolicy): wildcard domain. Optional. + ``hashalg`` + + Default: ``md5``. Cookies generated by different instances of + AuthTktAuthenticationPolicy using different ``hashalg`` options + are not compatible. Switching the ``hashalg`` will imply that + all existing users with a valid cookie will be required to re-login. + + Any hash algorithm supported by Python's ``hashlib.new()`` function + can be used as the ``hashalg``. + + This option is available as of :app:`Pyramid` 1.4. See the warning + above for reasons to change ``hashalg`` in your own apps. + + Optional. + + .. note:: + + ``sha512`` is recommended for improved security and to maintain + compatibility with Apache's ``mod_auth_tkt`` module. + ``debug`` Default: ``False``. If ``debug`` is ``True``, log messages to the @@ -508,7 +535,6 @@ class BaseAuthTktAuthenticationPolicy(CallbackAuthenticationPolicy): Objects of this class implement the interface described by :class:`pyramid.interfaces.IAuthenticationPolicy`. """ - hashalg = '' def __init__(self, secret, @@ -523,6 +549,7 @@ class BaseAuthTktAuthenticationPolicy(CallbackAuthenticationPolicy): http_only=False, wild_domain=True, debug=False, + hashalg='md5', ): self.cookie = AuthTktCookieHelper( secret, @@ -535,7 +562,7 @@ class BaseAuthTktAuthenticationPolicy(CallbackAuthenticationPolicy): http_only=http_only, path=path, wild_domain=wild_domain, - hashalg=self.hashalg, + hashalg=hashalg, ) self.callback = callback self.debug = debug @@ -560,39 +587,6 @@ class BaseAuthTktAuthenticationPolicy(CallbackAuthenticationPolicy): """ A list of headers which will delete appropriate cookies.""" return self.cookie.forget(request) -@implementer(IAuthenticationPolicy) -class SHA512AuthTktAuthenticationPolicy(BaseAuthTktAuthenticationPolicy): - __doc__ = """.. versionadded:: 1.4 - - """ + BaseAuthTktAuthenticationPolicy.__doc__ - hashalg = 'sha512' - -@implementer(IAuthenticationPolicy) -class AuthTktAuthenticationPolicy(BaseAuthTktAuthenticationPolicy): - __doc__ = """ - .. warning:: - - Deprecated in 1.4 due to security concerns, - use :class:`SHA512AuthTktAuthenticationPolicy` instead. - - """ + BaseAuthTktAuthenticationPolicy.__doc__ - hashalg = 'md5' - -deprecated( - 'AuthTktAuthenticationPolicy', - 'The AuthTktAuthenticationPolicy is deprecated in Pyramid 1.4 ' - 'due to its use of the MD5 hashing algorithm, which has known ' - 'hash collision vulnerabilities. The risk of an exploit is low. ' - 'However, for improved authentication security, use the ' - 'pyramid.authentication.SHA512AuthTktAuthenticationPolicy instead. ' - 'Cookies generated by the AuthTktAuthenticationPolicy are *not* ' - 'compatible with cookies generated by the ' - 'SHA512AuthTktAuthenticationPolicy, however, so switching to the ' - 'latter will imply that all existing users with a valid cookie ' - 'will be required to re-login. The SHA-512 version is not compatible ' - 'with Apache\'s mod_auth_tkt either.' - ) - def b64encode(v): return base64.b64encode(bytes_(v)).strip().replace(b'\n', b'') diff --git a/pyramid/tests/test_authentication.py b/pyramid/tests/test_authentication.py index 8b640b860..2d69173fa 100644 --- a/pyramid/tests/test_authentication.py +++ b/pyramid/tests/test_authentication.py @@ -431,14 +431,6 @@ class TestRemoteUserAuthenticationPolicy(unittest.TestCase): self.assertEqual(result, []) class TestAuthTktAuthenticationPolicy(unittest.TestCase): - def setUp(self): - from zope.deprecation import __show__ - __show__.off() - - def tearDown(self): - from zope.deprecation import __show__ - __show__.on() - def _getTargetClass(self): from pyramid.authentication import AuthTktAuthenticationPolicy return AuthTktAuthenticationPolicy @@ -448,72 +440,20 @@ class TestAuthTktAuthenticationPolicy(unittest.TestCase): inst.cookie = DummyCookieHelper(cookieidentity) return inst - def test_is_subclass(self): - from pyramid.authentication import BaseAuthTktAuthenticationPolicy - inst = self._makeOne(None, None) - self.assertTrue(isinstance(inst, BaseAuthTktAuthenticationPolicy)) - - def test_md5(self): - inst = self._makeOne(None, None) - self.assertEqual(inst.hashalg, 'md5') - - def test_class_implements_IAuthenticationPolicy(self): - from zope.interface.verify import verifyClass - from pyramid.interfaces import IAuthenticationPolicy - verifyClass(IAuthenticationPolicy, self._getTargetClass()) - - def test_instance_implements_IAuthenticationPolicy(self): - from zope.interface.verify import verifyObject - from pyramid.interfaces import IAuthenticationPolicy - verifyObject(IAuthenticationPolicy, self._makeOne(None, None)) - -class TestSHA512AuthTktAuthenticationPolicy(unittest.TestCase): - def _getTargetClass(self): - from pyramid.authentication import SHA512AuthTktAuthenticationPolicy - return SHA512AuthTktAuthenticationPolicy - - def _makeOne(self, callback, cookieidentity, **kw): - inst = self._getTargetClass()('secret', callback, **kw) - inst.cookie = DummyCookieHelper(cookieidentity) - return inst - - def test_is_subclass(self): - from pyramid.authentication import BaseAuthTktAuthenticationPolicy - inst = self._makeOne(None, None) - self.assertTrue(isinstance(inst, BaseAuthTktAuthenticationPolicy)) - - def test_sha512(self): - inst = self._makeOne(None, None) - self.assertEqual(inst.hashalg, 'sha512') - - def test_class_implements_IAuthenticationPolicy(self): - from zope.interface.verify import verifyClass - from pyramid.interfaces import IAuthenticationPolicy - verifyClass(IAuthenticationPolicy, self._getTargetClass()) - - def test_instance_implements_IAuthenticationPolicy(self): - from zope.interface.verify import verifyObject - from pyramid.interfaces import IAuthenticationPolicy - verifyObject(IAuthenticationPolicy, self._makeOne(None, None)) - -class TestBaseAutkTktAuthenticationPolicy(unittest.TestCase): - def _getTargetClass(self): - from pyramid.authentication import BaseAuthTktAuthenticationPolicy - return BaseAuthTktAuthenticationPolicy - - def _makeOne(self, callback, cookieidentity, **kw): - inst = self._getTargetClass()('secret', callback, **kw) - inst.cookie = DummyCookieHelper(cookieidentity) - return inst - def test_allargs(self): # pass all known args inst = self._getTargetClass()( 'secret', callback=None, cookie_name=None, secure=False, include_ip=False, timeout=None, reissue_time=None, + hashalg='sha512', ) self.assertEqual(inst.callback, None) + def test_hashalg_override(self): + # important to ensure hashalg is passed to cookie helper + inst = self._getTargetClass()('secret', hashalg='sha512') + self.assertEqual(inst.cookie.hashalg, 'sha512') + def test_unauthenticated_userid_returns_None(self): request = DummyRequest({}) policy = self._makeOne(None, None) @@ -586,6 +526,16 @@ class TestBaseAutkTktAuthenticationPolicy(unittest.TestCase): result = policy.forget(request) self.assertEqual(result, []) + def test_class_implements_IAuthenticationPolicy(self): + from zope.interface.verify import verifyClass + from pyramid.interfaces import IAuthenticationPolicy + verifyClass(IAuthenticationPolicy, self._getTargetClass()) + + def test_instance_implements_IAuthenticationPolicy(self): + from zope.interface.verify import verifyObject + from pyramid.interfaces import IAuthenticationPolicy + verifyObject(IAuthenticationPolicy, self._makeOne(None, None)) + class TestAuthTktCookieHelper(unittest.TestCase): def _getTargetClass(self): from pyramid.authentication import AuthTktCookieHelper -- cgit v1.2.3 From ca4656877ab909b88ee1730cab611c8007ff7a39 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Sun, 4 Nov 2012 11:37:35 -0600 Subject: garden --- CHANGES.txt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 45fc19762..a9ce80712 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -14,7 +14,8 @@ Features reserved for internal usage by Pyramid and they will not be accepted as valid userids. -- Slightly better debug logging from RepozeWho1AuthenticationPolicy. +- Slightly better debug logging from + ``pyramid.authentication.RepozeWho1AuthenticationPolicy``. - ``pyramid.security.view_execution_permitted`` used to return `True` if no view could be found. It now raises a ``TypeError`` exception in that case, as @@ -67,7 +68,7 @@ Internals - Move ``TopologicalSorter`` from ``pyramid.config.util`` to ``pyramid.util``, move ``CyclicDependencyError`` from ``pyramid.config.util`` to ``pyramid.exceptions``, rename ``Singleton`` to ``Sentinel`` and move from - ``pyramid.config.util`` to ``pyramid.config.util``; this is in an effort to + ``pyramid.config.util`` to ``pyramid.util``; this is in an effort to move that stuff that may be an API one day out of ``pyramid.config.util``, because that package should never be imported from non-Pyramid code. TopologicalSorter is still not an API, but may become one. -- cgit v1.2.3 From ca3df803c9afd04d7dee612e0bf321cc62cf900f Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Sun, 4 Nov 2012 12:34:22 -0600 Subject: emit a warning if a user is using the default hashalg to AuthTkt --- CHANGES.txt | 9 +++++++++ TODO.txt | 3 +++ pyramid/authentication.py | 17 ++++++++++++++++- pyramid/tests/test_authentication.py | 9 +++++++++ 4 files changed, 37 insertions(+), 1 deletion(-) diff --git a/CHANGES.txt b/CHANGES.txt index b3733a787..43a910f96 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -48,6 +48,15 @@ Bug Fixes attribute of the request. It no longer fails in this case. See https://github.com/Pylons/pyramid/issues/700 +Deprecations +------------ + +- ``pyramid.authentication.AuthTktAuthenticationPolicy`` will emit a warning + if an application is using the policy without explicitly setting the + ``hashalg``. This is because the default is "md5" which is considered + insecure. If you really want "md5" then you must specify it explicitly to + get rid of the warning. + Internals --------- diff --git a/TODO.txt b/TODO.txt index ac5738244..af164adc9 100644 --- a/TODO.txt +++ b/TODO.txt @@ -141,6 +141,9 @@ Future - 1.6: Remove IContextURL and TraversalContextURL. +- 1.7: Change ``pyramid.authentication.AuthTktAuthenticationPolicy`` default + ``hashalg`` to ``sha512``. + Probably Bad Ideas ------------------ diff --git a/pyramid/authentication.py b/pyramid/authentication.py index dbca68a11..0a406e370 100644 --- a/pyramid/authentication.py +++ b/pyramid/authentication.py @@ -6,6 +6,7 @@ import base64 import datetime import re import time as time_mod +import warnings from zope.interface import implementer @@ -405,6 +406,8 @@ class RemoteUserAuthenticationPolicy(CallbackAuthenticationPolicy): be done somewhere else or in a subclass.""" return [] +_marker = object() + @implementer(IAuthenticationPolicy) class AuthTktAuthenticationPolicy(CallbackAuthenticationPolicy): """A :app:`Pyramid` :term:`authentication policy` which @@ -549,8 +552,20 @@ class AuthTktAuthenticationPolicy(CallbackAuthenticationPolicy): http_only=False, wild_domain=True, debug=False, - hashalg='md5', + hashalg=_marker ): + if hashalg is _marker: + hashalg = 'md5' + warnings.warn('The MD5 hash function is known to have collisions. ' + 'We recommend instead that you update your code to ' + 'use the SHA512 algorithm by setting ' + 'hashalg=\'sha512\'. If you accept these risks ' + 'and want to continue using MD5, explicitly set ' + 'the hashalg=\'md5\' in your authentication policy. ' + 'The default algorithm used in this policy is ' + 'likely to change in the future.', + DeprecationWarning, + stacklevel=2) self.cookie = AuthTktCookieHelper( secret, cookie_name=cookie_name, diff --git a/pyramid/tests/test_authentication.py b/pyramid/tests/test_authentication.py index 2d69173fa..123e4f9f5 100644 --- a/pyramid/tests/test_authentication.py +++ b/pyramid/tests/test_authentication.py @@ -1,4 +1,5 @@ import unittest +import warnings from pyramid import testing from pyramid.compat import ( text_, @@ -440,6 +441,14 @@ class TestAuthTktAuthenticationPolicy(unittest.TestCase): inst.cookie = DummyCookieHelper(cookieidentity) return inst + def setUp(self): + self.warnings = warnings.catch_warnings() + self.warnings.__enter__() + warnings.simplefilter('ignore', DeprecationWarning) + + def tearDown(self): + self.warnings.__exit__(None, None, None) + def test_allargs(self): # pass all known args inst = self._getTargetClass()( -- cgit v1.2.3 From 4bc4b4f72ae4dee013376621806519349afd373a Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Sun, 4 Nov 2012 12:39:12 -0600 Subject: fix tests to use sha512 to avoid emitting warnings --- pyramid/tests/pkgs/conflictapp/__init__.py | 3 ++- pyramid/tests/pkgs/defpermbugapp/__init__.py | 2 +- pyramid/tests/pkgs/forbiddenapp/__init__.py | 2 +- pyramid/tests/pkgs/forbiddenview/__init__.py | 2 +- pyramid/tests/pkgs/permbugapp/__init__.py | 2 +- 5 files changed, 6 insertions(+), 5 deletions(-) diff --git a/pyramid/tests/pkgs/conflictapp/__init__.py b/pyramid/tests/pkgs/conflictapp/__init__.py index 07bef0832..38116ab2f 100644 --- a/pyramid/tests/pkgs/conflictapp/__init__.py +++ b/pyramid/tests/pkgs/conflictapp/__init__.py @@ -18,6 +18,7 @@ def includeme(config): config.add_view(protectedview, name='protected', permission='view') config.add_view(routeview, route_name='aroute') config.add_route('aroute', '/route') - config.set_authentication_policy(AuthTktAuthenticationPolicy('seekri1t')) + config.set_authentication_policy(AuthTktAuthenticationPolicy( + 'seekri1t', hashalg='sha512')) config.set_authorization_policy(ACLAuthorizationPolicy()) config.include('pyramid.tests.pkgs.conflictapp.included') diff --git a/pyramid/tests/pkgs/defpermbugapp/__init__.py b/pyramid/tests/pkgs/defpermbugapp/__init__.py index 1ce0ff32d..032e8c626 100644 --- a/pyramid/tests/pkgs/defpermbugapp/__init__.py +++ b/pyramid/tests/pkgs/defpermbugapp/__init__.py @@ -17,7 +17,7 @@ def z_view(request): def includeme(config): from pyramid.authorization import ACLAuthorizationPolicy from pyramid.authentication import AuthTktAuthenticationPolicy - authn_policy = AuthTktAuthenticationPolicy('seekt1t') + authn_policy = AuthTktAuthenticationPolicy('seekt1t', hashalg='sha512') authz_policy = ACLAuthorizationPolicy() config.scan('pyramid.tests.pkgs.defpermbugapp') config._set_authentication_policy(authn_policy) diff --git a/pyramid/tests/pkgs/forbiddenapp/__init__.py b/pyramid/tests/pkgs/forbiddenapp/__init__.py index 888dc9317..c378126fc 100644 --- a/pyramid/tests/pkgs/forbiddenapp/__init__.py +++ b/pyramid/tests/pkgs/forbiddenapp/__init__.py @@ -16,7 +16,7 @@ def forbidden_view(context, request): def includeme(config): from pyramid.authentication import AuthTktAuthenticationPolicy from pyramid.authorization import ACLAuthorizationPolicy - authn_policy = AuthTktAuthenticationPolicy('seekr1t') + authn_policy = AuthTktAuthenticationPolicy('seekr1t', hashalg='sha512') authz_policy = ACLAuthorizationPolicy() config._set_authentication_policy(authn_policy) config._set_authorization_policy(authz_policy) diff --git a/pyramid/tests/pkgs/forbiddenview/__init__.py b/pyramid/tests/pkgs/forbiddenview/__init__.py index 631a442d2..45fb8380b 100644 --- a/pyramid/tests/pkgs/forbiddenview/__init__.py +++ b/pyramid/tests/pkgs/forbiddenview/__init__.py @@ -20,7 +20,7 @@ def bar(request): # pragma: no cover return Response('OK bar') def includeme(config): - authn_policy = AuthTktAuthenticationPolicy('seekri1') + authn_policy = AuthTktAuthenticationPolicy('seekri1', hashalg='sha512') authz_policy = ACLAuthorizationPolicy() config.set_authentication_policy(authn_policy) config.set_authorization_policy(authz_policy) diff --git a/pyramid/tests/pkgs/permbugapp/__init__.py b/pyramid/tests/pkgs/permbugapp/__init__.py index 330d983ab..4868427a5 100644 --- a/pyramid/tests/pkgs/permbugapp/__init__.py +++ b/pyramid/tests/pkgs/permbugapp/__init__.py @@ -14,7 +14,7 @@ def test(context, request): def includeme(config): from pyramid.authentication import AuthTktAuthenticationPolicy from pyramid.authorization import ACLAuthorizationPolicy - authn_policy = AuthTktAuthenticationPolicy('seekt1t') + authn_policy = AuthTktAuthenticationPolicy('seekt1t', hashalg='sha512') authz_policy = ACLAuthorizationPolicy() config.set_authentication_policy(authn_policy) config.set_authorization_policy(authz_policy) -- cgit v1.2.3 From bba64b29653cc49f153baeb62c44b0fa7006d1a9 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Sun, 4 Nov 2012 16:06:07 -0500 Subject: reword docs --- pyramid/authentication.py | 55 +++++++++++++++++++++++++++++++---------------- 1 file changed, 37 insertions(+), 18 deletions(-) diff --git a/pyramid/authentication.py b/pyramid/authentication.py index 0a406e370..08d283acc 100644 --- a/pyramid/authentication.py +++ b/pyramid/authentication.py @@ -510,23 +510,30 @@ class AuthTktAuthenticationPolicy(CallbackAuthenticationPolicy): ``hashalg`` - Default: ``md5``. Cookies generated by different instances of - AuthTktAuthenticationPolicy using different ``hashalg`` options - are not compatible. Switching the ``hashalg`` will imply that - all existing users with a valid cookie will be required to re-login. + Default: ``md5`` (the literal string). Any hash algorithm supported by Python's ``hashlib.new()`` function can be used as the ``hashalg``. - This option is available as of :app:`Pyramid` 1.4. See the warning - above for reasons to change ``hashalg`` in your own apps. + Cookies generated by different instances of AuthTktAuthenticationPolicy + using different ``hashalg`` options are not compatible. Switching the + ``hashalg`` will imply that all existing users with a valid cookie will + be required to re-login. + + A warning is emitted at startup if an explicit ``hashalg`` is not + passed. This is for backwards compatibility reasons. + + This option is available as of :app:`Pyramid` 1.4. Optional. .. note:: - ``sha512`` is recommended for improved security and to maintain - compatibility with Apache's ``mod_auth_tkt`` module. + ``md5`` is the default for backwards compatibility reasons. However, + if you don't specify ``md5`` as the hashalg explicitly, a warning is + issued at application startup time. An explicit value of ``sha512`` + is recommended for improved security, and ``sha512`` will become the + default in a future Pyramid version. ``debug`` @@ -556,16 +563,28 @@ class AuthTktAuthenticationPolicy(CallbackAuthenticationPolicy): ): if hashalg is _marker: hashalg = 'md5' - warnings.warn('The MD5 hash function is known to have collisions. ' - 'We recommend instead that you update your code to ' - 'use the SHA512 algorithm by setting ' - 'hashalg=\'sha512\'. If you accept these risks ' - 'and want to continue using MD5, explicitly set ' - 'the hashalg=\'md5\' in your authentication policy. ' - 'The default algorithm used in this policy is ' - 'likely to change in the future.', - DeprecationWarning, - stacklevel=2) + warnings.warn( + 'The MD5 hash function used by default by the ' + 'AuthTktAuthenticationPolicy is known to be ' + 'susceptible to collision attacks. It is the current default ' + 'for backwards compatibility reasons, but we recommend that ' + 'you use the SHA512 algorithm instead for improved security. ' + 'Pass ``hashalg=\'sha512\'`` to the ' + 'AuthTktAuthenticationPolicy constructor to do so.\n\nNote ' + 'that a change to the hash algorithms will invalidate existing ' + 'auth tkt cookies set by your application. If backwards ' + 'compatibility of existing auth tkt cookies is of greater ' + 'concern than the risk posed by the potential for a hash ' + 'collision, you\'ll want to continue using MD5 explicitly. ' + 'To do so, pass ``hashalg=\'md5\'`` in your application to ' + 'the AuthTktAuthenticationPolicy constructor. When you do so ' + 'this warning will not be emitted again. The default ' + 'algorithm used in this policy will change in the future, so ' + 'setting an explicit hashalg will futureproof your ' + 'application.', + DeprecationWarning, + stacklevel=2 + ) self.cookie = AuthTktCookieHelper( secret, cookie_name=cookie_name, -- cgit v1.2.3 From a8d71ca12aa648aa70e0c00f303e60da1fa97a61 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Sun, 4 Nov 2012 18:39:25 -0500 Subject: - Allow a ``_depth`` argument to ``pyramid.view.view_config``, which will permit limited compisition reuse of the decorator by other software that wants to provide custom decorators that are much like view_config. Closes #637. --- CHANGES.txt | 4 ++++ pyramid/tests/test_view.py | 11 ++++++++++- pyramid/view.py | 11 ++++++++++- 3 files changed, 24 insertions(+), 2 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 43a910f96..5175baa5a 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -33,6 +33,10 @@ Features class; that's enough to satisfy WebOb and behave as it did before with the monkeypatching. +- Allow a ``_depth`` argument to ``pyramid.view.view_config``, which will + permit limited composition reuse of the decorator by other software that + wants to provide custom decorators that are much like view_config. + Bug Fixes --------- diff --git a/pyramid/tests/test_view.py b/pyramid/tests/test_view.py index f63e17bd8..df9d03490 100644 --- a/pyramid/tests/test_view.py +++ b/pyramid/tests/test_view.py @@ -519,6 +519,14 @@ class TestViewConfigDecorator(unittest.TestCase): self.assertTrue(renderer is renderer_helper) self.assertEqual(config.pkg, pyramid.tests) + def test_call_withdepth(self): + decorator = self._makeOne(_depth=2) + venusian = DummyVenusian() + decorator.venusian = venusian + def foo(): pass + decorator(foo) + self.assertEqual(venusian.depth, 2) + class Test_append_slash_notfound_view(BaseTest, unittest.TestCase): def _callFUT(self, context, request): from pyramid.view import append_slash_notfound_view @@ -746,8 +754,9 @@ class DummyVenusian(object): self.info = info self.attachments = [] - def attach(self, wrapped, callback, category=None): + def attach(self, wrapped, callback, category=None, depth=1): self.attachments.append((wrapped, callback, category)) + self.depth = depth return self.info class DummyRegistry(object): diff --git a/pyramid/view.py b/pyramid/view.py index 51ded423c..835982e79 100644 --- a/pyramid/view.py +++ b/pyramid/view.py @@ -176,6 +176,13 @@ class view_config(object): :meth:`pyramid.config.Configurator.add_view`. If any argument is left out, its default will be the equivalent ``add_view`` default. + An additional keyword argument named ``_depth`` is provided for people who + wish to reuse this class from another decorator. It will be passed in to + the :term:`venusian` ``attach`` function as the depth of the callstack when + Venusian checks if the decorator is being used in a class or module + context. It's not often used, but it can be useful in this circumstance. + See the ``attach`` function in Venusian for more information. + See :ref:`mapping_views_using_a_decorator_section` for details about using :class:`view_config`. @@ -189,12 +196,14 @@ class view_config(object): def __call__(self, wrapped): settings = self.__dict__.copy() + depth = settings.pop('_depth', 1) def callback(context, name, ob): config = context.config.with_package(info.module) config.add_view(view=ob, **settings) - info = self.venusian.attach(wrapped, callback, category='pyramid') + info = self.venusian.attach(wrapped, callback, category='pyramid', + depth=depth) if info.scope == 'class': # if the decorator was attached to a method in a class, or -- cgit v1.2.3 From 20b1a19653de95e1adfa864a4f9ef6b2522e3409 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Sun, 4 Nov 2012 19:02:02 -0500 Subject: garden --- TODO.txt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/TODO.txt b/TODO.txt index af164adc9..46edd8c6b 100644 --- a/TODO.txt +++ b/TODO.txt @@ -4,8 +4,6 @@ Pyramid TODOs Nice-to-Have ------------ -- config.set_registry_attr (with conflict detection). - - Provide the presumed renderer name to the called view as an attribute of the request. @@ -177,3 +175,6 @@ Probably Bad Ideas - _fix_registry should dictify the registry being fixed. +- config.set_registry_attr (with conflict detection)... bad idea because it + won't take effect until after a commit and folks will be confused by that. + -- cgit v1.2.3 From 76c9c20478f53c36d5ded59191e335dba1d47da6 Mon Sep 17 00:00:00 2001 From: rforkel Date: Tue, 26 Jun 2012 08:40:34 +0200 Subject: Added support for passing multiple decorators to add_view. --- pyramid/config/views.py | 20 ++++++++++++++++---- pyramid/scaffolds/copydir.py | 2 +- pyramid/tests/test_config/test_views.py | 22 ++++++++++++++++++++++ pyramid/tests/test_view.py | 4 ++++ 4 files changed, 43 insertions(+), 5 deletions(-) diff --git a/pyramid/config/views.py b/pyramid/config/views.py index b01d17efd..b17619356 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -837,11 +837,12 @@ class ViewsConfiguratorMixin(object): decorator - A :term:`dotted Python name` to function (or the function itself) + A :term:`dotted Python name` to function (or the function itself, + or a list or tuple of the aforementioned) which will be used to decorate the registered :term:`view - callable`. The decorator function will be called with the view + callable`. The decorator function(s) will be called with the view callable as a single argument. The view callable it is passed will - accept ``(context, request)``. The decorator must return a + accept ``(context, request)``. The decorator(s) must return a replacement view callable which also accepts ``(context, request)``. @@ -1071,7 +1072,18 @@ class ViewsConfiguratorMixin(object): for_ = self.maybe_dotted(for_) containment = self.maybe_dotted(containment) mapper = self.maybe_dotted(mapper) - decorator = self.maybe_dotted(decorator) + + def combine(*decorators): + def decorated(view_callable): + for decorator in decorators: + view_callable = decorator(view_callable) + return view_callable + return decorated + + if isinstance(decorator, (tuple, list)): + decorator = combine(*map(self.maybe_dotted, decorator)) + else: + decorator = self.maybe_dotted(decorator) if not view: if renderer: diff --git a/pyramid/scaffolds/copydir.py b/pyramid/scaffolds/copydir.py index d55ea165a..ba0988523 100644 --- a/pyramid/scaffolds/copydir.py +++ b/pyramid/scaffolds/copydir.py @@ -245,7 +245,7 @@ Responses: def makedirs(dir, verbosity, pad): parent = os.path.dirname(os.path.abspath(dir)) if not os.path.exists(parent): - makedirs(parent, verbosity, pad) + makedirs(parent, verbosity, pad) # pragma: no cover os.mkdir(dir) def substitute_filename(fn, vars): diff --git a/pyramid/tests/test_config/test_views.py b/pyramid/tests/test_config/test_views.py index 575d8c738..a62e5f2ea 100644 --- a/pyramid/tests/test_config/test_views.py +++ b/pyramid/tests/test_config/test_views.py @@ -185,6 +185,28 @@ class TestViewsConfigurationMixin(unittest.TestCase): result = wrapper(None, None) self.assertEqual(result, 'OK') + def test_add_view_with_decorator_tuple(self): + from pyramid.renderers import null_renderer + def view(request): + """ ABC """ + return 'OK' + def view_wrapper1(fn): + def inner(context, request): + return 'wrapped1' + fn(context, request) + return inner + def view_wrapper2(fn): + def inner(context, request): + return 'wrapped2' + fn(context, request) + return inner + config = self._makeOne(autocommit=True) + config.add_view(view=view, decorator=(view_wrapper1, view_wrapper2), + renderer=null_renderer) + wrapper = self._getViewCallable(config) + self.assertFalse(wrapper is view) + self.assertEqual(wrapper.__doc__, view.__doc__) + result = wrapper(None, None) + self.assertEqual(result, 'wrapped2wrapped1OK') + def test_add_view_with_http_cache(self): import datetime from pyramid.response import Response diff --git a/pyramid/tests/test_view.py b/pyramid/tests/test_view.py index df9d03490..0af941e0d 100644 --- a/pyramid/tests/test_view.py +++ b/pyramid/tests/test_view.py @@ -372,6 +372,10 @@ class TestViewConfigDecorator(unittest.TestCase): def test_create_with_other_predicates(self): decorator = self._makeOne(foo=1) self.assertEqual(decorator.foo, 1) + + def test_create_decorator_tuple(self): + decorator = self._makeOne(decorator=('decorator1', 'decorator2')) + self.assertEqual(decorator.decorator, ('decorator1', 'decorator2')) def test_call_function(self): decorator = self._makeOne() -- cgit v1.2.3 From 9aac76aad6d8e40278f0aab16322bd1dbe6803bc Mon Sep 17 00:00:00 2001 From: rforkel Date: Tue, 26 Jun 2012 10:07:59 +0200 Subject: Added explanations to docstring. --- pyramid/config/views.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/pyramid/config/views.py b/pyramid/config/views.py index b17619356..02d5b52bf 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -846,6 +846,27 @@ class ViewsConfiguratorMixin(object): replacement view callable which also accepts ``(context, request)``. + If decorator is a tuple or list of callables, the callables will be + combined and used in the order provided as a decorator. + For example:: + + @view_config(..., decorator=[decorator1, decorator2]) + def myview(request): + .... + + Is similar to doing:: + + @view_config(...) + @decorator2 + @decorator1 + def myview(request): + ... + + Except with the existing benefits of ``decorator=`` (having a common + decorator syntax for all view calling conventions and not having to + think about preserving function attributes such as ``__name__`` and + ``__module__`` within decorator logic). + mapper A Python object or :term:`dotted Python name` which refers to a -- cgit v1.2.3 From f194db1b3020e1c3610092f292c16a2f07219269 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Sun, 4 Nov 2012 19:10:16 -0600 Subject: reversed the ordering of decorator arg to add_view --- pyramid/config/views.py | 22 ++++++++++++---------- pyramid/tests/test_config/test_views.py | 2 +- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/pyramid/config/views.py b/pyramid/config/views.py index 02d5b52bf..0cd61cba5 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -42,6 +42,7 @@ from pyramid.compat import ( url_quote, WIN, is_bound_method, + is_nonstr_iter ) from pyramid.exceptions import ( @@ -838,19 +839,19 @@ class ViewsConfiguratorMixin(object): decorator A :term:`dotted Python name` to function (or the function itself, - or a list or tuple of the aforementioned) - which will be used to decorate the registered :term:`view - callable`. The decorator function(s) will be called with the view - callable as a single argument. The view callable it is passed will - accept ``(context, request)``. The decorator(s) must return a + or an iterable of the aforementioned) which will be used to + decorate the registered :term:`view callable`. The decorator + function(s) will be called with the view callable as a single + argument. The view callable it is passed will accept + ``(context, request)``. The decorator(s) must return a replacement view callable which also accepts ``(context, request)``. - If decorator is a tuple or list of callables, the callables will be - combined and used in the order provided as a decorator. + If decorator is an iterable, the callables will be combined and + used in the order provided as a decorator. For example:: - @view_config(..., decorator=[decorator1, decorator2]) + @view_config(..., decorator=[decorator2, decorator1]) def myview(request): .... @@ -1096,12 +1097,13 @@ class ViewsConfiguratorMixin(object): def combine(*decorators): def decorated(view_callable): - for decorator in decorators: + # reversed() is allows a more natural ordering in the api + for decorator in reversed(decorators): view_callable = decorator(view_callable) return view_callable return decorated - if isinstance(decorator, (tuple, list)): + if is_nonstr_iter(decorator): decorator = combine(*map(self.maybe_dotted, decorator)) else: decorator = self.maybe_dotted(decorator) diff --git a/pyramid/tests/test_config/test_views.py b/pyramid/tests/test_config/test_views.py index a62e5f2ea..8324eb2b9 100644 --- a/pyramid/tests/test_config/test_views.py +++ b/pyramid/tests/test_config/test_views.py @@ -199,7 +199,7 @@ class TestViewsConfigurationMixin(unittest.TestCase): return 'wrapped2' + fn(context, request) return inner config = self._makeOne(autocommit=True) - config.add_view(view=view, decorator=(view_wrapper1, view_wrapper2), + config.add_view(view=view, decorator=(view_wrapper2, view_wrapper1), renderer=null_renderer) wrapper = self._getViewCallable(config) self.assertFalse(wrapper is view) -- cgit v1.2.3 From 170124352a495438ef689d3376988bc8aefa8876 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Sun, 4 Nov 2012 19:10:16 -0600 Subject: update changelog, close #627 --- CHANGES.txt | 5 +++++ pyramid/config/views.py | 2 ++ 2 files changed, 7 insertions(+) diff --git a/CHANGES.txt b/CHANGES.txt index 5175baa5a..d57444ad0 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -37,6 +37,11 @@ Features permit limited composition reuse of the decorator by other software that wants to provide custom decorators that are much like view_config. +- Allow an iterable of decorators to be passed to + ``pyramid.config.Configurator.add_view``. This allows views to be wrapped + by more than one decorator without requiring combining the decorators + yourself. + Bug Fixes --------- diff --git a/pyramid/config/views.py b/pyramid/config/views.py index 0cd61cba5..10f972633 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -868,6 +868,8 @@ class ViewsConfiguratorMixin(object): think about preserving function attributes such as ``__name__`` and ``__module__`` within decorator logic). + Passing an iterable is only supported as of :app:`Pyramid` 1.4a4. + mapper A Python object or :term:`dotted Python name` which refers to a -- cgit v1.2.3 From ee0e41d020d3cc9f43a958a53528166e5d2293f7 Mon Sep 17 00:00:00 2001 From: Philip Jenvey Date: Sun, 4 Nov 2012 17:58:58 -0800 Subject: visually aid the decorator= example --- pyramid/config/views.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pyramid/config/views.py b/pyramid/config/views.py index 10f972633..8a4db149e 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -851,7 +851,9 @@ class ViewsConfiguratorMixin(object): used in the order provided as a decorator. For example:: - @view_config(..., decorator=[decorator2, decorator1]) + @view_config(..., + decorator=(decorator2, + decorator1)) def myview(request): .... -- cgit v1.2.3