[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

@ -246,14 +246,14 @@ class Field:
elif rp and isinstance(rp, basestring):
self.readPermission = rp
else:
self.readPermission = 'View'
self.readPermission = 'read'
wp = self.specificWritePermission
if wp and not isinstance(wp, basestring):
self.writePermission = '%s: Write %s %s' % (appName, prefix, name)
elif wp and isinstance(wp, basestring):
self.writePermission = wp
else:
self.writePermission = 'Modify portal content'
self.writePermission = 'write'
if (self.type == 'Ref') and not self.isBack:
# We must initialise the corresponding back reference
self.back.klass = klass

View file

@ -140,7 +140,7 @@ class Ref(Field):
folder=zobj.getCreateFolder();
tiedClassName=ztool.getPortalType(field.klass);
canWrite=not field.isBack and zobj.allows(field.writePermission);
showPlusIcon=zobj.mayAddReference(field.name);
showPlusIcon=field.mayAdd(zobj);
atMostOneRef=(field.multiplicity[1] == 1) and \
(len(zobjects)<=1);
addConfirmMsg=field.addConfirm and \
@ -592,12 +592,8 @@ class Ref(Field):
# May the user edit this Ref field?
if not obj.allows(self.writePermission):
return gutils.No('no_write_perm')
# Have the user the correct add permission?
tool = obj.getTool()
addPermission = '%s: Add %s' % (tool.getAppName(),
tool.getPortalType(self.klass))
folder = obj.getCreateFolder()
if not tool.getUser().has_permission(addPermission, folder):
# May the user create instances of the referred class?
if not obj.getTool().userMayCreate(self.klass):
return gutils.No('no_add_perm')
return True

View file

@ -147,7 +147,7 @@ class UiSearch:
pxView = Px('''
<div class="portletSearch">
<a href=":'%s?className=%s&amp;search=%s' % \
(queryUrl, rootClass, search.name)"
(queryUrl, className, search.name)"
class=":search.name == currentSearch and 'portletCurrent' or ''"
title=":search.translatedDescr">:search.translated</a>
</div>''')

View file

@ -59,12 +59,6 @@ class Import:
self.sort = sort
# Workflow-specific types and default workflows --------------------------------
appyToZopePermissions = {
'read': ('View', 'Access contents information'),
'write': 'Modify portal content',
'delete': 'Delete objects',
}
class Role:
'''Represents a role.'''
zopeRoles = ('Manager', 'Owner', 'Anonymous', 'Authenticated')
@ -108,7 +102,7 @@ class State:
name of a role, this method returns self.usedRoles[role] if it
exists, or creates a Role instance, puts it in self.usedRoles and
returns it else. If it is a Role instance, the method stores it in
self.usedRoles if it not in it yet and returns it.'''
self.usedRoles if it is not in it yet and returns it.'''
if isinstance(role, basestring):
if role in self.usedRoles:
return self.usedRoles[role]
@ -135,61 +129,6 @@ class State:
def getUsedRoles(self): return self.usedRoles.values()
def getPermissions(self):
'''If you get the permissions mapping through self.permissions, dict
values may be of different types (a list of roles, a single role or
None). Iy you call this method, you will always get a list which
may be empty.'''
res = {}
for permission, roleValue in self.permissions.iteritems():
if roleValue == None:
res[permission] = []
elif isinstance(roleValue, basestring):
res[permission] = [roleValue]
else:
res[permission] = roleValue
return res
def updatePermission(self, obj, zopePermission, roleNames):
'''Updates, on p_obj, list of p_roleNames which are granted a given
p_zopePermission. This method returns True if the list has been
effectively updated.'''
attr = Permission.getZopeAttrName(zopePermission)
if not hasattr(obj.aq_base, attr) or \
(getattr(obj.aq_base, attr) != roleNames):
setattr(obj, attr, roleNames)
return True
return False
def updatePermissions(self, wf, obj):
'''Zope requires permission-to-roles mappings to be stored as attributes
on the object itself. This method does this job, duplicating the info
from this state definition on p_obj. p_res is True if at least one
change has been effectively performed.'''
res = False
for permission, roles in self.getPermissions().iteritems():
roleNames = tuple([role.name for role in roles])
# Compute Zope permission(s) related to this permission.
if appyToZopePermissions.has_key(permission):
# It is a standard permission (r, w, d)
zopePerm = appyToZopePermissions[permission]
elif isinstance(permission, basestring):
# It is a user-defined permission
zopePerm = permission
else:
# It is a Permission instance
appName = obj.getProductConfig().PROJECTNAME
zopePerm = permission.getName(wf, appName)
# zopePerm contains a single permission or a tuple of permissions
if isinstance(zopePerm, basestring):
changed = self.updatePermission(obj, zopePerm, roleNames)
res = res or changed
else:
for zPerm in zopePerm:
changed = self.updatePermission(obj, zPerm, roleNames)
res = res or changed
return res
class Transition:
def __init__(self, states, condition=True, action=None, notify=None,
show=True, confirm=False):
@ -345,8 +284,6 @@ class Transition:
if not doHistory: comment = '_invisible_'
obj.addHistoryEvent(action, review_state=targetStateName,
comments=comment)
# Update permissions-to-roles attributes
targetState.updatePermissions(wf, obj)
# Reindex the object if required. Not only security-related indexes
# (Allowed, State) need to be updated here.
if not obj.isTemporary(): obj.reindex()
@ -384,8 +321,7 @@ class Permission:
self.fieldDescriptor = fieldDescriptor
def getName(self, wf, appName):
'''Returns the name of the Zope permission that corresponds to this
permission.'''
'''Returns the name of this permission.'''
className, fieldName = self.fieldDescriptor.rsplit('.', 1)
if className.find('.') == -1:
# The related class resides in the same module as the workflow
@ -398,16 +334,6 @@ class Permission:
else: access = 'Write'
return '%s: %s %s %s' % (appName, access, fullClassName, fieldName)
@staticmethod
def getZopeAttrName(zopePermission):
'''Gets the name of the attribute where Zope stores, on every object,
the tuple of roles who are granted a given p_zopePermission.'''
res = ''
for c in zopePermission:
if c in Permission.allowedChars: res += c
else: res += '_'
return '_%s_Permission' % res
class ReadPermission(Permission): pass
class WritePermission(Permission): pass
@ -475,7 +401,7 @@ class Config:
languageSelector = False
# People having one of these roles will be able to create instances
# of classes defined in your application.
defaultCreators = ['Manager', 'Owner']
defaultCreators = ['Manager']
# Number of translations for every page on a Translation object
translationsPerPage = 30
# Language that will be used as a basis for translating to other

View file

@ -557,12 +557,6 @@ class ZopeGenerator(Generator):
if theImport not in imports:
imports.append(theImport)
repls['imports'] = '\n'.join(imports)
# Compute list of add permissions
addPermissions = ''
for classDescr in classesAll:
addPermissions += ' "%s":"%s: Add %s",\n' % (classDescr.name,
self.applicationName, classDescr.name)
repls['addPermissions'] = addPermissions
# Compute root classes
repls['rootClasses'] = ','.join(["'%s'" % c.name \
for c in classesButTool if c.isRoot()])

View file

@ -63,7 +63,6 @@ class ZopeInstaller:
self.productName = config.PROJECTNAME
self.languages = config.appConfig.languages
self.logger = config.logger
self.addContentPermissions = config.ADD_CONTENT_PERMISSIONS
def installUi(self):
'''Installs the user interface.'''
@ -157,14 +156,9 @@ class ZopeInstaller:
catalog.reindexIndex('SearchableText', self.app.REQUEST)
self.logger.info('Done.')
def getAddPermission(self, className):
'''What is the name of the permission allowing to create instances of
class whose name is p_className?'''
return self.productName + ': Add ' + className
def installBaseObjects(self):
'''Creates the tool and the root data folder if they do not exist.'''
# Create or update the base folder for storing data
'''Creates the tool and the base data folder if they do not exist.'''
# Create the tool.
zopeContent = self.app.objectIds()
from OFS.Folder import manage_addFolder
@ -172,29 +166,8 @@ class ZopeInstaller:
toolName = '%sTool' % self.productName
gutils.createObject(self.app, 'config', toolName, self.productName,
wf=False, noSecurity=True)
if 'data' not in zopeContent:
manage_addFolder(self.app, 'data')
data = self.app.data
tool = self.app.config
# Manager has been granted Add permissions for all root classes.
# This may not be desired, so remove this.
for className in self.config.rootClasses:
permission = self.getAddPermission(className)
data.manage_permission(permission, (), acquire=0)
# All roles defined as creators should be able to create the
# corresponding root classes in this folder.
i = -1
for klass in self.config.appClasses:
i += 1
if not klass.__dict__.has_key('root') or \
not klass.__dict__['root']:
continue # It is not a root class
className = self.config.appClassNames[i]
wrapperClass = tool.getAppyClass(className, wrapper=True)
creators = wrapperClass.getCreators(self.config)
permission = self.getAddPermission(className)
gutils.updateRolesForPermission(permission,tuple(creators),data)
# Create the base data folder.
if 'data' not in zopeContent: manage_addFolder(self.app, 'data')
# Remove some default objects created by Zope but not useful to Appy
for name in ('standard_html_footer', 'standard_html_header',\
@ -206,14 +179,13 @@ class ZopeInstaller:
inner objects (users, groups, translations, documents).'''
tool = self.app.config
tool.createOrUpdate(True, None)
tool.refreshSecurity()
appyTool = tool.appy()
appyTool.log('Appy version is "%s".' % appy.version.short)
# Create the default users if they do not exist.
for login, roles in self.defaultUsers.iteritems():
if not appyTool.count('User', noSecurity=True, login=login):
appyTool.create('users', noSecurity=True, login=login,
appyTool.create('users', noSecurity=True, id=login, login=login,
password1=login, password2=login,
email='%s@appyframework.org'%login, roles=roles)
appyTool.log('User "%s" created.' % login)
@ -332,8 +304,7 @@ class ZopeInstaller:
wrapper = klass.wrapperClass
exec 'from %s import manage_add%s as ctor' % (module, name)
self.zopeContext.registerClass(meta_type=name,
constructors = (ctor,),
permission = self.addContentPermissions[name])
constructors = (ctor,), permission = None)
# Create workflow prototypical instances in __instance__ attributes
wf = wrapper.getWorkflow()
if not hasattr(wf, '__instance__'): wf.__instance__ = wf()

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,21 +508,26 @@ 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):
@ -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.

View file

@ -13,7 +13,6 @@ from ZPublisher.HTTPRequest import BaseRequest
from OFS.Image import File
from ZPublisher.HTTPRequest import FileUpload
from AccessControl import getSecurityManager
from AccessControl.PermissionRole import rolesForPermissionOn
from DateTime import DateTime
from Products.ExternalMethod.ExternalMethod import ExternalMethod
from Products.Transience.Transience import TransientObjectContainer
@ -24,8 +23,6 @@ logger = logging.getLogger('<!applicationName!>')
# Some global variables --------------------------------------------------------
PROJECTNAME = '<!applicationName!>'
diskFolder = os.path.dirname(<!applicationName!>.__file__)
ADD_CONTENT_PERMISSIONS = {
<!addPermissions!>}
# Applications classes, in various formats
rootClasses = [<!rootClasses!>]
@ -34,7 +31,7 @@ appClassNames = [<!appClassNames!>]
allClassNames = [<!allClassNames!>]
# In the following dict, we store, for every Appy class, the ordered list of
# appy types (included inherited ones).
# fields.
attributes = {<!attributes!>}
# Application roles

View file

@ -17,17 +17,11 @@ def createObject(folder, id, className, appName, wf=True, noSecurity=False):
user = tool.getUser()
if not noSecurity:
# Check that the user can create objects of className.
userRoles = user.getRoles()
allowedRoles=ZopeClass.wrapperClass.getCreators(tool.getProductConfig())
allowed = False
for role in userRoles:
if role in allowedRoles:
allowed = True
break
if not allowed:
klass = ZopeClass.wrapperClass.__bases__[-1]
if not tool.userMayCreate(klass):
from AccessControl import Unauthorized
raise Unauthorized("User can't create instances of %s" % \
ZopeClass.__name__)
klass.__name__)
obj = ZopeClass(id)
folder._objects = folder._objects + ({'id':id, 'meta_type':className},)
folder._setOb(id, obj)
@ -137,21 +131,6 @@ def getClassName(klass, appName=None):
res = klass.__module__.replace('.', '_') + '_' + klass.__name__
return res
# ------------------------------------------------------------------------------
def updateRolesForPermission(permission, roles, obj):
'''Adds roles from list p_roles to the list of roles that are granted
p_permission on p_obj.'''
from AccessControl.Permission import Permission
# Find existing roles that were granted p_permission on p_obj
existingRoles = ()
for p in obj.ac_inherited_permissions(1):
name, value = p[:2]
if name == permission:
perm = Permission(name, value, obj)
existingRoles = perm.getRoles()
allRoles = set(existingRoles).union(roles)
obj.manage_permission(permission, tuple(allRoles), acquire=0)
# ------------------------------------------------------------------------------
def callMethod(obj, method, klass=None, cache=True):
'''This function is used to call a p_method on some Appy p_obj. m_method

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