diff options
| author | Chris McDonough <chrism@agendaless.com> | 2008-11-02 17:27:33 +0000 |
|---|---|---|
| committer | Chris McDonough <chrism@agendaless.com> | 2008-11-02 17:27:33 +0000 |
| commit | 17ce5747ea36df10ec78e0af7140b55f691f5016 (patch) | |
| tree | 10c3a5ca6b460c59ecd72d29a4e2db587ce550e8 | |
| parent | 2fc5d11826931435cfb42e2f334391c783f31f1d (diff) | |
| download | pyramid-17ce5747ea36df10ec78e0af7140b55f691f5016.tar.gz pyramid-17ce5747ea36df10ec78e0af7140b55f691f5016.tar.bz2 pyramid-17ce5747ea36df10ec78e0af7140b55f691f5016.zip | |
Features
- The ``BFG_DEBUG_AUTHORIZATION`` envvar and the
``debug_authorization`` config file value now only imply debugging
of view-invoked security checks. Previously, information was
printed for every call to ``has_permission`` as well, which made
output confusing. To debug ``has_permission`` checks and other
manual permission checks, use the debugger and print statements in
your own code.
- Authorization debugging info is now only present in the HTTP
response body oif ``debug_authorization`` is true.
- The format of authorization debug messages was improved.
- A new ``BFG_DEBUG_NOTFOUND`` envvar was added and a symmetric
``debug_notfound`` config file value was added. When either is
true, and a NotFound response is returned by the BFG router
(because a view could not be found), debugging information is
printed to stderr. When this value is set true, the body of
HTTPNotFound responses will also contain the same debugging
information.
- ``Allowed`` and ``Denied`` responses from the security machinery
are now specialized into two types: ACL types, and non-ACL types.
The ACL-related responses are instances of
``repoze.bfg.security.ACLAllowed`` and
``repoze.bfg.security.ACLDenied``. The non-ACL-related responses
are ``repoze.bfg.security.Allowed`` and
``repoze.bfg.security.Denied``. The allowed-type responses
continue to evaluate equal to things that themselves evaluate
equal to the ``True`` boolean, while the denied-type responses
continue to evaluate equal to things that themselves evaluate
equal to the ``False`` boolean. The only difference between the
two types is the information attached to them for debugging
purposes.
- Added a new ``BFG_DEBUG_ALL`` envvar and a symmetric ``debug_all``
config file value. When either is true, all other debug-related
flags are set true unconditionally (e.g. ``debug_notfound`` and
``debug_authorization``).
Documentation
- Added info about debug flag changes.
- Added a section to the security chapter named "Debugging
Imperative Authorization Failures" (for e.g. ``has_permssion``).
| -rw-r--r-- | CHANGES.txt | 47 | ||||
| -rw-r--r-- | docs/api/security.rst | 7 | ||||
| -rw-r--r-- | docs/narr/environment.rst | 18 | ||||
| -rw-r--r-- | docs/narr/security.rst | 32 | ||||
| -rw-r--r-- | docs/narr/traversal.rst | 16 | ||||
| -rw-r--r-- | repoze/bfg/registry.py | 11 | ||||
| -rw-r--r-- | repoze/bfg/router.py | 44 | ||||
| -rw-r--r-- | repoze/bfg/security.py | 119 | ||||
| -rw-r--r-- | repoze/bfg/tests/test_registry.py | 38 | ||||
| -rw-r--r-- | repoze/bfg/tests/test_router.py | 165 | ||||
| -rw-r--r-- | repoze/bfg/tests/test_security.py | 205 | ||||
| -rw-r--r-- | repoze/bfg/tests/test_view.py | 64 | ||||
| -rw-r--r-- | repoze/bfg/view.py | 34 |
13 files changed, 628 insertions, 172 deletions
diff --git a/CHANGES.txt b/CHANGES.txt index 50e96a2ba..376040aa0 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -8,6 +8,53 @@ Next release it as necessary rather than inventing their own logger, for convenience. + - The ``BFG_DEBUG_AUTHORIZATION`` envvar and the + ``debug_authorization`` config file value now only imply debugging + of view-invoked security checks. Previously, information was + printed for every call to ``has_permission`` as well, which made + output confusing. To debug ``has_permission`` checks and other + manual permission checks, use the debugger and print statements in + your own code. + + - Authorization debugging info is now only present in the HTTP + response body oif ``debug_authorization`` is true. + + - The format of authorization debug messages was improved. + + - A new ``BFG_DEBUG_NOTFOUND`` envvar was added and a symmetric + ``debug_notfound`` config file value was added. When either is + true, and a NotFound response is returned by the BFG router + (because a view could not be found), debugging information is + printed to stderr. When this value is set true, the body of + HTTPNotFound responses will also contain the same debugging + information. + + - ``Allowed`` and ``Denied`` responses from the security machinery + are now specialized into two types: ACL types, and non-ACL types. + The ACL-related responses are instances of + ``repoze.bfg.security.ACLAllowed`` and + ``repoze.bfg.security.ACLDenied``. The non-ACL-related responses + are ``repoze.bfg.security.Allowed`` and + ``repoze.bfg.security.Denied``. The allowed-type responses + continue to evaluate equal to things that themselves evaluate + equal to the ``True`` boolean, while the denied-type responses + continue to evaluate equal to things that themselves evaluate + equal to the ``False`` boolean. The only difference between the + two types is the information attached to them for debugging + purposes. + + - Added a new ``BFG_DEBUG_ALL`` envvar and a symmetric ``debug_all`` + config file value. When either is true, all other debug-related + flags are set true unconditionally (e.g. ``debug_notfound`` and + ``debug_authorization``). + + Documentation + + - Added info about debug flag changes. + + - Added a section to the security chapter named "Debugging + Imperative Authorization Failures" (for e.g. ``has_permssion``). + Bug Fixes - Change default paster template generator to use ``Paste#http`` diff --git a/docs/api/security.rst b/docs/api/security.rst index 58a405633..c7088656e 100644 --- a/docs/api/security.rst +++ b/docs/api/security.rst @@ -38,9 +38,14 @@ 'george', 'read')`` that means deny access. A sequence of ACEs makes up an ACL. It is a string, and it's actual value is "Deny". + .. autoclass:: ACLDenied + :members: + + .. autoclass:: ACLAllowed + :members: + .. autoclass:: Denied :members: .. autoclass:: Allowed :members: - diff --git a/docs/narr/environment.rst b/docs/narr/environment.rst index 9ace43048..3fe3457c0 100644 --- a/docs/narr/environment.rst +++ b/docs/narr/environment.rst @@ -27,11 +27,18 @@ application-specific configuration settings. | | | See also: | | | | :ref:`reload_templates_section` | +-----------------------------+--------------------------+-------------------------------------+ -| ``BFG_DEBUG_AUTHORIZATION`` | ``debug_authorization`` | Print authorization failure/success| -| | | messages to stderr when true | +| ``BFG_DEBUG_AUTHORIZATION`` | ``debug_authorization`` | Print view authorization failure & | +| | | success info to stderr when true | | | | See also: | | | | :ref:`debug_authorization_section` | +-----------------------------+--------------------------+-------------------------------------+ +| ``BFG_DEBUG_NOTFOUND`` | ``debug_notfound`` | Print view-related NotFound debug | +| | | messages to stderr when true | +| | | See also: | +| | | :ref:`debug_notfound_section` | ++-----------------------------+--------------------------+-------------------------------------+ +| ``BFG_DEBUG_ALL`` | ``debug_all`` | Turns all debug_* settings on. | ++-----------------------------+--------------------------+-------------------------------------+ Examples -------- @@ -58,3 +65,10 @@ If you started your application this way, your :mod:`repoze.bfg` application would behave in the same manner as if you had placed the respective settings in the ``[app:main]`` section of your application's ``.ini`` file. + +If you want to turn all ``debug`` settings (every debug setting that +starts with ``debug_``). on in one fell swoop, you can use +``BFG_DEBUG_ALL=1`` as an environment variable setting or you may use +``debug_all=true`` in the config file. Note that this does not effect +settings that do not start with ``debug_*`` such as +``reload_templates``. diff --git a/docs/narr/security.rst b/docs/narr/security.rst index b57ad2958..041fff89d 100644 --- a/docs/narr/security.rst +++ b/docs/narr/security.rst @@ -166,19 +166,20 @@ location-awareness. .. _debug_authorization_section: -Debugging Authorization Failures --------------------------------- +Debugging View Authorization Failures +------------------------------------- -If your application in your judgment is allowing or denying access -inappropriately, start your application under a shell using the +If your application in your judgment is allowing or denying view +access inappropriately, start your application under a shell using the ``BFG_DEBUG_AUTHORIZATION`` environment variable set to ``1``. For example:: $ BFG_DEBUG_AUTHORIZATION=1 bin/paster serve myproject.ini -When any authorization takes place, a message will be logged to the -console (to stderr) about what ACE in which ACL permitted or denied -the authorization based on authentication information. +When any authorization takes place during a top-level view rendering, +a message will be logged to the console (to stderr) about what ACE in +which ACL permitted or denied the authorization based on +authentication information. This behavior can also be turned on in the application ``.ini`` file by setting the ``debug_authorization`` key to ``true`` within the @@ -188,3 +189,20 @@ application's configuration section, e.g.:: use = egg:MyProject#app debug_authorization = true +With this debug flag turned on, the response sent to the browser will +also contain security debugging information in its body. + +Debugging Imperative Authorization Failures +------------------------------------------- + +The ``has_permission`` API (see :ref:`security_module`) is used to +check security within view functions imperatively. It returns +instances of objects that are effectively booleans. But these objects +are not raw ``True`` or ``False`` objects, and have information +attached to them about why the permission was allowed or denied. The +object will be one of ``ACLAllowed``, ``ACLDenied``, ``Allowed``, and +``Denied``, documented in :ref:`security_module`. At very minimum +these objects will have a ``msg`` attribute, which is a string +indicating why permission was denied or allowed. Introspecting this +information in the debugger or via print statements when a +``has_permission`` fails is often useful. diff --git a/docs/narr/traversal.rst b/docs/narr/traversal.rst index d57cf8f02..6ed2dabd1 100644 --- a/docs/narr/traversal.rst +++ b/docs/narr/traversal.rst @@ -129,11 +129,25 @@ code to execute: and the context. If a view function is found, it is called with the context and the request. It returns a response, which is fed back upstream. If a view is not found, a generic WSGI - ``NotFound`` application is constructed. + ``NotFound`` application is constructed and returned. In either case, the result is returned upstream via the :term:`WSGI` protocol. +.. _debug_notfound_section: + +NotFound Errors +--------------- + +It's useful to be able to debug ``NotFound`` errors when they occur +unexpectedly due to an application registry misconfiguration. To +debug these errors, use the ``BFG_DEBUG_NOTFOUND`` environment +variable or the ``debug_notfound`` configuration file setting. +Details of why a view was not found will be printed to stderr, and the +browser representation of the error will include the same information. +See :ref:`environment_chapter` for more information about how and +where to set these values. + A Traversal Example ------------------- diff --git a/repoze/bfg/registry.py b/repoze/bfg/registry.py index 8fcebaf13..eccfa27e0 100644 --- a/repoze/bfg/registry.py +++ b/repoze/bfg/registry.py @@ -91,15 +91,22 @@ def asbool(s): def get_options(kw, environ=os.environ): # environ is passed in for unit tests eget = environ.get + config_debug_all = kw.get('debug_all', '') + effective_debug_all = asbool(eget('BFG_DEBUG_ALL', + config_debug_all)) config_debug_auth = kw.get('debug_authorization', '') effective_debug_auth = asbool(eget('BFG_DEBUG_AUTHORIZATION', config_debug_auth)) + config_debug_notfound = kw.get('debug_notfound', '') + effective_debug_notfound = asbool(eget('BFG_DEBUG_NOTFOUND', + config_debug_notfound)) config_reload_templates = kw.get('reload_templates') effective_reload_templates = asbool(eget('BFG_RELOAD_TEMPLATES', config_reload_templates)) return { - 'debug_authorization': effective_debug_auth, - 'reload_templates':effective_reload_templates, + 'debug_authorization': effective_debug_all or effective_debug_auth, + 'debug_notfound': effective_debug_all or effective_debug_notfound, + 'reload_templates': effective_reload_templates, } from zope.testing.cleanup import addCleanUp diff --git a/repoze/bfg/router.py b/repoze/bfg/router.py index 28cb319cb..d51d07dda 100644 --- a/repoze/bfg/router.py +++ b/repoze/bfg/router.py @@ -1,4 +1,7 @@ +from cgi import escape + from zope.component import getAdapter +from zope.component import queryUtility from zope.component.event import dispatch from zope.interface import directlyProvides from zope.interface import implements @@ -11,9 +14,11 @@ from repoze.bfg.events import NewRequest from repoze.bfg.events import NewResponse from repoze.bfg.events import WSGIApplicationCreatedEvent +from repoze.bfg.interfaces import ILogger from repoze.bfg.interfaces import ITraverserFactory from repoze.bfg.interfaces import IRequest from repoze.bfg.interfaces import IRouter +from repoze.bfg.interfaces import ISettings from repoze.bfg.registry import registry_manager from repoze.bfg.registry import makeRegistry @@ -21,6 +26,7 @@ from repoze.bfg.registry import makeRegistry from repoze.bfg.security import Unauthorized from repoze.bfg.view import render_view_to_response +from repoze.bfg.view import view_execution_permitted _marker = () @@ -41,22 +47,46 @@ class Router(object): dispatch(NewRequest(request)) root = self.root_policy(environ) traverser = getAdapter(root, ITraverserFactory) + settings = queryUtility(ISettings) context, name, subpath = traverser(environ) request.context = context request.view_name = name request.subpath = subpath - try: - response = render_view_to_response(context, request, name, - secure=True) - except Unauthorized, why: - app = HTTPUnauthorized() - app.explanation = str(why) + permitted = view_execution_permitted(context, request, name) + debug_authorization = settings and settings.debug_authorization + + if debug_authorization: + logger = queryUtility(ILogger, 'repoze.bfg.debug') + logger and logger.debug( + 'debug_authorization of url %s (view name %r against context ' + '%r): %s' % (request.url, name, context, permitted.msg) + ) + if not permitted: + if debug_authorization: + msg = permitted.msg + else: + msg = 'Unauthorized: failed security policy check' + app = HTTPUnauthorized(escape(msg)) return app(environ, start_response) + + response = render_view_to_response(context, request, name, + secure=False) if response is None: - app = HTTPNotFound(request.url) + debug_notfound = settings and settings.debug_notfound + if debug_notfound: + logger = queryUtility(ILogger, 'repoze.bfg.debug') + msg = ( + 'debug_notfound of url %s; path_info: %r, context: %r, ' + 'view_name: %r, subpath: %r' % ( + request.url, request.path_info, context, name, subpath) + ) + logger and logger.debug(msg) + else: + msg = request.url + app = HTTPNotFound(escape(msg)) return app(environ, start_response) dispatch(NewResponse(response)) diff --git a/repoze/bfg/security.py b/repoze/bfg/security.py index f1149cc7a..21b7f98d3 100644 --- a/repoze/bfg/security.py +++ b/repoze/bfg/security.py @@ -7,7 +7,6 @@ from repoze.bfg.interfaces import ISecurityPolicy from repoze.bfg.interfaces import IViewPermission from repoze.bfg.interfaces import IViewPermissionFactory from repoze.bfg.interfaces import NoAuthorizationInformation -from repoze.bfg.interfaces import ILogger Everyone = 'system.Everyone' Authenticated = 'system.Authenticated' @@ -65,14 +64,14 @@ def principals_allowed_by_permission(context, permission): class ACLAuthorizer(object): - def __init__(self, context, logger): + def __init__(self, context): self.context = context - self.logger = logger def permits(self, permission, *principals): acl = getattr(self.context, '__acl__', None) if acl is None: - raise NoAuthorizationInformation('%s item has no __acl__' % acl) + raise NoAuthorizationInformation('%s item has no __acl__' % + self.context) for ace in acl: ace_action, ace_principal, ace_permissions = ace @@ -80,18 +79,14 @@ class ACLAuthorizer(object): if ace_principal == principal: permissions = flatten(ace_permissions) if permission in permissions: - action = ace_action - if action == Allow: - result = Allowed(ace, acl, permission, principals, + if ace_action == Allow: + return ACLAllowed(ace, acl, permission, principals, + self.context) + else: + return ACLDenied(ace, acl, permission, principals, self.context) - self.logger and self.logger.debug(str(result)) - return result - result = Denied(ace, acl, permission, principals, - self.context) - self.logger and self.logger.debug(str(result)) - return result - result = Denied(None, acl, permission, principals, self.context) - self.logger and self.logger.debug(str(result)) + # default deny + result = ACLDenied(None, acl, permission, principals, self.context) return result class ACLSecurityPolicy(object): @@ -102,18 +97,18 @@ class ACLSecurityPolicy(object): self.get_principals = get_principals def permits(self, context, request, permission): - """ Return ``Allowed`` if the policy permits access, - ``Denied`` if not.""" - logger = queryUtility(ILogger, name='repoze.bfg.debug') + """ Return ``ACLAllowed`` if the policy permits access, + ``ACLDenied`` if not. """ principals = self.effective_principals(request) for location in lineage(context): - authorizer = self.authorizer_factory(location, logger) + authorizer = self.authorizer_factory(location) try: return authorizer.permits(permission, *principals) + except NoAuthorizationInformation: continue - return False + return Denied(None, None, permission, principals, self.context) def authenticated_userid(self, request): principals = self.get_principals(request) @@ -202,26 +197,22 @@ def RepozeWhoIdentityACLSecurityPolicy(): return ACLSecurityPolicy(get_who_principals) class PermitsResult: - def __init__(self, ace, acl, permission, principals, context): - self.acl = acl - self.ace = ace - self.permission = permission - self.principals = principals - self.context_repr = repr(context) - def __str__(self): - msg = '%s: %r via ace %r in acl %r or principals %r in context %s' - msg = msg % (self.__class__.__name__, - self.permission, self.ace, self.acl, self.principals, - self.context_repr) - return msg + return self.msg + + def __repr__(self): + return '<%s instance at %s with msg %r>' % (self.__class__.__name__, + id(self), + self.msg) class Denied(PermitsResult): - """ An instance of ``Denied`` is returned by an ACL denial. 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.""" + """ An instance of ``Denied`` is returned when a security policy + or other ``repoze.bfg`` code denies an action unlrelated to an ACL + check. It evaluates equal to all boolean false types. It has an + attribute named ``msg`` describing the circumstances for the deny.""" + def __init__(self, msg): + self.msg = msg + def __nonzero__(self): return False @@ -229,17 +220,55 @@ class Denied(PermitsResult): return bool(other) is False class Allowed(PermitsResult): - """ An instance of ``Allowed`` is returned by an ACL allow. 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.""" + """ An instance of ``Allowed`` is returned when a security policy + or other ``repoze.bfg`` 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.""" + def __init__(self, msg): + self.msg = msg + def __nonzero__(self): return True def __eq__(self, other): return bool(other) is True +class ACLPermitsResult: + def __init__(self, ace, acl, permission, principals, context): + self.permission = permission + self.ace = ace + self.acl = acl + self.principals = principals + self.context = context + msg = ('%s permission %r via ACE %r in ACL %r on context %r for ' + 'principals %r') + msg = msg % (self.__class__.__name__, + self.permission, + self.ace, + self.acl, + self.context, + self.principals) + self.msg = msg + +class ACLDenied(ACLPermitsResult, Denied): + """ 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 he + ``msg`` attribute.""" + +class ACLAllowed(ACLPermitsResult, Allowed): + """ An instance of ``ACLDenied`` 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 he ``msg`` attribute.""" + def flatten(x): """flatten(sequence) -> list @@ -269,8 +298,9 @@ class ViewPermission(object): self.request = request self.permission_name = permission_name - def __call__(self, security_policy): - return security_policy.permits(self.context, self.request, + def __call__(self, security_policy, debug_info=None): + return security_policy.permits(self.context, + self.request, self.permission_name) def __repr__(self): @@ -289,5 +319,6 @@ class ViewPermissionFactory(object): class Unauthorized(Exception): pass + diff --git a/repoze/bfg/tests/test_registry.py b/repoze/bfg/tests/test_registry.py index 86977e941..73bdc40fe 100644 --- a/repoze/bfg/tests/test_registry.py +++ b/repoze/bfg/tests/test_registry.py @@ -79,6 +79,44 @@ class TestGetOptions(unittest.TestCase): {'BFG_DEBUG_AUTHORIZATION':'1'}) self.assertEqual(result['debug_authorization'], True) + def test_debug_notfound(self): + get_options = self._getFUT() + result = get_options({}) + self.assertEqual(result['debug_notfound'], False) + result = get_options({'debug_notfound':'false'}) + self.assertEqual(result['debug_notfound'], False) + result = get_options({'debug_notfound':'t'}) + self.assertEqual(result['debug_notfound'], True) + result = get_options({'debug_notfound':'1'}) + self.assertEqual(result['debug_notfound'], True) + result = get_options({}, {'BFG_DEBUG_NOTFOUND':'1'}) + self.assertEqual(result['debug_notfound'], True) + result = get_options({'debug_notfound':'false'}, + {'BFG_DEBUG_NOTFOUND':'1'}) + self.assertEqual(result['debug_notfound'], True) + + def test_debug_all(self): + get_options = self._getFUT() + result = get_options({}) + self.assertEqual(result['debug_notfound'], False) + self.assertEqual(result['debug_authorization'], False) + result = get_options({'debug_all':'false'}) + self.assertEqual(result['debug_notfound'], False) + self.assertEqual(result['debug_authorization'], False) + result = get_options({'debug_all':'t'}) + self.assertEqual(result['debug_notfound'], True) + self.assertEqual(result['debug_authorization'], True) + result = get_options({'debug_all':'1'}) + self.assertEqual(result['debug_notfound'], True) + self.assertEqual(result['debug_authorization'], True) + result = get_options({}, {'BFG_DEBUG_ALL':'1'}) + self.assertEqual(result['debug_notfound'], True) + self.assertEqual(result['debug_authorization'], True) + result = get_options({'debug_all':'false'}, + {'BFG_DEBUG_ALL':'1'}) + self.assertEqual(result['debug_notfound'], True) + self.assertEqual(result['debug_authorization'], True) + class TestThreadLocalRegistryManager(unittest.TestCase, PlacelessSetup): def setUp(self): PlacelessSetup.setUp(self) diff --git a/repoze/bfg/tests/test_router.py b/repoze/bfg/tests/test_router.py index a1c2b0cfb..c61694fd1 100644 --- a/repoze/bfg/tests/test_router.py +++ b/repoze/bfg/tests/test_router.py @@ -9,6 +9,33 @@ class RouterTests(unittest.TestCase, PlacelessSetup): def tearDown(self): PlacelessSetup.tearDown(self) + def _registerLogger(self): + import zope.component + gsm = zope.component.getGlobalSiteManager() + from repoze.bfg.interfaces import ILogger + class Logger: + def __init__(self): + self.messages = [] + def info(self, msg): + self.messages.append(msg) + debug = info + logger = Logger() + gsm.registerUtility(logger, ILogger, name='repoze.bfg.debug') + return logger + + def _registerSettings(self, **kw): + import zope.component + gsm = zope.component.getGlobalSiteManager() + from repoze.bfg.interfaces import ISettings + class Settings: + def __init__(self, **kw): + self.__dict__.update(kw) + + defaultkw = {'debug_authorization':False, 'debug_notfound':False} + defaultkw.update(kw) + settings = Settings(**defaultkw) + gsm.registerUtility(settings, ISettings) + def _registerTraverserFactory(self, app, name, *for_): import zope.component gsm = zope.component.getGlobalSiteManager() @@ -56,12 +83,51 @@ class RouterTests(unittest.TestCase, PlacelessSetup): environ.update(extras) return environ - def test_call_no_view_registered(self): + def test_call_no_view_registered_no_isettings(self): + rootpolicy = make_rootpolicy(None) + environ = self._makeEnviron() + context = DummyContext() + traversalfactory = make_traversal_factory(context, '', []) + self._registerTraverserFactory(traversalfactory, '', None) + logger = self._registerLogger() + router = self._makeOne(rootpolicy, None) + start_response = DummyStartResponse() + result = router(environ, start_response) + headers = start_response.headers + self.assertEqual(len(headers), 2) + status = start_response.status + self.assertEqual(status, '404 Not Found') + self.failUnless('http://localhost:8080' in result[0], result) + self.failIf('debug_notfound' in result[0]) + self.assertEqual(len(logger.messages), 0) + + def test_call_no_view_registered_debug_notfound_false(self): + rootpolicy = make_rootpolicy(None) + environ = self._makeEnviron() + context = DummyContext() + traversalfactory = make_traversal_factory(context, '', []) + self._registerTraverserFactory(traversalfactory, '', None) + logger = self._registerLogger() + self._registerSettings(debug_notfound=False) + router = self._makeOne(rootpolicy, None) + start_response = DummyStartResponse() + result = router(environ, start_response) + headers = start_response.headers + self.assertEqual(len(headers), 2) + status = start_response.status + self.assertEqual(status, '404 Not Found') + self.failUnless('http://localhost:8080' in result[0], result) + self.failIf('debug_notfound' in result[0]) + self.assertEqual(len(logger.messages), 0) + + def test_call_no_view_registered_debug_notfound_true(self): rootpolicy = make_rootpolicy(None) environ = self._makeEnviron() context = DummyContext() traversalfactory = make_traversal_factory(context, '', []) self._registerTraverserFactory(traversalfactory, '', None) + self._registerSettings(debug_notfound=True) + logger = self._registerLogger() router = self._makeOne(rootpolicy, None) start_response = DummyStartResponse() result = router(environ, start_response) @@ -69,7 +135,19 @@ class RouterTests(unittest.TestCase, PlacelessSetup): self.assertEqual(len(headers), 2) status = start_response.status self.assertEqual(status, '404 Not Found') + self.failUnless( + "debug_notfound of url http://localhost:8080; path_info: '', " + "context:" in result[0]) + self.failUnless( + "view_name: '', subpath: []" in result[0]) self.failUnless('http://localhost:8080' in result[0], result) + self.assertEqual(len(logger.messages), 1) + message = logger.messages[0] + self.failUnless('of url http://localhost:8080' in message) + self.failUnless("path_info: ''" in message) + self.failUnless('DummyContext instance at' in message) + self.failUnless("view_name: ''" in message) + self.failUnless("subpath: []" in message) def test_call_view_registered_nonspecific_default_path(self): rootpolicy = make_rootpolicy(None) @@ -207,7 +285,68 @@ class RouterTests(unittest.TestCase, PlacelessSetup): self.assertEqual(start_response.status, '200 OK') self.assertEqual(permissionfactory.checked_with, secpol) - def test_call_view_registered_security_policy_permission_fails(self): + def test_call_view_permission_fails_nosettings(self): + rootpolicy = make_rootpolicy(None) + from zope.interface import Interface + from zope.interface import directlyProvides + class IContext(Interface): + pass + from repoze.bfg.interfaces import IRequest + context = DummyContext() + directlyProvides(context, IContext) + traversalfactory = make_traversal_factory(context, '', ['']) + response = DummyResponse() + view = make_view(response) + secpol = DummySecurityPolicy() + from repoze.bfg.security import ACLDenied + permissionfactory = make_permission_factory( + ACLDenied('ace', 'acl', 'permission', ['principals'], context) + ) + environ = self._makeEnviron() + self._registerTraverserFactory(traversalfactory, '', None) + self._registerView(view, '', IContext, IRequest) + self._registerSecurityPolicy(secpol) + self._registerPermission(permissionfactory, '', IContext, IRequest) + router = self._makeOne(rootpolicy, None) + start_response = DummyStartResponse() + result = router(environ, start_response) + self.assertEqual(start_response.status, '401 Unauthorized') + message = result[0] + self.failUnless('failed security policy check' in message) + self.assertEqual(permissionfactory.checked_with, secpol) + + def test_call_view_permission_fails_no_debug_auth(self): + rootpolicy = make_rootpolicy(None) + from zope.interface import Interface + from zope.interface import directlyProvides + class IContext(Interface): + pass + from repoze.bfg.interfaces import IRequest + context = DummyContext() + directlyProvides(context, IContext) + traversalfactory = make_traversal_factory(context, '', ['']) + response = DummyResponse() + view = make_view(response) + secpol = DummySecurityPolicy() + from repoze.bfg.security import ACLDenied + permissionfactory = make_permission_factory( + ACLDenied('ace', 'acl', 'permission', ['principals'], context) + ) + environ = self._makeEnviron() + self._registerTraverserFactory(traversalfactory, '', None) + self._registerView(view, '', IContext, IRequest) + self._registerSecurityPolicy(secpol) + self._registerPermission(permissionfactory, '', IContext, IRequest) + self._registerSettings(debug_authorization=False) + router = self._makeOne(rootpolicy, None) + start_response = DummyStartResponse() + result = router(environ, start_response) + self.assertEqual(start_response.status, '401 Unauthorized') + message = result[0] + self.failUnless('failed security policy check' in message) + self.assertEqual(permissionfactory.checked_with, secpol) + + def test_call_view_permission_fails_with_debug_auth(self): rootpolicy = make_rootpolicy(None) from zope.interface import Interface from zope.interface import directlyProvides @@ -220,21 +359,37 @@ class RouterTests(unittest.TestCase, PlacelessSetup): response = DummyResponse() view = make_view(response) secpol = DummySecurityPolicy() - from repoze.bfg.security import Denied + from repoze.bfg.security import ACLDenied permissionfactory = make_permission_factory( - Denied('ace', 'acl', 'permission', ['principals'], context) + ACLDenied('ace', 'acl', 'permission', ['principals'], context) ) environ = self._makeEnviron() self._registerTraverserFactory(traversalfactory, '', None) self._registerView(view, '', IContext, IRequest) self._registerSecurityPolicy(secpol) self._registerPermission(permissionfactory, '', IContext, IRequest) + self._registerSettings(debug_authorization=True) + logger = self._registerLogger() router = self._makeOne(rootpolicy, None) start_response = DummyStartResponse() result = router(environ, start_response) self.assertEqual(start_response.status, '401 Unauthorized') - self.failUnless('permission' in result[0]) + message = result[0] + self.failUnless( + "ACLDenied permission 'permission' via ACE 'ace' in ACL 'acl' " + "on context" in message) + self.failUnless("for principals ['principals']" in message) self.assertEqual(permissionfactory.checked_with, secpol) + self.assertEqual(len(logger.messages), 1) + logged = logger.messages[0] + self.failUnless( + "debug_authorization of url http://localhost:8080 (view name " + "'' against context" in logged) + self.failUnless( + "ACLDenied permission 'permission' via ACE 'ace' in ACL 'acl' on " + "context" in logged) + self.failUnless( + "for principals ['principals']" in logged) def test_call_eventsends(self): rootpolicy = make_rootpolicy(None) diff --git a/repoze/bfg/tests/test_security.py b/repoze/bfg/tests/test_security.py index a9a30ee6f..43dc38890 100644 --- a/repoze/bfg/tests/test_security.py +++ b/repoze/bfg/tests/test_security.py @@ -11,72 +11,85 @@ class TestACLAuthorizer(unittest.TestCase): klass = self._getTargetClass() return klass(*arg, **kw) + def test_deny_implicit(self): + context = DummyContext() + from repoze.bfg.security import Allow + ace = (Allow, 'somebodyelse', 'read') + acl = [ace] + context.__acl__ = acl + authorizer = self._makeOne(context) + principals = ['fred'] + result = authorizer.permits('read', *principals) + + def test_deny_explicit(self): + context = DummyContext() + from repoze.bfg.security import Deny + ace = (Deny, 'somebodyelse', 'read') + acl = [ace] + context.__acl__ = acl + authorizer = self._makeOne(context) + principals = ['somebodyelse'] + result = authorizer.permits('read', *principals) + def test_permits_no_acl_raises(self): context = DummyContext() - logger = DummyLogger() - authorizer = self._makeOne(context, logger) + authorizer = self._makeOne(context) from repoze.bfg.interfaces import NoAuthorizationInformation self.assertRaises(NoAuthorizationInformation, authorizer.permits, (), None) def test_permits_deny_implicit_empty_acl(self): context = DummyContext() - logger = DummyLogger() context.__acl__ = [] - authorizer = self._makeOne(context, logger) + authorizer = self._makeOne(context) result = authorizer.permits((), None) self.assertEqual(result, False) self.assertEqual(result.ace, None) def test_permits_deny_no_principals_implicit(self): context = DummyContext() - logger = DummyLogger() from repoze.bfg.security import Allow from repoze.bfg.security import Everyone acl = [(Allow, Everyone, 'view')] context.__acl__ = acl - authorizer = self._makeOne(context, logger) + authorizer = self._makeOne(context) result = authorizer.permits(None) self.assertEqual(result, False) self.assertEqual(result.ace, None) def test_permits_deny_oneacl_implicit(self): context = DummyContext() - logger = DummyLogger() from repoze.bfg.security import Allow acl = [(Allow, 'somebody', 'view')] context.__acl__ = acl - authorizer = self._makeOne(context, logger) + authorizer = self._makeOne(context) result = authorizer.permits('view', 'somebodyelse') self.assertEqual(result, False) self.assertEqual(result.ace, None) def test_permits_deny_twoacl_implicit(self): context = DummyContext() - logger = DummyLogger() from repoze.bfg.security import Allow acl = [(Allow, 'somebody', 'view'), (Allow, 'somebody', 'write')] context.__acl__ = acl - authorizer = self._makeOne(context, logger) + authorizer = self._makeOne(context) result = authorizer.permits('view', 'somebodyelse') self.assertEqual(result, False) self.assertEqual(result.ace, None) def test_permits_deny_oneacl_explcit(self): context = DummyContext() - logger = DummyLogger() from repoze.bfg.security import Deny ace = (Deny, 'somebody', 'view') acl = [ace] context.__acl__ = acl - authorizer = self._makeOne(context, logger) + authorizer = self._makeOne(context) result = authorizer.permits('view', 'somebody') self.assertEqual(result, False) self.assertEqual(result.ace, ace) def test_permits_deny_oneacl_multiperm_explcit(self): context = DummyContext() - logger = DummyLogger() acl = [] from repoze.bfg.security import Deny from repoze.bfg.security import Allow @@ -84,14 +97,13 @@ class TestACLAuthorizer(unittest.TestCase): allow = (Allow, 'somebody', 'view') acl = [deny, allow] context.__acl__ = acl - authorizer = self._makeOne(context, logger) + authorizer = self._makeOne(context) result = authorizer.permits('view', 'somebody') self.assertEqual(result, False) self.assertEqual(result.ace, deny) def test_permits_deny_twoacl_explicit(self): context = DummyContext() - logger = DummyLogger() acl = [] from repoze.bfg.security import Deny from repoze.bfg.security import Allow @@ -99,34 +111,32 @@ class TestACLAuthorizer(unittest.TestCase): deny = (Deny, 'somebody', 'view') acl = [allow, deny] context.__acl__ = acl - authorizer = self._makeOne(context, logger) + authorizer = self._makeOne(context) result = authorizer.permits('view', 'somebody') self.assertEqual(result, False) self.assertEqual(result.ace, deny) def test_permits_allow_twoacl_explicit(self): context = DummyContext() - logger = DummyLogger() from repoze.bfg.security import Deny from repoze.bfg.security import Allow allow = (Allow, 'somebody', 'read') deny = (Deny, 'somebody', 'view') acl = [allow, deny] context.__acl__ = acl - authorizer = self._makeOne(context, logger) + authorizer = self._makeOne(context) result = authorizer.permits('read', 'somebody') self.assertEqual(result, True) self.assertEqual(result.ace, allow) def test_permits_nested_principals_list_allow(self): context = DummyContext() - logger = DummyLogger() acl = [] from repoze.bfg.security import Allow ace = (Allow, 'larry', 'read') acl = [ace] context.__acl__ = acl - authorizer = self._makeOne(context, logger) + authorizer = self._makeOne(context) principals = (['fred', ['jim', ['bob', 'larry']]]) result = authorizer.permits('read', *principals) self.assertEqual(result, True) @@ -134,12 +144,11 @@ class TestACLAuthorizer(unittest.TestCase): def test_permits_nested_principals_list_deny_explicit(self): context = DummyContext() - logger = DummyLogger() from repoze.bfg.security import Deny ace = (Deny, 'larry', 'read') acl = [ace] context.__acl__ = acl - authorizer = self._makeOne(context, logger) + authorizer = self._makeOne(context) principals = (['fred', ['jim', ['bob', 'larry']]]) result = authorizer.permits('read', *principals) self.assertEqual(result, False) @@ -147,12 +156,11 @@ class TestACLAuthorizer(unittest.TestCase): def test_permits_nested_principals_list_deny_implicit(self): context = DummyContext() - logger = DummyLogger() from repoze.bfg.security import Allow ace = (Allow, 'somebodyelse', 'read') acl = [ace] context.__acl__ = acl - authorizer = self._makeOne(context, logger) + authorizer = self._makeOne(context) principals = (['fred', ['jim', ['bob', 'larry']]]) result = authorizer.permits('read', *principals) self.assertEqual(result, False) @@ -161,7 +169,6 @@ class TestACLAuthorizer(unittest.TestCase): context = DummyContext() context.__parent__ = None context.__name__ = None - logger = DummyLogger() from repoze.bfg.security import Allow ace = (Allow, 'fred', 'read') acl = [ace] @@ -169,46 +176,11 @@ class TestACLAuthorizer(unittest.TestCase): context2 = DummyContext() context2.__parent__ = context context2.__name__ = 'myname' - authorizer = self._makeOne(context, logger) + authorizer = self._makeOne(context) principals = ['fred'] result = authorizer.permits('read', *principals) self.assertEqual(result, True) - def test_logging_deny_implicit(self): - context = DummyContext() - logger = DummyLogger() - from repoze.bfg.security import Allow - ace = (Allow, 'somebodyelse', 'read') - acl = [ace] - context.__acl__ = acl - authorizer = self._makeOne(context, logger) - principals = ['fred'] - result = authorizer.permits('read', *principals) - self.assertEqual(len(logger.messages), 1) - - def test_logging_deny_explicit(self): - context = DummyContext() - logger = DummyLogger() - from repoze.bfg.security import Deny - ace = (Deny, 'somebodyelse', 'read') - acl = [ace] - context.__acl__ = acl - authorizer = self._makeOne(context, logger) - principals = ['somebodyelse'] - result = authorizer.permits('read', *principals) - self.assertEqual(len(logger.messages), 1) - - def test_logging_allow(self): - context = DummyContext() - logger = DummyLogger() - from repoze.bfg.security import Allow - ace = (Allow, 'somebodyelse', 'read') - acl = [ace] - context.__acl__ = acl - authorizer = self._makeOne(context, logger) - principals = ['somebodyelse'] - result = authorizer.permits('read', *principals) - self.assertEqual(len(logger.messages), 1) class TestACLSecurityPolicy(unittest.TestCase, PlacelessSetup): def _getTargetClass(self): @@ -219,12 +191,6 @@ class TestACLSecurityPolicy(unittest.TestCase, PlacelessSetup): klass = self._getTargetClass() return klass(*arg, **kw) - def _registerLogger(self, logger): - import zope.component - gsm = zope.component.getGlobalSiteManager() - from repoze.bfg.interfaces import ILogger - gsm.registerUtility(logger, ILogger, name='repoze.bfg.debug') - def setUp(self): PlacelessSetup.setUp(self) @@ -305,26 +271,6 @@ class TestACLSecurityPolicy(unittest.TestCase, PlacelessSetup): self.assertEqual(authorizer_factory.permission, 'view') self.assertEqual(authorizer_factory.context, context) - def test_permits_with_logger(self): - logger = DummyLogger() - self._registerLogger(logger) - context = DummyContext() - request = DummyRequest({}) - policy = self._makeOne(lambda *arg: None) - authorizer_factory = make_authorizer_factory(context) - policy.authorizer_factory = authorizer_factory - policy.permits(context, request, 'view') - self.assertEqual(authorizer_factory.logger, logger) - - def test_permits_no_logger(self): - context = DummyContext() - request = DummyRequest({}) - policy = self._makeOne(lambda *arg: None) - authorizer_factory = make_authorizer_factory(context) - policy.authorizer_factory = authorizer_factory - policy.permits(context, request, 'view') - self.assertEqual(authorizer_factory.logger, None) - def test_principals_allowed_by_permission_direct(self): from repoze.bfg.security import Allow context = DummyContext() @@ -526,6 +472,82 @@ class TestViewPermissionFactory(unittest.TestCase): self.assertEqual(result.permission_name, 'repoze.view') self.assertEqual(result.context, context) self.assertEqual(result.request, request) + +class TestAllowed(unittest.TestCase): + def _getTargetClass(self): + from repoze.bfg.security import Allowed + return Allowed + + def _makeOne(self, *arg, **kw): + klass = self._getTargetClass() + return klass(*arg, **kw) + + def test_it(self): + allowed = self._makeOne('hello') + self.assertEqual(allowed.msg, 'hello') + self.assertEqual(allowed, True) + self.failUnless(allowed) + self.assertEqual(str(allowed), 'hello') + self.failUnless('<Allowed instance at ' in repr(allowed)) + self.failUnless("with msg 'hello'>" in repr(allowed)) + +class TestDenied(unittest.TestCase): + def _getTargetClass(self): + from repoze.bfg.security import Denied + return Denied + + def _makeOne(self, *arg, **kw): + klass = self._getTargetClass() + return klass(*arg, **kw) + + def test_it(self): + denied = self._makeOne('hello') + self.assertEqual(denied.msg, 'hello') + self.assertEqual(denied, False) + self.failIf(denied) + self.assertEqual(str(denied), 'hello') + self.failUnless('<Denied instance at ' in repr(denied)) + self.failUnless("with msg 'hello'>" in repr(denied)) + +class TestACLAllowed(unittest.TestCase): + def _getTargetClass(self): + from repoze.bfg.security import ACLAllowed + return ACLAllowed + + def _makeOne(self, *arg, **kw): + klass = self._getTargetClass() + return klass(*arg, **kw) + + def test_it(self): + 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.failUnless(msg in allowed.msg) + self.assertEqual(allowed, True) + self.failUnless(allowed) + self.assertEqual(str(allowed), msg) + self.failUnless('<ACLAllowed instance at ' in repr(allowed)) + self.failUnless("with msg %r>" % msg in repr(allowed)) + +class TestACLDenied(unittest.TestCase): + def _getTargetClass(self): + from repoze.bfg.security import ACLDenied + return ACLDenied + + def _makeOne(self, *arg, **kw): + klass = self._getTargetClass() + return klass(*arg, **kw) + + def test_it(self): + 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.failUnless(msg in denied.msg) + self.assertEqual(denied, False) + self.failIf(denied) + self.assertEqual(str(denied), msg) + self.failUnless('<ACLDenied instance at ' in repr(denied)) + self.failUnless("with msg %r>" % msg in repr(denied)) class DummyContext: pass @@ -551,25 +573,18 @@ class DummySecurityPolicy: def principals_allowed_by_permission(self, context, permission): return ['fred', 'bob'] -class DummyLogger: - def __init__(self): - self.messages = [] - def debug(self, msg): - self.messages.append(msg) - class make_authorizer_factory: def __init__(self, expected_context, intermediates_raise=False): self.expected_context = expected_context self.intermediates_raise = intermediates_raise - def __call__(self, context, logger): + def __call__(self, context): authorizer = self class Authorizer: def permits(self, permission, *principals): authorizer.permission = permission authorizer.principals = principals authorizer.context = context - authorizer.logger = logger result = authorizer.expected_context == context if not result and authorizer.intermediates_raise: from repoze.bfg.interfaces import NoAuthorizationInformation diff --git a/repoze/bfg/tests/test_view.py b/repoze/bfg/tests/test_view.py index 3636692a8..46687e904 100644 --- a/repoze/bfg/tests/test_view.py +++ b/repoze/bfg/tests/test_view.py @@ -372,9 +372,73 @@ class TestIsResponse(unittest.TestCase): f = self._getFUT() self.assertEqual(f(response), False) +class TestViewExecutionPermitted(unittest.TestCase, PlacelessSetup): + def setUp(self): + PlacelessSetup.setUp(self) + + def tearDown(self): + PlacelessSetup.tearDown(self) + + def _callFUT(self, *arg, **kw): + from repoze.bfg.view import view_execution_permitted + return view_execution_permitted(*arg, **kw) + + def _registerSecurityPolicy(self, secpol): + import zope.component + gsm = zope.component.getGlobalSiteManager() + from repoze.bfg.interfaces import ISecurityPolicy + gsm.registerUtility(secpol, ISecurityPolicy) + + def _registerPermission(self, permission, name, *for_): + import zope.component + gsm = zope.component.getGlobalSiteManager() + from repoze.bfg.interfaces import IViewPermission + gsm.registerAdapter(permission, for_, IViewPermission, name) + + def test_no_secpol(self): + context = DummyContext() + request = DummyRequest() + result = self._callFUT(context, request, '') + msg = result.msg + self.failUnless("Allowed: view name '' in context" in msg) + self.failUnless('(no security policy in use)' in msg) + self.assertEqual(result, True) + + def test_secpol_no_permission(self): + secpol = DummySecurityPolicy() + self._registerSecurityPolicy(secpol) + context = DummyContext() + request = DummyRequest() + result = self._callFUT(context, request, '') + msg = result.msg + self.failUnless("Allowed: view name '' in context" in msg) + self.failUnless("(no permission registered for name '')" in msg) + self.assertEqual(result, True) + + def test_secpol_and_permission(self): + from zope.interface import Interface + from zope.interface import directlyProvides + from repoze.bfg.interfaces import IRequest + class IContext(Interface): + pass + context = DummyContext() + directlyProvides(context, IContext) + permissionfactory = make_permission_factory(True) + self._registerPermission(permissionfactory, '', IContext, + IRequest) + secpol = DummySecurityPolicy() + self._registerSecurityPolicy(secpol) + request = DummyRequest() + directlyProvides(request, IRequest) + result = self._callFUT(context, request, '') + self.failUnless(result is True) + class DummyContext: pass +class DummyRequest: + pass + def make_view(response): def view(context, request): return response diff --git a/repoze/bfg/view.py b/repoze/bfg/view.py index 0012019e7..ae4f304f0 100644 --- a/repoze/bfg/view.py +++ b/repoze/bfg/view.py @@ -4,10 +4,31 @@ from zope.component import queryUtility from repoze.bfg.interfaces import ISecurityPolicy from repoze.bfg.interfaces import IViewPermission from repoze.bfg.interfaces import IView + from repoze.bfg.security import Unauthorized +from repoze.bfg.security import Allowed _marker = () +def view_execution_permitted(context, request, name=''): + """ If the view specified by ``context`` and ``name`` is protected + by a permission, return the result of checking the permission + associated with the view using the effective security policy and + the ``request``. If no security policy is in effect, or if the + view is not protected by a permission, return a True value. """ + security_policy = queryUtility(ISecurityPolicy) + if security_policy: + permission = queryMultiAdapter((context, request), IViewPermission, + name=name) + if permission is None: + return Allowed( + 'Allowed: view name %r in context %r (no permission ' + 'registered for name %r).' % (name, context, name) + ) + return permission(security_policy) + return Allowed('Allowed: view name %r in context %r (no security policy ' + 'in use).' % (name, context)) + def render_view_to_response(context, request, name='', secure=True): """ Render the view named ``name`` against the specified ``context`` and ``request`` to an object implementing @@ -24,16 +45,13 @@ def render_view_to_response(context, request, name='', secure=True): ``args`` attribute explains why the view access was disallowed. If ``secure`` is ``False``, no permission checking is done.""" if secure: - security_policy = queryUtility(ISecurityPolicy) - if security_policy: - permission = queryMultiAdapter((context, request), IViewPermission, - name=name) - if permission is not None: - result = permission(security_policy) - if not result: - raise Unauthorized(result) + permitted = view_execution_permitted(context, request, name) + if not permitted: + raise Unauthorized(permitted) + response = queryMultiAdapter((context, request), IView, name=name, default=_marker) + if response is _marker: return None |
