summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGES.txt10
-rw-r--r--TODO.txt3
-rw-r--r--pyramid/config.py14
-rw-r--r--pyramid/exceptions.py36
-rw-r--r--pyramid/security.py28
-rw-r--r--pyramid/tests/forbiddenapp/__init__.py25
-rw-r--r--pyramid/tests/test_integration.py15
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
---------
diff --git a/TODO.txt b/TODO.txt
index 07e74ba0d..d8e6dcdac 100644
--- a/TODO.txt
+++ b/TODO.txt
@@ -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):