[gen] Get rid of some Zope-specific security aspects. This is a preparatory work to extend the Appy authentication system to perform ldap authentication as well.

This commit is contained in:
Gaetan Delannay 2013-08-23 18:57:27 +02:00
parent 04852360fa
commit 5223af2a62
14 changed files with 198 additions and 450 deletions

View file

@ -30,55 +30,18 @@ class GroupWrapper(AbstractWrapper):
'''Inter-field validation.'''
return self._callCustom('validate', new, errors)
def confirm(self, new):
'''Use this method for remembering the previous list of users for this
group.'''
obj = self.o
if hasattr(obj.aq_base, '_oldUsers'): del obj.aq_base._oldUsers
obj._oldUsers = self.users
def addUser(self, user):
'''Adds a p_user to this group.'''
# Update the Ref field.
self.link('users', user)
# Update the group-related info on the Zope user.
zopeUser = user.getZopeUser()
zopeUser.groups[self.login] = self.roles
def removeUser(self, user):
'''Removes a p_user from this group.'''
self.unlink('users', user)
# Update the group-related info on the Zope user.
zopeUser = user.getZopeUser()
del zopeUser.groups[self.login]
def onEdit(self, created):
# Create or update, on every Zope user of this group, group-related
# information.
# 1. Remove reference to this group for users that were removed from it
newUsers = self.users
# The list of previously existing users does not exist when editing a
# group from Python. For updating self.users, it is recommended to use
# methods m_addUser and m_removeUser above.
oldUsers = getattr(self.o.aq_base, '_oldUsers', ())
for user in oldUsers:
if user not in newUsers:
del user.getZopeUser().groups[self.login]
self.log('User "%s" removed from group "%s".' % \
(user.login, self.login))
# 2. Add reference to this group for users that were added to it
for user in newUsers:
zopeUser = user.getZopeUser()
# We refresh group-related info on the Zope user even if the user
# was already in the group.
zopeUser.groups[self.login] = self.roles
if user not in oldUsers:
self.log('User "%s" added to group "%s".' % \
(user.login, self.login))
if hasattr(self.o.aq_base, '_oldUsers'): del self.o._oldUsers
# If the group was created by an Anonymous, Anonymous can't stay Owner
# of the object.
if None in self.o.__ac_local_roles__:
del self.o.__ac_local_roles__[None]
# If the group was created by anon, anon can't stay its Owner.
if 'anon' in self.o.__ac_local_roles__:
del self.o.__ac_local_roles__['anon']
return self._callCustom('onEdit', created)
# ------------------------------------------------------------------------------

View file

