[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