[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

@ -199,7 +199,8 @@ class ToolMixin(BaseMixin):
def getRootClasses(self):
'''Returns the list of root classes for this application.'''
return self.getProductConfig().rootClasses
cfg = self.getProductConfig()
return [self.getAppyClass(k) for k in cfg.rootClasses]
def _appy_getAllFields(self, className):
'''Returns the (translated) names of fields of p_className.'''
@ -265,8 +266,8 @@ class ToolMixin(BaseMixin):
p_className.'''
appyClass = self.getAppyClass(className)
importParams = self.getCreateMeans(appyClass)['import']
onElement = importParams['onElement'].__get__('')
sortMethod = importParams['sort']
onElement = importParams.onElement.__get__('')
sortMethod = importParams.sort
if sortMethod: sortMethod = sortMethod.__get__('')
elems = []
importType = self.getAppyType('importPathFor%s' % className)
@ -280,7 +281,7 @@ class ToolMixin(BaseMixin):
elems.append(elemInfo)
if sortMethod:
elems = sortMethod(elems)
return [importParams['headers'], elems]
return [importParams.headers, elems]
def showPortlet(self, context, layoutType):
'''When must the portlet be shown?'''
@ -489,17 +490,13 @@ class ToolMixin(BaseMixin):
if wrapper: return zopeClass.wrapperClass
else: return zopeClass.wrapperClass.__bases__[-1]
def getCreateMeans(self, contentTypeOrAppyClass):
'''Gets the different ways objects of p_contentTypeOrAppyClass (which
can be a Zope content type or a Appy class) can be created
(via a web form, by importing external data, etc). Result is a
dict whose keys are strings (ie "form", "import"...) and whose
values are additional data about the particular mean.'''
pythonClass = contentTypeOrAppyClass
if isinstance(contentTypeOrAppyClass, basestring):
pythonClass = self.getAppyClass(pythonClass)
def getCreateMeans(self, klass):
'''Gets the different ways objects of p_klass can be created (via a web
form, by importing external data, etc). Result is a dict whose keys
are strings (ie "form", "import"...) and whose values are additional
data about the particular mean.'''
res = {}
if not pythonClass.__dict__.has_key('create'):
if not klass.__dict__.has_key('create'):
res['form'] = None
# No additional data for this means, which is the default one.
else:
@ -511,23 +508,28 @@ class ToolMixin(BaseMixin):
if isinstance(mean, basestring):
res[mean] = None
else:
res[mean.id] = mean.__dict__
res[mean.id] = mean
else:
res[means.id] = means.__dict__
res[means.id] = means
return res
def userMaySearch(self, rootClass):
'''This method checks if the currently logged user can trigger searches
on a given p_rootClass. This is done by calling method "maySearch"
on the class. If no such method exists, we return True.'''
# When editign a form, one should avoid annoying the user with this.
'''May the logged user search among instances of p_rootClass ?'''
# When editing a form, one should avoid annoying the user with this.
url = self.REQUEST['ACTUAL_URL']
if url.endswith('/edit') or url.endswith('/do'): return
pythonClass = self.getAppyClass(rootClass)
if 'maySearch' in pythonClass.__dict__:
return pythonClass.maySearch(self.appy())
if 'maySearch' in rootClass.__dict__:
return pythonClass.rootClass(self.appy())
return True
def userMayCreate(self, klass):
'''May the logged user create instances of p_klass ?'''
allowedRoles = getattr(klass, 'creators', None) or \
self.getProductConfig().appConfig.defaultCreators
for role in self.getUser().getRoles():
if role in allowedRoles:
return True
def onImportObjects(self):
'''This method is called when the user wants to create objects from
external data.'''
@ -737,23 +739,22 @@ class ToolMixin(BaseMixin):
obj = self.getObject(objectUid)
return obj, fieldName
def getGroupedSearches(self, className):
def getGroupedSearches(self, klass):
'''Returns an object with 2 attributes:
* "searches" stores the searches that are defined for p_className;
* "searches" stores the searches that are defined for p_klass;
* "default" stores the search defined as the default one.
Every item representing a search is a dict containing info about a
search or about a group of searches.
'''
appyClass = self.getAppyClass(className)
res = []
default = None # Also retrieve the default one here.
groups = {} # The already encountered groups
page = Page('main') # A dummy page required by class UiGroup
# Get the searches statically defined on the class
searches = ClassDescriptor.getSearches(appyClass, tool=self.appy())
searches = ClassDescriptor.getSearches(klass, tool=self.appy())
# Get the dynamically computed searches
if hasattr(appyClass, 'getDynamicSearches'):
searches += appyClass.getDynamicSearches(self.appy())
if hasattr(klass, 'getDynamicSearches'):
searches += klass.getDynamicSearches(self.appy())
for search in searches:
# Create the search descriptor
uiSearch = UiSearch(search, className, self)
@ -792,9 +793,8 @@ class ToolMixin(BaseMixin):
if ui: res = UiSearch(res, className, self)
return res
def advancedSearchEnabledFor(self, className):
def advancedSearchEnabledFor(self, klass):
'''Is advanced search visible for p_klass ?'''
klass = self.getAppyClass(className)
# By default, advanced search is enabled.
if not hasattr(klass, 'searchAdvanced'): return True
# Evaluate attribute "show" on this Search instance representing the
@ -1103,29 +1103,26 @@ class ToolMixin(BaseMixin):
login = (rq.__class__.__name__ == 'Object') and 'system' or 'anon'
# Get the User object from a query in the catalog.
user = tool.search1('User', noSecurity=True, login=login)
# It is possible that we find no user here: it happens before users
# "anon" and "system" are created, at first Zope startup.
if not user: return
rq.user = user
# Precompute some values or this usser for performance reasons
# Precompute some values or this user for performance reasons.
rq.userRoles = user.getRoles()
rq.userLogins = user.getLogins()
rq.zopeUser = user.getZopeUser()
return user
#from AccessControl import getSecurityManager
#user = getSecurityManager().getUser()
#if not user:
# from AccessControl.User import nobody
# return nobody
#return user
def getUserLine(self):
'''Returns a info about the currently logged user as a 2-tuple: first
elem is the one-line user info as shown on every page; second line is
the URL to edit user info.'''
user = self.getUser()
userRoles = self.appy().request.userRoles
info = [user.title]
rolesToShow = [r for r in userRoles if r != 'Authenticated']
if rolesToShow:
showable = [r for r in user.getRoles() if r != 'Authenticated']
if showable:
info.append(', '.join([self.translate('role_%s' % r) \
for r in rolesToShow]))
for r in showable]))
# Edit URL for the user.
url = None
if user.o.mayEdit():
@ -1134,17 +1131,17 @@ class ToolMixin(BaseMixin):
def getUserName(self, login=None, normalized=False):
'''Gets the user name corresponding to p_login (or the currently logged
login if None), or the p_login itself if the user does not exist
user if None), or the p_login itself if the user does not exist
anymore. If p_normalized is True, special chars in the first and last
names are normalized.'''
tool = self.appy()
if not login: login = tool.user.login
# Manage the special case of an anonymous user.
if login == 'Anonymous User':
if login == 'anon':
name = self.translate('anonymous')
if normalized: name = sutils.normalizeString(name)
return name
# Manage the case of a "real" user.
# Manage the case of any other user.
user = tool.search1('User', noSecurity=True, login=login)
if not user: return login
firstName = user.firstName

View file

@ -93,9 +93,6 @@ class BaseMixin:
# Manage potential link with an initiator object
if created and initiator: initiator.appy().link(initiatorField.name,obj)
# Manage "add" permissions and reindex the object
obj._appy_managePermissions()
# Call the custom "onEdit" if available
msg = None # The message to display to the user. It can be set by onEdit
if obj.wrapperClass:
@ -410,7 +407,7 @@ class BaseMixin:
return self.goto(tool.getSiteUrl(), msg)
# If the user can't access the object anymore, redirect him to the
# main site page.
if not obj.allows('View'):
if not obj.allows('read'):
return self.goto(tool.getSiteUrl(), msg)
if (buttonClicked == 'save') or saveConfirmed:
obj.say(msg)
@ -484,7 +481,7 @@ class BaseMixin:
corresponding Appy wrapper and returns, as XML, the its result.'''
self.REQUEST.RESPONSE.setHeader('Content-Type','text/xml;charset=utf-8')
# Check if the user is allowed to consult this object
if not self.allows('View'):
if not self.allows('read'):
return XmlMarshaller().marshall('Unauthorized')
if not action:
marshaller = XmlMarshaller(rootTag=self.getClass().__name__,
@ -670,11 +667,6 @@ class BaseMixin:
if not allValues: return ''
return getattr(allValues[rowIndex], name, '')
def mayAddReference(self, name):
'''May the user add references via Ref field named p_name in
p_folder?'''
return self.getAppyType(name).mayAdd(self)
def isDebug(self):
'''Are we in debug mode ?'''
for arg in sys.argv:
@ -937,30 +929,6 @@ class BaseMixin:
stateName = stateName or self.State()
return '%s_%s' % (self.getWorkflow(name=True), stateName)
def refreshSecurity(self):
'''Refresh security info on this object. Returns True if the info has
effectively been updated.'''
wf = self.getWorkflow()
try:
# Get the state definition of the object's current state.
state = getattr(wf, self.State())
except AttributeError:
# The workflow information for this object does not correspond to
# its current workflow attribution. Add a new fake event
# representing passage of this object to the initial state of his
# currently attributed workflow.
stateName = self.State(name=True, initial=True)
self.addHistoryEvent(None, review_state=stateName)
state = self.State(name=False, initial=True)
self.log('Wrong workflow info for a "%s"; is now in state "%s".' % \
(self.meta_type, stateName))
# Update permission attributes on the object if required
updated = state.updatePermissions(wf, self)
if updated:
# Reindex the object because security-related info is indexed.
self.reindex()
return updated
def applyUserIdChange(self, oldId, newId):
'''A user whose ID was p_oldId has now p_newId. If the old ID was
mentioned in self's local roles, update it to the new ID. This
@ -1105,14 +1073,14 @@ class BaseMixin:
def mayDelete(self):
'''May the currently logged user delete this object?'''
res = self.allows('Delete objects')
res = self.allows('delete')
if not res: return
# An additional, user-defined condition, may refine the base permission.
appyObj = self.appy()
if hasattr(appyObj, 'mayDelete'): return appyObj.mayDelete()
return True
def mayEdit(self, permission='Modify portal content'):
def mayEdit(self, permission='write'):
'''May the currently logged user edit this object? p_perm can be a
field-specific permission.'''
res = self.allows(permission)
@ -1195,6 +1163,12 @@ class BaseMixin:
self.reindex()
return self.goto(self.getUrl(rq['HTTP_REFERER']))
def getRolesFor(self, permission):
'''Gets, according to the workflow, the roles that are currently granted
p_permission on this object.'''
state = self.State(name=False)
return [role.name for role in state.permissions[permission]]
def appy(self):
'''Returns a wrapper object allowing to manipulate p_self the Appy
way.'''
@ -1284,18 +1258,17 @@ class BaseMixin:
'''Returns the list of roles and users that are allowed to view this
object. This index value will be used within catalog queries for
filtering objects the user is allowed to see.'''
res = set()
# Get, from the workflow, roles having permission 'View'.
for role in self.getProductConfig().rolesForPermissionOn('View', self):
res.add(role)
# Add users having, locally, this role on this object.
localRoles = getattr(self, '__ac_local_roles__', None)
if not localRoles: return list(res)
# Get, from the workflow, roles having permission 'read'.
res = self.getRolesFor('read')
# Add users or groups having, locally, this role on this object.
localRoles = getattr(self.aq_base, '__ac_local_roles__', None)
if not localRoles: return res
for id, roles in localRoles.iteritems():
for role in roles:
if role in res:
res.add('user:%s' % id)
return list(res)
usr = 'user:%s' % id
if usr not in res: res.append(usr)
return res
def showState(self):
'''Must I show self's current state ?'''
@ -1326,39 +1299,6 @@ class BaseMixin:
res.append((elem, self.translate(self.getWorkflowLabel(elem))))
return res
def _appy_managePermissions(self):
'''When an object is created or updated, we must update "add"
permissions accordingly: if the object is a folder, we must set on
it permissions that will allow to create, inside it, objects through
Ref fields; if it is not a folder, we must update permissions on its
parent folder instead.'''
# Determine on which folder we need to set "add" permissions
folder = self.getCreateFolder()
# On this folder, set "add" permissions for every content type that will
# be created through reference fields
allCreators = {} # One key for every add permission
addPermissions = self.getProductConfig().ADD_CONTENT_PERMISSIONS
for appyType in self.getAllAppyTypes():
if appyType.type != 'Ref': continue
if appyType.isBack or appyType.link: continue
# Indeed, no possibility to create objects with such Refs
tool = self.getTool()
refType = tool.getPortalType(appyType.klass)
if refType not in addPermissions: continue
# Get roles that may add this content type
appyWrapper = tool.getAppyClass(refType, wrapper=True)
creators = appyWrapper.getCreators(self.getProductConfig())
# Add those creators to the list of creators for this meta_type
addPermission = addPermissions[refType]
if addPermission in allCreators:
allCreators[addPermission] = allCreators[\
addPermission].union(creators)
else:
allCreators[addPermission] = set(creators)
# Update the permissions
for permission, creators in allCreators.iteritems():
updateRolesForPermission(permission, tuple(creators), folder)
getUrlDefaults = {'page':True, 'nav':True}
def getUrl(self, base=None, mode='view', **kwargs):
'''Returns an URL for this object.