Repository: Pylons/pyramid

Issue:
view_execution_permitted() when view does not exist?
`Pyramid.security.view_execution_permitted(context, request, name)` currently returns a true value if there is no appropriate view registered for the context.  I'm not sure whether that is the intended behavior or not (so I'm not sure whether this is a bug report or a feature request.)  I would argue that _"view exists and is permitted"_ is a more useful predicate than _"view execution permitted or view does not exist"_.

In my use case — which I'd guess is not uncommon — I'd like to determine whether the current user can execute a given view on a given context without knowing a priori whether that view is even registered for that context.  (The result of the check determines whether an entry for that view will appear in an admin nav menu.)

Currently, AFAICT, the only way to determine whether a particular view exists using Pyramid's public API is using `pyramid.traversal.traverse`, which seems awkward and overkill.   Have I missed a better way?



Discussion:
If this is unintentional it can be fixed by querying the registry for `IView` instead of `ISecuredView`, then if no `IView` is found returning `Denied`. If the `IView` is found and it's not an `ISecuredView`, return `Allowed`, otherwise perform an actual security check.

@mcdonc, is this behavior intentional?

Can you help me understand the situation where you don't know if the view exists or not before trying to do the security check?

Actually scratch that, I can imagine a case.  But can you help me understand what distinction you're trying to make when the thing returns "nonexistent" vs. "denied"?

**Main response**

I'm not sure that I do care about the distinction between “nonexistent” and “denied”.  The point I was trying to make was only that it seems incorrect — or at least non-useful — for `view_execution_permitted()` to return a true value when the view does not exist.  (Okay, fine, you’re not going to get a 403, but you are going to get a 404.)

**Ancillary comments**

The use case was the construction of an _admin menu_ for a CMS-ish system.  There were a list of menu entries ("Edit", "Add Page", "Rename", ...) each corresponding to a specific view name.   Not all contexts supported all those views — some contexts did not have an 'add_page' view, for example — and not all users were permitted to perform all views.  I wanted to display only those menu entries which the user was able to perform.

I’ve since refactored the admin menu configuration code.  At present, the above use case is no longer an issue for me.  (It’s probably still a valid one.)

I can also see a sticky point about this use case: What I was really interested in was whether a view could be executed by the user with request attributes that might differ from those of the current request.  E.g. I might want to know if the user can perform a GET even though the current request is a POST.  I can see that this is outside the domain of `pyramid.security`.

Returning Denied if no view is found sounds like a more sane default than allowing access to anything unknown.

@mmerickel  and I discussed this in IRC and I think we decided that the view_execution_permitted callable should raise an exception when asked for a view name that doesn't exist.  

(which exception?  i dunno)


Resolving patch:
diff --git a/pyramid/security.py b/pyramid/security.py
--- a/pyramid/security.py
+++ b/pyramid/security.py
@@ -4,6 +4,7 @@
     IAuthenticationPolicy,
     IAuthorizationPolicy,
     ISecuredView,
+    IView,
     IViewClassifier,
     )
 
@@ -132,7 +133,13 @@ def view_execution_permitted(context, request, name=''):
     view using the effective authentication/authorization policies and
     the ``request``.  Return a boolean result.  If no
     :term:`authorization policy` is in effect, or if the view is not
-    protected by a permission, return ``True``."""
+    protected by a permission, return ``True``. If no view can view found,
+    an exception will be raised.
+
+    .. versionchanged:: 1.4a4
+       An exception is raised if no view is found.
+
+    """
     try:
         reg = request.registry
     except AttributeError:
@@ -140,6 +147,11 @@ def view_execution_permitted(context, request, name=''):
     provides = [IViewClassifier] + map_(providedBy, (request, context))
     view = reg.adapters.lookup(provides, ISecuredView, name=name)
     if view is None:
+        view = reg.adapters.lookup(provides, IView, name=name)
+        if view is None:
+            raise TypeError('No registered view satisfies the constraints. '
+                            'It would not make sense to claim that this view '
+                            '"is" or "is not" permitted.')
         return Allowed(
             'Allowed: view name %r in context %r (no permission defined)' %
             (name, context))


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
@@ -131,19 +131,37 @@ def __permitted__(self, context, request):
         return checker
 
     def test_no_permission(self):
+        from zope.interface import Interface
         from pyramid.threadlocal import get_current_registry
         from pyramid.interfaces import ISettings
+        from pyramid.interfaces import IView
+        from pyramid.interfaces import IViewClassifier
         settings = dict(debug_authorization=True)
         reg = get_current_registry()
         reg.registerUtility(settings, ISettings)
         context = DummyContext()
         request = DummyRequest({})
+        class DummyView(object):
+            pass
+        view = DummyView()
+        reg.registerAdapter(view, (IViewClassifier, Interface, Interface),
+                            IView, '')
         result = self._callFUT(context, request, '')
         msg = result.msg
         self.assertTrue("Allowed: view name '' in context" in msg)
         self.assertTrue('(no permission defined)' in msg)
         self.assertEqual(result, True)
 
+    def test_no_view_registered(self):
+        from pyramid.threadlocal import get_current_registry
+        from pyramid.interfaces import ISettings
+        settings = dict(debug_authorization=True)
+        reg = get_current_registry()
+        reg.registerUtility(settings, ISettings)
+        context = DummyContext()
+        request = DummyRequest({})
+        self.assertRaises(TypeError, self._callFUT, context, request, '')
+
     def test_with_permission(self):
         from zope.interface import Interface
         from zope.interface import directlyProvides