summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChris McDonough <chrism@agendaless.com>2009-05-06 05:08:19 +0000
committerChris McDonough <chrism@agendaless.com>2009-05-06 05:08:19 +0000
commit226b49247817931b5f932980538c74dd8835491e (patch)
tree6784930ea34e4b4adccc134891e87c4d9f8b4741
parentf6bc62a37eb41f9eaf8fe91ef7c80af6b742f4ca (diff)
downloadpyramid-226b49247817931b5f932980538c74dd8835491e.tar.gz
pyramid-226b49247817931b5f932980538c74dd8835491e.tar.bz2
pyramid-226b49247817931b5f932980538c74dd8835491e.zip
Features
-------- - Two new security policies were added: RemoteUserInheritingACLSecurityPolicy and WhoInheritingACLSecurityPolicy. These are security policies which take into account *all* ACLs defined in the lineage of a context rather than stopping at the first ACL found in a lineage. See the "Security" chapter of the API documentation for more information. - The API and narrative documentation dealing with security was changed to introduce the new "inheriting" security policy variants. - Added glossary entry for "lineage". Deprecations ------------ - The security policy previously named ``RepozeWhoIdentityACLSecurityPolicy`` now has the slightly saner name of ``WhoACLSecurityPolicy``. A deprecation warning is emitted when this policy is imported under the "old" name; usually this is due to its use in ZCML within your application. If you're getting this deprecation warning, change your ZCML to use the new name, e.g. change:: <utility provides="repoze.bfg.interfaces.ISecurityPolicy" factory="repoze.bfg.security.RepozeWhoIdentityACLSecurityPolicy" /> To:: <utility provides="repoze.bfg.interfaces.ISecurityPolicy" factory="repoze.bfg.security.WhoACLSecurityPolicy" />
-rw-r--r--CHANGES.txt38
-rw-r--r--docs/api/security.rst20
-rw-r--r--docs/glossary.rst6
-rw-r--r--docs/narr/security.rst65
-rw-r--r--repoze/bfg/security.py320
-rw-r--r--repoze/bfg/tests/test_security.py243
6 files changed, 647 insertions, 45 deletions
diff --git a/CHANGES.txt b/CHANGES.txt
index 726658cf8..f45a6af1b 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -1,6 +1,42 @@
0.8dev (unreleased)
-...
+Features
+--------
+
+- Two new security policies were added:
+ RemoteUserInheritingACLSecurityPolicy and
+ WhoInheritingACLSecurityPolicy. These are security policies which
+ take into account *all* ACLs defined in the lineage of a context
+ rather than stopping at the first ACL found in a lineage. See the
+ "Security" chapter of the API documentation for more information.
+
+- The API and narrative documentation dealing with security was
+ changed to introduce the new "inheriting" security policy variants.
+
+- Added glossary entry for "lineage".
+
+Deprecations
+------------
+
+- The security policy previously named
+ ``RepozeWhoIdentityACLSecurityPolicy`` now has the slightly saner
+ name of ``WhoACLSecurityPolicy``. A deprecation warning is emitted
+ when this policy is imported under the "old" name; usually this is
+ due to its use in ZCML within your application. If you're getting
+ this deprecation warning, change your ZCML to use the new name,
+ e.g. change::
+
+ <utility
+ provides="repoze.bfg.interfaces.ISecurityPolicy"
+ factory="repoze.bfg.security.RepozeWhoIdentityACLSecurityPolicy"
+ />
+
+ To::
+
+ <utility
+ provides="repoze.bfg.interfaces.ISecurityPolicy"
+ factory="repoze.bfg.security.WhoACLSecurityPolicy"
+ />
0.8a4 (2009-05-04)
==================
diff --git a/docs/api/security.rst b/docs/api/security.rst
index accc46205..5990f1809 100644
--- a/docs/api/security.rst
+++ b/docs/api/security.rst
@@ -32,6 +32,20 @@ Constants
principal id (according to the security policy). Its actual value
is the string 'system.Authenticated'.
+.. attribute:: ALL_PERMISSIONS
+
+ An object that can be used as the ``permission`` member of an ACE
+ which matches all permissions unconditionally. For example, an
+ ACE that uses ``ALL_PERMISSIONS`` might be composed like so:
+ ``('Deny', 'system.Everyone', ALL_PERMISSIONS)``.
+
+.. attribute:: DENY_ALL
+
+ A convenience shorthand ACE that defines ``('Deny',
+ 'system.Everyone', ALL_PERMISSIONS)``. This is often used as the
+ last ACE in an ACL in systems that use an "inheriting" security
+ policy, representing the concept "don't inherit any other ACEs".
+
Return Values
~~~~~~~~~~~~~
@@ -64,6 +78,10 @@ Return Values
Security Policies
~~~~~~~~~~~~~~~~~
-.. autofunction:: RepozeWhoIdentityACLSecurityPolicy
+.. autofunction:: WhoACLSecurityPolicy
+
+.. autofunction:: WhoInheritingACLSecurityPolicy
.. autofunction:: RemoteUserACLSecurityPolicy
+
+.. autofunction:: RemoteUserInheritingACLSecurityPolicy
diff --git a/docs/glossary.rst b/docs/glossary.rst
index fc346252d..b55f8395c 100644
--- a/docs/glossary.rst
+++ b/docs/glossary.rst
@@ -332,3 +332,9 @@ Glossary
is typically the physical root object (the object returned by the
application root factory) unless :ref:`vhosting_chapter` is in
use.
+ Lineage
+ An ordered sequence of objects based on a ":term:`location` -aware"
+ context. The lineage of any given :term:`context` is composed of
+ itself, its parent, its parent's parent, and so on. The order of
+ the sequence is context-first, then the parent of the context,
+ then its parent's parent, and so on.
diff --git a/docs/narr/security.rst b/docs/narr/security.rst
index 87907730e..38335b6cd 100644
--- a/docs/narr/security.rst
+++ b/docs/narr/security.rst
@@ -25,26 +25,34 @@ However, if you add the following bit of code to your application's
<utility
provides="repoze.bfg.interfaces.ISecurityPolicy"
- factory="repoze.bfg.security.RemoteUserACLSecurityPolicy"
+ factory="repoze.bfg.security.RemoteUserInheritingACLSecurityPolicy"
/>
The above inscrutable stanza enables the
-``RemoteUserACLSecurityPolicy`` to be in effect for every request to
-your application. The ``RemoteUserACLSecurityPolicy`` is a policy
-which compares the ``REMOTE_USER`` variable passed in the request's
-environment (as the sole :term:`principal`) against the principals
-present in any :term:`ACL` found in model data when attempting to call
-some :term:`view`. The policy either allows the view that the
-permission was declared for to be called, or returns a ``401
-Unathorized`` response code to the upstream WSGI server.
-
-.. note:: Another security policy also exists:
- ``RepozeWhoIdentityACLSecurityPolicy``. This policy uses principal
+``RemoteUserInheritingACLSecurityPolicy`` to be in effect for every
+request to your application. The
+``RemoteUserInheritingACLSecurityPolicy`` is a policy which compares
+the ``REMOTE_USER`` variable passed in the request's environment (as
+the sole :term:`principal`) against the principals present in any
+:term:`ACL` found in model data when attempting to call some
+:term:`view`. The policy either allows the view that the permission
+was declared for to be called, or returns a ``401 Unathorized``
+response code to the upstream WSGI server.
+
+.. note:: Another "inheriting" security policy also exists:
+ ``WhoInheritingACLSecurityPolicy``. This policy uses principal
information found in the ``repoze.who.identity`` value set into the
WSGI environment by the :term:`repoze.who` middleware rather than
- ``REMOTE_USER`` information. This policy only works when
+ ``REMOTE_USER`` information. This policy only works properly when
:term:`repoze.who` middleware is present in the WSGI pipeline.
+.. note:: "non-inheriting" security policy variants of the
+ (``WhoACLSecurityPolicy`` and ``RemoteUserACLSecurityPolicy``) also
+ exist. These policies use the *first* ACL found as the canonical
+ ACL; they do not continue searching up the context lineage to find
+ "inherited" ACLs. It is recommended that you use the inheriting
+ variants unless you need this feature.
+
.. note:: See :ref:`security_policies_api_section` for more
information about the features of the default security policies.
@@ -129,21 +137,46 @@ group, we can collapse this into a single ACE, as below.
A principal is usually a user id, however it also may be a group id if
your authentication system provides group information and the security
policy is written to respect them. The
-``RemoteUserACLSecurityPolicy`` does not respect group information.
+``RemoteUserInheritingACLSecurityPolicy`` does not respect group
+information.
ACL Inheritance
---------------
-While the security policy is in place, if a model object does not have
+While any security policy is in place, if a model object does not have
an ACL when it is the context, its *parent* is consulted for an ACL.
If that object does not have an ACL, *its* parent is consulted for an
ACL, ad infinitum, until we've reached the root and there are no more
parents left.
-The *first* ACL found by the security policy will be used as the
+With *non-inheriting* security policy variants
+(e.g. ``WhoACLSecurityPolicy`` and ``RemoteUserACLSecurityPolicy``),
+the *first* ACL found by the security policy will be used as the
effective ACL. No combination of ACLs found during traversal or
backtracking is done.
+With *inheriting* security policy variants
+(e.g. ``WhoInheritingACLSecurityPolicy`` and
+``RemoteUserInheritingACLSecurityPolicy``), *all* ACLs in the
+context's :term:`lineage` are consulted when determining whether
+access is allowed or denied.
+
+:ref:`security_policies_api_section` for more information about the
+features of the default security policies and the difference between
+the inheriting and non-inheriting variants.
+
+.. note:: It is recommended that you use the inheriting variant of a
+ security policy. Inheriting variants of security policies make it
+ possible for you to form a security strategy based on context ACL
+ "inheritance" rather than needing to keep all information about an
+ object's security state in a single ACL attached to that object.
+ It's much easier to code applications that dynamically change ACLs
+ if ACL inheritance is used. In reality, the non-inheriting
+ security policy variants exist only for backwards compatibility
+ with applications that used them in versions of :mod:`repoze.bfg`
+ before 0.8. If this backwards compatibility was not required, the
+ non-inheriting variants probably just wouldn't exist.
+
Location-Awareness
------------------
diff --git a/repoze/bfg/security.py b/repoze/bfg/security.py
index 1ee7b28a0..bfc43d752 100644
--- a/repoze/bfg/security.py
+++ b/repoze/bfg/security.py
@@ -1,5 +1,6 @@
-from zope.interface import implements
from zope.component import queryUtility
+from zope.deprecation import deprecated
+from zope.interface import implements
from repoze.bfg.location import lineage
@@ -12,6 +13,18 @@ Authenticated = 'system.Authenticated'
Allow = 'Allow'
Deny = 'Deny'
+class AllPermissionsList(object):
+ """ Stand in 'permission list' to represent all permissions """
+ def __iter__(self):
+ return ()
+ def __contains__(self, other):
+ return True
+ def __eq__(self, other):
+ return isinstance(other, self.__class__)
+
+ALL_PERMISSIONS = AllPermissionsList()
+DENY_ALL = (Deny, Everyone, ALL_PERMISSIONS)
+
def has_permission(permission, context, request):
""" Provided a permission (a string or unicode object), a context
(a model instance) and a request object, return an instance of
@@ -132,6 +145,128 @@ class ACLSecurityPolicy(object):
return []
+class InheritingACLSecurityPolicy(object):
+ """ A security policy which uses ACLs in the following ways:
+
+ - When checking whether a user is permitted (via the ``permits``
+ method), the security policy consults the ``context`` for an ACL
+ first. If no ACL exists on the context, or one does exist but
+ the ACL does not explicitly allow or deny access for any of the
+ effective principals, consult the context's parent ACL, and so
+ on, until the lineage is exhausted or we determine that the
+ policy permits or denies.
+
+ During this processing, if any ``Deny`` ACE is found matching
+ any effective principal, stop processing by returning an
+ ``ACLDenied`` (equals False) immediately. If any ``Allow`` ACE
+ is found matching any effective principal, stop processing by
+ returning an ``ACLAllowed`` (equals True) immediately. If we
+ exhaust the context's lneage, and no ACE has explicitly
+ permitted or denied access, return an ``ACLDenied``. This
+ differs from the non-inheriting security policy (the
+ ``ACLSecurityPolicy``) by virtue of the fact that it does not
+ stop looking for ACLs in the object lineage after it finds the
+ first one.
+
+ - When computing principals allowed by a permission via the
+ ``principals_allowed_by_permission`` method, we compute the set
+ of principals that are explicitly granted the ``permission``.
+ We do this by walking 'up' the object graph *from the root* to
+ the context. During this walking process, if we find an
+ explicit ``Allow`` ACE for a principal that matches the
+ ``permission``, the principal is included in the allow list.
+ However, if later in the walking process that user is mentioned
+ in any ``Deny`` ACE for the permission, the user is removed from
+ the allow list. If a ``Deny`` to the principal ``Everyone`` is
+ encountered during the walking process that matches the
+ ``permission``, the allow list is cleared for all principals
+ encountered in previous ACLs. The walking process ends after
+ we've processed the any ACL directly attached to ``context``; a
+ list of principals is returned.
+
+ - Other aspects of this policy are the same as those in the
+ ACLSecurityPolicy (e.g. ``effective_principals``,
+ ``authenticated_userid``).
+ """
+ implements(ISecurityPolicy)
+
+ def __init__(self, get_principals):
+ self.get_principals = get_principals
+
+ def permits(self, context, request, permission):
+ """ Return ``ACLAllowed`` if the policy permits access,
+ ``ACLDenied`` if not. """
+ principals = set(self.effective_principals(request))
+
+ for location in lineage(context):
+ try:
+ acl = location.__acl__
+ except AttributeError:
+ continue
+
+ for ace in acl:
+ ace_action, ace_principal, ace_permissions = ace
+ if ace_principal in principals:
+ if not hasattr(ace_permissions, '__iter__'):
+ ace_permissions = [ace_permissions]
+ if permission in ace_permissions:
+ if ace_action == Allow:
+ return ACLAllowed(ace, acl, permission,
+ principals, location)
+ else:
+ return ACLDenied(ace, acl, permission,
+ principals, location)
+
+ # default deny if no ACL in lineage at all
+ return ACLDenied(None, None, permission, principals, context)
+
+ def authenticated_userid(self, request):
+ principals = self.get_principals(request)
+ if principals:
+ return principals[0]
+
+ def effective_principals(self, request):
+ effective_principals = [Everyone]
+ principal_ids = self.get_principals(request)
+
+ if principal_ids:
+ effective_principals.append(Authenticated)
+ effective_principals.extend(principal_ids)
+
+ return effective_principals
+
+ def principals_allowed_by_permission(self, context, permission):
+ allowed = set()
+
+ for location in reversed(list(lineage(context))):
+ # NB: we're walking *up* the object graph from the root
+ try:
+ acl = location.__acl__
+ except AttributeError:
+ continue
+
+ allowed_here = set()
+ denied_here = set()
+
+ for ace_action, ace_principal, ace_permissions in acl:
+ if not hasattr(ace_permissions, '__iter__'):
+ ace_permissions = [ace_permissions]
+ if ace_action == Allow and permission in ace_permissions:
+ if not ace_principal in denied_here:
+ allowed_here.add(ace_principal)
+ if ace_action == Deny and permission in ace_permissions:
+ denied_here.add(ace_principal)
+ if ace_principal == Everyone:
+ # clear the entire allowed set, as we've hit a
+ # deny of Everyone ala (Deny, Everyone, ALL)
+ allowed = set()
+ elif ace_principal in allowed:
+ allowed.remove(ace_principal)
+
+ allowed.update(allowed_here)
+
+ return allowed
+
def get_remoteuser(request):
user_id = request.environ.get('REMOTE_USER')
if user_id:
@@ -144,16 +279,21 @@ def RemoteUserACLSecurityPolicy():
- examines the request.environ for the REMOTE_USER variable and
uses any non-false value as a principal id for this request.
- - uses an ACL-based authorization model which attempts to find an
- ACL on the context, and which returns ``Allowed`` from its
- 'permits' method if the ACL found grants access to the current
- principal. It returns ``Denied`` if permission was not granted
- (either explicitly via a deny or implicitly by not finding a
- matching ACE action). An ACL is an ordered sequence of ACE
- tuples, e.g. ``[(Allow, Everyone, 'read'), (Deny, 'george',
- 'write')]``. ACLs stored on model instance objects as their
- __acl__ attribute will be used by the security machinery to
- grant or deny access.
+ - uses an ACL-based authorization model which attempts to find the
+ *first* ACL in the context' lineage. It returns ``Allowed`` from
+ its 'permits' method if the single ACL found grants access to the
+ current principal. It returns ``Denied`` if permission was not
+ granted (either explicitly via a deny or implicitly by not finding
+ a matching ACE action). The *first* ACL found in the context's
+ lineage is considered canonical; no searching is done for other
+ security attributes after the first ACL is found in the context'
+ lineage. Use the 'inheriting' variant of this policy to consider
+ more than one ACL in the lineage.
+
+ An ACL is an ordered sequence of ACE tuples, e.g. ``[(Allow,
+ Everyone, 'read'), (Deny, 'george', 'write')]``. ACLs stored on
+ model instance objects as their ``__acl__`` attribute will be used
+ by the security machinery to grant or deny access.
Enable this security policy by adding the following to your
application's ``configure.zcml``:
@@ -168,6 +308,66 @@ def RemoteUserACLSecurityPolicy():
"""
return ACLSecurityPolicy(get_remoteuser)
+def RemoteUserInheritingACLSecurityPolicy():
+ """ A security policy which:
+
+ - examines the request.environ for the REMOTE_USER variable and
+ uses any non-false value as a principal id for this request.
+
+ - Differs from the non-inheriting security policy variants
+ (e.g. ``ACLSecurityPolicy``) by virtue of the fact that it does
+ not stop looking for ACLs in the object lineage after it finds
+ the first one.
+
+ - When checking whether a user is permitted (via the ``permits``
+ method), the security policy consults the ``context`` for an ACL
+ first. If no ACL exists on the context, or one does exist but
+ the ACL does not explicitly allow or deny access for any of the
+ effective principals, consult the context's parent ACL, and so
+ on, until the lineage is exhausted or we determine that the
+ policy permits or denies.
+
+ During this processing, if any ``Deny`` ACE is found matching
+ any effective principal, stop processing by returning an
+ ``ACLDenied`` (equals False) immediately. If any ``Allow`` ACE
+ is found matching any effective principal, stop processing by
+ returning an ``ACLAllowed`` (equals True) immediately. If we
+ exhaust the context's lneage, and no ACE has explicitly
+ permitted or denied access, return an ``ACLDenied``.
+
+ - When computing principals allowed by a permission via the
+ ``principals_allowed_by_permission`` method, we compute the set
+ of principals that are explicitly granted the ``permission``.
+ We do this by walking 'up' the object graph *from the root* to
+ the context. During this walking process, if we find an
+ explicit ``Allow`` ACE for a principal that matches the
+ ``permission``, the principal is included in the allow list.
+ However, if later in the walking process that user is mentioned
+ in any ``Deny`` ACE for the permission, the user is removed from
+ the allow list. If a ``Deny`` to the principal ``Everyone`` is
+ encountered during the walking process that matches the
+ ``permission``, the allow list is cleared for all principals
+ encountered in previous ACLs. The walking process ends after
+ we've processed the any ACL directly attached to ``context``; a
+ list of principals is returned.
+
+ - Other aspects of this policy are the same as those in the
+ ACLSecurityPolicy (e.g. ``effective_principals``,
+ ``authenticated_userid``).
+
+ Enable this security policy by adding the following to your
+ application's ``configure.zcml``:
+
+ .. code-block:: xml
+
+ <utility
+ provides="repoze.bfg.interfaces.ISecurityPolicy"
+ factory="repoze.bfg.security.RemoteUserInheritingACLSecurityPolicy"
+ />
+
+ """
+ return InheritingACLSecurityPolicy(get_remoteuser)
+
def get_who_principals(request):
identity = request.environ.get('repoze.who.identity')
if not identity:
@@ -176,7 +376,7 @@ def get_who_principals(request):
principals.extend(identity.get('groups', []))
return principals
-def RepozeWhoIdentityACLSecurityPolicy():
+def WhoACLSecurityPolicy():
"""
A security policy which:
@@ -185,16 +385,21 @@ def RepozeWhoIdentityACLSecurityPolicy():
are composed of ``repoze.who.identity['repoze.who.userid']``
plus ``repoze.who.identity.get('groups', [])``.
- - uses an ACL-based authorization model which attempts to find an
- ACL on the context, and which returns ``Allowed`` from its
- 'permits' method if the ACL found grants access to the current
- principal. It returns ``Denied`` if permission was not granted
- (either explicitly via a deny or implicitly by not finding a
- matching ACE action). An ACL is an ordered sequence of ACE
- tuples, e.g. ``[(Allow, Everyone, 'read'), (Deny, 'george',
- 'write')]``. ACLs stored on model instance objects as their
- __acl__ attribute will be used by the security machinery to
- grant or deny access.
+ - uses an ACL-based authorization model which attempts to find the
+ *first* ACL in the context' lineage. It returns ``Allowed`` from
+ its 'permits' method if the single ACL found grants access to the
+ current principal. It returns ``Denied`` if permission was not
+ granted (either explicitly via a deny or implicitly by not finding
+ a matching ACE action). The *first* ACL found in the context's
+ lineage is considered canonical; no searching is done for other
+ security attributes after the first ACL is found in the context'
+ lineage. Use the 'inheriting' variant of this policy to consider
+ more than one ACL in the lineage.
+
+ An ACL is an ordered sequence of ACE tuples, e.g. ``[(Allow,
+ Everyone, 'read'), (Deny, 'george', 'write')]``. ACLs stored on
+ model instance objects as their ``__acl__`` attribute will be used
+ by the security machinery to grant or deny access.
Enable this security policy by adding the following to your
application's ``configure.zcml``:
@@ -203,11 +408,80 @@ def RepozeWhoIdentityACLSecurityPolicy():
<utility
provides="repoze.bfg.interfaces.ISecurityPolicy"
- factory="repoze.bfg.security.RepozeWhoIdentityACLSecurityPolicy"
+ factory="repoze.bfg.security.WhoACLSecurityPolicy"
/>
"""
return ACLSecurityPolicy(get_who_principals)
+RepozeWhoIdentityACLSecurityPolicy = WhoACLSecurityPolicy
+
+deprecated('RepozeWhoIdentityACLSecurityPolicy',
+ '(repoze.bfg.security.RepozeWhoIdentityACLSecurityPolicy '
+ 'should now be imported as '
+ 'repoze.bfg.security.WhoACLSecurityPolicy)',
+ )
+
+def WhoInheritingACLSecurityPolicy():
+ """ A security policy which:
+
+ - examines the request.environ for the ``repoze.who.identity``
+ dictionary. If one is found, the principal ids for the request
+ are composed of ``repoze.who.identity['repoze.who.userid']``
+ plus ``repoze.who.identity.get('groups', [])``.
+
+ - Differs from the non-inheriting security policy variants
+ (e.g. ``ACLSecurityPolicy``) by virtue of the fact that it does
+ not stop looking for ACLs in the object lineage after it finds
+ the first one.
+
+ - When checking whether a user is permitted (via the ``permits``
+ method), the security policy consults the ``context`` for an ACL
+ first. If no ACL exists on the context, or one does exist but
+ the ACL does not explicitly allow or deny access for any of the
+ effective principals, consult the context's parent ACL, and so
+ on, until the lineage is exhausted or we determine that the
+ policy permits or denies.
+
+ During this processing, if any ``Deny`` ACE is found matching
+ any effective principal, stop processing by returning an
+ ``ACLDenied`` (equals False) immediately. If any ``Allow`` ACE
+ is found matching any effective principal, stop processing by
+ returning an ``ACLAllowed`` (equals True) immediately. If we
+ exhaust the context's lneage, and no ACE has explicitly
+ permitted or denied access, return an ``ACLDenied``.
+
+ - When computing principals allowed by a permission via the
+ ``principals_allowed_by_permission`` method, we compute the set
+ of principals that are explicitly granted the ``permission``.
+ We do this by walking 'up' the object graph *from the root* to
+ the context. During this walking process, if we find an
+ explicit ``Allow`` ACE for a principal that matches the
+ ``permission``, the principal is included in the allow list.
+ However, if later in the walking process that user is mentioned
+ in any ``Deny`` ACE for the permission, the user is removed from
+ the allow list. If a ``Deny`` to the principal ``Everyone`` is
+ encountered during the walking process that matches the
+ ``permission``, the allow list is cleared for all principals
+ encountered in previous ACLs. The walking process ends after
+ we've processed the any ACL directly attached to ``context``; a
+ list of principals is returned.
+
+ - Other aspects of this policy are the same as those in the
+ ACLSecurityPolicy (e.g. ``effective_principals``,
+ ``authenticated_userid``).
+
+ Enable this security policy by adding the following to your
+ application's ``configure.zcml``:
+
+ .. code-block:: xml
+
+ <utility
+ provides="repoze.bfg.interfaces.ISecurityPolicy"
+ factory="repoze.bfg.security.WhoInheritingACLSecurityPolicy"
+ />
+ """
+ return InheritingACLSecurityPolicy(get_who_principals)
+
class PermitsResult(int):
def __new__(cls, s, *args):
inst = int.__new__(cls, cls.boolval)
diff --git a/repoze/bfg/tests/test_security.py b/repoze/bfg/tests/test_security.py
index a9aaaa30a..20ec1d4db 100644
--- a/repoze/bfg/tests/test_security.py
+++ b/repoze/bfg/tests/test_security.py
@@ -243,6 +243,217 @@ class TestACLSecurityPolicy(unittest.TestCase):
result = policy.principals_allowed_by_permission(None, 'read')
self.assertEqual(result, [])
+class TestInheritingACLSecurityPolicy(unittest.TestCase):
+ def setUp(self):
+ cleanUp()
+
+ def tearDown(self):
+ cleanUp()
+
+ def _getTargetClass(self):
+ from repoze.bfg.security import InheritingACLSecurityPolicy
+ return InheritingACLSecurityPolicy
+
+ def _makeOne(self, *arg, **kw):
+ klass = self._getTargetClass()
+ return klass(*arg, **kw)
+
+ def test_class_implements_ISecurityPolicy(self):
+ from zope.interface.verify import verifyClass
+ from repoze.bfg.interfaces import ISecurityPolicy
+ verifyClass(ISecurityPolicy, self._getTargetClass())
+
+ def test_instance_implements_ISecurityPolicy(self):
+ from zope.interface.verify import verifyObject
+ from repoze.bfg.interfaces import ISecurityPolicy
+ verifyObject(ISecurityPolicy, self._makeOne(lambda *arg: None))
+
+ def test_permits(self):
+ from repoze.bfg.security import Deny
+ from repoze.bfg.security import Allow
+ from repoze.bfg.security import Everyone
+ from repoze.bfg.security import Authenticated
+ from repoze.bfg.security import ALL_PERMISSIONS
+ from repoze.bfg.security import DENY_ALL
+ policy = self._makeOne(lambda *arg: [])
+ root = DummyContext()
+ community = DummyContext(__name__='community', __parent__=root)
+ blog = DummyContext(__name__='blog', __parent__=community)
+ root.__acl__ = [
+ (Allow, Authenticated, VIEW),
+ ]
+ community.__acl__ = [
+ (Allow, 'fred', ALL_PERMISSIONS),
+ (Allow, 'wilma', VIEW),
+ DENY_ALL,
+ ]
+ blog.__acl__ = [
+ (Allow, 'barney', MEMBER_PERMS),
+ (Allow, 'wilma', VIEW),
+ ]
+ policy = self._makeOne(lambda request: request.principals)
+ request = DummyRequest({})
+
+ request.principals = ['wilma']
+ result = policy.permits(blog, request, 'view')
+ self.assertEqual(result, True)
+ self.assertEqual(result.context, blog)
+ self.assertEqual(result.ace, (Allow, 'wilma', VIEW))
+ result = policy.permits(blog, request, 'delete')
+ self.assertEqual(result, False)
+ self.assertEqual(result.context, community)
+ self.assertEqual(result.ace, (Deny, Everyone, ALL_PERMISSIONS))
+
+ request.principals = ['fred']
+ result = policy.permits(blog, request, 'view')
+ self.assertEqual(result, True)
+ self.assertEqual(result.context, community)
+ self.assertEqual(result.ace, (Allow, 'fred', ALL_PERMISSIONS))
+ result = policy.permits(blog, request, 'doesntevenexistyet')
+ self.assertEqual(result, True)
+ self.assertEqual(result.context, community)
+ self.assertEqual(result.ace, (Allow, 'fred', ALL_PERMISSIONS))
+
+ request.principals = ['barney']
+ result = policy.permits(blog, request, 'view')
+ self.assertEqual(result, True)
+ self.assertEqual(result.context, blog)
+ self.assertEqual(result.ace, (Allow, 'barney', MEMBER_PERMS))
+ result = policy.permits(blog, request, 'administer')
+ self.assertEqual(result, False)
+ self.assertEqual(result.context, community)
+ self.assertEqual(result.ace, (Deny, Everyone, ALL_PERMISSIONS))
+
+ request.principals = ['someguy']
+ result = policy.permits(root, request, 'view')
+ self.assertEqual(result, True)
+ self.assertEqual(result.context, root)
+ self.assertEqual(result.ace, (Allow, Authenticated, VIEW))
+ result = policy.permits(blog, request, 'view')
+ self.assertEqual(result, False)
+ self.assertEqual(result.context, community)
+ self.assertEqual(result.ace, (Deny, Everyone, ALL_PERMISSIONS))
+
+ request.principals = []
+ result = policy.permits(root, request, 'view')
+ self.assertEqual(result, False)
+ self.assertEqual(result.context, root)
+ self.assertEqual(result.ace, None)
+
+ request.principals = []
+ context = DummyContext()
+ result = policy.permits(context, request, 'view')
+ self.assertEqual(result, False)
+
+ def test_principals_allowed_by_permission_direct(self):
+ from repoze.bfg.security import Allow
+ from repoze.bfg.security import DENY_ALL
+ context = DummyContext()
+ acl = [ (Allow, 'chrism', ('read', 'write')),
+ DENY_ALL,
+ (Allow, 'other', 'read') ]
+ context.__acl__ = acl
+ policy = self._makeOne(lambda *arg: None)
+ result = sorted(
+ policy.principals_allowed_by_permission(context, 'read'))
+ self.assertEqual(result, ['chrism', 'other'])
+
+ def test_principals_allowed_by_permission(self):
+ from repoze.bfg.security import Allow
+ from repoze.bfg.security import Deny
+ from repoze.bfg.security import DENY_ALL
+ from repoze.bfg.security import ALL_PERMISSIONS
+ root = DummyContext(__name__='', __parent__=None)
+ community = DummyContext(__name__='community', __parent__=root)
+ blog = DummyContext(__name__='blog', __parent__=community)
+ root.__acl__ = [ (Allow, 'chrism', ('read', 'write')),
+ (Allow, 'other', ('read',)),
+ (Allow, 'jim', ALL_PERMISSIONS)]
+ community.__acl__ = [ (Deny, 'flooz', 'read'),
+ (Allow, 'flooz', 'read'),
+ (Allow, 'mork', 'read'),
+ (Deny, 'jim', 'read'),
+ (Allow, 'someguy', 'manage')]
+ blog.__acl__ = [ (Allow, 'fred', 'read'),
+ DENY_ALL]
+
+ policy = self._makeOne(lambda *arg: None)
+ result = sorted(policy.principals_allowed_by_permission(blog, 'read'))
+ self.assertEqual(result, ['fred'])
+ result = sorted(policy.principals_allowed_by_permission(community,
+ 'read'))
+ self.assertEqual(result, ['chrism', 'mork', 'other'])
+ result = sorted(policy.principals_allowed_by_permission(community,
+ 'read'))
+ result = sorted(policy.principals_allowed_by_permission(root, 'read'))
+ self.assertEqual(result, ['chrism', 'jim', 'other'])
+
+ def test_principals_allowed_by_permission_no_acls(self):
+ policy = self._makeOne(lambda *arg: None)
+ context = DummyContext()
+ result = sorted(policy.principals_allowed_by_permission(context,'read'))
+ self.assertEqual(result, [])
+
+ def test_effective_principals(self):
+ context = DummyContext()
+ request = DummyRequest({})
+ request.principals = ['fred']
+ policy = self._makeOne(lambda request: request.principals)
+ result = sorted(policy.effective_principals(request))
+ from repoze.bfg.security import Everyone
+ from repoze.bfg.security import Authenticated
+ self.assertEqual(result,
+ ['fred', Authenticated, Everyone])
+
+ def test_no_effective_principals(self):
+ context = DummyContext()
+ request = DummyRequest({})
+ request.principals = []
+ policy = self._makeOne(lambda request: request.principals)
+ result = sorted(policy.effective_principals(request))
+ from repoze.bfg.security import Everyone
+ self.assertEqual(result, [Everyone])
+
+ def test_authenticated_userid(self):
+ context = DummyContext()
+ request = DummyRequest({})
+ request.principals = ['fred']
+ policy = self._makeOne(lambda request: request.principals)
+ result = policy.authenticated_userid(request)
+ self.assertEqual(result, 'fred')
+
+ def test_no_authenticated_userid(self):
+ context = DummyContext()
+ request = DummyRequest({})
+ request.principals = []
+ policy = self._makeOne(lambda request: request.principals)
+ result = policy.authenticated_userid(request)
+ self.assertEqual(result, None)
+
+class TestAllPermissionsList(unittest.TestCase):
+ def setUp(self):
+ cleanUp()
+
+ def tearDown(self):
+ cleanUp()
+
+ def _getTargetClass(self):
+ from repoze.bfg.security import AllPermissionsList
+ return AllPermissionsList
+
+ def _makeOne(self):
+ return self._getTargetClass()()
+
+ def test_it(self):
+ thing = self._makeOne()
+ self.failUnless(thing.__eq__(thing))
+ self.assertEqual(thing.__iter__(), ())
+ self.failUnless('anything' in thing)
+
+ def test_singleton(self):
+ from repoze.bfg.security import ALL_PERMISSIONS
+ self.assertEqual(ALL_PERMISSIONS.__class__, self._getTargetClass())
+
class TestRemoteUserACLSecurityPolicy(unittest.TestCase):
def setUp(self):
cleanUp()
@@ -294,7 +505,12 @@ class TestRemoteUserACLSecurityPolicy(unittest.TestCase):
from repoze.bfg.security import Everyone
self.assertEqual(result, [Everyone])
-class TestRepozeWhoIdentityACLSecurityPolicy(unittest.TestCase):
+class TestRemoteUserInheritingACLSecurityPolicy(TestRemoteUserACLSecurityPolicy):
+ def _getTargetClass(self):
+ from repoze.bfg.security import RemoteUserInheritingACLSecurityPolicy
+ return RemoteUserInheritingACLSecurityPolicy
+
+class TestWhoACLSecurityPolicy(unittest.TestCase):
def setUp(self):
cleanUp()
@@ -302,8 +518,8 @@ class TestRepozeWhoIdentityACLSecurityPolicy(unittest.TestCase):
cleanUp()
def _getTargetClass(self):
- from repoze.bfg.security import RepozeWhoIdentityACLSecurityPolicy
- return RepozeWhoIdentityACLSecurityPolicy
+ from repoze.bfg.security import WhoACLSecurityPolicy
+ return WhoACLSecurityPolicy
def _makeOne(self, *arg, **kw):
klass = self._getTargetClass()
@@ -347,6 +563,11 @@ class TestRepozeWhoIdentityACLSecurityPolicy(unittest.TestCase):
from repoze.bfg.security import Everyone
self.assertEqual(result, [Everyone])
+class TestWhoInheritingACLSecurityPolicy(TestWhoACLSecurityPolicy):
+ def _getTargetClass(self):
+ from repoze.bfg.security import WhoInheritingACLSecurityPolicy
+ return WhoInheritingACLSecurityPolicy
+
class TestAPIFunctions(unittest.TestCase):
def setUp(self):
cleanUp()
@@ -532,7 +753,8 @@ class TestACLDenied(unittest.TestCase):
self.failUnless("with msg %r>" % msg in repr(denied))
class DummyContext:
- pass
+ def __init__(self, *arg, **kw):
+ self.__dict__.update(kw)
class DummyRequest:
def __init__(self, environ):
@@ -555,3 +777,16 @@ class DummySecurityPolicy:
def principals_allowed_by_permission(self, context, permission):
return ['fred', 'bob']
+VIEW = 'view'
+EDIT = 'edit'
+CREATE = 'create'
+DELETE = 'delete'
+MODERATE = 'moderate'
+ADMINISTER = 'administer'
+COMMENT = 'comment'
+
+GUEST_PERMS = (VIEW, COMMENT)
+MEMBER_PERMS = GUEST_PERMS + (EDIT, CREATE, DELETE)
+MODERATOR_PERMS = MEMBER_PERMS + (MODERATE,)
+ADMINISTRATOR_PERMS = MODERATOR_PERMS + (ADMINISTER,)
+