summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGES.txt6
-rw-r--r--docs/api/security.rst22
-rw-r--r--docs/whatsnew-1.9.rst6
-rw-r--r--pyramid/interfaces.py8
-rw-r--r--pyramid/router.py29
-rw-r--r--pyramid/security.py109
-rw-r--r--pyramid/tests/test_security.py4
-rw-r--r--pyramid/tests/test_view.py27
-rw-r--r--pyramid/view.py26
9 files changed, 166 insertions, 71 deletions
diff --git a/CHANGES.txt b/CHANGES.txt
index 1402045d4..fdd9dd884 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -21,6 +21,12 @@ unreleased
a valid iterator in its ``__iter__`` implementation.
See https://github.com/Pylons/pyramid/pull/3074
+- Normalize the permission results to a proper class hierarchy.
+ ``pyramid.security.ACLAllowed`` is now a subclass of
+ ``pyramid.security.Allowed`` and ``pyramid.security.ACLDenied`` is now a
+ subclass of ``pyramid.security.Denied``.
+ See https://github.com/Pylons/pyramid/pull/3084
+
1.9a2 (2017-05-09)
==================
diff --git a/docs/api/security.rst b/docs/api/security.rst
index 88086dbbf..116459226 100644
--- a/docs/api/security.rst
+++ b/docs/api/security.rst
@@ -80,15 +80,23 @@ Return Values
'george', 'read')`` that means deny access. A sequence of ACEs
makes up an ACL. It is a string, and its actual value is "Deny".
+.. autoclass:: Denied
+ :members: msg
+
+ .. automethod:: __new__
+
+.. autoclass:: Allowed
+ :members: msg
+
+ .. automethod:: __new__
+
.. autoclass:: ACLDenied
- :members:
+ :members: msg
-.. autoclass:: ACLAllowed
- :members:
+ .. automethod:: __new__
-.. autoclass:: Denied
- :members:
+.. autoclass:: ACLAllowed
+ :members: msg
-.. autoclass:: Allowed
- :members:
+ .. automethod:: __new__
diff --git a/docs/whatsnew-1.9.rst b/docs/whatsnew-1.9.rst
index 0ba29625c..0c3385a66 100644
--- a/docs/whatsnew-1.9.rst
+++ b/docs/whatsnew-1.9.rst
@@ -35,6 +35,12 @@ Minor Feature Additions
- The threadlocals are now available inside any function invoked via :meth:`pyramid.config.Configurator.include`. This means the only config-time code that cannot rely on threadlocals is code executed from non-actions inside the main. This can be alleviated by invoking :meth:`pyramid.config.Configurator.begin` and :meth:`pyramid.config.Configurator.end` appropriately or using the new context manager feature of the configurator. See https://github.com/Pylons/pyramid/pull/2989
+- The threadlocals are now available inside exception views invoked via :meth:`pyramid.request.Request.invoke_exception_view` even when the ``request`` argument is overridden. See https://github.com/Pylons/pyramid/pull/3060
+
+- When unsupported predicates are supplied to :meth:`pyramid.config.Configurator.add_view`, :meth:`pyramid.config.Configurator.add_route` and :meth:`pyramid.config.Configurator.add_subscriber` a much more helpful error message is output with a guess as to which predicate was intended. See https://github.com/Pylons/pyramid/pull/3054
+
+- Normalize the permission results to a proper class hierarchy. :class:`pyramid.security.ACLAllowed` is now a subclass of :class:`pyramid.security.Allowed` and :class:`pyramid.security.ACLDenied` is now a subclass of :class:`pyramid.security.Denied`. See https://github.com/Pylons/pyramid/pull/3084
+
Deprecations
------------
diff --git a/pyramid/interfaces.py b/pyramid/interfaces.py
index ab83813c8..c6fbe3af8 100644
--- a/pyramid/interfaces.py
+++ b/pyramid/interfaces.py
@@ -503,8 +503,10 @@ class IAuthenticationPolicy(Interface):
class IAuthorizationPolicy(Interface):
""" An object representing a Pyramid authorization policy. """
def permits(context, principals, permission):
- """ Return ``True`` if any of the ``principals`` is allowed the
- ``permission`` in the current ``context``, else return ``False``
+ """ Return an instance of :class:`pyramid.security.Allowed` if any
+ of the ``principals`` is allowed the ``permission`` in the current
+ ``context``, else return an instance of
+ :class:`pyramid.security.Denied`.
"""
def principals_allowed_by_permission(context, permission):
@@ -713,7 +715,7 @@ class IExecutionPolicy(Interface):
The return value should be a :class:`pyramid.interfaces.IResponse`
object or an exception that will be handled by WSGI middleware.
- The default execution policy simple creates a request and sends it
+ The default execution policy simply creates a request and sends it
through the pipeline:
.. code-block:: python
diff --git a/pyramid/router.py b/pyramid/router.py
index 7f3f9fbea..a02ff1715 100644
--- a/pyramid/router.py
+++ b/pyramid/router.py
@@ -192,13 +192,21 @@ class Router(object):
"""
request.registry = self.registry
request.invoke_subrequest = self.invoke_subrequest
- return self.invoke_request(
- request,
- _use_tweens=use_tweens,
- _apply_extensions=True,
- )
+ extensions = self.request_extensions
+ if extensions is not None:
+ apply_request_extensions(request, extensions=extensions)
+ return self.invoke_request(request, _use_tweens=use_tweens)
def make_request(self, environ):
+ """
+ Configure a request object for use by the router.
+
+ The request is created using the configured
+ :class:`pyramid.interfaces.IRequestFactory` and will have any
+ configured request methods / properties added that were set by
+ :meth:`pyramid.config.Configurator.add_request_method`.
+
+ """
request = self.request_factory(environ)
request.registry = self.registry
request.invoke_subrequest = self.invoke_subrequest
@@ -207,8 +215,12 @@ class Router(object):
apply_request_extensions(request, extensions=extensions)
return request
- def invoke_request(self, request,
- _use_tweens=True, _apply_extensions=False):
+ def invoke_request(self, request, _use_tweens=True):
+ """
+ Execute a request through the request processing pipeline and
+ return the generated response.
+
+ """
registry = self.registry
has_listeners = self.registry.has_listeners
notify = self.registry.notify
@@ -224,9 +236,6 @@ class Router(object):
try:
try:
- extensions = self.request_extensions
- if _apply_extensions and extensions is not None:
- apply_request_extensions(request, extensions=extensions)
response = handle_request(request)
if request.response_callbacks:
diff --git a/pyramid/security.py b/pyramid/security.py
index 035f09f77..d12314684 100644
--- a/pyramid/security.py
+++ b/pyramid/security.py
@@ -245,6 +245,14 @@ def view_execution_permitted(context, request, name=''):
class PermitsResult(int):
def __new__(cls, s, *args):
+ """
+ Create a new instance.
+
+ :param fmt: A format string explaining the reason for denial.
+ :param args: Arguments are stored and used with the format string
+ to generate the ``msg``.
+
+ """
inst = int.__new__(cls, cls.boolval)
inst.s = s
inst.args = args
@@ -252,6 +260,7 @@ class PermitsResult(int):
@property
def msg(self):
+ """ A string indicating why the result was generated."""
return self.s % self.args
def __str__(self):
@@ -263,24 +272,52 @@ class PermitsResult(int):
self.msg)
class Denied(PermitsResult):
- """ An instance of ``Denied`` is returned when a security-related
+ """
+ An instance of ``Denied`` is returned when a security-related
API or other :app:`Pyramid` code denies an action unrelated to
an ACL check. It evaluates equal to all boolean false types. It
has an attribute named ``msg`` describing the circumstances for
- the deny."""
+ the deny.
+
+ """
boolval = 0
class Allowed(PermitsResult):
- """ An instance of ``Allowed`` is returned when a security-related
+ """
+ An instance of ``Allowed`` is returned when a security-related
API or other :app:`Pyramid` code allows an action unrelated to
an ACL check. It evaluates equal to all boolean true types. It
has an attribute named ``msg`` describing the circumstances for
- the allow."""
+ the allow.
+
+ """
boolval = 1
-class ACLPermitsResult(int):
+class ACLPermitsResult(PermitsResult):
def __new__(cls, ace, acl, permission, principals, context):
- inst = int.__new__(cls, cls.boolval)
+ """
+ Create a new instance.
+
+ :param ace: The :term:`ACE` that matched, triggering the result.
+ :param acl: The :term:`ACL` containing ``ace``.
+ :param permission: The required :term:`permission`.
+ :param principals: The list of :term:`principals <principal>` provided.
+ :param context: The :term:`context` providing the :term:`lineage`
+ searched.
+
+ """
+ fmt = ('%s permission %r via ACE %r in ACL %r on context %r for '
+ 'principals %r')
+ inst = PermitsResult.__new__(
+ cls,
+ fmt,
+ cls.__name__,
+ permission,
+ ace,
+ acl,
+ context,
+ principals,
+ )
inst.permission = permission
inst.ace = ace
inst.acl = acl
@@ -288,44 +325,31 @@ class ACLPermitsResult(int):
inst.context = context
return inst
- @property
- def msg(self):
- s = ('%s permission %r via ACE %r in ACL %r on context %r for '
- 'principals %r')
- return s % (self.__class__.__name__,
- self.permission,
- self.ace,
- self.acl,
- self.context,
- self.principals)
-
- def __str__(self):
- return self.msg
+class ACLDenied(ACLPermitsResult, Denied):
+ """
+ An instance of ``ACLDenied`` is a specialization of
+ :class:`pyramid.security.Denied` that represents that a security check
+ made explicitly against ACL was denied. It evaluates equal to all
+ boolean false types. It also has the following attributes: ``acl``,
+ ``ace``, ``permission``, ``principals``, and ``context``. These
+ attributes indicate the security values involved in the request. Its
+ ``__str__`` method prints a summary of these attributes for debugging
+ purposes. The same summary is available as the ``msg`` attribute.
- def __repr__(self):
- return '<%s instance at %s with msg %r>' % (self.__class__.__name__,
- id(self),
- self.msg)
+ """
-class ACLDenied(ACLPermitsResult):
- """ An instance of ``ACLDenied`` represents that a security check made
- explicitly against ACL was denied. It evaluates equal to all boolean
- false types. It also has the following attributes: ``acl``, ``ace``,
- ``permission``, ``principals``, and ``context``. These attributes
- indicate the security values involved in the request. Its __str__ method
- prints a summary of these attributes for debugging purposes. The same
- summary is available as the ``msg`` attribute."""
- boolval = 0
+class ACLAllowed(ACLPermitsResult, Allowed):
+ """
+ An instance of ``ACLAllowed`` is a specialization of
+ :class:`pyramid.security.Allowed` that represents that a security check
+ made explicitly against ACL was allowed. It evaluates equal to all
+ boolean true types. It also has the following attributes: ``acl``,
+ ``ace``, ``permission``, ``principals``, and ``context``. These
+ attributes indicate the security values involved in the request. Its
+ ``__str__`` method prints a summary of these attributes for debugging
+ purposes. The same summary is available as the ``msg`` attribute.
-class ACLAllowed(ACLPermitsResult):
- """ An instance of ``ACLAllowed`` represents that a security check made
- explicitly against ACL was allowed. It evaluates equal to all boolean
- true types. It also has the following attributes: ``acl``, ``ace``,
- ``permission``, ``principals``, and ``context``. These attributes
- indicate the security values involved in the request. Its __str__ method
- prints a summary of these attributes for debugging purposes. The same
- summary is available as the ``msg`` attribute."""
- boolval = 1
+ """
class AuthenticationAPIMixin(object):
@@ -395,7 +419,8 @@ class AuthorizationAPIMixin(object):
:type permission: unicode, str
:param context: A resource object or ``None``
:type context: object
- :returns: `pyramid.security.PermitsResult`
+ :returns: Either :class:`pyramid.security.Allowed` or
+ :class:`pyramid.security.Denied`.
.. versionadded:: 1.5
diff --git a/pyramid/tests/test_security.py b/pyramid/tests/test_security.py
index 5561a05d7..1da73ff73 100644
--- a/pyramid/tests/test_security.py
+++ b/pyramid/tests/test_security.py
@@ -92,9 +92,11 @@ class TestACLAllowed(unittest.TestCase):
return klass(*arg, **kw)
def test_it(self):
+ from pyramid.security import Allowed
msg = ("ACLAllowed permission 'permission' via ACE 'ace' in ACL 'acl' "
"on context 'ctx' for principals 'principals'")
allowed = self._makeOne('ace', 'acl', 'permission', 'principals', 'ctx')
+ self.assertIsInstance(allowed, Allowed)
self.assertTrue(msg in allowed.msg)
self.assertEqual(allowed, True)
self.assertTrue(allowed)
@@ -112,9 +114,11 @@ class TestACLDenied(unittest.TestCase):
return klass(*arg, **kw)
def test_it(self):
+ from pyramid.security import Denied
msg = ("ACLDenied permission 'permission' via ACE 'ace' in ACL 'acl' "
"on context 'ctx' for principals 'principals'")
denied = self._makeOne('ace', 'acl', 'permission', 'principals', 'ctx')
+ self.assertIsInstance(denied, Denied)
self.assertTrue(msg in denied.msg)
self.assertEqual(denied, False)
self.assertFalse(denied)
diff --git a/pyramid/tests/test_view.py b/pyramid/tests/test_view.py
index a9ce2234d..e03487a70 100644
--- a/pyramid/tests/test_view.py
+++ b/pyramid/tests/test_view.py
@@ -886,6 +886,18 @@ class TestViewMethodsMixin(unittest.TestCase):
else: # pragma: no cover
self.fail()
+ def test_it_reraises_if_not_found(self):
+ request = self._makeOne()
+ dummy_exc = RuntimeError()
+ try:
+ raise dummy_exc
+ except RuntimeError:
+ self.assertRaises(
+ RuntimeError,
+ lambda: request.invoke_exception_view(reraise=True))
+ else: # pragma: no cover
+ self.fail()
+
def test_it_raises_predicate_mismatch(self):
from pyramid.exceptions import PredicateMismatch
def exc_view(exc, request): pass
@@ -900,6 +912,21 @@ class TestViewMethodsMixin(unittest.TestCase):
else: # pragma: no cover
self.fail()
+ def test_it_reraises_after_predicate_mismatch(self):
+ def exc_view(exc, request): pass
+ self.config.add_view(exc_view, context=Exception, request_method='POST')
+ request = self._makeOne()
+ request.method = 'GET'
+ dummy_exc = RuntimeError()
+ try:
+ raise dummy_exc
+ except RuntimeError:
+ self.assertRaises(
+ RuntimeError,
+ lambda: request.invoke_exception_view(reraise=True))
+ else: # pragma: no cover
+ self.fail()
+
class ExceptionResponse(Exception):
status = '404 Not Found'
app_iter = ['Not Found']
diff --git a/pyramid/view.py b/pyramid/view.py
index 14d11825e..dc4aae3fa 100644
--- a/pyramid/view.py
+++ b/pyramid/view.py
@@ -16,6 +16,7 @@ from pyramid.interfaces import (
)
from pyramid.compat import decode_path_info
+from pyramid.compat import reraise as reraise_
from pyramid.exceptions import (
ConfigurationError,
@@ -630,8 +631,9 @@ class ViewMethodsMixin(object):
self,
exc_info=None,
request=None,
- secure=True
- ):
+ secure=True,
+ reraise=False,
+ ):
""" Executes an exception view related to the request it's called upon.
The arguments it takes are these:
@@ -654,14 +656,12 @@ class ViewMethodsMixin(object):
does not have the appropriate permission, this should be ``True``.
Default: ``True``.
- If called with no arguments, it uses the global exception information
- returned by ``sys.exc_info()`` as ``exc_info``, the request
- object that this method is attached to as the ``request``, and
- ``True`` for ``secure``.
+ ``reraise``
- This method returns a :term:`response` object or raises
- :class:`pyramid.httpexceptions.HTTPNotFound` if a matching view cannot
- be found.
+ A boolean indicating whether the original error should be reraised
+ if a :term:`response` object could not be created. If ``False``
+ then an :class:`pyramid.httpexceptions.HTTPNotFound`` exception
+ will be raised. Default: ``False``.
If a response is generated then ``request.exception`` and
``request.exc_info`` will be left at the values used to render the
@@ -675,6 +675,8 @@ class ViewMethodsMixin(object):
reflect the exception used to render the response where previously
they were reset to the values prior to invoking the method.
+ Also added the ``reraise`` argument.
+
"""
if request is None:
request = self
@@ -716,10 +718,16 @@ class ViewMethodsMixin(object):
secure=secure,
request_iface=request_iface.combined,
)
+ except:
+ if reraise:
+ reraise_(*exc_info)
+ raise
finally:
manager.pop()
if response is None:
+ if reraise:
+ reraise_(*exc_info)
raise HTTPNotFound
# successful response, overwrite exception/exc_info