diff options
| author | Chris McDonough <chrism@agendaless.com> | 2010-09-09 17:46:49 +0000 |
|---|---|---|
| committer | Chris McDonough <chrism@agendaless.com> | 2010-09-09 17:46:49 +0000 |
| commit | e25a70a7d1c2016eaeff9c630df9109e715bba3b (patch) | |
| tree | 520508b0bb66600e50b46db46c0a85ef05f0690c | |
| parent | 6ae0139d3682730e44a3b2330f83d10b31ebbc95 (diff) | |
| download | pyramid-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.
| -rw-r--r-- | CHANGES.txt | 35 | ||||
| -rw-r--r-- | TODO.txt | 7 | ||||
| -rw-r--r-- | docs/api/configuration.rst | 4 | ||||
| -rw-r--r-- | docs/glossary.rst | 4 | ||||
| -rw-r--r-- | docs/narr/security.rst | 35 | ||||
| -rw-r--r-- | docs/zcml.rst | 1 | ||||
| -rw-r--r-- | docs/zcml/default_permission.rst | 61 | ||||
| -rw-r--r-- | repoze/bfg/configuration.py | 86 | ||||
| -rw-r--r-- | repoze/bfg/includes/meta.zcml | 6 | ||||
| -rw-r--r-- | repoze/bfg/interfaces.py | 5 | ||||
| -rw-r--r-- | repoze/bfg/tests/test_configuration.py | 72 | ||||
| -rw-r--r-- | repoze/bfg/tests/test_zcml.py | 24 | ||||
| -rw-r--r-- | repoze/bfg/zcml.py | 13 |
13 files changed, 340 insertions, 13 deletions
diff --git a/CHANGES.txt b/CHANGES.txt index 44970b230..4b716414f 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,9 +1,44 @@ Next release ============ +Bug Fixes +--------- + - The ``traverse`` route predicate could not successfully generate a traversal path. +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. + 1.3a12 (2010-09-08) =================== @@ -62,10 +62,3 @@ - Change "Cleaning up After a Request" in the urldispatch chapter to use ``request.add_response_callback``. -- Add a default_view_permission setting: - - From IRC: if I use something like http://bfg.repoze.org/pastebin/764 - (does it even make any sense?), why do I still have to put - view_permission="something_random" inside every <route>, so that - those alc's kick in? or am I doing it completely wrong? - diff --git a/docs/api/configuration.rst b/docs/api/configuration.rst index 36e4c5186..1fb232275 100644 --- a/docs/api/configuration.rst +++ b/docs/api/configuration.rst @@ -5,7 +5,7 @@ .. automodule:: repoze.bfg.configuration - .. autoclass:: Configurator(registry=None, package=None, settings=None, root_factory=None, authentication_policy=None, authorization_policy=None, renderers=DEFAULT_RENDERERS, debug_logger=None, locale_negotiator=None, request_factory=None, renderer_globals_factory=None) + .. autoclass:: Configurator(registry=None, package=None, settings=None, root_factory=None, authentication_policy=None, authorization_policy=None, renderers=DEFAULT_RENDERERS, debug_logger=None, locale_negotiator=None, request_factory=None, renderer_globals_factory=None, default_permission=None) .. attribute:: registry @@ -60,6 +60,8 @@ .. automethod:: set_locale_negotiator + .. automethod:: set_default_permission + .. automethod:: set_request_factory .. automethod:: set_renderer_globals_factory diff --git a/docs/glossary.rst b/docs/glossary.rst index 81f5cb797..4c1c0ebab 100644 --- a/docs/glossary.rst +++ b/docs/glossary.rst @@ -10,6 +10,10 @@ Glossary A ``WebOb`` request object. See :ref:`webob_chapter` for information about request objects. + request factory + An object which, provided a WSGI environment as a single + positional argument, returns a ``WebOb`` compatible request. + response An object that has three attributes: ``app_iter`` (representing an iterable body), ``headerlist`` (representing the http headers sent diff --git a/docs/narr/security.rst b/docs/narr/security.rst index 3b1de27ad..85ab9ef58 100644 --- a/docs/narr/security.rst +++ b/docs/narr/security.rst @@ -232,6 +232,41 @@ possess the ``add`` permission against the :term:`context` to be able to invoke the ``blog_entry_add_view`` view. If he does not, the :term:`Forbidden view` will be invoked. +.. _setting_a_default_permission: + +Setting a Default Permission +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If a permission is not supplied to a view configuration, the +registered view always be executable by entirely anonymous users: any +authorization policy in effect is ignored. + +In support of making it easier to configure applications which are +"secure by default", :mod:`repoze.bfg` allows you to configure a +*default* permission. If supplied, the default permission is used as +the permission string to all view registrations which don't otherwise +name a ``permission`` argument. + +These APIs are in support of configuring a default permission for an +application: + +- The ``default_permission`` constructor argument to the + :mod:`repoze.bfg.configuration.Configurator` constructor. + +- The + :meth:`repoze.bfg.configuration.Configurator.set_default_permission` + method. + +- The :ref:`default_permission_directive` ZCML directive. + +When a default permission is registered, if a view configuration +*does* name its own permission, the default permission is ignored for +that view registration, and the view-configuration-named permission is +used. + +.. note:: All APIs and ZCML directives related to default permissions + are new in :mod:`repoze.bfg` 1.3. + .. index:: single: ACL single: access control list diff --git a/docs/zcml.rst b/docs/zcml.rst index e1bfc4f4b..9a41b8bcc 100644 --- a/docs/zcml.rst +++ b/docs/zcml.rst @@ -14,6 +14,7 @@ directive documentation is organized alphabetically by directive name. zcml/adapter zcml/authtktauthenticationpolicy zcml/configure + zcml/default_permission zcml/forbidden zcml/include zcml/localenegotiator diff --git a/docs/zcml/default_permission.rst b/docs/zcml/default_permission.rst new file mode 100644 index 000000000..39edbacd4 --- /dev/null +++ b/docs/zcml/default_permission.rst @@ -0,0 +1,61 @@ +.. _default_permission_directive: + +``default_permission`` +------------------------------- + +Set the default permission to be used by all :term:`view +configuration` registrations. + +This directive accepts a single attribute ,``name``, which should be +used as the default permission string. 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). + +There can be only one default permission active at a time within an +application, thus the default permission directive can only be used +once in any particular set of ZCML. + +.. note: This API is new as of :mod:`repoze.bfg` version 1.3. + +Attributes +~~~~~~~~~~ + +``name`` must be a string representing a :term:`permission`, + e.g. ``view``. + + + The ``secret`` is a string that will be used to encrypt the data + stored by the cookie. It is required and has no default. + +Example +~~~~~~~ + +.. code-block:: xml + :linenos: + + <default_permission + name="view" + /> + +Alternatives +~~~~~~~~~~~~ + +Using the ``default_permission`` argument to the +:class:`repoze.bfg.configuration.Configurator` constructor can be used +to achieve the same purpose. + +Using the +:meth:`repoze.bfg.configuration.Configurator.set_default_permission` +method can be used to achieve the same purpose when using imperative +configuration. + +See Also +~~~~~~~~ + +See also :ref:``setting_a_default_permission``. 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. |
