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).

This commit is contained in:
Gaetan Delannay 2011-09-08 16:33:16 +02:00
parent d2d3f9a745
commit 1cd9aaaf69
4 changed files with 114 additions and 52 deletions

View file

@ -559,13 +559,9 @@ class Type:
if self.name not in fieldValue: if self.name not in fieldValue:
return False return False
# Check if the user has the permission to view or edit the field # Check if the user has the permission to view or edit the field
user = obj.portal_membership.getAuthenticatedMember() if layoutType == 'edit': perm = self.writePermission
if layoutType == 'edit': else: perm = self.readPermission
perm = self.writePermission if not obj.allows(perm): return False
else:
perm = self.readPermission
if not user.has_permission(perm, obj):
return False
# Evaluate self.show # Evaluate self.show
if callable(self.show): if callable(self.show):
res = self.callMethod(obj, self.show) res = self.callMethod(obj, self.show)
@ -2287,16 +2283,21 @@ class State:
def updatePermission(self, obj, zopePermission, roleNames): def updatePermission(self, obj, zopePermission, roleNames):
'''Updates, on p_obj, list of p_roleNames which are granted a given '''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) attr = Permission.getZopeAttrName(zopePermission)
if not hasattr(obj.aq_base, attr) or \ if not hasattr(obj.aq_base, attr) or \
(getattr(obj.aq_base, attr) != roleNames): (getattr(obj.aq_base, attr) != roleNames):
setattr(obj, attr, roleNames) setattr(obj, attr, roleNames)
return True
return False
def updatePermissions(self, wf, obj): def updatePermissions(self, wf, obj):
'''Zope requires permission-to-roles mappings to be stored as attributes '''Zope requires permission-to-roles mappings to be stored as attributes
on the object itself. This method does this job, duplicating the info 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(): for permission, roles in self.getPermissions().iteritems():
roleNames = tuple([role.name for role in roles]) roleNames = tuple([role.name for role in roles])
# Compute Zope permission(s) related to this permission. # Compute Zope permission(s) related to this permission.
@ -2312,10 +2313,13 @@ class State:
zopePerm = permission.getName(wf, appName) zopePerm = permission.getName(wf, appName)
# zopePerm contains a single permission or a tuple of permissions # zopePerm contains a single permission or a tuple of permissions
if isinstance(zopePerm, basestring): if isinstance(zopePerm, basestring):
self.updatePermission(obj, zopePerm, roleNames) changed = self.updatePermission(obj, zopePerm, roleNames)
res = res or changed
else: else:
for zPerm in zopePerm: for zPerm in zopePerm:
self.updatePermission(obj, zPerm, roleNames) changed = self.updatePermission(obj, zPerm, roleNames)
res = res or changed
return res
class Transition: class Transition:
def __init__(self, states, condition=True, action=None, notify=None, def __init__(self, states, condition=True, action=None, notify=None,
@ -2462,15 +2466,12 @@ class Transition:
targetState = tState targetState = tState
targetStateName = targetState.getName(wf) targetStateName = targetState.getName(wf)
break break
# Create the event and put it in workflow_history # Create the event and add it in the object history
from DateTime import DateTime
action = transitionName action = transitionName
if transitionName == '_init_': action = None if transitionName == '_init_': action = None
userId = obj.portal_membership.getAuthenticatedMember().getId()
if not doHistory: comment = '_invisible_' if not doHistory: comment = '_invisible_'
obj.workflow_history[key] += ( obj.addHistoryEvent(action, review_state=targetStateName,
{'action':action, 'review_state': targetStateName, comments=comment)
'comments': comment, 'actor': userId, 'time': DateTime()},)
# Update permissions-to-roles attributes # Update permissions-to-roles attributes
targetState.updatePermissions(wf, obj) targetState.updatePermissions(wf, obj)
# Refresh catalog-related security if required # Refresh catalog-related security if required
@ -2553,13 +2554,13 @@ class No:
class WorkflowAnonymous: class WorkflowAnonymous:
'''One-state workflow allowing anyone to consult and Manager to edit.''' '''One-state workflow allowing anyone to consult and Manager to edit.'''
mgr = 'Manager' 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: class WorkflowAuthenticated:
'''One-state workflow allowing authenticated users to consult and Manager '''One-state workflow allowing authenticated users to consult and Manager
to edit.''' to edit.'''
mgr = 'Manager' 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: class Selection:

View file

@ -6,7 +6,7 @@
import os, os.path, sys, types, mimetypes, urllib, cgi import os, os.path, sys, types, mimetypes, urllib, cgi
import appy.gen import appy.gen
from appy.gen import Type, String, Selection, Role, No, WorkflowAnonymous, \ from appy.gen import Type, String, Selection, Role, No, WorkflowAnonymous, \
Transition Transition, Permission
from appy.gen.utils import * from appy.gen.utils import *
from appy.gen.layout import Table, defaultPageLayouts from appy.gen.layout import Table, defaultPageLayouts
from appy.gen.descriptors import WorkflowDescriptor from appy.gen.descriptors import WorkflowDescriptor
@ -236,8 +236,7 @@ class BaseMixin:
return self.goto(tool.getSiteUrl(), msg) return self.goto(tool.getSiteUrl(), msg)
# If the user can't access the object anymore, redirect him to the # If the user can't access the object anymore, redirect him to the
# main site page. # main site page.
user = self.portal_membership.getAuthenticatedMember() if not obj.allows('View'):
if not user.has_permission('View', obj):
return self.goto(tool.getSiteUrl(), msg) return self.goto(tool.getSiteUrl(), msg)
if rq.get('buttonOk.x', None) or saveConfirmed: if rq.get('buttonOk.x', None) or saveConfirmed:
# Go to the consult view for this object # Go to the consult view for this object
@ -302,26 +301,29 @@ class BaseMixin:
else: logMethod = logger.info else: logMethod = logger.info
logMethod(msg) logMethod(msg)
def getState(self, name=True): def getState(self, name=True, initial=False):
'''Returns state information about this object. If p_name is True, the '''Returns information about the current object state. If p_name is
returned info is the state name. Else, it is the State instance.''' True, the returned info is the state name. Else, it is the State
if hasattr(self.aq_base, 'workflow_history'): instance. If p_initial is True, instead of returning info about the
key = self.workflow_history.keys()[0] current state, it returns info about the workflow initial state.'''
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() wf = self.getWorkflow()
initStateName = 'active' 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): for elem in dir(wf):
attr = getattr(wf, elem) attr = getattr(wf, elem)
if (attr.__class__.__name__ == 'State') and attr.initial: if (attr.__class__.__name__ == 'State') and attr.initial:
initStateName = elem res = elem
break break
if name: return initStateName else:
else: return getattr(wf, initStateName) # 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): def rememberPreviousData(self):
'''This method is called before updating an object and remembers, for '''This method is called before updating an object and remembers, for
@ -333,6 +335,18 @@ class BaseMixin:
res[appyType.name] = appyType.getValue(self) res[appyType.name] = appyType.getValue(self)
return res 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): def addDataChange(self, changes, notForPreviouslyEmptyValues=False):
'''This method allows to add "manually" a data change into the objet's '''This method allows to add "manually" a data change into the objet's
history. Indeed, data changes are "automatically" recorded only when history. Indeed, data changes are "automatically" recorded only when
@ -349,15 +363,8 @@ class BaseMixin:
del changes[fieldName] del changes[fieldName]
else: else:
changes[fieldName] = (changes[fieldName], appyType.labelId) changes[fieldName] = (changes[fieldName], appyType.labelId)
# Create the event to record in the history # Add an event in the history
from DateTime import DateTime self.addHistoryEvent('_datachange_', changes=changes)
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,)
def historizeData(self, previousData): def historizeData(self, previousData):
'''Records in the object history potential changes on historized fields. '''Records in the object history potential changes on historized fields.
@ -808,10 +815,13 @@ class BaseMixin:
initialTransition = Transition((initialState, initialState)) initialTransition = Transition((initialState, initialState))
initialTransition.trigger('_init_', self, wf, '') initialTransition.trigger('_init_', self, wf, '')
def getWorkflow(self, name=False): def getWorkflow(self, name=False, className=None):
'''Returns the workflow applicable for p_self (or its name, if p_name '''Returns the workflow applicable for p_self (or for any instance of
is True).''' p_className if given), or its name, if p_name is True.'''
if not className:
appyClass = self.wrapperClass.__bases__[-1] appyClass = self.wrapperClass.__bases__[-1]
else:
appyClass = self.getTool().getAppyClass(className)
if hasattr(appyClass, 'workflow'): wf = appyClass.workflow if hasattr(appyClass, 'workflow'): wf = appyClass.workflow
else: wf = WorkflowAnonymous else: wf = WorkflowAnonymous
if not name: return wf if not name: return wf
@ -824,6 +834,26 @@ class BaseMixin:
stateName = stateName or self.getState() stateName = stateName or self.getState()
return '%s_%s' % (self.getWorkflow(name=True), stateName) 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): def hasHistory(self):
'''Has this object an history?''' '''Has this object an history?'''
if hasattr(self.aq_base, 'workflow_history') and self.workflow_history: if hasattr(self.aq_base, 'workflow_history') and self.workflow_history:
@ -1270,4 +1300,21 @@ class BaseMixin:
if not field.searchable: continue if not field.searchable: continue
res.append(field.getIndexValue(self, forSearch=True)) res.append(field.getIndexValue(self, forSearch=True))
return res 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
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------

