summaryrefslogtreecommitdiff
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
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.
-rw-r--r--CHANGES.txt35
-rw-r--r--TODO.txt7
-rw-r--r--docs/api/configuration.rst4
-rw-r--r--docs/glossary.rst4
-rw-r--r--docs/narr/security.rst35
-rw-r--r--docs/zcml.rst1
-rw-r--r--docs/zcml/default_permission.rst61
-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
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)
===================
diff --git a/TODO.txt b/TODO.txt
index 2a81a5741..ce29c9e46 100644
--- a/TODO.txt
+++ b/TODO.txt
@@ -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.