[Zope3-checkins] SVN: Zope3/trunk/src/zope/app/securitypolicy/ Extended the security policy to use groups.

Jim Fulton jim at zope.com
Thu Nov 11 12:14:35 EST 2004


Log message for revision 28439:
  Extended the security policy to use groups.
  

Changed:
  U   Zope3/trunk/src/zope/app/securitypolicy/tests/test_zopepolicy.py
  U   Zope3/trunk/src/zope/app/securitypolicy/zopepolicy.py
  U   Zope3/trunk/src/zope/app/securitypolicy/zopepolicy.txt

-=-
Modified: Zope3/trunk/src/zope/app/securitypolicy/tests/test_zopepolicy.py
===================================================================
--- Zope3/trunk/src/zope/app/securitypolicy/tests/test_zopepolicy.py	2004-11-11 17:14:33 UTC (rev 28438)
+++ Zope3/trunk/src/zope/app/securitypolicy/tests/test_zopepolicy.py	2004-11-11 17:14:35 UTC (rev 28439)
@@ -18,11 +18,13 @@
 
 import unittest
 from zope.testing.doctestunit import DocFileSuite
+from zope.app import zapi
 from zope.app.tests import placelesssetup, ztapi
 from zope.app.annotation.interfaces import IAnnotatable
 from zope.app.annotation.interfaces import IAttributeAnnotatable
 from zope.app.annotation.interfaces import IAnnotations
 from zope.app.annotation.attribute import AttributeAnnotations
+from zope.app.security.interfaces import IAuthenticationService
 from zope.app.securitypolicy.interfaces import IGrantInfo
 from zope.app.securitypolicy.interfaces import IPrincipalRoleManager
 from zope.app.securitypolicy.interfaces import IPrincipalPermissionManager
@@ -55,6 +57,8 @@
     ztapi.provideAdapter(
         IAnnotatable, IGrantInfo,
         AnnotationGrantInfo)
+    zapi.getGlobalServices().defineService('Authentication',
+                                           IAuthenticationService)
 
 
 def test_suite():

Modified: Zope3/trunk/src/zope/app/securitypolicy/zopepolicy.py
===================================================================
--- Zope3/trunk/src/zope/app/securitypolicy/zopepolicy.py	2004-11-11 17:14:33 UTC (rev 28438)
+++ Zope3/trunk/src/zope/app/securitypolicy/zopepolicy.py	2004-11-11 17:14:35 UTC (rev 28439)
@@ -24,8 +24,10 @@
 from zope.security.interfaces import ISecurityPolicy
 from zope.security.proxy import removeSecurityProxy
 
-from zope.app.security.settings import Allow, Deny
+from zope.app import zapi
 
+from zope.app.security.settings import Allow, Deny, Unset
+
 from zope.app.securitypolicy.principalpermission \
      import principalPermissionManager
 globalPrincipalPermissionSetting = principalPermissionManager.getSetting
@@ -41,9 +43,11 @@
 from zope.app.securitypolicy.interfaces import IPrincipalRoleMap
 from zope.app.securitypolicy.interfaces import IGrantInfo
 
+SettingAsBoolean = {Allow: True, Deny: False, Unset: None, None: None}
+
 class CacheEntry:
     pass
-    
+        
 class ZopeSecurityPolicy(ParanoidSecurityPolicy):
     zope.interface.classProvides(ISecurityPolicy)
 
@@ -63,7 +67,9 @@
             self._cache[id(parent)] = cache, parent
         return cache
     
-    def cached_decision(self, parent, principal, permission):
+    def cached_decision(self, parent, principal, groups, permission):
+        # Return the decision for a principal and permission
+
         cache = self.cache(parent)
         try:
             cache_decision = cache.decision
@@ -78,23 +84,34 @@
             return cache_decision_prin[permission]
         except KeyError:
             pass
+
+        # cache_decision_prin[permission] is the cached decision for a
+        # principal and permission.
             
-        decision = self.cached_prinper(parent, principal, permission)
+        decision = self.cached_prinper(parent, principal, groups, permission)
+        if (decision is None) and groups:
+            decision = self._group_based_cashed_prinper(parent, principal,
+                                                        groups, permission)
         if decision is not None:
             cache_decision_prin[permission] = decision
             return decision
 
         roles = self.cached_roles(parent, permission)
         if roles:
