diff options
34 files changed, 1102 insertions, 147 deletions
diff --git a/.gitignore b/.gitignore index 8e2f83e7d..5fa2a2ee4 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ *.pt.py *.txt.py *~ +.*.swp .coverage .tox/ nosetests.xml @@ -21,3 +22,4 @@ bookenv/ jyenv/ pypyenv/ env*/ +venv/ diff --git a/CHANGES.txt b/CHANGES.txt index df4ada7e9..0ef1a0593 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,9 +1,45 @@ Next release ============ +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. + +- Slightly better debug logging from 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. + +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) +================== + Bug Fixes --------- +- The match_param predicate's text method was fixed to sort its values. + Part of https://github.com/Pylons/pyramid/pull/705 + - 1.4a ``pyramid.scripting.prepare`` behaved differently than 1.3 series function of same name. In particular, if passed a request, it would not set the ``registry`` attribute of the request like 1.3 did. A symptom @@ -17,13 +53,23 @@ Bug Fixes - When registering a view configuration that named a Chameleon ZPT renderer with a macro name in it (e.g. ``renderer='some/template#somemacro.pt``) as well as a view configuration without a macro name it it that pointed to the - same template (e.g. ``renderer='some/template.pt'), internal caching could + same template (e.g. ``renderer='some/template.pt'``), internal caching could confuse the two, and your code might have rendered one instead of the other. Features -------- +- Allow multiple values to be specified to the ``request_param`` view/route + predicate as a sequence. Previously only a single string value was allowed. + See https://github.com/Pylons/pyramid/pull/705 + +- Comments with references to documentation sections placed in scaffold + ``.ini`` files. + +- Added an HTTP Basic authentication policy + at ``pyramid.authentication.BasicAuthAuthenticationPolicy``. + - The Configurator ``testing_securitypolicy`` method now returns the policy object it creates. @@ -40,6 +86,18 @@ Features ``remembered`` value on the policy, which is the value of the ``principal`` argument it's called with when its ``remember`` method is called. +- New ``physical_path`` view predicate. If specified, this value should be a + string or a tuple representing the physical traversal path of the context + found via traversal for this predicate to match as true. For example: + ``physical_path='/'`` or ``physical_path='/a/b/c'`` or ``physical_path=('', + 'a', 'b', 'c')``. This is not a path prefix match or a regex, it's a + whole-path match. It's useful when you want to always potentially show a + view when some object is traversed to, but you can't be sure about what kind + of object it will be, so you can't use the ``context`` predicate. The + individual path elements inbetween slash characters or in tuple elements + should be the Unicode representation of the name of the resource and should + not be encoded in any way. + 1.4a2 (2012-09-27) ================== diff --git a/RELEASING.txt b/RELEASING.txt index c97c8ef60..379965c53 100644 --- a/RELEASING.txt +++ b/RELEASING.txt @@ -13,10 +13,10 @@ Releasing Pyramid Make sure statement coverage is at 100%:: -- Run Windows tests for Python 2.6, 2.7, and 3.2 if feasible. +- Run Windows tests for Python 2.6, 2.7, 3.2, and 3.3 if feasible. -- Make sure all scaffold tests pass (Py 2.6, 2.7, 3.2 and pypy on UNIX; this - doesn't work on Windows): +- Make sure all scaffold tests pass (Py 2.6, 2.7, 3.2, 3.3 and pypy on UNIX; + this doesn't work on Windows): $ python pyramid/scaffolds/tests.py @@ -6,16 +6,11 @@ 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. @@ -177,3 +172,5 @@ Probably Bad Ideas with config.partial(introspection=False) as c: c.add_view(..) +- _fix_registry should dictify the registry being fixed. + diff --git a/docs/api/authentication.rst b/docs/api/authentication.rst index 5d4dbd9e3..19d08618b 100644 --- a/docs/api/authentication.rst +++ b/docs/api/authentication.rst @@ -9,12 +9,24 @@ Authentication Policies .. automodule:: pyramid.authentication .. autoclass:: AuthTktAuthenticationPolicy - - .. autoclass:: RepozeWho1AuthenticationPolicy + :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/conf.py b/docs/conf.py index 337b1d8bf..9bda4c798 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -81,7 +81,7 @@ copyright = '%s, Agendaless Consulting' % datetime.datetime.now().year # other places throughout the built documents. # # The short X.Y version. -version = '1.4a2' +version = '1.4a3' # The full version, including alpha/beta/rc tags. release = version diff --git a/docs/glossary.rst b/docs/glossary.rst index 96dd826d1..adcf36f7c 100644 --- a/docs/glossary.rst +++ b/docs/glossary.rst @@ -481,10 +481,24 @@ Glossary :app:`Pyramid` to form a workflow system. virtual root - A resource object representing the "virtual" root of a request; this - is typically the physical root object (the object returned by the - application root factory) unless :ref:`vhosting_chapter` is in - use. + A resource object representing the "virtual" root of a request; this is + typically the :term:`physical root` object unless :ref:`vhosting_chapter` + is in use. + + physical root + The object returned by the application :term:`root factory`. Unlike the + the :term:`virtual root` of a request, it is not impacted by + :ref:`vhosting_chapter`: it will always be the actual object returned by + the root factory, never a subobject. + + physical path + The path required by a traversal which resolve a :term:`resource` starting + from the :term:`physical root`. For example, the physical path of the + ``abc`` subobject of the physical root object is ``/abc``. Physical paths + can also be specified as tuples where the first element is the empty + string (representing the root), and every other element is a Unicode + object, e.g. ``('', 'abc')``. Physical paths are also sometimes called + "traversal paths". lineage An ordered sequence of objects based on a ":term:`location` -aware" 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/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/narr/viewconfig.rst b/docs/narr/viewconfig.rst index f65435cc6..6373a8d26 100644 --- a/docs/narr/viewconfig.rst +++ b/docs/narr/viewconfig.rst @@ -290,12 +290,13 @@ configured view. of the ``REQUEST_METHOD`` of the :term:`WSGI` environment. ``request_param`` - This value can be any string. A view declaration with this argument - ensures that the view will only be called when the :term:`request` has a - key in the ``request.params`` dictionary (an HTTP ``GET`` or ``POST`` - variable) that has a name which matches the supplied value. + This value can be any string or a sequence of strings. A view declaration + with this argument ensures that the view will only be called when the + :term:`request` has a key in the ``request.params`` dictionary (an HTTP + ``GET`` or ``POST`` variable) that has a name which matches the a + supplied value. - If the value supplied has a ``=`` sign in it, + If any value supplied has a ``=`` sign in it, e.g. ``request_param="foo=123"``, then the key (``foo``) must both exist in the ``request.params`` dictionary, *and* the value must match the right hand side of the expression (``123``) for the view to "match" the current @@ -416,6 +417,32 @@ configured view. .. versionadded:: 1.4a2 +``physical_path`` + If specified, this value should be a string or a tuple representing the + :term:`physical path` of the context found via traversal for this predicate + to match as true. For example: ``physical_path='/'`` or + ``physical_path='/a/b/c'`` or ``physical_path=('', 'a', 'b', 'c')``. This is + not a path prefix match or a regex, it's a whole-path match. It's useful + when you want to always potentially show a view when some object is traversed + to, but you can't be sure about what kind of object it will be, so you can't + use the ``context`` predicate. The individual path elements inbetween slash + characters or in tuple elements should be the Unicode representation of the + name of the resource and should not be encoded in any way. + + .. versionadded:: 1.4a3 + +``effective_principals`` + + If specified, this value should be a :term:`principal` identifier or a + sequence of principal identifiers. If the + :func:`pyramid.security.effective_principals` method indicates that every + principal named in the argument list is present in the current request, this + predicate will return True; otherwise it will return False. For example: + ``effective_principals=pyramid.security.Authenticated`` or + ``effective_principals=('fred', 'group:admins')``. + + .. versionadded:: 1.4a4 + ``custom_predicates`` If ``custom_predicates`` is specified, it must be a sequence of references to custom predicate callables. Use custom predicates when no set of diff --git a/docs/remake b/docs/remake index b236f2976..eb818289f 100755 --- a/docs/remake +++ b/docs/remake @@ -1 +1 @@ -make clean html SPHINXBUILD=../env26/bin/sphinx-build +make clean html SPHINXBUILD=../env27/bin/sphinx-build diff --git a/docs/whatsnew-1.4.rst b/docs/whatsnew-1.4.rst index 4e64d8162..59e1f7a96 100644 --- a/docs/whatsnew-1.4.rst +++ b/docs/whatsnew-1.4.rst @@ -63,7 +63,7 @@ Partial Mako and Chameleon Template Renderings of rendering the entire template. An example asset spec: ``package:path/to/template#macroname.pt``. This will render the macro defined as ``macroname`` within the ``template.pt`` template instead of the - entire templae. + entire template. Subrequest Support ~~~~~~~~~~~~~~~~~~ @@ -78,7 +78,7 @@ Minor Feature Additions ----------------------- - :meth:`pyramid.config.Configurator.add_directive` now accepts arbitrary - callables like partials or objects implementing ``__call__`` which dont + callables like partials or objects implementing ``__call__`` which don't have ``__name__`` and ``__doc__`` attributes. See https://github.com/Pylons/pyramid/issues/621 and https://github.com/Pylons/pyramid/pull/647. @@ -112,7 +112,7 @@ Minor Feature Additions - An :meth:`pyramid.config.Configurator.add_permission` directive method was added to the Configurator. This directive registers a free-standing permission introspectable into the Pyramid introspection system. - Frameworks built atop Pyramid can thus use the the ``permissions`` + Frameworks built atop Pyramid can thus use the ``permissions`` introspectable category data to build a comprehensive list of permissions supported by a running system. Before this method was added, permissions were already registered in this introspectable category as a side effect of @@ -165,6 +165,37 @@ Minor Feature Additions - Add ``Base.metadata.bind = engine`` to ``alchemy`` scaffold, so that tables defined imperatively will work. +- Comments with references to documentation sections placed in scaffold + ``.ini`` files. + +- Allow multiple values to be specified to the ``request_param`` view/route + predicate as a sequence. Previously only a single string value was allowed. + See https://github.com/Pylons/pyramid/pull/705 + +- Added an HTTP Basic authentication policy + at :class:`pyramid.authentication.BasicAuthAuthenticationPolicy`. + +- The :meth:`pyramid.config.Configurator.testing_securitypolicy` method now + returns the policy object it creates. + +- The DummySecurityPolicy created by + :meth:`pyramid.config.testing_securitypolicy` now sets a ``forgotten`` value + on the policy (the value ``True``) when its ``forget`` method is called. + + +- The DummySecurityPolicy created by + :meth:`pyramid.config.testing_securitypolicy` now sets a + ``remembered`` value on the policy, which is the value of the ``principal`` + argument it's called with when its ``remember`` method is called. + +- New ``physical_path`` view predicate. If specified, this value should be a + string or a tuple representing the physical traversal path of the context + found via traversal for this predicate to match as true. For example: + ``physical_path='/'`` or ``physical_path='/a/b/c'`` or ``physical_path=('', + 'a', 'b', 'c')``. It's useful when you want to always potentially show a + view when some object is traversed to, but you can't be sure about what kind + of object it will be, so you can't use the ``context`` predicate. + Backwards Incompatibilities --------------------------- @@ -172,7 +203,7 @@ Backwards Incompatibilities ``bfg.routes.matchdict`` to the request's WSGI environment dictionary. These values were docs-deprecated in ``repoze.bfg`` 1.0 (effectively seven minor releases ago). If your code depended on these values, use - request.matched_route and request.matchdict instead. + ``request.matched_route`` and ``request.matchdict`` instead. - It is no longer possible to pass an environ dictionary directly to ``pyramid.traversal.ResourceTreeTraverser.__call__`` (aka @@ -223,7 +254,7 @@ Backwards Incompatibilities * ``registerEventListener``, use :meth:`pyramid.config.Configurator.testing_add_subscriber` instead. - * ``registerTemplateRenderer`` (aka `registerDummyRenderer``), use + * ``registerTemplateRenderer`` (aka ``registerDummyRenderer``), use :meth:`pyramid.config.Configurator.testing_add_template` instead. * ``registerView``, use :meth:`pyramid.config.Configurator.add_view` instead. diff --git a/pyramid/authentication.py b/pyramid/authentication.py index 83bdb13d1..8be34cc0a 100644 --- a/pyramid/authentication.py +++ b/pyramid/authentication.py @@ -1,3 +1,4 @@ +import binascii from codecs import utf_8_decode from codecs import utf_8_encode from hashlib import md5 @@ -46,7 +47,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: @@ -55,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,), @@ -77,9 +100,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' % ( @@ -88,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 []', @@ -100,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' % ( @@ -162,39 +219,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 [] @@ -203,6 +341,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 [] @@ -246,12 +390,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) @@ -330,13 +481,13 @@ class AuthTktAuthenticationPolicy(CallbackAuthenticationPolicy): Optional. ``path`` - + Default: ``/``. The path for which the auth_tkt cookie is valid. May be desirable if the application only serves part of a domain. Optional. - + ``http_only`` - + Default: ``False``. Hide cookie from JavaScript by setting the HttpOnly flag. Not honored by all browsers. Optional. @@ -387,16 +538,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=<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): @@ -553,7 +711,7 @@ class AuthTktCookieHelper(object): text_type: ('b64unicode', lambda x: b64encode(utf_8_encode(x)[0])), binary_type: ('b64str', lambda x: b64encode(x)), } - + 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): @@ -632,7 +790,7 @@ class AuthTktCookieHelper(object): remote_addr = environ['REMOTE_ADDR'] else: remote_addr = '0.0.0.0' - + try: timestamp, userid, tokens, user_data = self.parse_ticket( self.secret, cookie, remote_addr) @@ -641,7 +799,7 @@ class AuthTktCookieHelper(object): now = self.now # service tests - if now is None: + if now is None: now = time_mod.time() if self.timeout and ( (timestamp + self.timeout) < now ): @@ -689,7 +847,7 @@ class AuthTktCookieHelper(object): environ = request.environ request._authtkt_reissue_revoked = True return self._get_cookies(environ, '', max_age=EXPIRE) - + def remember(self, request, userid, max_age=None, tokens=()): """ Return a set of Set-Cookie headers; when set into a response, these headers will represent a valid authentication ticket. @@ -783,7 +941,7 @@ class SessionAuthenticationPolicy(CallbackAuthenticationPolicy): Pyramid debug logger about the results of various authentication steps. The output from debugging is useful for reporting to maillist or IRC channels when asking for support. - + """ def __init__(self, prefix='auth.', callback=None, debug=False): @@ -806,3 +964,101 @@ class SessionAuthenticationPolicy(CallbackAuthenticationPolicy): def unauthenticated_userid(self, request): return request.session.get(self.userid_key) + +@implementer(IAuthenticationPolicy) +class BasicAuthAuthenticationPolicy(CallbackAuthenticationPolicy): + """ A :app:`Pyramid` authentication policy which uses HTTP standard basic + authentication protocol to authenticate users. To use this policy you will + need to provide a callback which checks the supplied user credentials + against your source of login data. + + Constructor Arguments + + ``check`` + + A callback function passed a username, password and request, in that + order as positional arguments. Expected to return ``None`` if the + userid doesn't exist or a sequence of principal identifiers (possibly + empty) if the user does exist. + + ``realm`` + + Default: ``"Realm"``. The Basic Auth Realm string. Usually displayed to + the user by the browser in the login dialog. + + ``debug`` + + Default: ``False``. If ``debug`` is ``True``, log messages to the + Pyramid debug logger about the results of various authentication + steps. The output from debugging is useful for reporting to maillist + or IRC channels when asking for support. + + **Issuing a challenge** + + Regular browsers will not send username/password credentials unless they + first receive a challenge from the server. The following recipe will + register a view that will send a Basic Auth challenge to the user whenever + there is an attempt to call a view which results in a Forbidden response:: + + from pyramid.httpexceptions import HTTPForbidden + from pyramid.httpexceptions import HTTPUnauthorized + from pyramid.security import forget + from pyramid.view import view_config + + @view_config(context=HTTPForbidden) + def basic_challenge(request): + response = HTTPUnauthorized() + response.headers.update(forget(request)) + return response + """ + def __init__(self, check, realm='Realm', debug=False): + self.check = check + self.realm = realm + 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): + # Username arg is ignored. Unfortunately _get_credentials winds up + # getting called twice when authenticated_userid is called. Avoiding + # that, however, winds up duplicating logic from the superclass. + credentials = self._get_credentials(request) + if credentials: + username, password = credentials + return self.check(username, password, request) + + def _get_credentials(self, request): + authorization = request.headers.get('Authorization') + if not authorization: + return None + try: + authmeth, auth = authorization.split(' ', 1) + except ValueError: # not enough values to unpack + return None + if authmeth.lower() != 'basic': + return None + try: + auth = b64decode(auth.strip()).decode('ascii') + except (TypeError, binascii.Error): # can't decode + return None + try: + username, password = auth.split(':', 1) + except ValueError: # not enough values to unpack + return None + return username, password diff --git a/pyramid/chameleon_zpt.py b/pyramid/chameleon_zpt.py index 73203a7cb..d8a8ee1be 100644 --- a/pyramid/chameleon_zpt.py +++ b/pyramid/chameleon_zpt.py @@ -18,10 +18,12 @@ class ZPTTemplateRenderer(object): @reify # avoid looking up reload_templates before manager pushed def template(self): - tf = PageTemplateFile(self.path, - auto_reload=self.lookup.auto_reload, - debug=self.lookup.debug, - translate=self.lookup.translate) + tf = PageTemplateFile( + self.path, + auto_reload=self.lookup.auto_reload, + debug=self.lookup.debug, + translate=self.lookup.translate + ) if self.macro: # render only the portion of the template included in a # define-macro named the value of self.macro diff --git a/pyramid/config/predicates.py b/pyramid/config/predicates.py index 77b55d9b3..e31425899 100644 --- a/pyramid/config/predicates.py +++ b/pyramid/config/predicates.py @@ -1,19 +1,19 @@ import re -from pyramid.compat import is_nonstr_iter - from pyramid.exceptions import ConfigurationError +from pyramid.compat import is_nonstr_iter + from pyramid.traversal import ( find_interface, traversal_path, + resource_path_tuple ) from pyramid.urldispatch import _compile_route - from pyramid.util import object_description - from pyramid.session import check_csrf_token +from pyramid.security import effective_principals from .util import as_sorted_tuple @@ -64,43 +64,48 @@ class PathInfoPredicate(object): class RequestParamPredicate(object): def __init__(self, val, config): - name = val - v = None - if '=' in name: - name, v = name.split('=', 1) - name, v = name.strip(), v.strip() - if v is None: - self._text = 'request_param %s' % (name,) - else: - self._text = 'request_param %s = %s' % (name, v) - self.name = name - self.val = v + val = as_sorted_tuple(val) + reqs = [] + for p in val: + k = p + v = None + if '=' in p: + k, v = p.split('=', 1) + k, v = k.strip(), v.strip() + reqs.append((k, v)) + self.val = val + self.reqs = reqs def text(self): - return self._text + return 'request_param %s' % ','.join( + ['%s=%s' % (x,y) if y else x for x, y in self.reqs] + ) phash = text def __call__(self, context, request): - if self.val is None: - return self.name in request.params - return request.params.get(self.name) == self.val - + for k, v in self.reqs: + actual = request.params.get(k) + if actual is None: + return False + if v is not None and actual != v: + return False + return True class HeaderPredicate(object): def __init__(self, val, config): name = val v = None if ':' in name: - name, v = name.split(':', 1) + name, val_str = name.split(':', 1) try: - v = re.compile(v) + v = re.compile(val_str) except re.error as why: raise ConfigurationError(why.args[0]) if v is None: self._text = 'header %s' % (name,) else: - self._text = 'header %s = %s' % (name, v) + self._text = 'header %s=%s' % (name, val_str) self.name = name self.val = v @@ -156,9 +161,7 @@ class RequestTypePredicate(object): class MatchParamPredicate(object): def __init__(self, val, config): - if not is_nonstr_iter(val): - val = (val,) - val = sorted(val) + val = as_sorted_tuple(val) self.val = val reqs = [ p.split('=', 1) for p in val ] self.reqs = [ (x.strip(), y.strip()) for x, y in reqs ] @@ -249,3 +252,38 @@ class CheckCSRFTokenPredicate(object): return self.check_csrf_token(request, val, raises=False) return True +class PhysicalPathPredicate(object): + def __init__(self, val, config): + if is_nonstr_iter(val): + self.val = tuple(val) + else: + val = tuple(filter(None, val.split('/'))) + self.val = ('',) + val + + def text(self): + return 'physical_path = %s' % (self.val,) + + phash = text + + def __call__(self, context, request): + return resource_path_tuple(context) == self.val + +class EffectivePrincipalsPredicate(object): + def __init__(self, val, config): + if is_nonstr_iter(val): + self.val = set(val) + else: + self.val = set((val,)) + + def text(self): + return 'effective_principals = %s' % sorted(list(self.val)) + + phash = text + + def __call__(self, context, request): + req_principals = effective_principals(request) + if is_nonstr_iter(req_principals): + rpset = set(req_principals) + if self.val.issubset(rpset): + return True + return False diff --git a/pyramid/config/routes.py b/pyramid/config/routes.py index 30bebfb98..7c61d5912 100644 --- a/pyramid/config/routes.py +++ b/pyramid/config/routes.py @@ -238,6 +238,19 @@ class RoutesConfiguratorMixin(object): request, this predicate will be true. If this predicate returns ``False``, route matching continues. + effective_principals + + If specified, this value should be a :term:`principal` identifier or + a sequence of principal identifiers. If the + :func:`pyramid.security.effective_principals` method indicates that + every principal named in the argument list is present in the current + request, this predicate will return True; otherwise it will return + False. For example: + ``effective_principals=pyramid.security.Authenticated`` or + ``effective_principals=('fred', 'group:admins')``. + + .. versionadded:: 1.4a4 + custom_predicates This value should be a sequence of references to custom @@ -499,6 +512,7 @@ class RoutesConfiguratorMixin(object): ('request_param', p.RequestParamPredicate), ('header', p.HeaderPredicate), ('accept', p.AcceptPredicate), + ('effective_principals', p.EffectivePrincipalsPredicate), ('custom', p.CustomPredicate), ('traverse', p.TraversePredicate), ): diff --git a/pyramid/config/views.py b/pyramid/config/views.py index 9ace96c1d..b01d17efd 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -904,11 +904,12 @@ class ViewsConfiguratorMixin(object): request_param - This value can be any string. A view declaration with this - argument ensures that the view will only be called when the - :term:`request` has a key in the ``request.params`` + This value can be any string or any sequence of strings. A view + declaration with this argument ensures that the view will only be + called when the :term:`request` has a key in the ``request.params`` dictionary (an HTTP ``GET`` or ``POST`` variable) that has a - name which matches the supplied value. If the value + name which matches the supplied value (if the value is a string) + or values (if the value is a tuple). If any value supplied has a ``=`` sign in it, e.g. ``request_param="foo=123"``, then the key (``foo``) must both exist in the ``request.params`` dictionary, *and* @@ -1013,6 +1014,35 @@ class ViewsConfiguratorMixin(object): .. versionadded:: 1.4a2 + physical_path + + If specified, this value should be a string or a tuple representing + the :term:`physical path` of the context found via traversal for this + predicate to match as true. For example: ``physical_path='/'`` or + ``physical_path='/a/b/c'`` or ``physical_path=('', 'a', 'b', 'c')``. + This is not a path prefix match or a regex, it's a whole-path match. + It's useful when you want to always potentially show a view when some + object is traversed to, but you can't be sure about what kind of + object it will be, so you can't use the ``context`` predicate. The + individual path elements inbetween slash characters or in tuple + elements should be the Unicode representation of the name of the + resource and should not be encoded in any way. + + .. versionadded:: 1.4a3 + + effective_principals + + If specified, this value should be a :term:`principal` identifier or + a sequence of principal identifiers. If the + :func:`pyramid.security.effective_principals` method indicates that + every principal named in the argument list is present in the current + request, this predicate will return True; otherwise it will return + False. For example: + ``effective_principals=pyramid.security.Authenticated`` or + ``effective_principals=('fred', 'group:admins')``. + + .. versionadded:: 1.4a4 + custom_predicates This value should be a sequence of references to custom @@ -1369,6 +1399,8 @@ class ViewsConfiguratorMixin(object): ('request_type', p.RequestTypePredicate), ('match_param', p.MatchParamPredicate), ('check_csrf', p.CheckCSRFTokenPredicate), + ('physical_path', p.PhysicalPathPredicate), + ('effective_principals', p.EffectivePrincipalsPredicate), ('custom', p.CustomPredicate), ): self.add_view_predicate(name, factory) 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/scaffolds/alchemy/+package+/templates/mytemplate.pt_tmpl b/pyramid/scaffolds/alchemy/+package+/templates/mytemplate.pt_tmpl index ac0140789..99606fe0e 100644 --- a/pyramid/scaffolds/alchemy/+package+/templates/mytemplate.pt_tmpl +++ b/pyramid/scaffolds/alchemy/+package+/templates/mytemplate.pt_tmpl @@ -32,7 +32,7 @@ <div class="bottom"> <div id="left" class="align-right"> <h2>Search documentation</h2> - <form method="get" action="http://docs.pylonsproject.org/projects/pyramid/en/1.3-branch/search.html"> + <form method="get" action="http://docs.pylonsproject.org/projects/pyramid/en/1.4-branch/search.html"> <input type="text" id="q" name="q" value="" /> <input type="submit" id="x" value="Go" /> </form> @@ -44,22 +44,22 @@ <a href="http://pylonsproject.org">Pylons Website</a> </li> <li> - <a href="http://docs.pylonsproject.org/projects/pyramid/en/1.3-branch/#narrative-documentation">Narrative Documentation</a> + <a href="http://docs.pylonsproject.org/projects/pyramid/en/1.4-branch/#narrative-documentation">Narrative Documentation</a> </li> <li> - <a href="http://docs.pylonsproject.org/projects/pyramid/en/1.3-branch/#reference-material">API Documentation</a> + <a href="http://docs.pylonsproject.org/projects/pyramid/en/1.4-branch/#reference-material">API Documentation</a> </li> <li> - <a href="http://docs.pylonsproject.org/projects/pyramid/en/1.3-branch/#tutorials">Tutorials</a> + <a href="http://docs.pylonsproject.org/projects/pyramid/en/1.4-branch/#tutorials">Tutorials</a> </li> <li> - <a href="http://docs.pylonsproject.org/projects/pyramid/en/1.3-branch/#detailed-change-history">Change History</a> + <a href="http://docs.pylonsproject.org/projects/pyramid/en/1.4-branch/#detailed-change-history">Change History</a> </li> <li> - <a href="http://docs.pylonsproject.org/projects/pyramid/en/1.3-branch/#sample-applications">Sample Applications</a> + <a href="http://docs.pylonsproject.org/projects/pyramid/en/1.4-branch/#sample-applications">Sample Applications</a> </li> <li> - <a href="http://docs.pylonsproject.org/projects/pyramid/en/1.3-branch/#support-and-development">Support and Development</a> + <a href="http://docs.pylonsproject.org/projects/pyramid/en/1.4-branch/#support-and-development">Support and Development</a> </li> <li> <a href="irc://irc.freenode.net#pyramid">IRC Channel</a> @@ -70,7 +70,7 @@ </div> </div> <div id="footer"> - <div class="footer">© Copyright 2008-2011, Agendaless Consulting.</div> + <div class="footer">© Copyright 2008-2012, Agendaless Consulting.</div> </div> </body> </html> diff --git a/pyramid/scaffolds/alchemy/development.ini_tmpl b/pyramid/scaffolds/alchemy/development.ini_tmpl index eebfbcc3e..bdf08171c 100644 --- a/pyramid/scaffolds/alchemy/development.ini_tmpl +++ b/pyramid/scaffolds/alchemy/development.ini_tmpl @@ -1,3 +1,8 @@ +### +# app configuration +# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/environment.html +### + [app:main] use = egg:{{project}} @@ -12,12 +17,23 @@ pyramid.includes = sqlalchemy.url = sqlite:///%(here)s/{{project}}.sqlite +# By default, the toolbar only appears for clients from IP addresses +# '127.0.0.1' and '::1'. +# debugtoolbar.hosts = 127.0.0.1 ::1 + +### +# wsgi server configuration +### + [server:main] use = egg:waitress#main host = 0.0.0.0 port = 6543 -# Begin logging configuration +### +# logging configuration +# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/logging.html +### [loggers] keys = root, {{package_logger}}, sqlalchemy @@ -53,5 +69,3 @@ formatter = generic [formatter_generic] format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s - -# End logging configuration diff --git a/pyramid/scaffolds/alchemy/production.ini_tmpl b/pyramid/scaffolds/alchemy/production.ini_tmpl index 9488f1811..69b08e458 100644 --- a/pyramid/scaffolds/alchemy/production.ini_tmpl +++ b/pyramid/scaffolds/alchemy/production.ini_tmpl @@ -1,3 +1,8 @@ +### +# app configuration +# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/environment.html +### + [app:main] use = egg:{{project}} @@ -16,7 +21,10 @@ use = egg:waitress#main host = 0.0.0.0 port = 6543 -# Begin logging configuration +### +# logging configuration +# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/logging.html +### [loggers] keys = root, {{package_logger}}, sqlalchemy @@ -52,5 +60,3 @@ formatter = generic [formatter_generic] format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s - -# End logging configuration diff --git a/pyramid/scaffolds/starter/+package+/templates/mytemplate.pt_tmpl b/pyramid/scaffolds/starter/+package+/templates/mytemplate.pt_tmpl index 743eab026..4a71dd992 100644 --- a/pyramid/scaffolds/starter/+package+/templates/mytemplate.pt_tmpl +++ b/pyramid/scaffolds/starter/+package+/templates/mytemplate.pt_tmpl @@ -32,7 +32,7 @@ <div class="bottom"> <div id="left" class="align-right"> <h2>Search documentation</h2> - <form method="get" action="http://docs.pylonsproject.org/projects/pyramid/en/1.3-branch/search.html"> + <form method="get" action="http://docs.pylonsproject.org/projects/pyramid/en/1.4-branch/search.html"> <input type="text" id="q" name="q" value="" /> <input type="submit" id="x" value="Go" /> </form> @@ -44,22 +44,22 @@ <a href="http://pylonsproject.org">Pylons Website</a> </li> <li> - <a href="http://docs.pylonsproject.org/projects/pyramid/en/1.3-branch/#narrative-documentation">Narrative Documentation</a> + <a href="http://docs.pylonsproject.org/projects/pyramid/en/1.4-branch/#narrative-documentation">Narrative Documentation</a> </li> <li> - <a href="http://docs.pylonsproject.org/projects/pyramid/en/1.3-branch/#reference-material">API Documentation</a> + <a href="http://docs.pylonsproject.org/projects/pyramid/en/1.4-branch/#reference-material">API Documentation</a> </li> <li> - <a href="http://docs.pylonsproject.org/projects/pyramid/en/1.3-branch/#tutorials">Tutorials</a> + <a href="http://docs.pylonsproject.org/projects/pyramid/en/1.4-branch/#tutorials">Tutorials</a> </li> <li> - <a href="http://docs.pylonsproject.org/projects/pyramid/en/1.3-branch/#detailed-change-history">Change History</a> + <a href="http://docs.pylonsproject.org/projects/pyramid/en/1.4-branch/#detailed-change-history">Change History</a> </li> <li> - <a href="http://docs.pylonsproject.org/projects/pyramid/en/1.3-branch/#sample-applications">Sample Applications</a> + <a href="http://docs.pylonsproject.org/projects/pyramid/en/1.4-branch/#sample-applications">Sample Applications</a> </li> <li> - <a href="http://docs.pylonsproject.org/projects/pyramid/en/1.3-branch/#support-and-development">Support and Development</a> + <a href="http://docs.pylonsproject.org/projects/pyramid/en/1.4-branch/#support-and-development">Support and Development</a> </li> <li> <a href="irc://irc.freenode.net#pyramid">IRC Channel</a> @@ -70,7 +70,7 @@ </div> </div> <div id="footer"> - <div class="footer">© Copyright 2008-2011, Agendaless Consulting.</div> + <div class="footer">© Copyright 2008-2012, Agendaless Consulting.</div> </div> </body> </html> diff --git a/pyramid/scaffolds/starter/development.ini_tmpl b/pyramid/scaffolds/starter/development.ini_tmpl index c92e13906..33c454086 100644 --- a/pyramid/scaffolds/starter/development.ini_tmpl +++ b/pyramid/scaffolds/starter/development.ini_tmpl @@ -1,3 +1,8 @@ +### +# app configuration +# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/environment.html +### + [app:main] use = egg:{{project}} @@ -9,12 +14,23 @@ pyramid.default_locale_name = en pyramid.includes = pyramid_debugtoolbar +# By default, the toolbar only appears for clients from IP addresses +# '127.0.0.1' and '::1'. +# debugtoolbar.hosts = 127.0.0.1 ::1 + +### +# wsgi server configuration +### + [server:main] use = egg:waitress#main host = 0.0.0.0 port = 6543 -# Begin logging configuration +### +# logging configuration +# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/logging.html +### [loggers] keys = root, {{package_logger}} @@ -42,5 +58,3 @@ formatter = generic [formatter_generic] format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s - -# End logging configuration diff --git a/pyramid/scaffolds/starter/production.ini_tmpl b/pyramid/scaffolds/starter/production.ini_tmpl index 28957b5c1..dd2637e5b 100644 --- a/pyramid/scaffolds/starter/production.ini_tmpl +++ b/pyramid/scaffolds/starter/production.ini_tmpl @@ -1,3 +1,8 @@ +### +# app configuration +# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/environment.html +### + [app:main] use = egg:{{project}} @@ -7,12 +12,19 @@ pyramid.debug_notfound = false pyramid.debug_routematch = false pyramid.default_locale_name = en +### +# wsgi server configuration +### + [server:main] use = egg:waitress#main host = 0.0.0.0 port = 6543 -# Begin logging configuration +### +# logging configuration +# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/logging.html +### [loggers] keys = root, {{package_logger}} @@ -40,5 +52,3 @@ formatter = generic [formatter_generic] format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s - -# End logging configuration diff --git a/pyramid/scaffolds/zodb/+package+/templates/mytemplate.pt b/pyramid/scaffolds/zodb/+package+/templates/mytemplate.pt index d64f18fca..5391509fe 100644 --- a/pyramid/scaffolds/zodb/+package+/templates/mytemplate.pt +++ b/pyramid/scaffolds/zodb/+package+/templates/mytemplate.pt @@ -32,7 +32,7 @@ <div class="bottom"> <div id="left" class="align-right"> <h2>Search documentation</h2> - <form method="get" action="http://docs.pylonsproject.org/projects/pyramid/en/1.3-branch/search.html"> + <form method="get" action="http://docs.pylonsproject.org/projects/pyramid/en/1.4-branch/search.html"> <input type="text" id="q" name="q" value="" /> <input type="submit" id="x" value="Go" /> </form> @@ -44,22 +44,22 @@ <a href="http://pylonsproject.org">Pylons Website</a> </li> <li> - <a href="http://docs.pylonsproject.org/projects/pyramid/en/1.3-branch/#narrative-documentation">Narrative Documentation</a> + <a href="http://docs.pylonsproject.org/projects/pyramid/en/1.4-branch/#narrative-documentation">Narrative Documentation</a> </li> <li> - <a href="http://docs.pylonsproject.org/projects/pyramid/en/1.3-branch/#reference-material">API Documentation</a> + <a href="http://docs.pylonsproject.org/projects/pyramid/en/1.4-branch/#reference-material">API Documentation</a> </li> <li> - <a href="http://docs.pylonsproject.org/projects/pyramid/en/1.3-branch/#tutorials">Tutorials</a> + <a href="http://docs.pylonsproject.org/projects/pyramid/en/1.4-branch/#tutorials">Tutorials</a> </li> <li> - <a href="http://docs.pylonsproject.org/projects/pyramid/en/1.3-branch/#detailed-change-history">Change History</a> + <a href="http://docs.pylonsproject.org/projects/pyramid/en/1.4-branch/#detailed-change-history">Change History</a> </li> <li> - <a href="http://docs.pylonsproject.org/projects/pyramid/en/1.3-branch/#sample-applications">Sample Applications</a> + <a href="http://docs.pylonsproject.org/projects/pyramid/en/1.4-branch/#sample-applications">Sample Applications</a> </li> <li> - <a href="http://docs.pylonsproject.org/projects/pyramid/en/1.3-branch/#support-and-development">Support and Development</a> + <a href="http://docs.pylonsproject.org/projects/pyramid/en/1.4-branch/#support-and-development">Support and Development</a> </li> <li> <a href="irc://irc.freenode.net#pyramid">IRC Channel</a> @@ -70,7 +70,7 @@ </div> </div> <div id="footer"> - <div class="footer">© Copyright 2008-2011, Agendaless Consulting.</div> + <div class="footer">© Copyright 2008-2012, Agendaless Consulting.</div> </div> </body> </html> diff --git a/pyramid/scaffolds/zodb/development.ini_tmpl b/pyramid/scaffolds/zodb/development.ini_tmpl index 1260b4db3..746f7ded3 100644 --- a/pyramid/scaffolds/zodb/development.ini_tmpl +++ b/pyramid/scaffolds/zodb/development.ini_tmpl @@ -1,3 +1,8 @@ +### +# app configuration +# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/environment.html +### + [app:main] use = egg:{{project}} @@ -14,12 +19,23 @@ pyramid.includes = tm.attempts = 3 zodbconn.uri = file://%(here)s/Data.fs?connection_cache_size=20000 +# By default, the toolbar only appears for clients from IP addresses +# '127.0.0.1' and '::1'. +# debugtoolbar.hosts = 127.0.0.1 ::1 + +### +# wsgi server configuration +### + [server:main] use = egg:waitress#main host = 0.0.0.0 port = 6543 -# Begin logging configuration +### +# logging configuration +# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/logging.html +### [loggers] keys = root, {{package_logger}} @@ -47,5 +63,3 @@ formatter = generic [formatter_generic] format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s - -# End logging configuration diff --git a/pyramid/scaffolds/zodb/production.ini_tmpl b/pyramid/scaffolds/zodb/production.ini_tmpl index 1640009fa..9ce639ec3 100644 --- a/pyramid/scaffolds/zodb/production.ini_tmpl +++ b/pyramid/scaffolds/zodb/production.ini_tmpl @@ -1,3 +1,8 @@ +### +# app configuration +# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/environment.html +### + [app:main] use = egg:{{project}} @@ -13,12 +18,19 @@ pyramid.includes = tm.attempts = 3 zodbconn.uri = file://%(here)s/Data.fs?connection_cache_size=20000 +### +# wsgi server configuration +### + [server:main] use = egg:waitress#main host = 0.0.0.0 port = 6543 -# Begin logging configuration +### +# logging configuration +# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/logging.html +### [loggers] keys = root, {{package_logger}} @@ -46,5 +58,3 @@ formatter = generic [formatter_generic] format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s - -# End logging configuration 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/test_authentication.py b/pyramid/tests/test_authentication.py index e513b9a48..2b7a770c1 100644 --- a/pyramid/tests/test_authentication.py +++ b/pyramid/tests/test_authentication.py @@ -14,7 +14,7 @@ class TestCallbackAuthenticationPolicyDebugging(unittest.TestCase): def tearDown(self): del self.config - + def debug(self, msg): self.messages.append(msg) @@ -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 @@ -151,7 +203,7 @@ class TestRepozeWho1AuthenticationPolicy(unittest.TestCase): def _makeOne(self, identifier_name='auth_tkt', callback=None): return self._getTargetClass()(identifier_name, callback) - + def test_class_implements_IAuthenticationPolicy(self): from zope.interface.verify import verifyClass from pyramid.interfaces import IAuthenticationPolicy @@ -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() @@ -251,7 +348,7 @@ class TestRepozeWho1AuthenticationPolicy(unittest.TestCase): result = policy.remember(request, 'fred') self.assertEqual(result[0], request.environ) self.assertEqual(result[1], {'repoze.who.userid':'fred'}) - + def test_forget_no_plugins(self): request = DummyRequest({}) policy = self._makeOne() @@ -276,7 +373,7 @@ class TestRemoteUserAuthenticationPolicy(unittest.TestCase): def _makeOne(self, environ_key='REMOTE_USER', callback=None): return self._getTargetClass()(environ_key, callback) - + def test_class_implements_IAuthenticationPolicy(self): from zope.interface.verify import verifyClass from pyramid.interfaces import IAuthenticationPolicy @@ -301,7 +398,7 @@ class TestRemoteUserAuthenticationPolicy(unittest.TestCase): request = DummyRequest({}) policy = self._makeOne() self.assertEqual(policy.authenticated_userid(request), None) - + def test_authenticated_userid(self): request = DummyRequest({'REMOTE_USER':'fred'}) policy = self._makeOne() @@ -326,7 +423,7 @@ class TestRemoteUserAuthenticationPolicy(unittest.TestCase): policy = self._makeOne() result = policy.remember(request, 'fred') self.assertEqual(result, []) - + def test_forget(self): request = DummyRequest({'REMOTE_USER':'fred'}) policy = self._makeOne() @@ -375,7 +472,7 @@ class TestAutkTktAuthenticationPolicy(unittest.TestCase): request = DummyRequest({}) policy = self._makeOne(None, None) self.assertEqual(policy.authenticated_userid(request), None) - + def test_authenticated_userid_callback_returns_None(self): request = DummyRequest({}) def callback(userid, request): @@ -426,7 +523,7 @@ class TestAutkTktAuthenticationPolicy(unittest.TestCase): result = policy.remember(request, 'fred', a=1, b=2) self.assertEqual(policy.cookie.kw, {'a':1, 'b':2}) self.assertEqual(result, []) - + def test_forget(self): request = DummyRequest({}) policy = self._makeOne(None, None) @@ -482,7 +579,7 @@ class TestAuthTktCookieHelper(unittest.TestCase): request = self._makeRequest(None) result = helper.identify(request) self.assertEqual(result, None) - + def test_identify_good_cookie_include_ip(self): helper = self._makeOne('secret', include_ip=True) request = self._makeRequest('ticket') @@ -605,7 +702,7 @@ class TestAuthTktCookieHelper(unittest.TestCase): request = self._makeRequest('ticket') result = helper.identify(request) self.assertEqual(result, None) - + def test_identify_cookie_timed_out(self): helper = self._makeOne('secret', timeout=1) request = self._makeRequest({'HTTP_COOKIE':'auth_tkt=bogus'}) @@ -828,7 +925,7 @@ class TestAuthTktCookieHelper(unittest.TestCase): self.assertEqual(result[1][0], 'Set-Cookie') self.assertTrue(result[1][1].endswith('; Path=/; Domain=example.com')) self.assertTrue(result[1][1].startswith('auth_tkt=')) - + def test_remember_binary_userid(self): import base64 helper = self._makeOne('secret') @@ -1106,6 +1203,78 @@ class TestSessionAuthenticationPolicy(unittest.TestCase): self.assertEqual(request.session.get('userid'), None) self.assertEqual(result, []) +class TestBasicAuthAuthenticationPolicy(unittest.TestCase): + def _getTargetClass(self): + from pyramid.authentication import BasicAuthAuthenticationPolicy as cls + return cls + + def _makeOne(self, check): + return self._getTargetClass()(check, realm='SomeRealm') + + def test_class_implements_IAuthenticationPolicy(self): + from zope.interface.verify import verifyClass + from pyramid.interfaces import IAuthenticationPolicy + verifyClass(IAuthenticationPolicy, self._getTargetClass()) + + def test_unauthenticated_userid(self): + import base64 + request = testing.DummyRequest() + request.headers['Authorization'] = 'Basic %s' % base64.b64encode( + bytes_('chrisr:password')).decode('ascii') + policy = self._makeOne(None) + self.assertEqual(policy.unauthenticated_userid(request), 'chrisr') + + def test_unauthenticated_userid_no_credentials(self): + request = testing.DummyRequest() + policy = self._makeOne(None) + self.assertEqual(policy.unauthenticated_userid(request), None) + + def test_unauthenticated_bad_header(self): + request = testing.DummyRequest() + request.headers['Authorization'] = '...' + policy = self._makeOne(None) + self.assertEqual(policy.unauthenticated_userid(request), None) + + def test_unauthenticated_userid_not_basic(self): + request = testing.DummyRequest() + request.headers['Authorization'] = 'Complicated things' + policy = self._makeOne(None) + self.assertEqual(policy.unauthenticated_userid(request), None) + + def test_unauthenticated_userid_corrupt_base64(self): + request = testing.DummyRequest() + request.headers['Authorization'] = 'Basic chrisr:password' + policy = self._makeOne(None) + self.assertEqual(policy.unauthenticated_userid(request), None) + + def test_authenticated_userid(self): + import base64 + request = testing.DummyRequest() + request.headers['Authorization'] = 'Basic %s' % base64.b64encode( + bytes_('chrisr:password')).decode('ascii') + def check(username, password, request): + return [] + policy = self._makeOne(check) + self.assertEqual(policy.authenticated_userid(request), 'chrisr') + + def test_unauthenticated_userid_invalid_payload(self): + import base64 + request = testing.DummyRequest() + request.headers['Authorization'] = 'Basic %s' % base64.b64encode( + bytes_('chrisrpassword')).decode('ascii') + policy = self._makeOne(None) + self.assertEqual(policy.unauthenticated_userid(request), None) + + def test_remember(self): + policy = self._makeOne(None) + self.assertEqual(policy.remember(None, None), []) + + def test_forget(self): + policy = self._makeOne(None) + self.assertEqual(policy.forget(None), [ + ('WWW-Authenticate', 'Basic realm="SomeRealm"')]) + + class DummyContext: pass @@ -1130,7 +1299,7 @@ class DummyRequest: class DummyWhoPlugin: def remember(self, environ, identity): return environ, identity - + def forget(self, environ, identity): return environ, identity @@ -1164,7 +1333,7 @@ class DummyAuthTktModule(object): raise self.BadTicket() return self.timestamp, self.userid, self.tokens, self.user_data self.parse_ticket = parse_ticket - + class AuthTicket(object): def __init__(self, secret, userid, remote_addr, **kw): self.secret = secret @@ -1186,4 +1355,4 @@ class DummyAuthTktModule(object): class DummyResponse: def __init__(self): self.headerlist = [] - + diff --git a/pyramid/tests/test_config/test_predicates.py b/pyramid/tests/test_config/test_predicates.py index 005b1b27a..91dfb0fb6 100644 --- a/pyramid/tests/test_config/test_predicates.py +++ b/pyramid/tests/test_config/test_predicates.py @@ -1,5 +1,7 @@ import unittest +from pyramid import testing + from pyramid.compat import text_ class TestXHRPredicate(unittest.TestCase): @@ -117,6 +119,20 @@ class TestRequestParamPredicate(unittest.TestCase): result = inst(None, request) self.assertTrue(result) + def test___call___true_multi(self): + inst = self._makeOne(('abc', 'def =2 ')) + request = Dummy() + request.params = {'abc':'1', 'def': '2'} + result = inst(None, request) + self.assertTrue(result) + + def test___call___false_multi(self): + inst = self._makeOne(('abc=3', 'def =2 ')) + request = Dummy() + request.params = {'abc':'3', 'def': '1'} + result = inst(None, request) + self.assertFalse(result) + def test___call___false(self): inst = self._makeOne('abc') request = Dummy() @@ -130,7 +146,11 @@ class TestRequestParamPredicate(unittest.TestCase): def test_text_withval(self): inst = self._makeOne('abc= 1') - self.assertEqual(inst.text(), 'request_param abc = 1') + self.assertEqual(inst.text(), 'request_param abc=1') + + def test_text_multi(self): + inst = self._makeOne(('abc= 1', 'def')) + self.assertEqual(inst.text(), 'request_param abc=1,def') def test_phash_exists(self): inst = self._makeOne('abc') @@ -138,7 +158,7 @@ class TestRequestParamPredicate(unittest.TestCase): def test_phash_withval(self): inst = self._makeOne('abc= 1') - self.assertEqual(inst.phash(), "request_param abc = 1") + self.assertEqual(inst.phash(), "request_param abc=1") class TestMatchParamPredicate(unittest.TestCase): def _makeOne(self, val): @@ -299,6 +319,177 @@ class Test_CheckCSRFTokenPredicate(unittest.TestCase): result = inst(None, request) self.assertEqual(result, True) +class TestHeaderPredicate(unittest.TestCase): + def _makeOne(self, val): + from pyramid.config.predicates import HeaderPredicate + return HeaderPredicate(val, None) + + def test___call___true_exists(self): + inst = self._makeOne('abc') + request = Dummy() + request.headers = {'abc':1} + result = inst(None, request) + self.assertTrue(result) + + def test___call___true_withval(self): + inst = self._makeOne('abc:1') + request = Dummy() + request.headers = {'abc':'1'} + result = inst(None, request) + self.assertTrue(result) + + def test___call___true_withregex(self): + inst = self._makeOne(r'abc:\d+') + request = Dummy() + request.headers = {'abc':'1'} + result = inst(None, request) + self.assertTrue(result) + + def test___call___false_withregex(self): + inst = self._makeOne(r'abc:\d+') + request = Dummy() + request.headers = {'abc':'a'} + result = inst(None, request) + self.assertFalse(result) + + def test___call___false(self): + inst = self._makeOne('abc') + request = Dummy() + request.headers = {} + result = inst(None, request) + self.assertFalse(result) + + def test_text_exists(self): + inst = self._makeOne('abc') + self.assertEqual(inst.text(), 'header abc') + + def test_text_withval(self): + inst = self._makeOne('abc:1') + self.assertEqual(inst.text(), 'header abc=1') + + def test_text_withregex(self): + inst = self._makeOne(r'abc:\d+') + self.assertEqual(inst.text(), r'header abc=\d+') + + def test_phash_exists(self): + inst = self._makeOne('abc') + self.assertEqual(inst.phash(), 'header abc') + + def test_phash_withval(self): + inst = self._makeOne('abc:1') + self.assertEqual(inst.phash(), "header abc=1") + + def test_phash_withregex(self): + inst = self._makeOne(r'abc:\d+') + self.assertEqual(inst.phash(), r'header abc=\d+') + +class Test_PhysicalPathPredicate(unittest.TestCase): + def _makeOne(self, val, config): + from pyramid.config.predicates import PhysicalPathPredicate + return PhysicalPathPredicate(val, config) + + def test_text(self): + inst = self._makeOne('/', None) + self.assertEqual(inst.text(), "physical_path = ('',)") + + def test_phash(self): + inst = self._makeOne('/', None) + self.assertEqual(inst.phash(), "physical_path = ('',)") + + def test_it_call_val_tuple_True(self): + inst = self._makeOne(('', 'abc'), None) + root = Dummy() + root.__name__ = '' + root.__parent__ = None + context = Dummy() + context.__name__ = 'abc' + context.__parent__ = root + self.assertTrue(inst(context, None)) + + def test_it_call_val_list_True(self): + inst = self._makeOne(['', 'abc'], None) + root = Dummy() + root.__name__ = '' + root.__parent__ = None + context = Dummy() + context.__name__ = 'abc' + context.__parent__ = root + self.assertTrue(inst(context, None)) + + def test_it_call_val_str_True(self): + inst = self._makeOne('/abc', None) + root = Dummy() + root.__name__ = '' + root.__parent__ = None + context = Dummy() + context.__name__ = 'abc' + context.__parent__ = root + self.assertTrue(inst(context, None)) + + def test_it_call_False(self): + inst = self._makeOne('/', None) + root = Dummy() + root.__name__ = '' + root.__parent__ = None + context = Dummy() + context.__name__ = 'abc' + context.__parent__ = root + self.assertFalse(inst(context, None)) + +class Test_EffectivePrincipalsPredicate(unittest.TestCase): + def setUp(self): + self.config = testing.setUp() + + def tearDown(self): + testing.tearDown() + + def _makeOne(self, val, config): + from pyramid.config.predicates import EffectivePrincipalsPredicate + return EffectivePrincipalsPredicate(val, config) + + def test_text(self): + inst = self._makeOne(('verna', 'fred'), None) + self.assertEqual(inst.text(), + "effective_principals = ['fred', 'verna']") + + def test_text_noniter(self): + inst = self._makeOne('verna', None) + self.assertEqual(inst.text(), + "effective_principals = ['verna']") + + def test_phash(self): + inst = self._makeOne(('verna', 'fred'), None) + self.assertEqual(inst.phash(), + "effective_principals = ['fred', 'verna']") + + def test_it_call_no_authentication_policy(self): + request = testing.DummyRequest() + inst = self._makeOne(('verna', 'fred'), None) + context = Dummy() + self.assertFalse(inst(context, request)) + + def test_it_call_authentication_policy_provides_superset(self): + request = testing.DummyRequest() + self.config.testing_securitypolicy('fred', groupids=('verna', 'bambi')) + inst = self._makeOne(('verna', 'fred'), None) + context = Dummy() + self.assertTrue(inst(context, request)) + + def test_it_call_authentication_policy_provides_superset_implicit(self): + from pyramid.security import Authenticated + request = testing.DummyRequest() + self.config.testing_securitypolicy('fred', groupids=('verna', 'bambi')) + inst = self._makeOne(Authenticated, None) + context = Dummy() + self.assertTrue(inst(context, request)) + + def test_it_call_authentication_policy_doesnt_provide_superset(self): + request = testing.DummyRequest() + self.config.testing_securitypolicy('fred') + inst = self._makeOne(('verna', 'fred'), None) + context = Dummy() + self.assertFalse(inst(context, request)) + class predicate(object): def __repr__(self): return 'predicate' 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_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/view.py b/pyramid/view.py index 76f466b83..51ded423c 100644 --- a/pyramid/view.py +++ b/pyramid/view.py @@ -170,7 +170,7 @@ class view_config(object): ``request_type``, ``route_name``, ``request_method``, ``request_param``, ``containment``, ``xhr``, ``accept``, ``header``, ``path_info``, ``custom_predicates``, ``decorator``, ``mapper``, ``http_cache``, - ``match_param``, ``csrf_token``, and ``predicates``. + ``match_param``, ``csrf_token``, ``physical_path``, and ``predicates``. The meanings of these arguments are the same as the arguments passed to :meth:`pyramid.config.Configurator.add_view`. If any argument is left @@ -68,7 +68,7 @@ testing_extras = tests_require + [ ] setup(name='pyramid', - version='1.4a2', + version='1.4a3', description=('The Pyramid web application development framework, a ' 'Pylons project'), long_description=README + '\n\n' + CHANGES, @@ -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", |
