diff options
| author | Chris McDonough <chrism@agendaless.com> | 2009-01-25 02:30:46 +0000 |
|---|---|---|
| committer | Chris McDonough <chrism@agendaless.com> | 2009-01-25 02:30:46 +0000 |
| commit | cfd4e5e06d05dac3e8f1c03b63bc3cf37242334a (patch) | |
| tree | 506481ffc66fef568749c27bcd2611fe9a98d1b0 | |
| parent | fbd7ff75bde1c59c1b791b0db1be4f81f81d2d3d (diff) | |
| download | pyramid-cfd4e5e06d05dac3e8f1c03b63bc3cf37242334a.tar.gz pyramid-cfd4e5e06d05dac3e8f1c03b63bc3cf37242334a.tar.bz2 pyramid-cfd4e5e06d05dac3e8f1c03b63bc3cf37242334a.zip | |
- You can now override the NotFound and Unauthorized responses that
:mod:`repoze.bfg` generates when a view cannot be found or cannot be
invoked due to lack of permission. See the "ZCML Hooks" chapter in
the docs for more information.
- Use a homegrown Unauthorized error instead of
``webob.exc.Unauthorized`` (the latter is slow).
- Various speed micro-tweaks.
| -rw-r--r-- | CHANGES.txt | 13 | ||||
| -rw-r--r-- | docs/narr/hooks.rst | 135 | ||||
| -rw-r--r-- | repoze/bfg/interfaces.py | 25 | ||||
| -rw-r--r-- | repoze/bfg/router.py | 82 | ||||
| -rw-r--r-- | repoze/bfg/wsgi.py | 25 |
5 files changed, 222 insertions, 58 deletions
diff --git a/CHANGES.txt b/CHANGES.txt index abd884923..8eea97d1f 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,5 +1,13 @@ Next Release +Features +-------- + +- You can now override the NotFound and Unauthorized responses that + :mod:`repoze.bfg` generates when a view cannot be found or cannot be + invoked due to lack of permission. See the "ZCML Hooks" chapter in + the docs for more information. + Behavior Changes ---------------- @@ -19,9 +27,12 @@ Behavior Changes Implementation Changes ---------------------- -- Use a homegrown NotFound error instead of ``webob.HTTPNotFound`` +- Use a homegrown NotFound error instead of ``webob.exc.HTTPNotFound`` (the latter is slow). +- Use a homegrown Unauthorized error instead of + ``webob.exc.Unauthorized`` (the latter is slow). + - Various speed micro-tweaks. Bug Fixes diff --git a/docs/narr/hooks.rst b/docs/narr/hooks.rst index 08164eddc..585e3e655 100644 --- a/docs/narr/hooks.rst +++ b/docs/narr/hooks.rst @@ -5,7 +5,7 @@ Using ZCML Hooks ZCML "hooks" can be used to influence the behavior of the :mod:`repoze.bfg` framework in various ways. This is an advanced -topic; very few people will want or need to do any of these things. +topic; not many people will want or need to do any of these things. Changing the request factory ---------------------------- @@ -21,22 +21,36 @@ ZCML in your ``configure.zcml`` file. :linenos: <utility provides="repoze.bfg.interfaces.IRequestFactory" - component=".my.request.factory"/> + component="helloworld.factories.request_factory"/> -Replace ``my.request.factory`` with the Python dotted name to the -request factory you want to use. +Replace ``helloworld.factories.request_factory`` with the Python +dotted name to the request factory you want to use. Here's some +sample code that implements a minimal request factory: -.. warning:: If you register an IRequestFactory utility in such a way, - you *must* be sure that the factory returns an object that +.. code-block:: python + + from webob import Request + from repoze.bfg.interfaces import IRequest + + class MyRequest(Request): + implements(IRequest) + + def request_factory(): + return MyRequest + +.. warning:: If you register an ``IRequestFactory`` utility in this + way, you *must* be sure that the factory returns an object that implements *at least* the ``repoze.bfg.interfaces.IRequest`` interface. Otherwise all application view lookups will fail (they will all return a 404 response code). Likewise, if you want to be able to use method-related interfaces such as ``IGETRequest``, - ``IPOSTRequest``, etc. in your view declarations, your factory must - also do the same introspection of the environ that the default - request factory does, and cause the custom factory to decorate the + ``IPOSTRequest``, etc. in your view declarations, the callable + returned by the factory must also do the same introspection of the + environ that the default request factory does and decorate the returned object to implement one of these interfaces based on the - ``HTTP_METHOD`` present in the environ. + ``HTTP_METHOD`` present in the environ. Note that the above + example does not do this, so lookups for method-related interfaces + will fail. Changing the response factory ----------------------------- @@ -53,8 +67,103 @@ following ZCML in your ``configure.zcml`` file. :linenos: <utility provides="repoze.bfg.interfaces.IResponseFactory" - component=".my.response.factory"/> + component="helloworld.factories.response_factory"/> + +Replace ``helloworld.factories.response_factory`` with the Python +dotted name to the response factory you want to use. Here's some +sample code that implements a minimal response factory: + +.. code-block:: python + + from webob import Response + + class MyResponse(Response): + pass + + def response_factory(): + return MyResponse + +Unlike a request factory, a response factory does not need to return +an object that implements any particular interface; it simply needs +have a ``status`` attribute, a ``headerlist`` attribute, and and +``app_iter`` attribute. + +Changing the NotFound application +--------------------------------- + +When :mod:`repoze.bfg` can't map a URL to code, it creates and invokes +a NotFound WSGI application. The application it invokes can be +customized by placing something like the following ZCML in your +``configure.zcml`` file. + +.. code-block:: xml + :linenos: + + <utility provides="repoze.bfg.interfaces.INotFoundAppFactory" + component="helloworld.factories.notfound_app_factory"/> + +Replace ``helloworld.factories.notfound_app_factory`` with the Python +dotted name to the request factory you want to use. Here's some +sample code that implements a minimal NotFound application factory: + +.. code-block:: python + + from webob.exc import HTTPNotFound + + class MyNotFound(HTTPNotFound): + pass + + def notfound_app_factory(): + return MyNotFound + +.. note:: When a NotFound application factory is invoked, it is passed + the WSGI environ and the WSGI ``start_response`` handler by + :mod:`repoze.bfg`. Within the WSGI environ will be a key named + ``message`` that has a value explaining why the not found error was + raised. This error will be different when the ``debug_notfound`` + environment setting is true than it is when it is false. + +Changing the Unauthorized application +------------------------------------- + +When :mod:`repoze.bfg` can't authorize execution of a view based on +the security policy in use, it creates and invokes an Unauthorized +WSGI application. The application it invokes can be customized by +placing something like the following ZCML in your ``configure.zcml`` +file. + +.. code-block:: xml + :linenos: + + <utility provides="repoze.bfg.interfaces.IUnauthorizedAppFactory" + component="helloworld.factories.unauthorized_app_factory"/> + +Replace ``helloworld.factories.unauthorized_app_factory`` with the +Python dotted name to the request factory you want to use. Here's +some sample code that implements a minimal Unauthorized application +factory: + +.. code-block:: python + + from webob.exc import HTTPUnauthorized + + class MyUnauthorized(HTTPUnauthorized): + pass + + def notfound_app_factory(): + return MyUnauthorized -Replace ``my.response.factory`` with the Python dotted name to the -response factory you want to use. +.. note:: When an Unauthorized application factory is invoked, it is + passed the WSGI environ and the WSGI ``start_response`` handler by + :mod:`repoze.bfg`. Within the WSGI environ will be a key named + ``message`` that has a value explaining why the action was not + authorized. This error will be different when the + ``debug_authorization`` environment setting is true than it is when + it is false. +.. note:: You can influence the status code of Unauthorized responses + by using an alterate unauthorized application factory. For + example, you may return an unauthorized application with a ``403 + Forbidden`` status code, rather than use the default unauthorized + application factory, which sends a response with a ``401 + Unauthorized`` status code. diff --git a/repoze/bfg/interfaces.py b/repoze/bfg/interfaces.py index 58a0e3257..595bce5c8 100644 --- a/repoze/bfg/interfaces.py +++ b/repoze/bfg/interfaces.py @@ -1,14 +1,10 @@ from zope.interface import Attribute from zope.interface import Interface -from zope.interface import implements from zope.deferredimport import deprecated from zope.component.interfaces import IObjectEvent -from webob import Request as WebobRequest - - deprecated( '(repoze.bfg.interfaces.ITemplate should now be imported ' 'as repoze.bfg.interfaces.ITemplateRenderer)', @@ -34,9 +30,9 @@ deprecated( ) class IRequestFactory(Interface): - """ A utility which generates a request factory """ + """ A utility which generates a request object """ def __call__(): - """ Return a request factory (e.g. a callable that accepts an + """ Return a request factory (a callable that accepts an environ and returns an object implementing IRequest, e.g. ``webob.Request``)""" @@ -191,3 +187,20 @@ class IContextNotFound(Interface): """ Interface implemented by contexts generated by code which cannot find a context during root finding or traversal """ +class INotFoundAppFactory(Interface): + """ A utility which returns a NotFound WSGI application factory """ + def __call__(): + """ Return a callable which returns a notfound WSGI + application. When the WSGI application is invoked, + a``message`` key in the WSGI environ provides information + pertaining to the reason for the notfound.""" + +class IUnauthorizedAppFactory(Interface): + """ A utility which returns an Unauthorized WSGI application + factory""" + def __call__(): + """ Return a callable which returns an unauthorized WSGI + application. When the WSGI application is invoked, a + ``message`` key in the WSGI environ provides information + pertaining to the reason for the unauthorized.""" + diff --git a/repoze/bfg/router.py b/repoze/bfg/router.py index 84f267791..c7c38e1d1 100644 --- a/repoze/bfg/router.py +++ b/repoze/bfg/router.py @@ -1,17 +1,15 @@ import sys -from cgi import escape from zope.component.event import dispatch from zope.interface import implements -from webob.exc import HTTPUnauthorized - 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 INotFoundAppFactory from repoze.bfg.interfaces import IRequestFactory from repoze.bfg.interfaces import IRootFactory from repoze.bfg.interfaces import IRouter @@ -19,6 +17,7 @@ from repoze.bfg.interfaces import IRoutesMapper from repoze.bfg.interfaces import ITraverserFactory from repoze.bfg.interfaces import ISecurityPolicy from repoze.bfg.interfaces import ISettings +from repoze.bfg.interfaces import IUnauthorizedAppFactory from repoze.bfg.interfaces import IView from repoze.bfg.interfaces import IViewPermission @@ -36,6 +35,8 @@ from repoze.bfg.settings import Settings from repoze.bfg.urldispatch import RoutesRootFactory from repoze.bfg.view import _view_execution_permitted +from repoze.bfg.wsgi import Unauthorized +from repoze.bfg.wsgi import NotFound _marker = object() @@ -45,15 +46,31 @@ class Router(object): def __init__(self, registry): self.registry = registry - self.settings = registry.queryUtility(ISettings) + self.request_factory = registry.queryUtility(IRequestFactory) self.security_policy = registry.queryUtility(ISecurityPolicy) - self.logger = registry.queryUtility(ILogger, 'repoze.bfg.debug') - @property - def root_policy(self): - """ Backwards compatibility alias """ - return self.registry.getUtility(IRootFactory) + notfound_app_factory = registry.queryUtility(INotFoundAppFactory) + if notfound_app_factory is None: + notfound_app_factory = NotFound + self.notfound_app_factory = notfound_app_factory + + unauth_app_factory = registry.queryUtility(IUnauthorizedAppFactory) + if unauth_app_factory is None: + unauth_app_factory = Unauthorized + self.unauth_app_factory = unauth_app_factory + + settings = registry.queryUtility(ISettings) + if settings is None: + self.debug_authorization = False + self.debug_notfound = False + else: + self.debug_authorization = settings.debug_authorization + self.debug_notfound = settings.debug_notfound + + self.logger = registry.queryUtility(ILogger, 'repoze.bfg.debug') + self.root_factory = registry.getUtility(IRootFactory) + self.root_policy = self.root_factory # b/w compat def __call__(self, environ, start_response): """ @@ -66,16 +83,20 @@ class Router(object): registry_manager.push(registry) try: - request_factory = self.request_factory - if request_factory is None: + if self.request_factory is None: method = environ['REQUEST_METHOD'] - request_factory = HTTP_METHOD_FACTORIES.get(method, Request) + try: + # for speed we disuse HTTP_METHOD_FACTORIES.get + request_factory = HTTP_METHOD_FACTORIES[method] + except KeyError: + request_factory = Request + else: + request_factory = self.request_factory request = request_factory(environ) registry.has_listeners and registry.notify(NewRequest(request)) - root_factory = registry.getUtility(IRootFactory) - root = root_factory(environ) + root = self.root_factory(environ) traverser = registry.getAdapter(root, ITraverserFactory) context, view_name, subpath = traverser(environ) @@ -85,9 +106,6 @@ class Router(object): request.view_name = view_name request.subpath = subpath - settings = self.settings - - debug_authorization = settings and settings.debug_authorization security_policy = self.security_policy permission = None @@ -97,6 +115,8 @@ class Router(object): IViewPermission, name=view_name) + debug_authorization = self.debug_authorization + permitted = _view_execution_permitted(context, request, view_name, security_policy, permission, debug_authorization) @@ -114,15 +134,15 @@ class Router(object): msg = str(permitted) else: msg = 'Unauthorized: failed security policy check' - app = HTTPUnauthorized(escape(msg)) - return app(environ, start_response) + environ['message'] = msg + unauth_app = self.unauth_app_factory() + return unauth_app(environ, start_response) response = registry.queryMultiAdapter( (context, request), IView, name=view_name) if response is None: - debug_notfound = settings and settings.debug_notfound - if debug_notfound: + if self.debug_notfound: msg = ( 'debug_notfound of url %s; path_info: %r, context: %r, ' 'view_name: %r, subpath: %r' % ( @@ -132,9 +152,9 @@ class Router(object): logger and logger.debug(msg) else: msg = request.url - notfound = NotFound(msg) - start_response(notfound.status, notfound.headerlist) - return notfound.app_iter + environ['message'] = msg + notfound_app = self.notfound_app_factory() + return notfound_app(environ, start_response) registry.has_listeners and registry.notify(NewResponse(response)) @@ -148,20 +168,6 @@ class Router(object): finally: registry_manager.pop() -class NotFound(object): - """ Avoid using WebOb's NotFound WSGI response app; it's slow. """ - def __init__(self, msg=''): - html = """<body> - <html><title>404 Not Found</title><body><h1>404 Not Found</h1> - <code>%s</code> - """ % msg - self.headerlist = [ - ('Content-Length', len(html) ), - ('Content-Type', 'text/html') - ] - self.app_iter = [html] - self.status = '404 Not Found' - def make_app(root_factory, package=None, filename='configure.zcml', options=None): """ Return a Router object, representing a ``repoze.bfg`` WSGI diff --git a/repoze/bfg/wsgi.py b/repoze/bfg/wsgi.py index b0feef29e..38cca58a3 100644 --- a/repoze/bfg/wsgi.py +++ b/repoze/bfg/wsgi.py @@ -1,3 +1,5 @@ +from cgi import escape + try: from functools import wraps except ImportError: @@ -31,3 +33,26 @@ def wsgiapp(wrapped): def decorator(context, request): return request.get_response(wrapped) return wraps(wrapped)(decorator) # pickleability + +class HTTPException(object): + def __call__(self, environ, start_response, exc_info=False): + try: + msg = escape(environ['message']) + except KeyError: + msg = '' + html = """<body> + <html><title>%s</title><body><h1>%s</h1> + <code>%s</code> + """ % (self.status, self.status, msg) + headers = [('Content-Length', len(html)), ('Content-Type', 'text/html')] + start_response(self.status, headers) + return [html] + +class NotFound(HTTPException): + """ The default NotFound WSGI application """ + status = '404 Not Found' + +class Unauthorized(HTTPException): + """ The default Unauthorized WSGI application """ + status = '401 Unauthorized' + |