@ -174,8 +174,8 @@ class ToolWrapper(AbstractWrapper):
</table>
<!-- One section for every searchable root class -->
<x for="rootClass in [rc for rc in rootClasses \
if ztool.userMaySearch(rc)]">
<x for="rootClass in rootClasses" if="ztool.userMaySearch(rootClass)"
var2="className=ztool.getPortalType(rootClass)">
<!-- A separator if required -->
<div class="portletSep" var="nb=loop.rootClass.nb"
@ -188,31 +188,30 @@ class ToolWrapper(AbstractWrapper):
<a var="queryParam=searchInfo.default and \
searchInfo.default.name or ''"
href=":'%s?className=%s&amp;search=%s' % \
(queryUrl,rootClass,queryParam)"
class=":(not currentSearch and (currentClass==rootClass) and \
(queryUrl, className, queryParam)"
class=":(not currentSearch and (currentClass==className) and \
(currentPage=='query')) and \
'portletCurrent' or ''">::_(rootClass + '_plural')</a>
'portletCurrent' or ''">::_(className + '_plural')</a>
</div>
<!-- Actions -->
<x var="addPermission='%s: Add %s' % (appName, rootClass);
userMayAdd=user.has_permission(addPermission, appFolder);
<x var="mayCreate=ztool.userMayCreate(rootClass);
createMeans=ztool.getCreateMeans(rootClass)">
<!-- Create a new object from a web form -->
<!-- Create a new object from a web form. -->
<input type="button" class="button"
if="userMayAdd and ('form' in createMeans)"
if="mayCreate and ('form' in createMeans)"
style=":url('buttonAdd', bg=True)" value=":_('query_create')"
onclick=":'goto(%s)' % \
q('%s/do?action=Create&amp;className=%s' % \
(toolUrl, rootClass))"/>
(toolUrl, className))"/>
<!-- Create object(s) by importing data -->
<input type="button" class="button"
if="userMayAdd and ('import' in createMeans)"
if="mayCreate and ('import' in createMeans)"
style=":url('buttonImport', bg=True)" value=":_('query_import')"
onclick=":'goto(%s)' % \
q('%s/import?className=%s' % (toolUrl, rootClass))"/>
q('%s/import?className=%s' % (toolUrl, className))"/>
</x>
<!-- Searches -->
@ -221,7 +220,7 @@ class ToolWrapper(AbstractWrapper):
<!-- Live search -->
<form action=":'%s/do' % toolUrl">
<input type="hidden" name="action" value="SearchObjects"/>
<input type="hidden" name="className" value=":rootClass"/>
<input type="hidden" name="className" value=":className"/>
<table cellpadding="0" cellspacing="0">
<tr valign="bottom">
<td><input type="text" size="14" name="w_SearchableText"
@ -234,13 +233,13 @@ class ToolWrapper(AbstractWrapper):
</form>
<!-- Advanced search -->
<div var="highlighted=(currentClass == rootClass) and \
<div var="highlighted=(currentClass == className) and \
(currentPage == 'search')"
class=":highlighted and 'portletSearch portletCurrent' or \
'portletSearch'"
align=":dright">
<a var="text=_('search_title')" style="font-size: 88%"
href=":'%s/search?className=%s' % (toolUrl, rootClass)"
href=":'%s/search?className=%s' % (toolUrl, className)"
title=":text"><x>:text</x>...</a>
</div>
</x>
@ -754,16 +753,6 @@ class ToolWrapper(AbstractWrapper):
'''Sends a mail. See doc for appy.gen.mail.sendMail.'''
sendMail(self, to, subject, body, attachments=attachments)
def refreshSecurity(self):
'''Refreshes, on every object in the database, security-related,
workflow-managed information.'''
context = {'nb': 0}
for className in self.o.getProductConfig().allClassNames:
self.compute(className, context=context, noSecurity=True,
expression="ctx['nb'] += int(obj.o.refreshSecurity())")
msg = 'Security refresh: %d object(s) updated.' % context['nb']
self.log(msg)
def refreshCatalog(self, startObject=None):
'''Reindex all Appy objects. For some unknown reason, method
catalog.refreshCatalog is not able to recatalog Appy objects.'''

View file

