summaryrefslogtreecommitdiff
path: root/repoze
diff options
context:
space:
mode:
authorChris McDonough <chrism@agendaless.com>2010-09-09 17:46:49 +0000
committerChris McDonough <chrism@agendaless.com>2010-09-09 17:46:49 +0000
commite25a70a7d1c2016eaeff9c630df9109e715bba3b (patch)
tree520508b0bb66600e50b46db46c0a85ef05f0690c /repoze
parent6ae0139d3682730e44a3b2330f83d10b31ebbc95 (diff)
downloadpyramid-e25a70a7d1c2016eaeff9c630df9109e715bba3b.tar.gz
pyramid-e25a70a7d1c2016eaeff9c630df9109e715bba3b.tar.bz2
pyramid-e25a70a7d1c2016eaeff9c630df9109e715bba3b.zip
Features
-------- - In support of making it easier to configure applications which are "secure by default", a default permission feature was added. If supplied, the default permission is used as the permission string to all view registrations which don't otherwise name a permission. These APIs are in support of that: - A new constructor argument was added to the Configurator: ``default_permission``. - A new method was added to the Configurator: ``set_default_permission``. - A new ZCML directive was added: ``default_permission``. Documentation ------------- - Added documentation for the ``default_permission`` ZCML directive. - Added documentation for the ``default_permission`` constructor value and the ``set_default_permission`` method in the Configurator API documentation. - Added a new section to the "security" chapter named "Setting a Default Permission". - Document ``renderer_globals_factory`` and ``request_factory`` arguments to Configurator constructor.
Diffstat (limited to 'repoze')
-rw-r--r--repoze/bfg/configuration.py86
-rw-r--r--repoze/bfg/includes/meta.zcml6
-rw-r--r--repoze/bfg/interfaces.py5
-rw-r--r--repoze/bfg/tests/test_configuration.py72
-rw-r--r--repoze/bfg/tests/test_zcml.py24
-rw-r--r--repoze/bfg/zcml.py13
6 files changed, 201 insertions, 5 deletions
diff --git a/repoze/bfg/configuration.py b/repoze/bfg/configuration.py
index 06115abf7..f8fdcca07 100644
--- a/repoze/bfg/configuration.py
+++ b/repoze/bfg/configuration.py
@@ -20,6 +20,7 @@ from repoze.bfg.interfaces import IAuthenticationPolicy
from repoze.bfg.interfaces import IAuthorizationPolicy
from repoze.bfg.interfaces import IChameleonTranslate
from repoze.bfg.interfaces import IDebugLogger
+from repoze.bfg.interfaces import IDefaultPermission
from repoze.bfg.interfaces import IDefaultRootFactory
from repoze.bfg.interfaces import IExceptionViewClassifier
from repoze.bfg.interfaces import ILocaleNegotiator
@@ -85,6 +86,8 @@ DEFAULT_RENDERERS = (
('string', renderers.string_renderer_factory),
)
+_marker = object()
+
class Configurator(object):
"""
A Configurator is used to configure a :mod:`repoze.bfg`
@@ -160,7 +163,31 @@ class Configurator(object):
negotiator` implementation or a :term:`dotted Python name` to
same. See :ref:`custom_locale_negotiator`.
+ If ``request_factory`` is passed, it should be a :term:`request
+ factory` implementation or a :term:`dotted Python name` to same.
+ See :ref:`custom_request_factory`. By default it is ``None``,
+ which means use the default request factory.
+
+ If ``renderer_globals_factory`` is passed, it should be a
+ :term:`renderer globals` factory implementation or a :term:`dotted
+ Python name` to same. See :ref:`custom_renderer_globals_factory`.
+ By default, it is ``None``, which means use no renderer globals
+ factory.
+
+ If ``default_permission`` is passed, it should be a
+ :term:`permission` string to be used as the default permission for
+ all view configuration registrations performed against this
+ Configurator. An example of a permission string:``'view'``.
+ Adding a default permission makes it unnecessary to protect each
+ view configuration with an explicit permission, unless your
+ application policy requires some exception for a particular view.
+ By default, ``default_permission`` is ``None``, meaning that view
+ configurations which do not explicitly declare a permission will
+ always be executable by entirely anonymous users (any
+ authorization policy in effect is ignored). See also
+ :ref:`setting_a_default_permission`.
"""
+
manager = manager # for testing injection
venusian = venusian # for testing injection
def __init__(self,
@@ -174,7 +201,8 @@ class Configurator(object):
debug_logger=None,
locale_negotiator=None,
request_factory=None,
- renderer_globals_factory=None):
+ renderer_globals_factory=None,
+ default_permission=None):
if package is None:
package = caller_package()
name_resolver = DottedNameResolver(package)
@@ -194,7 +222,8 @@ class Configurator(object):
debug_logger=debug_logger,
locale_negotiator=locale_negotiator,
request_factory=request_factory,
- renderer_globals_factory=renderer_globals_factory
+ renderer_globals_factory=renderer_globals_factory,
+ default_permission=default_permission,
)
def _set_settings(self, mapping):
@@ -326,7 +355,8 @@ class Configurator(object):
authentication_policy=None, authorization_policy=None,
renderers=DEFAULT_RENDERERS, debug_logger=None,
locale_negotiator=None, request_factory=None,
- renderer_globals_factory=None):
+ renderer_globals_factory=None,
+ default_permission=None):
""" When you pass a non-``None`` ``registry`` argument to the
:term:`Configurator` constructor, no initial 'setup' is
performed against the registry. This is because the registry
@@ -371,6 +401,8 @@ class Configurator(object):
if renderer_globals_factory:
renderer_globals_factory=self.maybe_dotted(renderer_globals_factory)
self.set_renderer_globals_factory(renderer_globals_factory)
+ if default_permission:
+ self.set_default_permission(default_permission)
# getSiteManager is a unit testing dep injection
def hook_zca(self, getSiteManager=None):
@@ -598,7 +630,7 @@ class Configurator(object):
self.manager.pop()
return self.registry
- def add_view(self, view=None, name="", for_=None, permission=None,
+ def add_view(self, view=None, name="", for_=None, permission=_marker,
request_type=None, route_name=None, request_method=None,
request_param=None, containment=None, attr=None,
renderer=None, wrapper=None, xhr=False, accept=None,
@@ -629,7 +661,17 @@ class Configurator(object):
The name of a :term:`permission` that the user must possess
in order to invoke the :term:`view callable`. See
:ref:`view_security_section` for more information about view
- security and permissions.
+ security and permissions. If ``permission`` is omitted, a
+ *default* permission may be used for this view registration
+ if one was named as the
+ :class:`repoze.bfg.configuration.Configurator` constructor's
+ ``default_permission`` argument, or if
+ :meth:`repoze.bfg.configuration.Configurator.set_default_permission`
+ was used prior to this view registration. Pass ``None`` as
+ the permission to explicitly indicate that the view should
+ always be executable by entirely anonymous users, regardless
+ of the default permission, bypassing any
+ :term:`authorization policy` that may be in effect.
attr
@@ -894,6 +936,10 @@ class Configurator(object):
containment=containment, request_type=request_type,
custom=custom_predicates)
+ if permission is _marker:
+ # intent: will be None if no default permission is registered
+ permission = self.registry.queryUtility(IDefaultPermission)
+
derived_view = self._derive_view(view, permission, predicates, attr,
renderer, wrapper, name, accept, order,
phash)
@@ -1610,6 +1656,36 @@ class Configurator(object):
negotiator = self.maybe_dotted(negotiator)
self.registry.registerUtility(negotiator, ILocaleNegotiator)
+ def set_default_permission(self, permission):
+ """
+ Set the default permission to be used by all subsequent
+ :term:`view configuration` registrations. ``permission``
+ should be a :term:`permission` string to be used as the
+ default permission. An example of a permission
+ string:``'view'``. Adding a default permission makes it
+ unnecessary to protect each view configuration with an
+ explicit permission, unless your application policy requires
+ some exception for a particular view.
+
+ If a default permission is *not* set, views represented by
+ view configuration registrations which do not explicitly
+ declare a permission will be executable by entirely anonymous
+ users (any authorization policy is ignored).
+
+ Later calls to this method override earlier calls; there can
+ be only one default permission active at a time within an
+ application.
+
+ See also :ref:`setting_a_default_permission`.
+
+ .. note: This API is new as of :mod:`repoze.bfg` version 1.3.
+
+ .. note:: Using the ``default_permission`` argument to the
+ :class:`repoze.bfg.configuration.Configurator` constructor
+ can be used to achieve the same purpose.
+ """
+ self.registry.registerUtility(permission, IDefaultPermission)
+
def add_translation_dirs(self, *specs):
""" Add one or more :term:`translation directory` paths to the
current configuration state. The ``specs`` argument is a
diff --git a/repoze/bfg/includes/meta.zcml b/repoze/bfg/includes/meta.zcml
index 5ecebd868..58ab1c782 100644
--- a/repoze/bfg/includes/meta.zcml
+++ b/repoze/bfg/includes/meta.zcml
@@ -106,6 +106,12 @@
handler="repoze.bfg.zcml.utility"
/>
+ <meta:directive
+ name="default_permission"
+ schema="repoze.bfg.zcml.IDefaultPermissionDirective"
+ handler="repoze.bfg.zcml.default_permission"
+ />
+
</meta:directives>
</configure>
diff --git a/repoze/bfg/interfaces.py b/repoze/bfg/interfaces.py
index ae6bf3b20..44fe84c18 100644
--- a/repoze/bfg/interfaces.py
+++ b/repoze/bfg/interfaces.py
@@ -323,3 +323,8 @@ class ITranslationDirectories(Interface):
""" A list object representing all known translation directories
for an application"""
+class IDefaultPermission(Interface):
+ """ A string object representing the default permission to be used
+ for all view configurations which do not explicitly declare their
+ own."""
+
diff --git a/repoze/bfg/tests/test_configuration.py b/repoze/bfg/tests/test_configuration.py
index 2724b3381..943e6b832 100644
--- a/repoze/bfg/tests/test_configuration.py
+++ b/repoze/bfg/tests/test_configuration.py
@@ -179,6 +179,11 @@ class ConfiguratorTests(unittest.TestCase):
self.assertEqual(config.registry.getUtility(IRendererFactory, 'yeah'),
renderer)
+ def test_ctor_default_permission(self):
+ from repoze.bfg.interfaces import IDefaultPermission
+ config = self._makeOne(default_permission='view')
+ self.assertEqual(config.registry.getUtility(IDefaultPermission), 'view')
+
def test_with_package_module(self):
from repoze.bfg.tests import test_configuration
import repoze.bfg.tests
@@ -445,6 +450,14 @@ class ConfiguratorTests(unittest.TestCase):
self.assertEqual(reg.getUtility(IRendererFactory, 'yeah'),
renderer)
+ def test_setup_registry_default_permission(self):
+ from repoze.bfg.registry import Registry
+ from repoze.bfg.interfaces import IDefaultPermission
+ reg = Registry()
+ config = self._makeOne(reg)
+ config.setup_registry(default_permission='view')
+ self.assertEqual(reg.getUtility(IDefaultPermission), 'view')
+
def test_get_settings_nosettings(self):
from repoze.bfg.registry import Registry
reg = Registry()
@@ -1704,6 +1717,58 @@ class ConfiguratorTests(unittest.TestCase):
request = self._makeRequest(config)
self.assertEqual(view(None, request), 'second')
+ def test_add_view_with_permission(self):
+ view1 = lambda *arg: 'OK'
+ outerself = self
+ class DummyPolicy(object):
+ def effective_principals(self, r):
+ outerself.assertEqual(r, request)
+ return ['abc']
+ def permits(self, context, principals, permission):
+ outerself.assertEqual(context, None)
+ outerself.assertEqual(principals, ['abc'])
+ outerself.assertEqual(permission, 'view')
+ return True
+ policy = DummyPolicy()
+ config = self._makeOne(authorization_policy=policy,
+ authentication_policy=policy)
+ config.add_view(view=view1, permission='view')
+ view = self._getViewCallable(config)
+ request = self._makeRequest(config)
+ self.assertEqual(view(None, request), 'OK')
+
+ def test_add_view_with_default_permission_no_explicit_permission(self):
+ view1 = lambda *arg: 'OK'
+ outerself = self
+ class DummyPolicy(object):
+ def effective_principals(self, r):
+ outerself.assertEqual(r, request)
+ return ['abc']
+ def permits(self, context, principals, permission):
+ outerself.assertEqual(context, None)
+ outerself.assertEqual(principals, ['abc'])
+ outerself.assertEqual(permission, 'view')
+ return True
+ policy = DummyPolicy()
+ config = self._makeOne(authorization_policy=policy,
+ authentication_policy=policy,
+ default_permission='view')
+ config.add_view(view=view1)
+ view = self._getViewCallable(config)
+ request = self._makeRequest(config)
+ self.assertEqual(view(None, request), 'OK')
+
+ def test_add_view_with_no_default_permission_no_explicit_permission(self):
+ view1 = lambda *arg: 'OK'
+ class DummyPolicy(object): pass # wont be called
+ policy = DummyPolicy()
+ config = self._makeOne(authorization_policy=policy,
+ authentication_policy=policy)
+ config.add_view(view=view1)
+ view = self._getViewCallable(config)
+ request = self._makeRequest(config)
+ self.assertEqual(view(None, request), 'OK')
+
def _assertRoute(self, config, name, path, num_predicates=0):
from repoze.bfg.interfaces import IRoutesMapper
mapper = config.registry.getUtility(IRoutesMapper)
@@ -2149,6 +2214,13 @@ class ConfiguratorTests(unittest.TestCase):
self.assertEqual(config.registry.getUtility(IRendererGlobalsFactory),
dummyfactory)
+ def test_set_default_permission(self):
+ from repoze.bfg.interfaces import IDefaultPermission
+ config = self._makeOne()
+ config.set_default_permission('view')
+ self.assertEqual(config.registry.getUtility(IDefaultPermission),
+ 'view')
+
def test_add_translation_dirs_missing_dir(self):
from repoze.bfg.exceptions import ConfigurationError
config = self._makeOne()
diff --git a/repoze/bfg/tests/test_zcml.py b/repoze/bfg/tests/test_zcml.py
index 4cd7f88d3..131122d7b 100644
--- a/repoze/bfg/tests/test_zcml.py
+++ b/repoze/bfg/tests/test_zcml.py
@@ -1123,6 +1123,30 @@ class TestLocaleNegotiatorDirective(unittest.TestCase):
self.assertEqual(action['args'], (dummy_negotiator,))
action['callable'](*action['args']) # doesn't blow up
+class TestDefaultPermissionDirective(unittest.TestCase):
+ def setUp(self):
+ testing.setUp()
+
+ def tearDown(self):
+ testing.tearDown()
+
+ def _callFUT(self, context, name):
+ from repoze.bfg.zcml import default_permission
+ return default_permission(context, name)
+
+ def test_it(self):
+ from repoze.bfg.threadlocal import get_current_registry
+ from repoze.bfg.interfaces import IDefaultPermission
+ reg = get_current_registry()
+ context = DummyContext()
+ self._callFUT(context, 'view')
+ actions = context.actions
+ self.assertEqual(len(actions), 1)
+ regadapt = actions[0]
+ self.assertEqual(regadapt['discriminator'], IDefaultPermission)
+ perm = reg.getUtility(IDefaultPermission)
+ self.assertEqual(perm, 'view')
+
class TestLoadZCML(unittest.TestCase):
def setUp(self):
testing.setUp()
diff --git a/repoze/bfg/zcml.py b/repoze/bfg/zcml.py
index 5320554bc..2bf394eb8 100644
--- a/repoze/bfg/zcml.py
+++ b/repoze/bfg/zcml.py
@@ -18,6 +18,7 @@ from zope.schema import TextLine
from repoze.bfg.interfaces import IAuthenticationPolicy
from repoze.bfg.interfaces import IAuthorizationPolicy
+from repoze.bfg.interfaces import IDefaultPermission
from repoze.bfg.interfaces import IRendererFactory
from repoze.bfg.interfaces import IRouteRequest
from repoze.bfg.interfaces import IView
@@ -852,6 +853,18 @@ def utility(_context, provides=None, component=None, factory=None, name=''):
kw = kw,
)
+class IDefaultPermissionDirective(Interface):
+ name = TextLine(title=u'name', required=True)
+
+def default_permission(_context, name):
+ """ Register a default permission name """
+ # the default permission must be registered eagerly so it can
+ # be found by the view registration machinery
+ reg = get_current_registry()
+ config = Configurator(reg, package=_context.package)
+ config.set_default_permission(name)
+ _context.action(discriminator=IDefaultPermission)
+
def path_spec(context, path):
# we prefer registering resource specifications over absolute
# paths because these can be overridden by the resource directive.