diff options
| author | Chris McDonough <chrism@plope.com> | 2012-11-05 16:03:07 -0500 |
|---|---|---|
| committer | Chris McDonough <chrism@plope.com> | 2012-11-05 16:03:07 -0500 |
| commit | 8c30a3d9c2437e661eac6f23315837fccb4741ea (patch) | |
| tree | 308b4cbdea04bc582450a57e583e4e93d9ec5d72 | |
| parent | 3c247503042c94b792a6b1a5701fdba7c832b99c (diff) | |
| parent | ee0e41d020d3cc9f43a958a53528166e5d2293f7 (diff) | |
| download | pyramid-8c30a3d9c2437e661eac6f23315837fccb4741ea.tar.gz pyramid-8c30a3d9c2437e661eac6f23315837fccb4741ea.tar.bz2 pyramid-8c30a3d9c2437e661eac6f23315837fccb4741ea.zip | |
Merge branch 'master' of github.com:Pylons/pyramid
40 files changed, 1143 insertions, 528 deletions
diff --git a/CHANGES.txt b/CHANGES.txt index 740de0f17..d57444ad0 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -4,8 +4,79 @@ 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 + 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. + +- 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 + 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. + +- 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. + +- 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 +--------- + +- 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 + +- 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 + +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 +--------- + +- 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.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) ================== @@ -4,18 +4,11 @@ Pyramid TODOs 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. - 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. @@ -146,6 +139,9 @@ Future - 1.6: Remove IContextURL and TraversalContextURL. +- 1.7: Change ``pyramid.authentication.AuthTktAuthenticationPolicy`` default + ``hashalg`` to ``sha512``. + Probably Bad Ideas ------------------ @@ -177,3 +173,8 @@ Probably Bad Ideas with config.partial(introspection=False) as c: c.add_view(..) +- _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. + 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/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 diff --git a/docs/narr/security.rst b/docs/narr/security.rst index 07ec0f21e..3a94b4f7d 100644 --- a/docs/narr/security.rst +++ b/docs/narr/security.rst @@ -92,11 +92,11 @@ For example: from pyramid.config import Configurator from pyramid.authentication import AuthTktAuthenticationPolicy from pyramid.authorization import ACLAuthorizationPolicy - authentication_policy = AuthTktAuthenticationPolicy('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/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 diff --git a/docs/tutorials/wiki/authorization.rst b/docs/tutorials/wiki/authorization.rst index 9e0bf0f09..24249945a 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 ``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 ``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..b42e01d03 100644 --- a/docs/tutorials/wiki/src/authorization/tutorial/__init__.py +++ b/docs/tutorials/wiki/src/authorization/tutorial/__init__.py @@ -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 = 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 6989145d8..b42e01d03 100644 --- a/docs/tutorials/wiki/src/tests/tutorial/__init__.py +++ b/docs/tutorials/wiki/src/tests/tutorial/__init__.py @@ -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 = 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 6b2d44410..1ddf8c82d 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 ``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 ``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..76071173a 100644 --- a/docs/tutorials/wiki2/src/authorization/tutorial/__init__.py +++ b/docs/tutorials/wiki2/src/authorization/tutorial/__init__.py @@ -18,7 +18,7 @@ def main(global_config, **settings): DBSession.configure(bind=engine) Base.metadata.bind = engine authn_policy = AuthTktAuthenticationPolicy( - 'sosecret', callback=groupfinder) + '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 8922a3cc0..76071173a 100644 --- a/docs/tutorials/wiki2/src/tests/tutorial/__init__.py +++ b/docs/tutorials/wiki2/src/tests/tutorial/__init__.py @@ -18,7 +18,7 @@ def main(global_config, **settings): DBSession.configure(bind=engine) Base.metadata.bind = engine authn_policy = AuthTktAuthenticationPolicy( - 'sosecret', callback=groupfinder) + 'sosecret', callback=groupfinder, hashalg='sha512') authz_policy = ACLAuthorizationPolicy() config = Configurator(settings=settings, root_factory='tutorial.models.RootFactory') diff --git a/pyramid/__init__.py b/pyramid/__init__.py index 473d5e1c6..5bb534f79 100644 --- a/pyramid/__init__.py +++ b/pyramid/__init__.py @@ -1,5 +1 @@ -from pyramid.request import Request -from pyramid.response import Response -Response.RequestClass = Request -Request.ResponseClass = Response -del Request, Response +# package diff --git a/pyramid/authentication.py b/pyramid/authentication.py index d4fd7ab8b..08d283acc 100644 --- a/pyramid/authentication.py +++ b/pyramid/authentication.py @@ -1,11 +1,12 @@ import binascii 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 @@ -47,7 +48,21 @@ 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``. + + 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: @@ -56,6 +71,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,), @@ -78,9 +101,32 @@ 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) + if userid is None: debug and self._log( 'unauthenticated_userid returned %r; returning %r' % ( @@ -89,6 +135,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 []', @@ -101,6 +157,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' % ( @@ -163,39 +220,120 @@ 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: + 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.""" 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: + 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): + """ Store the ``principal`` as ``repoze.who.userid``.""" identifier = self._get_identifier(request) if identifier is None: return [] @@ -204,6 +342,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,19 +391,35 @@ 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 [] +_marker = object() + @implementer(IAuthenticationPolicy) class AuthTktAuthenticationPolicy(CallbackAuthenticationPolicy): - """ A :app:`Pyramid` :term:`authentication policy` which + """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`` @@ -348,6 +508,33 @@ class AuthTktAuthenticationPolicy(CallbackAuthenticationPolicy): wildcard domain. Optional. + ``hashalg`` + + Default: ``md5`` (the literal string). + + Any hash algorithm supported by Python's ``hashlib.new()`` function + can be used as the ``hashalg``. + + 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:: + + ``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`` Default: ``False``. If ``debug`` is ``True``, log messages to the @@ -358,6 +545,7 @@ class AuthTktAuthenticationPolicy(CallbackAuthenticationPolicy): Objects of this class implement the interface described by :class:`pyramid.interfaces.IAuthenticationPolicy`. """ + def __init__(self, secret, callback=None, @@ -371,7 +559,32 @@ class AuthTktAuthenticationPolicy(CallbackAuthenticationPolicy): http_only=False, wild_domain=True, debug=False, + hashalg=_marker ): + if hashalg is _marker: + hashalg = 'md5' + 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, @@ -383,21 +596,29 @@ class AuthTktAuthenticationPolicy(CallbackAuthenticationPolicy): http_only=http_only, path=path, wild_domain=wild_domain, + hashalg=hashalg, ) self.callback = callback 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=<int-seconds>, - ``tokens=<sequence-of-ascii-strings>``""" + ``tokens=<sequence-of-ascii-strings>``. + + 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): @@ -428,7 +649,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 @@ -440,11 +662,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), @@ -466,7 +689,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='md5'): """ Parse the ticket, returning (timestamp, userid, tokens, user_data). @@ -474,13 +697,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) @@ -492,7 +716,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) @@ -505,16 +729,20 @@ 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='md5'): 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): @@ -557,7 +785,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): + 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 @@ -568,6 +797,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: @@ -636,7 +866,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 @@ -751,7 +981,9 @@ 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) @@ -860,14 +1092,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): 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/config/views.py b/pyramid/config/views.py index b01d17efd..8a4db149e 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 ( @@ -837,14 +838,40 @@ class ViewsConfiguratorMixin(object): decorator - A :term:`dotted Python name` to function (or the function itself) - which will be used to decorate the registered :term:`view - callable`. The decorator function 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 + A :term:`dotted Python name` to function (or the function itself, + 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 an iterable, the callables will be combined and + used in the order provided as a decorator. + For example:: + + @view_config(..., + decorator=(decorator2, + decorator1)) + 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). + + 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 @@ -1071,7 +1098,19 @@ 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): + # 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 is_nonstr_iter(decorator): + decorator = combine(*map(self.maybe_dotted, decorator)) + else: + decorator = self.maybe_dotted(decorator) if not view: if renderer: 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/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/request.py b/pyramid/request.py index af3310829..9e275c2c0 100644 --- a/pyramid/request.py +++ b/pyramid/request.py @@ -328,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/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/security.py b/pyramid/security.py index 4b929241e..3e25f9b2f 100644 --- a/pyramid/security.py +++ b/pyramid/security.py @@ -4,6 +4,7 @@ from pyramid.interfaces import ( IAuthenticationPolicy, IAuthorizationPolicy, ISecuredView, + IView, IViewClassifier, ) @@ -132,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: @@ -140,6 +147,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/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) diff --git a/pyramid/tests/test_authentication.py b/pyramid/tests/test_authentication.py index dfe3cf0b0..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_, @@ -76,6 +77,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 +169,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 +237,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 +259,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 +310,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() @@ -333,7 +431,7 @@ class TestRemoteUserAuthenticationPolicy(unittest.TestCase): result = policy.forget(request) self.assertEqual(result, []) -class TestAutkTktAuthenticationPolicy(unittest.TestCase): +class TestAuthTktAuthenticationPolicy(unittest.TestCase): def _getTargetClass(self): from pyramid.authentication import AuthTktAuthenticationPolicy return AuthTktAuthenticationPolicy @@ -343,23 +441,27 @@ class TestAutkTktAuthenticationPolicy(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()( '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_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)) + 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({}) @@ -433,6 +535,16 @@ class TestAutkTktAuthenticationPolicy(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 @@ -971,6 +1083,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 +1109,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 +1134,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 @@ -1222,13 +1349,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 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 --- a/pyramid/tests/test_config/test_init.py:TestConfigurator_add_directive +++ /dev/null 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_config/test_views.py b/pyramid/tests/test_config/test_views.py index 575d8c738..8324eb2b9 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_wrapper2, view_wrapper1), + 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_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_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 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') 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 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/tests/test_view.py b/pyramid/tests/test_view.py index f63e17bd8..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() @@ -519,6 +523,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 +758,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/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) 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 + 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 |
