Repository: Pylons/pyramid

Issue:
Need clarity about True/False vs. ACLAllowed/ACLDenied in custom AuthorizationPolicy
This is a request for clarification in the documentation:

The documentation about [Creating Your Own Authorization Policy](http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/security.html#creating-your-own-authorization-policy) states that `permits(...)` shall return `True` or `False`. However, the [ACLAuthorizationPolicy documentation](http://docs.pylonsproject.org/projects/pyramid/en/latest/api/authorization.html) states that it returns instead instances of `ACLAllowed` or `ACLDenied`, which are only compatible to `True` or `False`, but not the same.

Roughly speaking, the only implementation of `IAuthorizationPolicy` violates that interface, which is confusing.

So what is a custom AuthorizationPolicy supposed to do? Should it return instances of `ACLAllowed`/`ACLDenied`, or should it return plain booleans (`True`/`False`)?

Another question: If the framework only evaluates the boolean value of `ACLAllowed`/`ACLDenied`, what are the additional fields in those objects good for? Where are these used or shown?


Discussion:
The interface requires truthy/falsey return types. It could do better to explain this and it is not very consistent with `request.has_permission` documentation which states a `pyramid.security.PermitsResult` object which is a subclass of `int` and is more practically implemented as `pyramid.security.Allowed` and `pyramid.security.Denied`. These docs should definitely be fixed because at least right now `PermitsResult` is not even a public api despite being referenced in the docs.

Practically you can return `Allowed` and `Denied` or the more specific `ACLAllowed` and `ACLDenied` if they work in your policy. They can carry along more information about exactly why things were allowed or denied. The underlying system, however, only cares about how they compare to boolean values. The advantage of adding more information to the values is that they can be used in a forbidden exception view to deduce what happened and explain things to the user if that's a goal of yours.

If you're interested in turning any of this into improvements to the documentation they would be greatly appreciated.
After re-reading my comment the immediate action items would be:

1) Fix `request.has_permission` documentation to mention that the expected return type is something that can be truthy/falsey. I'm blanking on a good term for that right now.

2) Fix the `IAuthorizationPolicy` documentation to mention the same thing. It could mention `Allowed` and `Denied` as well as possible return types.

Resolving patch:
diff --git a/pyramid/interfaces.py b/pyramid/interfaces.py
--- a/pyramid/interfaces.py
+++ b/pyramid/interfaces.py
@@ -503,8 +503,10 @@ def forget(request):
 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):
diff --git a/pyramid/security.py b/pyramid/security.py
--- 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 @@ def __new__(cls, s, *args):
 
     @property
     def msg(self):
+        """ A string indicating why the result was generated."""
         return self.s % self.args
 
     def __str__(self):
@@ -263,24 +272,52 @@ def __repr__(self):
                                                     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 @@ def __new__(cls, ace, acl, permission, principals, context):
         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 @@ def has_permission(self, permission, context=None):
         :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
 


Test patch:
diff --git a/pyramid/tests/test_security.py b/pyramid/tests/test_security.py
--- a/pyramid/tests/test_security.py
+++ b/pyramid/tests/test_security.py
@@ -92,9 +92,11 @@ def _makeOne(self, *arg, **kw):
         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 @@ def _makeOne(self, *arg, **kw):
         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)