@ -140,8 +140,18 @@ class UserWrapper(AbstractWrapper):
else:
self.title = self.login
def ensureAdminIsManager(self):
'''User 'admin' must always have role 'Manager'.'''
if self.o.id == 'admin':
roles = self.roles
if 'Manager' not in roles:
if not roles: roles = ['Manager']
else: roles.append('Manager')
self.roles = roles
def onEdit(self, created):
self.updateTitle()
self.ensureAdminIsManager()
aclUsers = self.o.acl_users
login = self.login
if created:
@ -173,19 +183,25 @@ class UserWrapper(AbstractWrapper):
self.o.manage_addLocalRoles(login, ('Owner',))
# If the user was created by an Anonymous, Anonymous can't stay Owner
# of the object.
if None in self.o.__ac_local_roles__:
del self.o.__ac_local_roles__[None]
if 'anon' in self.o.__ac_local_roles__:
del self.o.__ac_local_roles__['anon']
return self._callCustom('onEdit', created)
def mayEdit(self):
'''No one can edit users "system" and "anon".'''
if self.o.id in ('system', 'anon'): return
# Call custom "mayEdit" when present.
custom = self._getCustomMethod('mayEdit')
if custom: return self._callCustom('mayEdit')
else: return True
return True
def mayDelete(self):
'''No one can delete users "system", "anon" and "admin".'''
if self.o.id in ('system', 'anon', 'admin'): return
# Call custom "mayDelete" when present.
custom = self._getCustomMethod('mayDelete')
if custom: return self._callCustom('mayDelete')
else: return True
return True
def getZopeUser(self):
'''Gets the Zope user corresponding to this user.'''
@ -198,97 +214,87 @@ class UserWrapper(AbstractWrapper):
# Call a custom "onDelete" if any.
return self._callCustom('onDelete')
# Standard Zope user methods -----------------------------------------------
def getLogins(self):
'''Gets all the logins that can "match" this user: it own login and the
logins of all the groups he belongs to.'''
# Try first to get those logins from a cache on the request.
try:
return self.request.userLogins
except AttributeError:
res = [group.login for group in self.groups]
res.append(self.login)
return res
def getRoles(self):
'''This method returns all the global roles for this user, not simply
self.roles, but also "ungrantable roles" (like Anonymous or
Authenticated) and roles inherited from group membership.'''
# Try first to get those roles from a cache on the request.
try:
return self.request.userRoles
except AttributeError:
res = list(self.roles)
# Add ungrantable roles
if self.o.id == 'anon':
res.append('Anonymous')
else:
res.append('Authenticated')
# Add group global roles
for group in self.groups:
for role in group.roles:
if role not in res: res.append(role)
return res
def getRolesFor(self, obj):
'''Gets the roles the user has in the context of p_obj: its global roles
+ its roles which are local to p_obj.'''
obj = obj.o
# Start with user global roles.
res = self.getRoles()
# Add local roles, granted to the user directly or to one of its groups.
localRoles = getattr(obj.aq_base, '__ac_local_roles__', None)
if not localRoles: return res
# Gets the logins of this user and all its groups.
logins = self.getLogins()
for login, roles in localRoles.iteritems():
# Ignore logins not corresponding to this user.
if login not in logins: continue
for role in roles:
if role not in res: res.append(role)
return res
def has_role(self, role, obj=None):
zopeUser = self.request.zopeUser
if obj: return zopeUser.has_role(role, obj)
return zopeUser.has_role(role)
'''Has the logged user some p_role? If p_obj is None, check if the user
has p_role globally; else, check if he has this p_role in the context
of p_obj.'''
if obj:
roles = self.getRolesFor(obj)
else:
roles = self.getRoles()
return role in roles
def has_permission(self, permission, obj):
return self.request.zopeUser.has_permission(permission, obj)
def getRoles(self):
'''This method collects all the roles for this user, not simply
user.roles, but also roles inherited from group membership.'''
return self.getZopeUser().getRoles()
# ------------------------------------------------------------------------------
try:
from AccessControl.PermissionRole import _what_not_even_god_should_do, \
rolesForPermissionOn
from Acquisition import aq_base
except ImportError:
pass # For those using Appy without Zope
class ZopeUserPatches:
'''This class is a fake one that defines Appy variants of some of Zope's
AccessControl.User methods. The idea is to implement the notion of group
of users.'''
def getRoles(self):
'''Returns the global roles that this user (or any of its groups)
possesses.'''
res = list(self.roles)
if 'Anonymous' not in res: res.append('Authenticated')
# Add group global roles
if not hasattr(aq_base(self), 'groups'): return res
for roles in self.groups.itervalues():
for role in roles:
if role not in res: res.append(role)
return res
def getRolesInContext(self, object):
'''Return the list of global and local (to p_object) roles granted to
this user (or to any of its groups).'''
if isinstance(object, AbstractWrapper): object = object.o
object = getattr(object, 'aq_inner', object)
# Start with user global roles
res = self.getRoles()
# Add local roles
localRoles = getattr(object, '__ac_local_roles__', None)
if not localRoles: return res
userId = self.getId()
groups = getattr(self, 'groups', ())
for id, roles in localRoles.iteritems():
if (id != userId) and (id not in groups): continue
for role in roles:
if role not in res: res.append(role)
return res
def allowed(self, object, object_roles=None):
'''Checks whether the user has access to p_object. The user (or one of
its groups) must have one of the roles in p_object_roles.'''
if object_roles is _what_not_even_god_should_do: return 0
# If "Anonymous" is among p_object_roles, grant access.
if (object_roles is None) or ('Anonymous' in object_roles): return 1
# If "Authenticated" is among p_object_roles, grant access if the user
# is not anonymous.
if 'Authenticated' in object_roles and \
(self.getUserName() != 'Anonymous User'):
if self._check_context(object): return 1
# Try first to grant access based on global user roles
'''Has the logged user p_permission on p_obj?'''
obj = obj.o
# What are the roles which are granted p_permission on p_obj?
allowedRoles = obj.getRolesFor(permission)
# Grant access if "Anonymous" is among roles.
if ('Anonymous' in allowedRoles): return True
# Grant access if "Authenticated" is among p_roles and the user is not
# anonymous.
if ('Authenticated' in allowedRoles) and (self.o.id != 'anon'):
return True
# Grant access based on global user roles.
for role in self.getRoles():
if role not in object_roles: continue
if self._check_context(object): return 1
return
# Try then to grant access based on local roles
innerObject = getattr(object, 'aq_inner', object)
localRoles = getattr(innerObject, '__ac_local_roles__', None)
if role in allowedRoles: return True
# Grant access based on local roles
localRoles = getattr(obj.aq_base, '__ac_local_roles__', None)
if not localRoles: return
userId = self.getId()
groups = getattr(self, 'groups', ())
for id, roles in localRoles.iteritems():
if (id != userId) and (id not in groups): continue
# Gets the logins of this user and all its groups.
userLogins = self.getLogins()
for login, roles in localRoles.iteritems():
# Ignore logins not corresponding to this user.
if login not in logins: continue
for role in roles:
if role not in object_roles: continue
if self._check_context(object): return 1
return
try:
from AccessControl.User import SimpleUser
SimpleUser.getRoles = getRoles
SimpleUser.getRolesInContext = getRolesInContext
SimpleUser.allowed = allowed
except ImportError:
pass
if role in allowedRoles: return True
# ------------------------------------------------------------------------------