-            for role in self.cached_principal_roles(parent, principal):
-                if role in roles:
+            prin_roles = self.cached_principal_roles(parent, principal)
+            if groups:
+                prin_roles = self.cached_principal_roles_w_groups(
+                    parent, principal, groups, prin_roles)
+            for role, setting in prin_roles.items():
+                if setting and (role in roles):
                     cache_decision_prin[permission] = decision = True
                     return decision
 
         cache_decision_prin[permission] = decision = False
         return decision
         
-    def cached_prinper(self, parent, principal, permission):
+    def cached_prinper(self, parent, principal, groups, permission):
+        # Compute the permission, if any, for the principal.
         cache = self.cache(parent)
         try:
             cache_prin = cache.prin
@@ -111,25 +128,48 @@
             pass
 
         if parent is None:
-            prinper = globalPrincipalPermissionSetting(
-                permission, principal, None)
-            if prinper is not None:
-                prinper = prinper is Allow
+            prinper = SettingAsBoolean[
+                globalPrincipalPermissionSetting(permission, principal, None)
+                ]
             cache_prin_per[permission] = prinper
             return prinper
 
         prinper = IPrincipalPermissionMap(parent, None)
         if prinper is not None:
-            prinper = prinper.getSetting(permission, principal, None)
+            prinper = SettingAsBoolean[
+                prinper.getSetting(permission, principal, None)
+                ]
             if prinper is not None:
-                prinper = prinper is Allow
                 cache_prin_per[permission] = prinper
                 return prinper
 
         parent = removeSecurityProxy(getattr(parent, '__parent__', None))
-        prinper = self.cached_prinper(parent, principal, permission)
+        prinper = self.cached_prinper(parent, principal, groups, permission)
         cache_prin_per[permission] = prinper
         return prinper
+
+    def _group_based_cashed_prinper(self, parent, principal, groups,
+                                    permission):
+        denied = False
+        for group_id, ggroups in groups:
+            decision = self.cached_prinper(parent, group_id, ggroups,
+                                           permission)
+            if (decision is None) and ggroups:
+                decision = self._group_based_cashed_prinper(
+                    parent, group_id, ggroups, permission)
+            
+            if decision is None:
+                continue
+            
+            if decision:
+                return decision
+
+            denied = True
+
+        if denied:
+            return False
+
+        return None
         
     def cached_roles(self, parent, permission):
         cache = self.cache(parent)
@@ -167,6 +207,25 @@
         cache_roles[permission] = roles
         return roles
 
+    def cached_principal_roles_w_groups(self, parent,
+                                        principal, groups, prin_roles):
+        denied = {}
+        allowed = {}
+        for group_id, ggroups in groups:
+            group_roles = dict(self.cached_principal_roles(parent, group_id))
+            if ggroups:
+                group_roles = self.cached_principal_roles_w_groups(
+                    parent, group_id, ggroups, group_roles)
+            for role, setting in group_roles.items():
+                if setting:
+                    allowed[role] = setting
+                else:
+                    denied[role] = setting
+
+        denied.update(allowed)
+        denied.update(prin_roles)
+        return denied
+
     def cached_principal_roles(self, parent, principal):
         cache = self.cache(parent)
         try:
