diff options
| -rw-r--r-- | CHANGES.txt | 6 | ||||
| -rw-r--r-- | docs/api/security.rst | 22 | ||||
| -rw-r--r-- | docs/whatsnew-1.9.rst | 6 | ||||
| -rw-r--r-- | pyramid/interfaces.py | 8 | ||||
| -rw-r--r-- | pyramid/router.py | 29 | ||||
| -rw-r--r-- | pyramid/security.py | 109 | ||||
| -rw-r--r-- | pyramid/tests/test_security.py | 4 | ||||
| -rw-r--r-- | pyramid/tests/test_view.py | 27 | ||||
| -rw-r--r-- | pyramid/view.py | 26 |
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 |