View file

@ -533,7 +533,7 @@ class AbstractWrapper(object):
</table>''')
pxView = Px('''
<x var="x=zobj.allows('View', raiseError=True);
<x var="x=zobj.allows('read', raiseError=True);
errors=req.get('errors', {});
layout=zobj.getPageLayout(layoutType);
phaseObj=zobj.getAppyPhases(currentOnly=True, layoutType='view');
@ -549,7 +549,7 @@ class AbstractWrapper(object):
</x>''', template=pxTemplate, hook='content')
pxEdit = Px('''
<x var="x=zobj.allows('Modify portal content', raiseError=True);
<x var="x=zobj.allows('write', raiseError=True);
errors=req.get('errors', None) or {};
layout=zobj.getPageLayout(layoutType);
cssJs={};
@ -625,9 +625,9 @@ class AbstractWrapper(object):
def _getParentAttr(klass, attr):
'''Gets value of p_attr on p_klass base classes (if this attr exists).
Scan base classes in the reverse order as Python does. Used by
classmethods m_getWorkflow and m_getCreators below. Scanning base
classes in reverse order allows user-defined elements to override
default Appy elements.'''
classmethod m_getWorkflow below. Scanning base classes in reverse
order allows user-defined elements to override default Appy
elements.'''
i = len(klass.__bases__) - 1
res = None
while i >= 0:
@ -643,15 +643,6 @@ class AbstractWrapper(object):
if not res: res = WorkflowAnonymous
return res
@classmethod
def getCreators(klass, cfg):
'''Returns the roles that are allowed to create instances of p_klass.
p_cfg is the product config that holds the default value.'''
res = klass._getParentAttr('creators')
# Return default creators if no creators was found.
if not res: res = cfg.appConfig.defaultCreators
return res
@classmethod
def getIndexes(klass, includeDefaults=True):
'''Returns a dict whose keys are the names of the indexes that are
@ -823,7 +814,6 @@ class AbstractWrapper(object):
if isField:
# Link the object to this one
appyType.linkObject(self.o, zopeObj)
zopeObj._appy_managePermissions()
# Call custom initialization
if externalData: param = externalData
else: param = True