summaryrefslogtreecommitdiff
path: root/repoze/bfg/zcml.py
diff options
context:
space:
mode:
authorChris McDonough <chrism@agendaless.com>2009-09-06 03:36:59 +0000
committerChris McDonough <chrism@agendaless.com>2009-09-06 03:36:59 +0000
commitd66bfb5d1f1aef5cce4941b49740dbd136c95605 (patch)
tree0204e233befdb8ad99332e498308468015f3558e /repoze/bfg/zcml.py
parent50df953770409dc9c9558c77bd5c0bbb17ac54f6 (diff)
downloadpyramid-d66bfb5d1f1aef5cce4941b49740dbd136c95605.tar.gz
pyramid-d66bfb5d1f1aef5cce4941b49740dbd136c95605.tar.bz2
pyramid-d66bfb5d1f1aef5cce4941b49740dbd136c95605.zip
Merge multiview2 branch to HEAD.
Diffstat (limited to 'repoze/bfg/zcml.py')
-rw-r--r--repoze/bfg/zcml.py494
1 files changed, 336 insertions, 158 deletions
diff --git a/repoze/bfg/zcml.py b/repoze/bfg/zcml.py
index 5a41babd1..fb4e8d720 100644
--- a/repoze/bfg/zcml.py
+++ b/repoze/bfg/zcml.py
@@ -1,5 +1,5 @@
import os
-import inspect
+import sys
import types
from zope.configuration import xmlconfig
@@ -14,6 +14,8 @@ from zope.configuration.exceptions import ConfigurationError
from zope.configuration.fields import GlobalObject
from zope.interface import Interface
+from zope.interface.interfaces import IInterface
+from zope.interface import implementedBy
from zope.schema import TextLine
from zope.schema import Bool
@@ -32,30 +34,45 @@ from repoze.bfg.interfaces import IForbiddenView
from repoze.bfg.interfaces import IAuthenticationPolicy
from repoze.bfg.interfaces import IAuthorizationPolicy
from repoze.bfg.interfaces import ISecurityPolicy
+from repoze.bfg.interfaces import ISecuredView
+from repoze.bfg.interfaces import IMultiView
from repoze.bfg.interfaces import IView
from repoze.bfg.interfaces import IUnauthorizedAppFactory
from repoze.bfg.interfaces import ILogger
-from repoze.bfg.interfaces import IRequestFactories
from repoze.bfg.interfaces import IPackageOverrides
+from repoze.bfg.interfaces import IRequest
+from repoze.bfg.interfaces import IRouteRequest
from repoze.bfg.path import package_name
from repoze.bfg.resource import PackageOverrides
-from repoze.bfg.request import DEFAULT_REQUEST_FACTORIES
-from repoze.bfg.request import named_request_factories
+from repoze.bfg.request import create_route_request_factory
-from repoze.bfg.security import ViewPermissionFactory
+from repoze.bfg.security import Unauthorized
from repoze.bfg.secpols import registerBBBAuthn
+from repoze.bfg.settings import get_settings
+
+from repoze.bfg.traversal import find_interface
from repoze.bfg.view import static as static_view
+from repoze.bfg.view import NotFound
+from repoze.bfg.view import MultiView
+from repoze.bfg.view import map_view
+from repoze.bfg.view import decorate_view
+
import martian
-def handler(methodName, *args, **kwargs):
- method = getattr(getSiteManager(), methodName)
- method(*args, **kwargs)
+try:
+ all = all
+except NameError: # pragma: no cover
+ def all(iterable):
+ for element in iterable:
+ if not element:
+ return False
+ return True
def view(
_context,
@@ -65,56 +82,154 @@ def view(
name="",
request_type=None,
route_name=None,
+ request_method=None,
+ request_param=None,
+ containment=None,
cacheable=True, # not used, here for b/w compat < 0.8
):
if not view:
raise ConfigurationError('"view" attribute was not specified')
- if route_name is None:
- request_factories = DEFAULT_REQUEST_FACTORIES
- else:
- request_factories = queryUtility(IRequestFactories, name=route_name)
- if request_factories is None:
- raise ConfigurationError(
- 'Unknown route_name "%s". <route> definitions must be ordered '
- 'before the view definition which mentions the route\'s name '
- 'within ZCML (or before the "scan" directive is invoked '
- 'within a bfg_view decorator).' % route_name)
-
- if request_type in request_factories:
- request_type = request_factories[request_type]['interface']
- else:
- request_type = _context.resolve(request_type)
+ sm = getSiteManager()
+
+ if request_type in ('GET', 'HEAD', 'PUT', 'POST', 'DELETE'):
+ # b/w compat for 1.0
+ request_method = request_type
+ request_type = None
- derived_view = derive_view(view)
+ if request_type is None:
+ if route_name is None:
+ request_type = IRequest
+ else:
+ request_type = queryUtility(IRouteRequest, name=route_name)
+ if request_type is None:
+ factory = create_route_request_factory(route_name)
+ request_type = implementedBy(factory)
+ sm.registerUtility(factory, IRouteRequest, name=route_name)
- if permission:
- pfactory = ViewPermissionFactory(permission)
- _context.action(
- discriminator = ('permission', for_, name, request_type,
- IViewPermission),
- callable = handler,
- args = ('registerAdapter',
- pfactory, (for_, request_type), IViewPermission, name,
- _context.info),
- )
+ if isinstance(request_type, basestring):
+ request_type = _context.resolve(request_type)
+ predicates = []
+ weight = sys.maxint
+
+ # Predicates are added to the predicate list in (presumed)
+ # computation expense order. All predicates associated with a
+ # view must evaluate true for the view to "match" a request.
+ # Elsewhere in the code, we evaluate them using a generator
+ # expression. The fastest predicate should be evaluated first,
+ # then the next fastest, and so on, as if one returns false, the
+ # remainder of the predicates won't need to be evaluated.
+
+ # Each predicate is associated with a weight value. The weight
+ # symbolizes the relative potential "importance" of the predicate
+ # to all other predicates. A larger weight indicates greater
+ # importance. These weights are subtracted from an aggregate
+ # 'weight' variable. The aggregate weight is then divided by the
+ # length of the predicate list to compute a "score" for this view.
+ # The score represents the ordering in which a "multiview" ( a
+ # collection of views that share the same context/request/name
+ # triad but differ in other ways via predicates) will attempt to
+ # call its set of views. Views with lower scores will be tried
+ # first. The intent is to a) ensure that views with more
+ # predicates are always evaluated before views with fewer
+ # predicates and b) to ensure a stable call ordering of views that
+ # share the same number of predicates.
+
+ # Views which do not have any predicates get a score of
+ # "sys.maxint", meaning that they will be tried very last.
+
+ if request_method is not None:
+ def request_method_predicate(context, request):
+ return request.method == request_method
+ weight = weight - 10
+ predicates.append(request_method_predicate)
+
+ if request_param is not None:
+ request_param_val = None
+ if '=' in request_param:
+ request_param, request_param_val = request_param.split('=', 1)
+ def request_param_predicate(context, request):
+ if request_param_val is None:
+ return request_param in request.params
+ return request.params.get(request_param) == request_param_val
+ weight = weight - 20
+ predicates.append(request_param_predicate)
+
+ if containment is not None:
+ def containment_predicate(context, request):
+ return find_interface(context, containment) is not None
+ weight = weight - 30
+ predicates.append(containment_predicate)
+
+ if predicates:
+ score = float(weight) / len(predicates)
+ else:
+ score = sys.maxint
+
+ def register():
+ derived_view = derive_view(view, permission, predicates)
+ r_for_ = for_
+ r_request_type = request_type
+ if r_for_ is None:
+ r_for_ = Interface
+ if not IInterface.providedBy(r_for_):
+ r_for_ = implementedBy(r_for_)
+ if not IInterface.providedBy(r_request_type):
+ r_request_type = implementedBy(r_request_type)
+ old_view = sm.adapters.lookup((r_for_, r_request_type), IView,name=name)
+ if old_view is None:
+ if hasattr(derived_view, '__call_permissive__'):
+ sm.registerAdapter(derived_view, (for_, request_type),
+ ISecuredView, name, _context.info)
+ if hasattr(derived_view, '__permitted__'):
+ # bw compat
+ sm.registerAdapter(derived_view.__permitted__,
+ (for_, request_type), IViewPermission,
+ name, _context.info)
+ else:
+ sm.registerAdapter(derived_view, (for_, request_type),
+ IView, name, _context.info)
+ else:
+ # XXX we could try to be more efficient here and register
+ # a non-secured view for a multiview if none of the
+ # multiview's consituent views have a permission
+ # associated with them, but this code is getting pretty
+ # rough already
+ if IMultiView.providedBy(old_view):
+ multiview = old_view
+ else:
+ multiview = MultiView(name)
+ multiview.add(old_view, sys.maxint)
+ multiview.add(derived_view, score)
+ for i in (IView, ISecuredView):
+ # unregister any existing views
+ sm.adapters.unregister((r_for_, r_request_type), i, name=name)
+ sm.registerAdapter(multiview, (for_, request_type), IMultiView,
+ name, _context.info)
+ # b/w compat
+ sm.registerAdapter(multiview.__permitted__,
+ (for_, request_type), IViewPermission,
+ name, _context.info)
_context.action(
- discriminator = ('view', for_, name, request_type, IView),
- callable = handler,
- args = ('registerAdapter',
- derived_view, (for_, request_type), IView, name, _context.info),
+ discriminator = ('view', for_, name, request_type, IView, containment,
+ request_param, request_method, route_name),
+ callable = register,
+ args = (),
)
_view = view # for directives that take a view arg
def view_utility(_context, view, iface):
- derived_view = derive_view(view)
+ def register():
+ derived_view = derive_view(view)
+ sm = getSiteManager()
+ sm.registerUtility(derived_view, iface, '', _context.info)
+
_context.action(
- discriminator = ('notfound_view',),
- callable = handler,
- args = ('registerUtility', derived_view, iface, '', _context.info),
+ discriminator = iface,
+ callable = register,
)
def notfound(_context, view):
@@ -123,46 +238,83 @@ def notfound(_context, view):
def forbidden(_context, view):
view_utility(_context, view, IForbiddenView)
-def derive_view(view):
- derived_view = view
- if inspect.isclass(view):
- # If the object we've located is a class, turn it into a
- # function that operates like a Zope view (when it's invoked,
- # construct an instance using 'context' and 'request' as
- # position arguments, then immediately invoke the __call__
- # method of the instance with no arguments; __call__ should
- # return an IResponse).
- if requestonly(view):
- # its __init__ accepts only a single request argument,
- # instead of both context and request
- def _bfg_class_requestonly_view(context, request):
- inst = view(request)
- return inst()
- derived_view = _bfg_class_requestonly_view
- else:
- # its __init__ accepts both context and request
- def _bfg_class_view(context, request):
- inst = view(context, request)
- return inst()
- derived_view = _bfg_class_view
-
- elif requestonly(view):
- # its __call__ accepts only a single request argument,
- # instead of both context and request
- def _bfg_requestonly_view(context, request):
- return view(request)
- derived_view = _bfg_requestonly_view
-
- if derived_view is not view:
- derived_view.__module__ = view.__module__
- derived_view.__doc__ = view.__doc__
- try:
- derived_view.__name__ = view.__name__
- except AttributeError:
- derived_view.__name__ = repr(view)
-
+def derive_view(original_view, permission=None, predicates=()):
+ mapped_view = map_view(original_view)
+ secured_view = secure_view(mapped_view, permission)
+ debug_view = authdebug_view(secured_view, permission)
+ derived_view = predicate_wrap(debug_view, predicates)
return derived_view
-
+
+def predicate_wrap(view, predicates):
+ if not predicates:
+ return view
+ def _wrapped(context, request):
+ if all((predicate(context, request) for predicate in predicates)):
+ return view(context, request)
+ raise NotFound('predicate mismatch for view %s' % view)
+ def checker(context, request):
+ return all((predicate(context, request) for predicate in predicates))
+ _wrapped.__predicated__ = checker
+ decorate_view(_wrapped, view)
+ return _wrapped
+
+def secure_view(view, permission):
+ wrapped_view = view
+ authn_policy = queryUtility(IAuthenticationPolicy)
+ authz_policy = queryUtility(IAuthorizationPolicy)
+ if authn_policy and authz_policy and (permission is not None):
+ def _secured_view(context, request):
+ principals = authn_policy.effective_principals(request)
+ if authz_policy.permits(context, principals, permission):
+ return view(context, request)
+ msg = getattr(request, 'authdebug_message',
+ 'Unauthorized: %s failed permission check' % view)
+ raise Unauthorized(msg)
+ _secured_view.__call_permissive__ = view
+ def _permitted(context, request):
+ principals = authn_policy.effective_principals(request)
+ return authz_policy.permits(context, principals, permission)
+ _secured_view.__permitted__ = _permitted
+ wrapped_view = _secured_view
+ decorate_view(wrapped_view, view)
+
+ return wrapped_view
+
+def authdebug_view(view, permission):
+ wrapped_view = view
+ authn_policy = queryUtility(IAuthenticationPolicy)
+ authz_policy = queryUtility(IAuthorizationPolicy)
+ settings = get_settings()
+ debug_authorization = getattr(settings, 'debug_authorization', False)
+ if debug_authorization:
+ def _authdebug_view(context, request):
+ view_name = getattr(request, 'view_name', None)
+
+ if authn_policy and authz_policy:
+ if permission is None:
+ msg = 'Allowed (no permission registered)'
+ else:
+ principals = authn_policy.effective_principals(request)
+ msg = str(authz_policy.permits(context, principals,
+ permission))
+ else:
+ msg = 'Allowed (no authorization policy in use)'
+
+ view_name = getattr(request, 'view_name', None)
+ url = getattr(request, 'url', None)
+ msg = ('debug_authorization of url %s (view name %r against '
+ 'context %r): %s' % (url, view_name, context, msg))
+ logger = getUtility(ILogger, 'repoze.bfg.debug')
+ logger and logger.debug(msg)
+ if request is not None:
+ request.authdebug_message = msg
+ return view(context, request)
+
+ wrapped_view = _authdebug_view
+ decorate_view(wrapped_view, view)
+
+ return wrapped_view
+
def scan(_context, package, martian=martian):
# martian overrideable only for unit tests
module_grokker = martian.ModuleGrokker()
@@ -240,12 +392,11 @@ def repozewho1authenticationpolicy(_context, identifier_name='auth_tkt',
callback=None):
policy = RepozeWho1AuthenticationPolicy(identifier_name=identifier_name,
callback=callback)
- _context.action(
- discriminator = 'authentication_policy',
- callable = handler,
- args = ('registerUtility', policy, IAuthenticationPolicy, '',
- _context.info),
- )
+ # authentication policies must be registered eagerly so they can
+ # be found by the view registration machinery
+ sm = getSiteManager()
+ sm.registerUtility(policy, IAuthenticationPolicy)
+ _context.action(discriminator=IAuthenticationPolicy)
class IRemoteUserAuthenticationPolicyDirective(Interface):
environ_key = TextLine(title=u'environ_key', required=False,
@@ -256,12 +407,11 @@ def remoteuserauthenticationpolicy(_context, environ_key='REMOTE_USER',
callback=None):
policy = RemoteUserAuthenticationPolicy(environ_key=environ_key,
callback=callback)
- _context.action(
- discriminator = 'authentication_policy',
- callable = handler,
- args = ('registerUtility', policy, IAuthenticationPolicy, '',
- _context.info),
- )
+ # authentication policies must be registered eagerly so they can
+ # be found by the view registration machinery
+ sm = getSiteManager()
+ sm.registerUtility(policy, IAuthenticationPolicy)
+ _context.action(discriminator=IAuthenticationPolicy)
class IAuthTktAuthenticationPolicyDirective(Interface):
secret = TextLine(title=u'secret', required=True)
@@ -291,49 +441,91 @@ def authtktauthenticationpolicy(_context,
reissue_time = reissue_time)
except ValueError, why:
raise ConfigurationError(str(why))
- _context.action(
- discriminator = 'authentication_policy',
- callable = handler,
- args = ('registerUtility', policy, IAuthenticationPolicy, '',
- _context.info),
- )
+ # authentication policies must be registered eagerly so they can
+ # be found by the view registration machinery
+ sm = getSiteManager()
+ sm.registerUtility(policy, IAuthenticationPolicy)
+ _context.action(discriminator=IAuthenticationPolicy)
class IACLAuthorizationPolicyDirective(Interface):
pass
def aclauthorizationpolicy(_context):
policy = ACLAuthorizationPolicy()
- _context.action(
- discriminator = 'authorization_policy',
- callable = handler,
- args = ('registerUtility', policy, IAuthorizationPolicy, '',
- _context.info),
- )
+ # authorization policies must be registered eagerly so they can be
+ # found by the view registration machinery
+ sm = getSiteManager()
+ sm.registerUtility(policy, IAuthorizationPolicy)
+ _context.action(discriminator=IAuthorizationPolicy)
class IRouteDirective(Interface):
""" The interface for the ``route`` ZCML directive
"""
name = TextLine(title=u'name', required=True)
path = TextLine(title=u'path', required=True)
+ factory = GlobalObject(title=u'context factory', required=False)
view = GlobalObject(title=u'view', required=False)
view_for = GlobalObject(title=u'view_for', required=False)
+ view_permission = TextLine(title=u'view_permission', required=False)
+ view_request_type = TextLine(title=u'view_request_type', required=False)
+ view_request_method = TextLine(title=u'view_request_method', required=False)
+ view_containment = GlobalObject(
+ title = u'Dotted name of a containment class or interface',
+ required=False)
+ # alias for "view_for"
+ for_ = GlobalObject(title=u'for', required=False)
+ # alias for "view_permission"
permission = TextLine(title=u'permission', required=False)
- factory = GlobalObject(title=u'context factory', required=False)
+ # alias for "view_request_type"
request_type = TextLine(title=u'request_type', required=False)
-
-def route(_context, name, path, view=None, view_for=None, permission=None,
- factory=None, request_type=None):
+ # alias for "view_request_method"
+ request_method = TextLine(title=u'request_method', required=False)
+ # alias for "view_request_param"
+ request_param = TextLine(title=u'request_param', required=False)
+ # alias for "view_containment"
+ containment = GlobalObject(
+ title = u'Dotted name of a containment class or interface',
+ required=False)
+
+def route(_context, name, path, view=None, view_for=None,
+ permission=None, factory=None, request_type=None, for_=None,
+ view_permission=None, view_request_type=None,
+ request_method=None, view_request_method=None,
+ request_param=None, view_request_param=None, containment=None,
+ view_containment=None):
""" Handle ``route`` ZCML directives
"""
+ # the strange ordering of the request kw args above is for b/w
+ # compatibility purposes.
+ for_ = view_for or for_
+ request_type = view_request_type or request_type
+ permission = view_permission or permission
+ request_method = view_request_method or request_method
+ request_param = view_request_param or request_param
+ containment = view_containment or containment
+
sm = getSiteManager()
- request_factories = named_request_factories(name)
- sm.registerUtility(request_factories, IRequestFactories, name=name)
+
+ if request_type in ('GET', 'HEAD', 'PUT', 'POST', 'DELETE'):
+ # b/w compat for 1.0
+ request_method = request_type
+ request_type = None
+
+ if request_type is None:
+ request_factory = queryUtility(IRouteRequest, name=name)
+ if request_factory is None:
+ request_factory = create_route_request_factory(name)
+ sm.registerUtility(request_factory, IRouteRequest, name=name)
+ request_type = implementedBy(request_factory)
if view:
- _view(_context, permission, view_for, view, '', request_type, name)
+ _view(_context, permission=permission, for_=for_, view=view, name='',
+ request_type=request_type, route_name=name,
+ request_method=request_method, request_param=request_param,
+ containment=containment)
_context.action(
- discriminator = ('route', name, view_for, request_type),
+ discriminator = ('route', name),
callable = connect_route,
args = (path, name, factory),
)
@@ -361,6 +553,10 @@ class IStaticDirective(Interface):
required=False,
default=None)
+class StaticRootFactory:
+ def __init__(self, environ):
+ pass
+
def static(_context, name, path, cache_max_age=3600):
""" Handle ``static`` ZCML directives
"""
@@ -371,7 +567,8 @@ def static(_context, name, path, cache_max_age=3600):
path = '%s:%s' % (package_name(_context.resolve('.')), path)
view = static_view(path, cache_max_age=cache_max_age)
- route(_context, name, "%s*subpath" % name, view=view)
+ route(_context, name, "%s*subpath" % name, view=view,
+ view_for=StaticRootFactory, factory=StaticRootFactory)
class IViewDirective(Interface):
for_ = GlobalObject(
@@ -394,8 +591,7 @@ class IViewDirective(Interface):
name = TextLine(
title=u"The name of the view",
description=u"""
- The name shows up in URLs/paths. For example 'foo' or
- 'foo.html'.""",
+ The name shows up in URLs/paths. For example 'foo' or 'foo.html'.""",
required=False,
)
@@ -411,6 +607,25 @@ class IViewDirective(Interface):
title = u'The route that must match for this view to be used',
required = False)
+ containment = GlobalObject(
+ title = u'Dotted name of a containment class or interface',
+ required=False)
+
+ request_method = TextLine(
+ title = u'Request method name that must be matched (e.g. GET/POST)',
+ description = (u'The view will be called if and only if the request '
+ 'method (``request.method``) matches this string. This'
+ 'functionality replaces the older ``request_type`` '
+ 'functionality.'),
+ required=False)
+
+ request_param = TextLine(
+ title = (u'Request parameter name that must exist in '
+ '``request.params`` for this view to match'),
+ description = (u'The view will be called if and only if the request '
+ 'parameter exists which matches this string.'),
+ required=False)
+
class INotFoundViewDirective(Interface):
view = GlobalObject(
title=u"",
@@ -523,10 +738,14 @@ class BFGViewFunctionGrokker(martian.InstanceGrokker):
name = obj.__view_name__
request_type = obj.__request_type__
route_name = obj.__route_name__
+ request_method = obj.__request_method__
+ request_param = obj.__request_param__
+ containment = obj.__containment__
context = kw['context']
view(context, permission=permission, for_=for_,
view=obj, name=name, request_type=request_type,
- route_name=route_name)
+ route_name=route_name, request_method=request_method,
+ request_param=request_param, containment=containment)
return True
return False
@@ -535,47 +754,6 @@ def exclude(name):
return True
return False
-def requestonly(class_or_callable):
- """ Return true of the class or callable accepts only a request argument,
- as opposed to something that accepts context, request """
- if inspect.isfunction(class_or_callable):
- fn = class_or_callable
- elif inspect.isclass(class_or_callable):
- try:
- fn = class_or_callable.__init__
- except AttributeError:
- return False
- else:
- try:
- fn = class_or_callable.__call__
- except AttributeError:
- return False
-
- try:
- argspec = inspect.getargspec(fn)
- except TypeError:
- return False
-
- args = argspec[0]
- defaults = argspec[3]
-
- if hasattr(fn, 'im_func'):
- # it's an instance method
- if not args:
- return False
- args = args[1:]
- if not args:
- return False
-
- if len(args) == 1:
- return True
-
- elif args[0] == 'request':
- if len(args) - len(defaults) == 1:
- return True
-
- return False
-
class Uncacheable(object):
""" Include in discriminators of actions which are not cacheable;
this class only exists for backwards compatibility (<0.8.1)"""