diff options
| -rw-r--r-- | CHANGES.txt | 10 | ||||
| -rw-r--r-- | TODO.txt | 3 | ||||
| -rw-r--r-- | pyramid/config.py | 14 | ||||
| -rw-r--r-- | pyramid/exceptions.py | 36 | ||||
| -rw-r--r-- | pyramid/security.py | 28 | ||||
| -rw-r--r-- | pyramid/tests/forbiddenapp/__init__.py | 25 | ||||
| -rw-r--r-- | pyramid/tests/test_integration.py | 15 |
7 files changed, 96 insertions, 35 deletions
diff --git a/CHANGES.txt b/CHANGES.txt index c1a413642..d0cda2f39 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -33,6 +33,16 @@ Features - An exception raised by a NewRequest event subscriber can now be caught by an exception view. +- It is now possible to get information about why Pyramid raised a Forbidden + exception from within an exception view. The ``ACLDenied`` object returned + by the ``permits`` method of each stock authorization policy + (``pyramid.interfaces.IAuthorizationPolicy.permits``) is now attached to + the Forbidden exception as its ``result`` attribute. Therefore, if you've + created a Forbidden exception view, you can see the ACE, ACL, permission, + and principals involved in the request as + eg. ``context.result.permission``, ``context.result.acl``, etc within the + logic of the Forbidden exception view. + Bug Fixes --------- @@ -32,9 +32,6 @@ Should-Have - Provide a response_set_cookie method on the request for rendered responses that can be used as input to response.set_cookie? -- Make it possible to get at ACLDenied data from Forbidden response in - exceptionview. - Nice-to-Have ------------ diff --git a/pyramid/config.py b/pyramid/config.py index aea58dddf..11a639286 100644 --- a/pyramid/config.py +++ b/pyramid/config.py @@ -2726,18 +2726,18 @@ class ViewDeriver(object): wrapped_view = view if self.authn_policy and self.authz_policy and (permission is not None): - def _secured_view(context, request): + def _permitted(context, request): principals = self.authn_policy.effective_principals(request) - if self.authz_policy.permits(context, principals, permission): + return self.authz_policy.permits(context, principals, + permission) + def _secured_view(context, request): + result = _permitted(context, request) + if result: return view(context, request) msg = getattr(request, 'authdebug_message', 'Unauthorized: %s failed permission check' % view) - raise Forbidden(msg) + raise Forbidden(msg, result) _secured_view.__call_permissive__ = view - def _permitted(context, request): - principals = self.authn_policy.effective_principals(request) - return self.authz_policy.permits(context, principals, - permission) _secured_view.__permitted__ = _permitted wrapped_view = _secured_view diff --git a/pyramid/exceptions.py b/pyramid/exceptions.py index bcfc4ba5e..771d71b88 100644 --- a/pyramid/exceptions.py +++ b/pyramid/exceptions.py @@ -38,17 +38,31 @@ class ExceptionResponse(Exception): class Forbidden(ExceptionResponse): """ - Raise this exception within :term:`view` code to immediately - return the :term:`forbidden view` to the invoking user. Usually - this is a basic ``403`` page, but the forbidden view can be - customized as necessary. See :ref:`changing_the_forbidden_view`. + Raise this exception within :term:`view` code to immediately return the + :term:`forbidden view` to the invoking user. Usually this is a basic + ``403`` page, but the forbidden view can be customized as necessary. See + :ref:`changing_the_forbidden_view`. A ``Forbidden`` exception will be + the ``context`` of a :term:`Forbidden View`. - This exception's constructor accepts a single positional argument, which - should be a string. The value of this string will be placed onto the - request by the router as the ``exception_message`` attribute, for - availability to the :term:`Forbidden View`. + This exception's constructor accepts two arguments. The first argument, + ``message``, should be a string. The value of this string will be used + as the ``message`` attribute of the exception object. The second + argument, ``result`` is usually an instance of + :class:`pyramid.security.Denied` or :class:`pyramid.security.ACLDenied` + each of which indicates a reason for the forbidden error. However, + ``result`` is also permitted to be just a plain boolean ``False`` object. + The ``result`` value will be used as the ``result`` attribute of the + exception object. + + The :term:`Forbidden View` can use the attributes of a Forbidden + exception as necessary to provide extended information in an error + report shown to a user. """ status = '403 Forbidden' + def __init__(self, message='', result=None): + ExceptionResponse.__init__(self, message) + self.message = message + self.result = result class NotFound(ExceptionResponse): """ @@ -58,9 +72,9 @@ class NotFound(ExceptionResponse): customized as necessary. See :ref:`changing_the_notfound_view`. This exception's constructor accepts a single positional argument, which - should be a string. The value of this string will be placed into the WSGI - environment by the router as the ``exception_message`` attribute, for - availability to the :term:`Not Found View`. + should be a string. The value of this string will be available as the + ``message`` attribute of this exception, for availability to the + :term:`Not Found View`. """ status = '404 Not Found' diff --git a/pyramid/security.py b/pyramid/security.py index 4da42a966..6cf63b0b3 100644 --- a/pyramid/security.py +++ b/pyramid/security.py @@ -256,22 +256,22 @@ class ACLPermitsResult(int): 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 attributes which indicate which - acl, ace, permission, principals, and context were 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.""" + """ 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): - """ 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 attributes which indicate - which acl, ace, permission, principals, and context were 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.""" + """ 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 diff --git a/pyramid/tests/forbiddenapp/__init__.py b/pyramid/tests/forbiddenapp/__init__.py new file mode 100644 index 000000000..ed9aa8357 --- /dev/null +++ b/pyramid/tests/forbiddenapp/__init__.py @@ -0,0 +1,25 @@ +from cgi import escape +from webob import Response +from pyramid.httpexceptions import HTTPForbidden +from pyramid.exceptions import Forbidden + +def x_view(request): # pragma: no cover + return Response('this is private!') + +def forbidden_view(context, request): + msg = context.message + result = context.result + message = msg + '\n' + str(result) + resp = HTTPForbidden() + resp.body = message + return resp + +def includeme(config): + from pyramid.authentication import AuthTktAuthenticationPolicy + from pyramid.authorization import ACLAuthorizationPolicy + authn_policy = AuthTktAuthenticationPolicy('seekr1t') + authz_policy = ACLAuthorizationPolicy() + config._set_authentication_policy(authn_policy) + config._set_authorization_policy(authz_policy) + config.add_view(x_view, name='x', permission='private') + config.add_view(forbidden_view, context=Forbidden) diff --git a/pyramid/tests/test_integration.py b/pyramid/tests/test_integration.py index 6d5d7d675..dc7525080 100644 --- a/pyramid/tests/test_integration.py +++ b/pyramid/tests/test_integration.py @@ -195,6 +195,21 @@ class TestRestBugApp(IntegrationBase): res = self.testapp.get('/pet', status=200) self.assertEqual(res.body, 'gotten') +class TestForbiddenAppHasResult(IntegrationBase): + # test that forbidden exception has ACLDenied result attached + package = 'pyramid.tests.forbiddenapp' + def test_it(self): + res = self.testapp.get('/x', status=403) + message, result = [x.strip() for x in res.body.split('\n')] + self.failUnless(message.endswith('failed permission check')) + self.failUnless( + result.startswith("ACLDenied permission 'private' via ACE " + "'<default deny>' in ACL " + "'<No ACL found on any object in resource " + "lineage>' on context")) + self.failUnless( + result.endswith("for principals ['system.Everyone']")) + class TestViewDecoratorApp(IntegrationBase): package = 'pyramid.tests.viewdecoratorapp' def _configure_mako(self): |
