diff options
| author | Chris McDonough <chrism@agendaless.com> | 2009-05-06 05:08:19 +0000 |
|---|---|---|
| committer | Chris McDonough <chrism@agendaless.com> | 2009-05-06 05:08:19 +0000 |
| commit | 226b49247817931b5f932980538c74dd8835491e (patch) | |
| tree | 6784930ea34e4b4adccc134891e87c4d9f8b4741 | |
| parent | f6bc62a37eb41f9eaf8fe91ef7c80af6b742f4ca (diff) | |
| download | pyramid-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.txt | 38 | ||||
| -rw-r--r-- | docs/api/security.rst | 20 | ||||
| -rw-r--r-- | docs/glossary.rst | 6 | ||||
| -rw-r--r-- | docs/narr/security.rst | 65 | ||||
| -rw-r--r-- | repoze/bfg/security.py | 320 | ||||
| -rw-r--r-- | repoze/bfg/tests/test_security.py | 243 |
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,) + |
