diff --git a/fields/__init__.py b/fields/__init__.py index 3b250d1..d9125c4 100644 --- a/fields/__init__.py +++ b/fields/__init__.py @@ -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 diff --git a/fields/ref.py b/fields/ref.py index 8d09387..0654d1b 100644 --- a/fields/ref.py +++ b/fields/ref.py @@ -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 diff --git a/fields/search.py b/fields/search.py index f61b8a6..d5571fe 100644 --- a/fields/search.py +++ b/fields/search.py @@ -147,7 +147,7 @@ class UiSearch: pxView = Px('''
:search.translated
''') diff --git a/gen/__init__.py b/gen/__init__.py index 6459369..fa4e184 100644 --- a/gen/__init__.py +++ b/gen/__init__.py @@ -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 diff --git a/gen/generator.py b/gen/generator.py index af3b9c5..c34606a 100644 --- a/gen/generator.py +++ b/gen/generator.py @@ -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()]) diff --git a/gen/installer.py b/gen/installer.py index d97dcf2..b29147c 100644 --- a/gen/installer.py +++ b/gen/installer.py @@ -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() diff --git a/gen/mixins/ToolMixin.py b/gen/mixins/ToolMixin.py index ee79b5e..0a11e04 100644 --- a/gen/mixins/ToolMixin.py +++ b/gen/mixins/ToolMixin.py @@ -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 diff --git a/gen/mixins/__init__.py b/gen/mixins/__init__.py index 7ed2073..2e2fbff 100644 --- a/gen/mixins/__init__.py +++ b/gen/mixins/__init__.py @@ -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. diff --git a/gen/templates/config.pyt b/gen/templates/config.pyt index 0db1f63..795c346 100644 --- a/gen/templates/config.pyt +++ b/gen/templates/config.pyt @@ -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('') # Some global variables -------------------------------------------------------- PROJECTNAME = '' diskFolder = os.path.dirname(.__file__) -ADD_CONTENT_PERMISSIONS = { -} # Applications classes, in various formats rootClasses = [] @@ -34,7 +31,7 @@ appClassNames = [] allClassNames = [] # In the following dict, we store, for every Appy class, the ordered list of -# appy types (included inherited ones). +# fields. attributes = {} # Application roles diff --git a/gen/utils.py b/gen/utils.py index e6ca05d..f3476c6 100644 --- a/gen/utils.py +++ b/gen/utils.py @@ -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 diff --git a/gen/wrappers/GroupWrapper.py b/gen/wrappers/GroupWrapper.py index 5c5ca95..01a8995 100644 --- a/gen/wrappers/GroupWrapper.py +++ b/gen/wrappers/GroupWrapper.py @@ -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) # ------------------------------------------------------------------------------ diff --git a/gen/wrappers/ToolWrapper.py b/gen/wrappers/ToolWrapper.py index 14635e3..4be1719 100644 --- a/gen/wrappers/ToolWrapper.py +++ b/gen/wrappers/ToolWrapper.py @@ -174,8 +174,8 @@ class ToolWrapper(AbstractWrapper): - +
::_(rootClass + '_plural') + 'portletCurrent' or ''">::_(className + '_plural')
- - + + (toolUrl, className))"/> + q('%s/import?className=%s' % (toolUrl, className))"/> @@ -221,7 +220,7 @@ class ToolWrapper(AbstractWrapper):
- +
- @@ -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.''' diff --git a/gen/wrappers/UserWrapper.py b/gen/wrappers/UserWrapper.py index 6e1fcc5..1bdbfa3 100644 --- a/gen/wrappers/UserWrapper.py +++ b/gen/wrappers/UserWrapper.py @@ -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 # ------------------------------------------------------------------------------ diff --git a/gen/wrappers/__init__.py b/gen/wrappers/__init__.py index 60819c8..fe0c85b 100644 --- a/gen/wrappers/__init__.py +++ b/gen/wrappers/__init__.py @@ -533,7 +533,7 @@ class AbstractWrapper(object):
''') pxView = Px(''' - ''', template=pxTemplate, hook='content') pxEdit = Px(''' - = 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