View file

@ -167,7 +167,8 @@ toolFieldPrefixes = ('defaultValue', 'podTemplate', 'formats', 'resultColumns',
'showWorkflowCommentField', 'showAllStatesInPhase') 'showWorkflowCommentField', 'showAllStatesInPhase')
defaultToolFields = ('users', 'translations', 'enableNotifications', defaultToolFields = ('users', 'translations', 'enableNotifications',
'unoEnabledPython', 'openOfficePort', 'unoEnabledPython', 'openOfficePort',
'numberOfResultsPerPage', 'listBoxesMaximumWidth') 'numberOfResultsPerPage', 'listBoxesMaximumWidth',
'refreshSecurity')
class Tool(ModelClass): class Tool(ModelClass):
# In a ModelClass we need to declare attributes in the following list. # 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") openOfficePort = Integer(default=2002, group="connectionToOpenOffice")
numberOfResultsPerPage = Integer(default=30) numberOfResultsPerPage = Integer(default=30)
listBoxesMaximumWidth = Integer(default=100) 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 # 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 # link to the predefined User class or a custom class defined in the
# application. # application.

View file

@ -127,4 +127,15 @@ class ToolWrapper(AbstractWrapper):
fileName, format, self.openOfficePort) fileName, format, self.openOfficePort)
self.log('Executing %s...' % cmd) self.log('Executing %s...' % cmd)
return executeCommand(cmd) # The result can contain an error message 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)
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------