[gen] Security: added missing checks at the code level, ensuring that a user can create instances of a given class (root classes, or instances created via an initiator field); bugfixes in the test system, which works again (was broken after deplonization); [shared] XmlUnmarshaller can now be ran in 'non utf-8' mode: if enabled, any marshalled string will no be Python unicode, but simple str.

This commit is contained in:
Gaetan Delannay 2012-06-02 14:36:49 +02:00
parent 0d7afb685f
commit f843d5b7d6
11 changed files with 167 additions and 79 deletions

View file

@ -1990,9 +1990,8 @@ class Ref(Type):
res.select = None # Not callable from tool.
return res
def mayAdd(self, obj, folder):
'''May the user create a new referred object to p_obj via this Ref,
in p_folder?'''
def mayAdd(self, obj):
'''May the user create a new referred object from p_obj via this Ref?'''
# We can't (yet) do that on back references.
if self.isBack: return
# Check if this Ref is addable
@ -2007,13 +2006,21 @@ class Ref(Type):
if refCount >= self.multiplicity[1]: return
# May the user edit this Ref field?
if not obj.allows(self.writePermission): return
# Have the user the correct add permission on p_folder?
# 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 obj.getUser().has_permission(addPermission, folder): return
return True
def checkAdd(self, obj):
'''Compute m_mayAdd above, and raise an Unauthorized exception if
m_mayAdd returns False.'''
if not self.mayAdd(obj):
from AccessControl import Unauthorized
raise Unauthorized("User can't write Ref field '%s'." % self.name)
class Computed(Type):
def __init__(self, validator=None, multiplicity=(0,1), index=None,
default=None, optional=False, editDefault=False, show='view',

View file

@ -214,9 +214,15 @@ class ZopeInstaller:
zopeContent = self.app.objectIds()
from OFS.Folder import manage_addFolder
if 'config' not in zopeContent:
toolName = '%sTool' % self.productName
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:
@ -230,15 +236,12 @@ class ZopeInstaller:
if not klass.__dict__.has_key('root') or \
not klass.__dict__['root']:
continue # It is not a root class
creators = getattr(klass, 'creators', None)
if not creators: creators = self.config.defaultAddRoles
className = self.config.appClassNames[i]
wrapperClass = tool.getAppyClass(className, wrapper=True)
creators = wrapperClass.getCreators(self.config)
permission = self.getAddPermission(className)
updateRolesForPermission(permission, tuple(creators), data)
if 'config' not in zopeContent:
toolName = '%sTool' % self.productName
createObject(self.app, 'config', toolName,self.productName,wf=False)
# Remove some default objects created by Zope but not useful to Appy
for name in ('standard_html_footer', 'standard_html_header',\
'standard_template.pt'):
@ -261,15 +264,15 @@ class ZopeInstaller:
# may still be in the way for migration purposes.
users = ('admin',) # We suppose there is at least a user.
if not users:
appyTool.create('users', login='admin', password1='admin',
password2='admin',
appyTool.create('users', noSecurity=True, login='admin',
password1='admin', password2='admin',
email='admin@appyframework.org', roles=['Manager'])
appyTool.log('Admin user "admin" created.')
# Create group "admins" if it does not exist
if not appyTool.count('Group', noSecurity=True, login='admins'):
appyTool.create('groups', login='admins', title='Administrators',
roles=['Manager'])
appyTool.create('groups', noSecurity=True, login='admins',
title='Administrators', roles=['Manager'])
appyTool.log('Group "admins" created.')
# Create a group for every global role defined in the application
@ -277,8 +280,8 @@ class ZopeInstaller:
relatedGroup = '%s_group' % role
if appyTool.count('Group', noSecurity=True, login=relatedGroup):
continue
appyTool.create('groups', login=relatedGroup, title=relatedGroup,
roles=[role])
appyTool.create('groups', noSecurity=True, login=relatedGroup,
title=relatedGroup, roles=[role])
appyTool.log('Group "%s", related to global role "%s", was ' \
'created.' % (relatedGroup, role))
@ -320,7 +323,8 @@ class ZopeInstaller:
title = '%s (%s)' % (langEn, langNat)
else:
title = langEn
appyTool.create('translations', id=language, title=title)
appyTool.create('translations', noSecurity=True,
id=language, title=title)
appyTool.log('Translation object created for "%s".' % language)
# Now, we synchronise every Translation object with the corresponding
# "po" file on disk.

View file

@ -1,14 +1,14 @@
# ------------------------------------------------------------------------------
import os, os.path, sys
try:
from AccessControl.SecurityManagement import \
newSecurityManager, noSecurityManager
except ImportError:
pass
# ------------------------------------------------------------------------------
class TestMixin:
'''This class is mixed in with any ZopeTestCase.'''
def createUser(self, userId, roles):
'''Creates a user with id p_userId with some p_roles.'''
self.acl_users.addMember(userId, 'password', [], [])
self.setRoles(roles, name=userId)
def changeUser(self, userId):
'''Logs out currently logged user and logs in p_loginName.'''
self.logout()
@ -55,6 +55,16 @@ class TestMixin:
return arg[10:].strip(']')
return None
def login(self, name='admin'):
user = self.app.acl_users.getUserById(name)
newSecurityManager(None, user)
def logout(self):
'''Logs out.'''
noSecurityManager()
def _setup(self): pass
# Functions executed before and after every test -------------------------------
def beforeTest(test):
'''Is executed before every test.'''
@ -64,7 +74,6 @@ def beforeTest(test):
g['appFolder'] = cfg.diskFolder
moduleOrClassName = g['test'].name # Not used yet.
# Initialize the test
test.createUser('admin', ('Member','Manager'))
test.login('admin')
g['t'] = g['test']

View file

@ -898,11 +898,16 @@ class ToolMixin(BaseMixin):
userId = self.getUser().getId()
# Perform the logout in acl_users
rq.RESPONSE.expireCookie('__ac', path='/')
# Invalidate existing sessions.
sdm = self.session_data_manager
session = sdm.getSessionData(create=0)
if session is not None:
session.invalidate()
# Invalidate session.
try:
sdm = self.session_data_manager
except AttributeError, ae:
# When ran in test mode, session_data_manager is not there.
sdm = None
if sdm:
session = sdm.getSessionData(create=0)
if session is not None:
session.invalidate()
self.log('User "%s" has been logged out.' % userId)
# Remove user from variable "loggedUsers"
from appy.gen.installer import loggedUsers

View file

@ -25,6 +25,20 @@ class BaseMixin:
return self
o = property(get_o)
def getInitiatorInfo(self):
'''Gets information about a potential initiator object from the request.
Returns a 3-tuple (initiator, pageName, field):
* initiator is the initiator (Zope) object;
* pageName is the page on the initiator where the origin of the Ref
field lies;
* field is the Ref instance.
'''
if not rq.get('nav', '').startswith('ref.'): return (None,)*4
splitted = rq['nav'].split('.')
initiator = self.tool.getObject(splitted[1])
fieldName, page = splitted[2].split(':')
return (initiator, page, self.getAppyType(fieldName))
def createOrUpdate(self, created, values,
initiator=None, initiatorField=None):
'''This method creates (if p_created is True) or updates an object.
@ -43,10 +57,9 @@ class BaseMixin:
if not initiator:
folder = tool.getPath('/data')
else:
if initiator.isPrincipiaFolderish:
folder = initiator
else:
folder = initiator.getParentNode()
folder = initiator.getCreateFolder()
# Check that the user can add objects through this Ref.
initiatorField.checkAdd(initiator)
obj = createObject(folder, id, obj.portal_type, tool.getAppName())
previousData = None
if not created: previousData = obj.rememberPreviousData()
@ -61,7 +74,7 @@ class BaseMixin:
obj.historizeData(previousData)
# Manage potential link with an initiator object
if created and initiator: initiator.appy().link(initiatorField, obj)
if created and initiator: initiator.appy().link(initiatorField.name,obj)
# Manage "add" permissions and reindex the object
obj._appy_managePermissions()
@ -112,13 +125,16 @@ class BaseMixin:
# Create the params to add to the URL we will redirect the user to
# create the object.
urlParams = {'mode':'edit', 'page':'main', 'nav':''}
if rq.get('nav', None):
initiator, initiatorPage, initiatorField = self.getInitiatorInfo()
if initiator:
# The object to create will be linked to an initiator object through
# a ref field. We create here a new navigation string with one more
# a Ref field. We create here a new navigation string with one more
# item, that will be the currently created item.
splitted = rq.get('nav').split('.')
splitted[-1] = splitted[-2] = str(int(splitted[-1])+1)
urlParams['nav'] = '.'.join(splitted)
# Check that the user can add objects through this Ref field
initiatiorField.checkAdd(initiator)
# Create a temp object in /temp_folder
tool = self.getTool()
id = tool.generateUid(className)
@ -188,13 +204,7 @@ class BaseMixin:
errorMessage = 'Please correct the indicated errors.' # XXX Translate
isNew = rq.get('is_new') == 'True'
# If this object is created from an initiator, get info about him.
initiator = None
initiatorPage = None
initiatorField = None
if rq.get('nav', '').startswith('ref.'):
splitted = rq['nav'].split('.')
initiator = tool.getObject(splitted[1])
initiatorField, initiatorPage = splitted[2].split(':')
initiator, initiatorPage, initiatorField = self.getInitiatorInfo()
# If the user clicked on 'Cancel', go back to the previous page.
if rq.get('buttonCancel.x', None):
if initiator:
@ -434,6 +444,14 @@ class BaseMixin:
# broken on returned object.
return getattr(self, methodName, None)
def getCreateFolder(self):
'''When an object must be created from this one through a Ref field, we
must know where to put the newly create object: within this one if it
is folderish, besides this one in its parent else.
'''
if self.isPrincipiaFolderish: return self
return self.getParentNode()
def getFieldValue(self, name, onlyIfSync=False, layoutType=None,
outerValue=None):
'''Returns the database value of field named p_name for p_self.
@ -534,10 +552,10 @@ class BaseMixin:
if not refs: raise IndexError()
return refs.index(obj.UID())
def mayAddReference(self, name, folder):
def mayAddReference(self, name):
'''May the user add references via Ref field named p_name in
p_folder?'''
return self.getAppyType(name).mayAdd(self, folder)
return self.getAppyType(name).mayAdd(self)
def isDebug(self):
'''Are we in debug mode ?'''
@ -1227,9 +1245,7 @@ class BaseMixin:
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
if not self.isPrincipiaFolderish:
folder = self.getParentNode()
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
@ -1238,12 +1254,12 @@ class BaseMixin:
if appyType.type != 'Ref': continue
if appyType.isBack or appyType.link: continue
# Indeed, no possibility to create objects with such Refs
refType = self.getTool().getPortalType(appyType.klass)
tool = self.getTool()
refType = tool.getPortalType(appyType.klass)
if refType not in addPermissions: continue
# Get roles that may add this content type
creators = getattr(appyType.klass, 'creators', None)
if not creators:
creators = self.getProductConfig().defaultAddRoles
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:

View file

@ -9,7 +9,7 @@ from appy.gen.mixins.TestMixin import TestMixin, beforeTest, afterTest
# Initialize the Zope test system ----------------------------------------------
ZopeTestCase.installProduct('<!applicationName!>')
class Test(ZopeTestCase.ZopeTestCase, TestMixin):
class Test(TestMixin, ZopeTestCase.ZopeTestCase):
'''Base test class for <!applicationName!> test cases.'''
# Data needed for defining the tests -------------------------------------------

View file

@ -294,7 +294,8 @@ function initSlaves() {
while (i >= 0) {
masterName = getSlaveInfo(slaves[i], 'masterName');
master = document.getElementById(masterName);
updateSlaves(master, slaves[i]);
// If master is not here, we can't hide its slaves when appropriate.
if (master) updateSlaves(master, slaves[i]);
i -= 1;
}
}

View file

@ -124,10 +124,10 @@
objs refObjects/objects;
totalNumber refObjects/totalNumber;
batchSize refObjects/batchSize;
folder python: contextObj.isPrincipiaFolderish and contextObj or contextObj.getParentNode();
folder contextObj/getCreateFolder;
linkedPortalType python: tool.getPortalType(appyType['klass']);
canWrite python: not appyType['isBack'] and contextObj.allows(appyType['writePermission']);
showPlusIcon python: contextObj.mayAddReference(fieldName, folder);
showPlusIcon python: contextObj.mayAddReference(fieldName);
atMostOneRef python: (appyType['multiplicity'][1] == 1) and (len(objs)&lt;=1);
addConfirmMsg python: appyType['addConfirm'] and _('%s_addConfirm' % appyType['labelId']) or '';
navBaseCall python: 'askRefField(\'%s\',\'%s\',\'%s\',\'%s\',**v**)' % (ajaxHookId, contextObj.absolute_url(), fieldName, innerRef)">

View file

@ -2,7 +2,7 @@
import re, os, os.path
# Function for creating a Zope object ------------------------------------------
def createObject(folder, id, className, appName, wf=True):
def createObject(folder, id, className, appName, wf=True, noSecurity=False):
'''Creates, in p_folder, object with some p_id. Object will be an instance
of p_className from application p_appName. In a very special case (the
creation of the config object), computing workflow-related info is not
@ -10,6 +10,24 @@ def createObject(folder, id, className, appName, wf=True):
p_wf=False.'''
exec 'from Products.%s.%s import %s as ZopeClass' % (appName, className,
className)
if not noSecurity:
# Check that the user can create objects of className
if folder.meta_type.endswith('Folder'): # Folder or temp folder.
tool = folder.config
else:
tool = folder.getTool()
user = tool.getUser()
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:
from AccessControl import Unauthorized
raise Unauthorized("User can't create instances of %s" % \
ZopeClass.__name__)
obj = ZopeClass(id)
folder._objects = folder._objects + \
({'id':id, 'meta_type':className},)

View file

@ -22,8 +22,46 @@ FREEZE_FATAL_ERROR = 'A server error occurred. Please contact the system ' \
# ------------------------------------------------------------------------------
class AbstractWrapper(object):
'''Any real Zope object has a companion object that is an instance of this
class.'''
'''Any real Appy-managed Zope object has a companion object that is an
instance of this class.'''
# --------------------------------------------------------------------------
# Class methods
# --------------------------------------------------------------------------
@classmethod
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.'''
i = len(klass.__bases__) - 1
res = None
while i >= 0:
res = getattr(klass.__bases__[i], attr, None)
if res: return res
i -= 1
@classmethod
def getWorkflow(klass):
'''Returns the workflow tied to p_klass.'''
res = klass._getParentAttr('workflow')
# Return a default workflow if no workflow was found.
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.defaultAddRoles
return res
# --------------------------------------------------------------------------
# Instance methods
# --------------------------------------------------------------------------
def __init__(self, o): self.__dict__['o'] = o
def appy(self): return self
@ -87,21 +125,6 @@ class AbstractWrapper(object):
return customUser.__dict__[methodName](self, *args, **kwargs)
def getField(self, name): return self.o.getAppyType(name)
@classmethod
def getWorkflow(klass):
'''Returns the workflow tied to p_klass.'''
# Browse parent classes of p_klass in reverse order. This way, a
# user-defined workflow will override a Appy default workflow.
i = len(klass.__bases__)-1
res = None
while i >= 0:
res = getattr(klass.__bases__[i], 'workflow', None)
if res: break
i -= 1
# Return a default workflow if no workflow was found.
if not res:
res = WorkflowAnonymous
return res
def link(self, fieldName, obj):
'''This method links p_obj (which can be a list of objects) to this one
@ -124,7 +147,7 @@ class AbstractWrapper(object):
getattr(tool.getObject(y), sortKey)))
if reverse: refs.reverse()
def create(self, fieldNameOrClass, **kwargs):
def create(self, fieldNameOrClass, noSecurity=False, **kwargs):
'''If p_fieldNameOrClass is the name of a field, this method allows to
create an object and link it to the current one (self) through
reference field named p_fieldName.
@ -156,12 +179,13 @@ class AbstractWrapper(object):
if not isField:
folder = tool.getPath('/data')
else:
if hasattr(self, 'folder') and self.folder:
folder = self.o
else:
folder = self.o.getParentNode()
folder = self.o.getCreateFolder()
if not noSecurity:
# Check that the user can edit this field.
appyType.checkAdd(self.o)
# Create the object
zopeObj = createObject(folder, objId,portalType, tool.getAppName())
zopeObj = createObject(folder, objId, portalType, tool.getAppName(),
noSecurity=noSecurity)
appyObj = zopeObj.appy()
# Set object attributes
for attrName, attrValue in kwargs.iteritems():

View file

@ -216,7 +216,8 @@ class XmlUnmarshaller(XmlParser):
If "object" is specified, it means that the tag contains sub-tags, each
one corresponding to the value of an attribute for this object.
if "tuple" is specified, it will be converted to a list.'''
def __init__(self, classes={}, tagTypes={}, conversionFunctions={}):
def __init__(self, classes={}, tagTypes={}, conversionFunctions={},
utf8=True):
XmlParser.__init__(self)
# self.classes below is a dict whose keys are tag names and values are
# Python classes. During the unmarshalling process, when an object is
@ -253,6 +254,7 @@ class XmlUnmarshaller(XmlParser):
# for example convert strings that have specific values (in this case,
# knowing that the value is a 'string' is not sufficient).
self.conversionFunctions = conversionFunctions
self.utf8 = utf8
def convertAttrs(self, attrs):
'''Converts XML attrs to a dict.'''
@ -360,6 +362,8 @@ class XmlUnmarshaller(XmlParser):
setattr(currentContainer, name, attrValue)
def characters(self, content):
if not self.utf8:
content = content.encode('utf-8')
e = XmlParser.characters(self, content)
if e.currentBasicType:
e.currentContent += content