summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGES.txt13
-rw-r--r--docs/narr/hooks.rst135
-rw-r--r--repoze/bfg/interfaces.py25
-rw-r--r--repoze/bfg/router.py82
-rw-r--r--repoze/bfg/wsgi.py25
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'
+