summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore2
-rw-r--r--CHANGES.txt60
-rw-r--r--RELEASING.txt6
-rw-r--r--TODO.txt7
-rw-r--r--docs/api/authentication.rst16
-rw-r--r--docs/conf.py2
-rw-r--r--docs/glossary.rst22
-rw-r--r--docs/narr/renderers.rst2
-rw-r--r--docs/narr/sessions.rst13
-rw-r--r--docs/narr/viewconfig.rst37
-rwxr-xr-xdocs/remake2
-rw-r--r--docs/whatsnew-1.4.rst41
-rw-r--r--pyramid/authentication.py280
-rw-r--r--pyramid/chameleon_zpt.py10
-rw-r--r--pyramid/config/predicates.py90
-rw-r--r--pyramid/config/routes.py14
-rw-r--r--pyramid/config/views.py40
-rw-r--r--pyramid/renderers.py12
-rw-r--r--pyramid/scaffolds/alchemy/+package+/templates/mytemplate.pt_tmpl16
-rw-r--r--pyramid/scaffolds/alchemy/development.ini_tmpl20
-rw-r--r--pyramid/scaffolds/alchemy/production.ini_tmpl12
-rw-r--r--pyramid/scaffolds/starter/+package+/templates/mytemplate.pt_tmpl16
-rw-r--r--pyramid/scaffolds/starter/development.ini_tmpl20
-rw-r--r--pyramid/scaffolds/starter/production.ini_tmpl16
-rw-r--r--pyramid/scaffolds/zodb/+package+/templates/mytemplate.pt16
-rw-r--r--pyramid/scaffolds/zodb/development.ini_tmpl20
-rw-r--r--pyramid/scaffolds/zodb/production.ini_tmpl16
-rw-r--r--pyramid/security.py14
-rw-r--r--pyramid/tests/test_authentication.py197
-rw-r--r--pyramid/tests/test_config/test_predicates.py195
-rw-r--r--pyramid/tests/test_renderers.py12
-rw-r--r--pyramid/tests/test_security.py18
-rw-r--r--pyramid/view.py2
-rw-r--r--setup.py3
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
diff --git a/TODO.txt b/TODO.txt
index 6787b8a55..ac5738244 100644
--- a/TODO.txt
+++ b/TODO.txt
@@ -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">&copy; Copyright 2008-2011, Agendaless Consulting.</div>
+ <div class="footer">&copy; 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">&copy; Copyright 2008-2011, Agendaless Consulting.</div>
+ <div class="footer">&copy; 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">&copy; Copyright 2008-2011, Agendaless Consulting.</div>
+ <div class="footer">&copy; 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
diff --git a/setup.py b/setup.py
index 9af2f2100..4ea63a3ee 100644
--- a/setup.py
+++ b/setup.py
@@ -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",