From 1cd9aaaf692d74d3d67bdb30837357f328f879d4 Mon Sep 17 00:00:00 2001 From: Gaetan Delannay Date: Thu, 8 Sep 2011 16:33:16 +0200 Subject: [PATCH] appy.gen: implemented a variant of Zope's 'has_permission' in method called 'mixin.allows', which does not make some assumptions, like the fact that an admin is Owner of any object or the fact that an Authenticated user has also role Anonymous; added button 'refresh security' for refreshing security information on every database object (it is needed because Zope requires us to duplicate security info on every object). --- gen/__init__.py | 39 +++++----- gen/plone25/mixins/__init__.py | 111 ++++++++++++++++++++-------- gen/plone25/model.py | 5 +- gen/plone25/wrappers/ToolWrapper.py | 11 +++ 4 files changed, 114 insertions(+), 52 deletions(-) diff --git a/gen/__init__.py b/gen/__init__.py index 8d4d5ff..03f1431 100644 --- a/gen/__init__.py +++ b/gen/__init__.py @@ -559,13 +559,9 @@ class Type: if self.name not in fieldValue: return False # Check if the user has the permission to view or edit the field - user = obj.portal_membership.getAuthenticatedMember() - if layoutType == 'edit': - perm = self.writePermission - else: - perm = self.readPermission - if not user.has_permission(perm, obj): - return False + if layoutType == 'edit': perm = self.writePermission + else: perm = self.readPermission + if not obj.allows(perm): return False # Evaluate self.show if callable(self.show): res = self.callMethod(obj, self.show) @@ -2287,16 +2283,21 @@ class State: def updatePermission(self, obj, zopePermission, roleNames): '''Updates, on p_obj, list of p_roleNames which are granted a given - p_zopePermission.''' + 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 on p_obj.''' + 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. @@ -2312,10 +2313,13 @@ class State: zopePerm = permission.getName(wf, appName) # zopePerm contains a single permission or a tuple of permissions if isinstance(zopePerm, basestring): - self.updatePermission(obj, zopePerm, roleNames) + changed = self.updatePermission(obj, zopePerm, roleNames) + res = res or changed else: for zPerm in zopePerm: - self.updatePermission(obj, zPerm, roleNames) + changed = self.updatePermission(obj, zPerm, roleNames) + res = res or changed + return res class Transition: def __init__(self, states, condition=True, action=None, notify=None, @@ -2462,15 +2466,12 @@ class Transition: targetState = tState targetStateName = targetState.getName(wf) break - # Create the event and put it in workflow_history - from DateTime import DateTime + # Create the event and add it in the object history action = transitionName if transitionName == '_init_': action = None - userId = obj.portal_membership.getAuthenticatedMember().getId() if not doHistory: comment = '_invisible_' - obj.workflow_history[key] += ( - {'action':action, 'review_state': targetStateName, - 'comments': comment, 'actor': userId, 'time': DateTime()},) + obj.addHistoryEvent(action, review_state=targetStateName, + comments=comment) # Update permissions-to-roles attributes targetState.updatePermissions(wf, obj) # Refresh catalog-related security if required @@ -2553,13 +2554,13 @@ class No: class WorkflowAnonymous: '''One-state workflow allowing anyone to consult and Manager to edit.''' mgr = 'Manager' - active = State({r:[mgr, 'Anonymous'], w:mgr, d:mgr}, initial=True) + active = State({r:(mgr, 'Anonymous'), w:mgr, d:mgr}, initial=True) class WorkflowAuthenticated: '''One-state workflow allowing authenticated users to consult and Manager to edit.''' mgr = 'Manager' - active = State({r:[mgr, 'Authenticated'], w:mgr, d:mgr}, initial=True) + active = State({r:(mgr, 'Authenticated'), w:mgr, d:mgr}, initial=True) # ------------------------------------------------------------------------------ class Selection: diff --git a/gen/plone25/mixins/__init__.py b/gen/plone25/mixins/__init__.py index b47e4e7..5e0cfd5 100644 --- a/gen/plone25/mixins/__init__.py +++ b/gen/plone25/mixins/__init__.py @@ -6,7 +6,7 @@ import os, os.path, sys, types, mimetypes, urllib, cgi import appy.gen from appy.gen import Type, String, Selection, Role, No, WorkflowAnonymous, \ - Transition + Transition, Permission from appy.gen.utils import * from appy.gen.layout import Table, defaultPageLayouts from appy.gen.descriptors import WorkflowDescriptor @@ -236,8 +236,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. - user = self.portal_membership.getAuthenticatedMember() - if not user.has_permission('View', obj): + if not obj.allows('View'): return self.goto(tool.getSiteUrl(), msg) if rq.get('buttonOk.x', None) or saveConfirmed: # Go to the consult view for this object @@ -302,26 +301,29 @@ class BaseMixin: else: logMethod = logger.info logMethod(msg) - def getState(self, name=True): - '''Returns state information about this object. If p_name is True, the - returned info is the state name. Else, it is the State instance.''' - if hasattr(self.aq_base, 'workflow_history'): - key = self.workflow_history.keys()[0] - stateName = self.workflow_history[key][-1]['review_state'] - if name: return stateName - else: return getattr(self.getWorkflow(), stateName) - else: - # No workflow information is available (yet) on this object. So - # return the workflow initial state. - wf = self.getWorkflow() - initStateName = 'active' + def getState(self, name=True, initial=False): + '''Returns information about the current object state. If p_name is + True, the returned info is the state name. Else, it is the State + instance. If p_initial is True, instead of returning info about the + current state, it returns info about the workflow initial state.''' + wf = self.getWorkflow() + if initial or not hasattr(self.aq_base, 'workflow_history'): + # No workflow information is available (yet) on this object, or + # initial state is asked. In both cases, return info about this + # initial state. + res = 'active' for elem in dir(wf): attr = getattr(wf, elem) if (attr.__class__.__name__ == 'State') and attr.initial: - initStateName = elem + res = elem break - if name: return initStateName - else: return getattr(wf, initStateName) + else: + # Return info about the current object state + key = self.workflow_history.keys()[0] + res = self.workflow_history[key][-1]['review_state'] + # Return state name or state definition? + if name: return res + else: return getattr(wf, res) def rememberPreviousData(self): '''This method is called before updating an object and remembers, for @@ -333,6 +335,18 @@ class BaseMixin: res[appyType.name] = appyType.getValue(self) return res + def addHistoryEvent(self, action, **kw): + '''Adds an event in the object history.''' + userId = self.portal_membership.getAuthenticatedMember().getId() + from DateTime import DateTime + event = {'action': action, 'actor': userId, 'time': DateTime(), + 'comments': ''} + event.update(kw) + if 'review_state' not in event: event['review_state']=self.getState() + # Add the event to the history + histKey = self.workflow_history.keys()[0] + self.workflow_history[histKey] += (event,) + def addDataChange(self, changes, notForPreviouslyEmptyValues=False): '''This method allows to add "manually" a data change into the objet's history. Indeed, data changes are "automatically" recorded only when @@ -349,15 +363,8 @@ class BaseMixin: del changes[fieldName] else: changes[fieldName] = (changes[fieldName], appyType.labelId) - # Create the event to record in the history - from DateTime import DateTime - user = self.portal_membership.getAuthenticatedMember() - event = {'action': '_datachange_', 'changes': changes, - 'review_state': self.getState(), 'actor': user.id, - 'time': DateTime(), 'comments': ''} - # Add the event to the history - histKey = self.workflow_history.keys()[0] - self.workflow_history[histKey] += (event,) + # Add an event in the history + self.addHistoryEvent('_datachange_', changes=changes) def historizeData(self, previousData): '''Records in the object history potential changes on historized fields. @@ -808,10 +815,13 @@ class BaseMixin: initialTransition = Transition((initialState, initialState)) initialTransition.trigger('_init_', self, wf, '') - def getWorkflow(self, name=False): - '''Returns the workflow applicable for p_self (or its name, if p_name - is True).''' - appyClass = self.wrapperClass.__bases__[-1] + def getWorkflow(self, name=False, className=None): + '''Returns the workflow applicable for p_self (or for any instance of + p_className if given), or its name, if p_name is True.''' + if not className: + appyClass = self.wrapperClass.__bases__[-1] + else: + appyClass = self.getTool().getAppyClass(className) if hasattr(appyClass, 'workflow'): wf = appyClass.workflow else: wf = WorkflowAnonymous if not name: return wf @@ -824,6 +834,26 @@ class BaseMixin: stateName = stateName or self.getState() 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.getState()) + 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.getState(name=True, initial=True) + self.addHistoryEvent(None, review_state=stateName) + state = self.getState(name=False, initial=True) + self.log('Wrong workflow info for a "%s"; is not in state "%s".' % \ + (self.meta_type, stateName)) + # Update permission attributes on the object if required + return state.updatePermissions(wf, self) + def hasHistory(self): '''Has this object an history?''' if hasattr(self.aq_base, 'workflow_history') and self.workflow_history: @@ -1270,4 +1300,21 @@ class BaseMixin: if not field.searchable: continue res.append(field.getIndexValue(self, forSearch=True)) return res + + def allows(self, permission): + '''Has the logged user p_permission on p_self ?''' + # Get first the roles that have this permission on p_self. + zopeAttr = Permission.getZopeAttrName(permission) + if not hasattr(self.aq_base, zopeAttr): return + allowedRoles = getattr(self.aq_base, zopeAttr) + # Has the user one of those roles? + user = self.portal_membership.getAuthenticatedMember() + ids = [user.getId()] + user.getGroups() + userGlobalRoles = user.getRoles() + for role in allowedRoles: + # Has the user this role ? Check in the local roles first. + for id, roles in self.__ac_local_roles__.iteritems(): + if (role in roles) and (id in ids): return True + # Check then in the global roles. + if role in userGlobalRoles: return True # ------------------------------------------------------------------------------ diff --git a/gen/plone25/model.py b/gen/plone25/model.py index 2194b7c..0b908c5 100644 --- a/gen/plone25/model.py +++ b/gen/plone25/model.py @@ -167,7 +167,8 @@ toolFieldPrefixes = ('defaultValue', 'podTemplate', 'formats', 'resultColumns', 'showWorkflowCommentField', 'showAllStatesInPhase') defaultToolFields = ('users', 'translations', 'enableNotifications', 'unoEnabledPython', 'openOfficePort', - 'numberOfResultsPerPage', 'listBoxesMaximumWidth') + 'numberOfResultsPerPage', 'listBoxesMaximumWidth', + 'refreshSecurity') class Tool(ModelClass): # In a ModelClass we need to declare attributes in the following list. @@ -180,6 +181,8 @@ class Tool(ModelClass): openOfficePort = Integer(default=2002, group="connectionToOpenOffice") numberOfResultsPerPage = Integer(default=30) listBoxesMaximumWidth = Integer(default=100) + def refreshSecurity(self): pass # Real method in the wrapper + refreshSecurity = Action(action=refreshSecurity, confirm=True) # First arg of Ref field below is None because we don't know yet if it will # link to the predefined User class or a custom class defined in the # application. diff --git a/gen/plone25/wrappers/ToolWrapper.py b/gen/plone25/wrappers/ToolWrapper.py index c86ee30..49e2d95 100644 --- a/gen/plone25/wrappers/ToolWrapper.py +++ b/gen/plone25/wrappers/ToolWrapper.py @@ -127,4 +127,15 @@ class ToolWrapper(AbstractWrapper): fileName, format, self.openOfficePort) self.log('Executing %s...' % cmd) return executeCommand(cmd) # The result can contain an error message + + 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) + self.say(msg) # ------------------------------------------------------------------------------