@@ -180,107 +239,137 @@
 
         if parent is None:
             roles = dict(
-                [(role, 1)
+                [(role, SettingAsBoolean[setting])
                  for (role, setting) in globalRolesForPrincipal(principal)
-                 if setting is Allow
                  ]
                  )
-            roles['zope.Anonymous'] = 1 # Everybody has Anonymous
+            roles['zope.Anonymous'] = True # Everybody has Anonymous
             cache_principal_roles[principal] = roles
             return roles
             
         roles = self.cached_principal_roles(
             removeSecurityProxy(getattr(parent, '__parent__', None)),
             principal)
+
         prinrole = IPrincipalRoleMap(parent, None)
         if prinrole:
             roles = roles.copy()
             for role, setting in prinrole.getRolesForPrincipal(principal):
-                if setting is Allow:
-                    roles[role] = 1
-                elif role in roles:
-                    del roles[role]
+                roles[role] = SettingAsBoolean[setting]
 
         cache_principal_roles[principal] = roles
         return roles
-        
 
     def checkPermission(self, permission, object):
         if permission is CheckerPublic:
             return True
 
-        principals = {}
+        object = removeSecurityProxy(object)
+        seen = {}
         for participation in self.participations:
             principal = participation.principal
             if principal is system_user:
                 continue # always allow system_user
-            principals[principal.id] = 1
 
-        if not principals:
-            return True
+            if principal.id in seen:
+                continue
 
-        object = removeSecurityProxy(object)
-        parent = removeSecurityProxy(getattr(object, '__parent__', None))
+            if not self.cached_decision(
+                object, principal.id, self._groupsFor(principal), permission,
+                ):
+                return False
 
-        grant_info = IGrantInfo(object, None)
-        if not grant_info:
-            # No local grants; just use cached decision for parent
-            for principal in principals:
-                if not self.cached_decision(parent, principal, permission):
-                    return False
-            return True
+            seen[principal.id] = 1
 
-        # We need to combine local and parent info
+        return True
+
+    def _findGroupsFor(self, principal, getPrincipal, seen):
+        result = []
+        for group_id in getattr(principal, 'groups', ()):
+            if group_id in seen:
+                # Dang, we have a cycle.  We don't want to
+                # raise an exception here (or do we), so we'll skip it
+                continue
+            seen.append(group_id)
             
-        # First, look for principal grants
-        for principal in principals.keys():
-            setting = grant_info.principalPermissionGrant(
-                principal, permission)
-            if setting is Deny:
-                return False
-            elif setting is Allow: # setting could be None
-                del principals[principal]
-                if not principals:
-                    return True
+            try:
+                group = getPrincipal(group_id)
+            except PrincipalLookupError:
+                # It's bad if we have an undefined principal,
+                # but we don't want to fail here.  But we won't
+                # honor any grants for the group. We'll just skip it.
                 continue
 
-            decision = self.cached_prinper(parent, principal, permission)
-            if decision is not None:
-                if decision:
-                    del principals[principal]
-                    if not principals:
-                        return True
-                else:
-                    return decision # False
+            result.append((group_id,
+                           self._findGroupsFor(group, getPrincipal, seen)))
+            seen.pop()
+            
+        return tuple(result)
 
-        roles = self.cached_roles(parent, permission)
-        local_roles = grant_info.getRolesForPermission(permission)
-        if local_roles:
-            roles = roles.copy()
-            for role, setting in local_roles:
-                if setting is Allow:
-                    roles[role] = 1
-                elif role in roles:
-                    del roles[role]
+    def _groupsFor(self, principal):
+        groups = self._cache.get(principal.id)
+        if groups is None:
+            groups = getattr(principal, 'groups', ())
+            if groups:
+                getPrincipal = zapi.principals().getPrincipal
+                groups = self._findGroupsFor(principal, getPrincipal, [])
+            else:
+                groups = ()
 
-        for principal in principals.keys():
-            proles = self.cached_principal_roles(parent, principal).copy()
-            for role, setting in grant_info.getRolesForPrincipal(principal):
-                if setting is Allow:
-                    if role in roles:
-                        del principals[principal]
-                        if not principals:
-                            return True
-                        break
-                elif role in proles:
-                    del proles[role]
-            else:
-                for role in proles:
-                    if role in roles:
-                        del principals[principal]
-                        if not principals:
-                            return True
-                        break                        
+            self._cache[principal.id] = groups
+
+        return groups
+
+def settingsForObject(ob):
+    """Analysis tool to show all of the grants to a process
+    """
+    result = []
+    while ob is not None:
+        data = {}
+        result.append((getattr(ob, '__name__', '(no name)'), data))
+        
+        principalPermissions = IPrincipalPermissionMap(ob, None)
+        if principalPermissions is not None:
+            settings = principalPermissions.getPrincipalsAndPermissions()
+            settings.sort()
+            data['principalPermissions'] = [
+                {'principal': pr, 'permission': p, 'setting': s}
+                for (p, pr, s) in settings]
+
+        principalRoles = IPrincipalRoleMap(ob, None)
+        if principalRoles is not None:
+            settings = principalRoles.getPrincipalsAndRoles()
+            data['principalRoles'] = [
+                {'principal': p, 'role': r, 'setting': s}
+                for (r, p, s) in settings]
+
+        rolePermissions = IRolePermissionMap(ob, None)
+        if rolePermissions is not None:
+            settings = rolePermissions.getRolesAndPermissions()
+            data['rolePermissions'] = [
+                {'permission': p, 'role': r, 'setting': s}
+                for (p, r, s) in settings]
                 
-        return False
+        ob = getattr(ob, '__parent__', None)
 
+    data = {}
+    result.append(('global settings', data))
+
+    settings = principalPermissionManager.getPrincipalsAndPermissions()
+    settings.sort()
+    data['principalPermissions'] = [
+        {'principal': pr, 'permission': p, 'setting': s}
+        for (p, pr, s) in settings]
+
+    settings = principalRoleManager.getPrincipalsAndRoles()
+    data['principalRoles'] = [
+        {'principal': p, 'role': r, 'setting': s}
+        for (r, p, s) in settings]
+
+    settings = rolePermissionManager.getRolesAndPermissions()
+    data['rolePermissions'] = [
+        {'permission': p, 'role': r, 'setting': s}
+        for (p, r, s) in settings]
+
+    return result
+

Modified: Zope3/trunk/src/zope/app/securitypolicy/zopepolicy.txt
===================================================================
--- Zope3/trunk/src/zope/app/securitypolicy/zopepolicy.txt	2004-11-11 17:14:33 UTC (rev 28438)
+++ Zope3/trunk/src/zope/app/securitypolicy/zopepolicy.txt	2004-11-11 17:14:35 UTC (rev 28439)
@@ -20,16 +20,19 @@
   >>> from zope.app.annotation.interfaces import IAttributeAnnotatable
   >>> class Ob:
   ...     zope.interface.implements(IAttributeAnnotatable)
+
   >>> ob = Ob()
 
 We use objects to represent principals.  These objects implement an
 interface named `IPrincipal`, but the security policy only uses the `id`
-attribute: 
+and `groups` attributes:
 
   >>> class Principal:
-  ...     pass
-  >>> principal = Principal()
-  >>> principal.id = 'bob'
+  ...     def __init__(self, id):
+  ...         self.id = id
+  ...         self.groups = []
+
+  >>> principal = Principal('bob')
  
 Roles and permissions are also represented by objects, however, for
 the purposes of the scurity policy, only string `ids` are used.
@@ -341,6 +344,7 @@
 
   >>> class C:
   ...     pass
+
   >>> ob3 = C()
   >>> ob3.__parent__ = ob
 
@@ -382,27 +386,27 @@
 
 and if an object doesn't have a parent:
 
-  >>> del ob3.__parent__
+  >>> ob4 = C()
 
 it will have whatever grants were made globally:
 
-  >>> interaction.checkPermission('P1', ob3)
+  >>> interaction.checkPermission('P1', ob4)
   False
-  >>> interaction.checkPermission('P2', ob3)
+  >>> interaction.checkPermission('P2', ob4)
   False
-  >>> interaction.checkPermission('P3', ob3)
+  >>> interaction.checkPermission('P3', ob4)
   False
-  >>> interaction.checkPermission('P1G', ob3)
+  >>> interaction.checkPermission('P1G', ob4)
   False
-  >>> interaction.checkPermission('P2G', ob3)
+  >>> interaction.checkPermission('P2G', ob4)
   True
-  >>> interaction.checkPermission('P3G', ob3)
+  >>> interaction.checkPermission('P3G', ob4)
   False
-  >>> interaction.checkPermission('P4G', ob3)
+  >>> interaction.checkPermission('P4G', ob4)
   False
 
   >>> prinroleG.assignRoleToPrincipal('R1G', "bob", False)
-  >>> interaction.checkPermission('P3G', ob3)
+  >>> interaction.checkPermission('P3G', ob4)
   True
 
 We'll get the same result if we have a non-annotatble parent without a
@@ -477,10 +481,171 @@
   >>> interaction.checkPermission('P4G', ob3)
   True
 
+Groups
+------
+
+Principals may have groups.  Groups are also principals (and, thus,
+may have groups).
+
+If a principal has groups, the groups are available as group ids in
+the principal's `groups` attribute.  The interaction has to convert
+these group ids to group objects, so that it can tell whether the
+groups have groups.  It does this by calling the `getPrincipal` method
+on the principal authentication service, which is responsible for,
+among other things, converting a principal id to a principal.
+For our examples here, we'll create and register a stub principal
+authentication service:
+
+  >>> from zope.app.security.interfaces import IAuthenticationService
+  >>> class FauxPrincipals(dict):
+  ...     zope.interface.implements(IAuthenticationService)
+  ...     def getPrincipal(self, id):
+  ...         return self[id]
+
+  >>> auth = FauxPrincipals()
+
+  >>> from zope.app.tests import ztapi
+  >>> ztapi.provideService('Authentication', auth)
+
+Let's define a group:
+
+  >>> auth['g1'] = Principal('g1')
+
+Lets put the principal in our group.  We do that by adding the group id
+to the new principals groups:
+
+  >>> principal.groups.append('g1')
+
+Of course, the principal doesn't have permissions not granted:
+
+  >>> interaction.checkPermission('gP1', ob)
+  False
+
+Now, if we grant a permission to the group:
+
+  >>> prinper.grantPermissionToPrincipal('gP1', 'g1')
+
+We see that our principal has the permission:
+
+  >>> interaction.checkPermission('gP1', ob)
+  True
+
+This works even if the group grant is global:
+
+  >>> interaction.checkPermission('gP1G', ob)
+  False
+
+  >>> prinperG.grantPermissionToPrincipal('gP1G', 'g1', True)
+
+  >>> interaction.checkPermission('gP1G', ob)
+  True
+
+Grants are, of course, acquired:
+
+  >>> interaction.checkPermission('gP1', ob2)
+  True
+
+  >>> interaction.checkPermission('gP1G', ob2)
+  True
+
+Inner grants can override outer grants:
+
+  >>> prinper2.denyPermissionToPrincipal('gP1', 'g1')
+  >>> interaction.checkPermission('gP1', ob2)
+  False
+
+But principal grants always trump group grants:
+
+  >>> prinper2.grantPermissionToPrincipal('gP1', 'bob')
+  >>> interaction.checkPermission('gP1', ob2)
+  True
+
+Groups can have groups too:
+
+  >>> auth['g2'] = Principal('g2')
+  >>> auth['g1'].groups.append('g2')
+
+If we grant to the new group:
+
+  >>> prinper.grantPermissionToPrincipal('gP2', 'g2')
+
+Then we, of course have that permission too:
+
+  >>> interaction.checkPermission('gP2', ob2)
+  True
+
+Just as principal grants override group grants, group grants can
+override other group grants:
+
+  >>> prinper.denyPermissionToPrincipal('gP2', 'g1')
+  >>> interaction.checkPermission('gP2', ob2)
+  False
+
+Principals can be in more than one group. Let's define a new group:
+
+  >>> auth['g3'] = Principal('g3')
+  >>> principal.groups.append('g3')
+  >>> prinper.grantPermissionToPrincipal('gP2', 'g3')
+
+Now, the principal has two groups. In one group, the permission 'gP2'
+is denied, but in the other, it is allowed.  In a case like this, the
+premission is allowed:
+
+  >>> interaction.checkPermission('gP2', ob2)
+  True
+
+In a case where a principal has two or more groups, the group denys
+prevent allows from thier parents. They don't prevent the principal
+from getting an allow from another principal.
+
+Grants can be inherited from ancestor groups through multiple paths.
+Let's grant a permission to g2 and deny it to g1:
+
+  >>> prinper.grantPermissionToPrincipal('gP3', 'g2')
+  >>> prinper.denyPermissionToPrincipal('gP3', 'g1')
+
+Now, as before, the deny in g1 blocks the grant in g2:
+
+  >>> interaction.checkPermission('gP3', ob2)
+  False
+  
+Let's make g2 a group of g3:
+
+  >>> auth['g3'].groups.append('g2')
+
+Now, we get g2's grant through g3, and access is allowed:
+
+  >>> interaction.invalidate_cache()
+  >>> interaction.checkPermission('gP3', ob2)
+  True
+
+We can assign roles to groups:
+
+  >>> prinrole.assignRoleToPrincipal('gR1', 'g2')
+
+and get permissions through the roles:
+
+  >>> roleper.grantPermissionToRole('gP4', 'gR1')
+  >>> interaction.checkPermission('gP4', ob2)
+  True
+
+we can override role assignments to groups through subgroups:
+
+  >>> prinrole.removeRoleFromPrincipal('gR1', 'g1')
+  >>> prinrole.removeRoleFromPrincipal('gR1', 'g3')
+  >>> interaction.checkPermission('gP4', ob2)
+  False
+
+and through principals:
+
+  >>> prinrole.assignRoleToPrincipal('gR1', 'bob')
+  >>> interaction.checkPermission('gP4', ob2)
+  True
+  
 Cleanup
 -------
 
-We clean up the changes we made:
+We clean up the changes we made in these examples:
 
   >>> zope.security.management.endInteraction()
   >>> ignore = zope.security.management.setSecurityPolicy(oldpolicy)



More information about the Zope3-Checkins mailing list