From 2160ce9152944393caf34113e49982481385e27a Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Tue, 26 Apr 2016 17:49:23 -0500 Subject: do not enforce default permissions on exception views - this normalizes the behavior to work similar to require_csrf - if an explicit permission= is set on the view it will still be enforced, this just affects a default permission via config.set_default_permission - permission=NO_PERMISSION_REQUIRED was already forced on for notfound and forbidden views, this just helps out with other exception views --- pyramid/config/views.py | 18 ++++++------ pyramid/interfaces.py | 4 +-- pyramid/tests/test_viewderivers.py | 57 ++++++++++++++++++++++++++++++++++++++ pyramid/viewderivers.py | 21 ++++++++++++-- 4 files changed, 88 insertions(+), 12 deletions(-) diff --git a/pyramid/config/views.py b/pyramid/config/views.py index 34f289fcc..9e46ba155 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -13,7 +13,6 @@ from zope.interface import ( from zope.interface.interfaces import IInterface from pyramid.interfaces import ( - IDefaultPermission, IException, IExceptionViewClassifier, IMultiView, @@ -878,11 +877,6 @@ class ViewsConfiguratorMixin(object): registry=self.registry ) - if permission is None: - # intent: will be None if no default permission is registered - # (reg'd in phase 1) - permission = self.registry.queryUtility(IDefaultPermission) - # added by discrim_func above during conflict resolving preds = view_intr['predicates'] order = view_intr['order'] @@ -1436,7 +1430,10 @@ class ViewsConfiguratorMixin(object): .. versionadded:: 1.3 """ - for arg in ('name', 'permission', 'context', 'for_', 'http_cache'): + for arg in ( + 'name', 'permission', 'context', 'for_', 'http_cache', + 'require_csrf', + ): if arg in view_options: raise ConfigurationError( '%s may not be used as an argument to add_forbidden_view' @@ -1464,6 +1461,7 @@ class ViewsConfiguratorMixin(object): match_param=match_param, route_name=route_name, permission=NO_PERMISSION_REQUIRED, + require_csrf=False, attr=attr, renderer=renderer, ) @@ -1548,7 +1546,10 @@ class ViewsConfiguratorMixin(object): .. versionchanged:: 1.6 .. versionadded:: 1.3 """ - for arg in ('name', 'permission', 'context', 'for_', 'http_cache'): + for arg in ( + 'name', 'permission', 'context', 'for_', 'http_cache', + 'require_csrf', + ): if arg in view_options: raise ConfigurationError( '%s may not be used as an argument to add_notfound_view' @@ -1576,6 +1577,7 @@ class ViewsConfiguratorMixin(object): match_param=match_param, route_name=route_name, permission=NO_PERMISSION_REQUIRED, + require_csrf=False, ) settings.update(view_options) if append_slash: diff --git a/pyramid/interfaces.py b/pyramid/interfaces.py index c03afbd39..b252d0f4a 100644 --- a/pyramid/interfaces.py +++ b/pyramid/interfaces.py @@ -1224,9 +1224,9 @@ class IViewDeriver(Interface): class IViewDeriverInfo(Interface): """ An object implementing this interface is passed to every :term:`view deriver` during configuration.""" - registry = Attribute('The "current" application registry when the ' + registry = Attribute('The "current" application registry where the ' 'view was created') - package = Attribute('The "current package" when the view ' + package = Attribute('The "current package" where the view ' 'configuration statement was found') settings = Attribute('The deployment settings dictionary related ' 'to the current application') diff --git a/pyramid/tests/test_viewderivers.py b/pyramid/tests/test_viewderivers.py index e84863d69..79fcd6e71 100644 --- a/pyramid/tests/test_viewderivers.py +++ b/pyramid/tests/test_viewderivers.py @@ -628,6 +628,63 @@ class TestDeriveView(unittest.TestCase): else: # pragma: no cover raise AssertionError + def test_secured_view_skipped_by_default_on_exception_view(self): + from pyramid.request import Request + from pyramid.security import NO_PERMISSION_REQUIRED + def view(request): + raise ValueError + def excview(request): + return 'hello' + self._registerSecurityPolicy(False) + self.config.add_settings({'debug_authorization': True}) + self.config.set_default_permission('view') + self.config.add_view(view, name='foo', permission=NO_PERMISSION_REQUIRED) + self.config.add_view(excview, context=ValueError, renderer='string') + app = self.config.make_wsgi_app() + request = Request.blank('/foo', base_url='http://example.com') + request.method = 'POST' + response = request.get_response(app) + self.assertTrue(b'hello' in response.body) + + def test_secured_view_failed_on_explicit_exception_view(self): + from pyramid.httpexceptions import HTTPForbidden + from pyramid.request import Request + from pyramid.security import NO_PERMISSION_REQUIRED + def view(request): + raise ValueError + def excview(request): pass + self._registerSecurityPolicy(False) + self.config.add_view(view, name='foo', permission=NO_PERMISSION_REQUIRED) + self.config.add_view(excview, context=ValueError, renderer='string', + permission='view') + app = self.config.make_wsgi_app() + request = Request.blank('/foo', base_url='http://example.com') + request.method = 'POST' + try: + request.get_response(app) + except HTTPForbidden: + pass + else: # pragma: no cover + raise AssertionError + + def test_secured_view_passed_on_explicit_exception_view(self): + from pyramid.request import Request + from pyramid.security import NO_PERMISSION_REQUIRED + def view(request): + raise ValueError + def excview(request): + return 'hello' + self._registerSecurityPolicy(True) + self.config.add_view(view, name='foo', permission=NO_PERMISSION_REQUIRED) + self.config.add_view(excview, context=ValueError, renderer='string', + permission='view') + app = self.config.make_wsgi_app() + request = Request.blank('/foo', base_url='http://example.com') + request.method = 'POST' + request.headers['X-CSRF-Token'] = 'foo' + response = request.get_response(app) + self.assertTrue(b'hello' in response.body) + def test_predicate_mismatch_view_has_no_name(self): from pyramid.exceptions import PredicateMismatch response = DummyResponse() diff --git a/pyramid/viewderivers.py b/pyramid/viewderivers.py index 1b922b89e..5d138a02a 100644 --- a/pyramid/viewderivers.py +++ b/pyramid/viewderivers.py @@ -16,6 +16,7 @@ from pyramid.interfaces import ( IAuthenticationPolicy, IAuthorizationPolicy, IDefaultCSRFOptions, + IDefaultPermission, IDebugLogger, IResponse, IViewMapper, @@ -272,7 +273,9 @@ def secured_view(view, info): secured_view.options = ('permission',) def _secured_view(view, info): - permission = info.options.get('permission') + permission = explicit_val = info.options.get('permission') + if permission is None: + permission = info.registry.queryUtility(IDefaultPermission) if permission == NO_PERMISSION_REQUIRED: # allow views registered within configurations that have a # default permission to explicitly override the default @@ -288,6 +291,12 @@ def _secured_view(view, info): principals = authn_policy.effective_principals(request) return authz_policy.permits(context, principals, permission) def _secured_view(context, request): + if ( + getattr(request, 'exception', None) is not None and + explicit_val is None + ): + return view(context, request) + result = _permitted(context, request) if result: return view(context, request) @@ -306,12 +315,20 @@ def _secured_view(view, info): def _authdebug_view(view, info): wrapped_view = view settings = info.settings - permission = info.options.get('permission') + permission = explicit_val = info.options.get('permission') + if permission is None: + permission = info.registry.queryUtility(IDefaultPermission) authn_policy = info.registry.queryUtility(IAuthenticationPolicy) authz_policy = info.registry.queryUtility(IAuthorizationPolicy) logger = info.registry.queryUtility(IDebugLogger) if settings and settings.get('debug_authorization', False): def _authdebug_view(context, request): + if ( + getattr(request, 'exception', None) is not None and + explicit_val is None + ): + return view(context, request) + view_name = getattr(request, 'view_name', None) if authn_policy and authz_policy: -- cgit v1.2.3