[gen] Security improvements.

This commit is contained in:
Gaetan Delannay 2014-05-03 22:45:51 +02:00
parent b2dbef2bc4
commit 5c6a7f0f97
9 changed files with 146 additions and 120 deletions

View file

@ -307,9 +307,9 @@ class Field:
'''When displaying p_obj on a given p_layoutType, must we show this '''When displaying p_obj on a given p_layoutType, must we show this
field?''' field?'''
# 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
if layoutType == 'edit': perm = self.writePermission perm = (layoutType == 'edit') and self.writePermission or \
else: perm = self.readPermission self.readPermission
if not obj.allows(perm): return False if not obj.allows(perm): return
# 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)
@ -319,7 +319,7 @@ class Field:
if type(res) in sutils.sequenceTypes: if type(res) in sutils.sequenceTypes:
for r in res: for r in res:
if r == layoutType: return True if r == layoutType: return True
return False return
elif res in ('view', 'edit', 'result'): elif res in ('view', 'edit', 'result'):
return res == layoutType return res == layoutType
return bool(res) return bool(res)

View file

@ -16,7 +16,7 @@ class Calendar(Field):
# Month view for a calendar. Called by pxView, and directly from the UI, # Month view for a calendar. Called by pxView, and directly from the UI,
# via Ajax, when the user selects another month. # via Ajax, when the user selects another month.
pxMonthView = Px(''' pxMonthView = Px('''
<div var="ajaxHookId=zobj.UID() + field.name; <div var="ajaxHookId=zobj.id + field.name;
month=req['month']; month=req['month'];
monthDayOne=DateTime('%s/01' % month); monthDayOne=DateTime('%s/01' % month);
today=DateTime('00:00'); today=DateTime('00:00');
@ -27,7 +27,7 @@ class Calendar(Field):
defaultDateMonth=defaultDate.strftime('%Y/%m'); defaultDateMonth=defaultDate.strftime('%Y/%m');
previousMonth=field.getSiblingMonth(month, 'previous'); previousMonth=field.getSiblingMonth(month, 'previous');
nextMonth=field.getSiblingMonth(month, 'next'); nextMonth=field.getSiblingMonth(month, 'next');
mayEdit=zobj.allows(field.writePermission); mayEdit=zobj.mayEdit(field.writePermission);
objUrl=zobj.absolute_url(); objUrl=zobj.absolute_url();
startDate=field.getStartDate(zobj); startDate=field.getStartDate(zobj);
endDate=field.getEndDate(zobj); endDate=field.getEndDate(zobj);
@ -630,6 +630,8 @@ class Calendar(Field):
or deletion of a calendar event.''' or deletion of a calendar event.'''
rq = obj.REQUEST rq = obj.REQUEST
action = rq['actionType'] action = rq['actionType']
# Security check
obj.mayEdit(self.writePermission, raiseError=True)
# Get the date for this action # Get the date for this action
if action == 'createEvent': if action == 'createEvent':
return self.createEvent(obj, DateTime(rq['day'])) return self.createEvent(obj, DateTime(rq['day']))

View file

@ -468,7 +468,7 @@ class Pod(Field):
# What is the action to perform? # What is the action to perform?
action = rq.get('action', 'generate') action = rq.get('action', 'generate')
# Security check. # Security check.
obj.o.allows('read', raiseError=True) obj.o.mayView(self.readPermission, raiseError=True)
# Perform the requested action. # Perform the requested action.
tool = obj.tool.o tool = obj.tool.o
template = rq.get('template') template = rq.get('template')
@ -486,7 +486,7 @@ class Pod(Field):
res.writeResponse(rq.RESPONSE) res.writeResponse(rq.RESPONSE)
return return
# Performing any other action requires write access to p_obj. # Performing any other action requires write access to p_obj.
obj.o.allows('write', raiseError=True) obj.o.mayEdit(self.writePermission, raiseError=True)
msg = 'action_done' msg = 'action_done'
if action == 'freeze': if action == 'freeze':
# (Re-)freeze a document in the database. # (Re-)freeze a document in the database.

View file

@ -70,7 +70,7 @@ class Ref(Field):
style=":'%s; %s' % (url(imgName, bg=True), \ style=":'%s; %s' % (url(imgName, bg=True), \
ztool.getButtonWidth(label))"/> ztool.getButtonWidth(label))"/>
<!-- Delete several objects --> <!-- Delete several objects -->
<input if="not isBack and field.delete and canWrite" <input if="mayEdit and field.delete"
var2="action='delete'; label=_('object_delete_many')" var2="action='delete'; label=_('object_delete_many')"
type="button" class="button" value=":label" type="button" class="button" value=":label"
onclick=":'onLinkMany(%s,%s)' % (q(action), q(ajaxHookId))" onclick=":'onLinkMany(%s,%s)' % (q(action), q(ajaxHookId))"
@ -84,8 +84,7 @@ class Ref(Field):
<table class="noStyle"> <table class="noStyle">
<tr> <tr>
<!-- Arrows for moving objects up or down --> <!-- Arrows for moving objects up or down -->
<td if="not isBack and (totalNumber &gt;1) and changeOrder and canWrite \ <td if="(totalNumber &gt;1) and changeOrder and not inPickList"
and not inPickList"
var2="ajaxBaseCall=navBaseCall.replace('**v**','%s,%s,{%s:%s,%s:%s}'%\ var2="ajaxBaseCall=navBaseCall.replace('**v**','%s,%s,{%s:%s,%s:%s}'%\
(q(startNumber), q('doChangeOrder'), q('refObjectUid'), (q(startNumber), q('doChangeOrder'), q('refObjectUid'),
q(tiedUid), q('move'), q('**v**')))"> q(tiedUid), q('move'), q('**v**')))">
@ -114,7 +113,7 @@ class Ref(Field):
<img src=":url('edit')" title=":_('object_edit')"/></a> <img src=":url('edit')" title=":_('object_edit')"/></a>
</td> </td>
<!-- Delete --> <!-- Delete -->
<td if="not isBack and field.delete and canWrite and tied.o.mayDelete()"> <td if="mayEdit and field.delete and tied.o.mayDelete()">
<img class="clickable" title=":_('object_delete')" src=":url('delete')" <img class="clickable" title=":_('object_delete')" src=":url('delete')"
onclick=":'onDeleteObject(%s)' % q(tiedUid)"/> onclick=":'onDeleteObject(%s)' % q(tiedUid)"/>
</td> </td>
@ -141,7 +140,7 @@ class Ref(Field):
# Displays the button allowing to add a new object through a Ref field, if # Displays the button allowing to add a new object through a Ref field, if
# it has been declared as addable and if multiplicities allow it. # it has been declared as addable and if multiplicities allow it.
pxAdd = Px(''' pxAdd = Px('''
<input if="showPlusIcon and not inPickList" type="button" <input if="mayAdd and not inPickList" type="button"
class="buttonSmall button" class="buttonSmall button"
var2="navInfo='ref.%s.%s:%s.%d.%d' % (zobj.id, field.name, \ var2="navInfo='ref.%s.%s:%s.%d.%d' % (zobj.id, field.name, \
field.pageName, 0, totalNumber); field.pageName, 0, totalNumber);
@ -163,8 +162,7 @@ class Ref(Field):
# This PX displays, in a cell header from a ref table, icons for sorting the # This PX displays, in a cell header from a ref table, icons for sorting the
# ref field according to the field that corresponds to this column. # ref field according to the field that corresponds to this column.
pxSortIcons = Px(''' pxSortIcons = Px('''
<x if="changeOrder and canWrite and ztool.isSortable(refField.name, \ <x if="changeOrder and ztool.isSortable(refField.name,tiedClassName,'ref')"
tiedClassName, 'ref')"
var2="ajaxBaseCall=navBaseCall.replace('**v**', '%s,%s,{%s:%s,%s:%s}'% \ var2="ajaxBaseCall=navBaseCall.replace('**v**', '%s,%s,{%s:%s,%s:%s}'% \
(q(startNumber), q('sort'), q('sortKey'), q(refField.name), \ (q(startNumber), q('sort'), q('sortKey'), q(refField.name), \
q('reverse'), q('**v**')))"> q('reverse'), q('**v**')))">
@ -195,7 +193,7 @@ class Ref(Field):
# PX that displays referred objects as a list. # PX that displays referred objects as a list.
pxViewList = Px(''' pxViewList = Px('''
<div if="not innerRef or showPlusIcon" style="margin-bottom: 4px"> <div if="not innerRef or mayAdd" style="margin-bottom: 4px">
<span if="subLabel" class="discreet">:_(subLabel)</span> <span if="subLabel" class="discreet">:_(subLabel)</span>
(<span class="discreet">:totalNumber</span>) (<span class="discreet">:totalNumber</span>)
<x>:field.pxAdd</x> <x>:field.pxAdd</x>
@ -215,7 +213,7 @@ class Ref(Field):
<!-- No object is present --> <!-- No object is present -->
<p class="discreet" <p class="discreet"
if="not objects and (innerRef and showPlusIcon)">:_('no_ref')</p> if="not objects and (innerRef and mayAdd)">:_('no_ref')</p>
<!-- Linked objects --> <!-- Linked objects -->
<table if="objects" class=":not innerRef and 'list' or ''" <table if="objects" class=":not innerRef and 'list' or ''"
@ -242,7 +240,7 @@ class Ref(Field):
class=":loop.tied.odd and 'even' or 'odd'" class=":loop.tied.odd and 'even' or 'odd'"
var2="tiedUid=tied.o.id; var2="tiedUid=tied.o.id;
objectIndex=field.getIndexOf(zobj, tiedUid)|None; objectIndex=field.getIndexOf(zobj, tiedUid)|None;
mayView=tied.allows('read')"> mayView=tied.o.mayView()">
<td if="not inPickList and numbered">:field.pxNumber</td> <td if="not inPickList and numbered">:field.pxNumber</td>
<td for="column in columns" width=":column.width" align=":column.align" <td for="column in columns" width=":column.width" align=":column.align"
var2="refField=column.field"> var2="refField=column.field">
@ -263,15 +261,15 @@ class Ref(Field):
if="field.isShowable(zobj, 'result')">:field.pxRender</x> if="field.isShowable(zobj, 'result')">:field.pxRender</x>
</x> </x>
</td> </td>
<td if="checkboxes and mayView" class="cbCell"> <td if="checkboxes" class="cbCell">
<input type="checkbox" name=":ajaxHookId" checked="checked" <input if="mayView" type="checkbox" name=":ajaxHookId" checked="checked"
value=":tiedUid" onclick="toggleRefCb(this)"/> value=":tiedUid" onclick="toggleRefCb(this)"/>
</td> </td>
</tr> </tr>
</table> </table>
<!-- Global actions --> <!-- Global actions -->
<div if="canWrite and (totalNumber &gt; 1)" <div if="mayEdit and (totalNumber &gt; 1)"
align=":dright">:field.pxGlobalActions</div> align=":dright">:field.pxGlobalActions</div>
<!-- (Bottom) navigation --> <!-- (Bottom) navigation -->
@ -288,7 +286,6 @@ class Ref(Field):
<x var="innerRef=False; <x var="innerRef=False;
ajaxHookId=ajaxHookId|'%s_%s_poss' % (zobj.id, field.name); ajaxHookId=ajaxHookId|'%s_%s_poss' % (zobj.id, field.name);
inPickList=True; inPickList=True;
isBack=field.isBack;
startNumber=field.getStartNumber('list', req, ajaxHookId); startNumber=field.getStartNumber('list', req, ajaxHookId);
info=field.getPossibleValues(zobj, startNumber=startNumber, \ info=field.getPossibleValues(zobj, startNumber=startNumber, \
someObjects=True, removeLinked=True); someObjects=True, removeLinked=True);
@ -297,10 +294,10 @@ class Ref(Field):
batchSize=info.batchSize; batchSize=info.batchSize;
batchNumber=len(objects); batchNumber=len(objects);
tiedClassName=tiedClassName|ztool.getPortalType(field.klass); tiedClassName=tiedClassName|ztool.getPortalType(field.klass);
canWrite=canWrite|\ mayEdit=mayEdit|\
not field.isBack and zobj.allows(field.writePermission); not field.isBack and zobj.mayEdit(field.writePermission);
mayUnlink=False; mayUnlink=False;
showPlusIcon=False; mayAdd=False;
navBaseCall='askRefField(%s,%s,%s,%s,**v**)' % \ navBaseCall='askRefField(%s,%s,%s,%s,**v**)' % \
(q(ajaxHookId), q(zobj.absolute_url()), \ (q(ajaxHookId), q(zobj.absolute_url()), \
q(field.name), q(innerRef)); q(field.name), q(innerRef));
@ -360,7 +357,6 @@ class Ref(Field):
linkList=field.link == 'list'; linkList=field.link == 'list';
renderAll=req.get('scope') != 'objs'; renderAll=req.get('scope') != 'objs';
inPickList=False; inPickList=False;
isBack=field.isBack;
startNumber=field.getStartNumber(render, req, ajaxHookId); startNumber=field.getStartNumber(render, req, ajaxHookId);
info=field.getValue(zobj,startNumber=startNumber,someObjects=True); info=field.getValue(zobj,startNumber=startNumber,someObjects=True);
objects=info.objects; objects=info.objects;
@ -370,19 +366,18 @@ class Ref(Field):
batchNumber=len(objects); batchNumber=len(objects);
folder=zobj.getCreateFolder(); folder=zobj.getCreateFolder();
tiedClassName=ztool.getPortalType(field.klass); tiedClassName=ztool.getPortalType(field.klass);
canWrite=not field.isBack and zobj.allows(field.writePermission); mayEdit=not field.isBack and zobj.mayEdit(field.writePermission);
mayUnlink=not isBack and canWrite and \ mayUnlink=mayEdit and field.getAttribute(zobj, 'unlink');
field.getAttribute(zobj, 'unlink'); mayAdd=mayEdit and field.mayAdd(zobj, checkMayEdit=False);
showPlusIcon=field.mayAdd(zobj);
addConfirmMsg=field.addConfirm and \ addConfirmMsg=field.addConfirm and \
_('%s_addConfirm' % field.labelId) or ''; _('%s_addConfirm' % field.labelId) or '';
navBaseCall='askRefField(%s,%s,%s,%s,**v**)' % \ navBaseCall='askRefField(%s,%s,%s,%s,**v**)' % \
(q(ajaxHookId), q(zobj.absolute_url()), \ (q(ajaxHookId), q(zobj.absolute_url()), \
q(field.name), q(innerRef)); q(field.name), q(innerRef));
changeOrder=field.getAttribute(zobj, 'changeOrder'); changeOrder=mayEdit and field.getAttribute(zobj, 'changeOrder');
numbered=field.isNumbered(zobj); numbered=field.isNumbered(zobj);
changeNumber=not inPickList and numbered and canWrite and \ changeNumber=not inPickList and numbered and changeOrder and \
changeOrder and (totalNumber &gt; 3); (totalNumber &gt; 3);
checkboxesEnabled=field.getAttribute(zobj, 'checkboxes') and \ checkboxesEnabled=field.getAttribute(zobj, 'checkboxes') and \
(layoutType != 'cell'); (layoutType != 'cell');
checkboxes=checkboxesEnabled and (totalNumber &gt; 1); checkboxes=checkboxesEnabled and (totalNumber &gt; 1);
@ -390,7 +385,7 @@ class Ref(Field):
<!-- JS tables storing checkbox statuses if checkboxes are enabled --> <!-- JS tables storing checkbox statuses if checkboxes are enabled -->
<script if="checkboxesEnabled and renderAll" <script if="checkboxesEnabled and renderAll"
type="text/javascript">:field.getCbJsInit(zobj)</script> type="text/javascript">:field.getCbJsInit(zobj)</script>
<div if="linkList and renderAll and canWrite" <div if="linkList and renderAll and mayEdit"
var2="ajaxHookId='%s_%s_poss' % (zobj.id, field.name)" var2="ajaxHookId='%s_%s_poss' % (zobj.id, field.name)"
id=":ajaxHookId">:field.pxViewPickList</div> id=":ajaxHookId">:field.pxViewPickList</div>
<x if="render == 'list'" <x if="render == 'list'"
@ -602,10 +597,10 @@ class Ref(Field):
# We add here specific Ref rules for preventing to show the field under # We add here specific Ref rules for preventing to show the field under
# some inappropriate circumstances. # some inappropriate circumstances.
if layoutType == 'edit': if layoutType == 'edit':
if self.mayAdd(obj): return False if self.mayAdd(obj): return
if self.link in (False, 'list'): return False if self.link in (False, 'list'): return
if self.isBack: if self.isBack:
if layoutType == 'edit': return False if layoutType == 'edit': return
else: return getattr(obj.aq_base, self.name, None) else: return getattr(obj.aq_base, self.name, None)
return res return res
@ -835,7 +830,7 @@ class Ref(Field):
'''This method links p_value (which can be a list of objects) to p_obj '''This method links p_value (which can be a list of objects) to p_obj
through this Ref field.''' through this Ref field.'''
# Security check # Security check
if not noSecurity: obj.allows(self.writePermission, raiseError=True) if not noSecurity: obj.mayEdit(self.writePermission, raiseError=True)
# p_value can be a list of objects # p_value can be a list of objects
if type(value) in sutils.sequenceTypes: if type(value) in sutils.sequenceTypes:
for v in value: self.linkObject(obj, v, back=back) for v in value: self.linkObject(obj, v, back=back)
@ -865,7 +860,7 @@ class Ref(Field):
'''This method unlinks p_value (which can be a list of objects) from '''This method unlinks p_value (which can be a list of objects) from
p_obj through this Ref field.''' p_obj through this Ref field.'''
# Security check # Security check
if not noSecurity: obj.allows(self.writePermission, raiseError=True) if not noSecurity: obj.mayEdit(self.writePermission, raiseError=True)
# p_value can be a list of objects # p_value can be a list of objects
if type(value) in sutils.sequenceTypes: if type(value) in sutils.sequenceTypes:
for v in value: self.unlinkObject(obj, v, back=back) for v in value: self.unlinkObject(obj, v, back=back)
@ -916,8 +911,12 @@ class Ref(Field):
if objects: if objects:
self.linkObject(obj, objects) self.linkObject(obj, objects)
def mayAdd(self, obj): def mayAdd(self, obj, checkMayEdit=True):
'''May the user create a new referred object from p_obj via this Ref?''' '''May the user create a new referred object from p_obj via this Ref?
If p_checkMayEdit is False, it means that the condition of being
allowed to edit this Ref field has already been checked somewhere
else (it is always required, we just want to avoid checking it
twice).'''
# We can't (yet) do that on back references. # We can't (yet) do that on back references.
if self.isBack: return gutils.No('is_back') if self.isBack: return gutils.No('is_back')
# Check if this Ref is addable # Check if this Ref is addable
@ -931,7 +930,8 @@ class Ref(Field):
refCount = len(getattr(obj, self.name, ())) refCount = len(getattr(obj, self.name, ()))
if refCount >= self.multiplicity[1]: return gutils.No('max_reached') if refCount >= self.multiplicity[1]: return gutils.No('max_reached')
# May the user edit this Ref field? # May the user edit this Ref field?
if not obj.allows(self.writePermission): if checkMayEdit:
if not obj.mayEdit(self.writePermission):
return gutils.No('no_write_perm') return gutils.No('no_write_perm')
# May the user create instances of the referred class? # May the user create instances of the referred class?
if not obj.getTool().userMayCreate(self.klass): if not obj.getTool().userMayCreate(self.klass):
@ -943,8 +943,7 @@ class Ref(Field):
m_mayAdd returns False.''' m_mayAdd returns False.'''
may = self.mayAdd(obj) may = self.mayAdd(obj)
if not may: if not may:
from AccessControl import Unauthorized obj.raiseUnauthorized("User can't write Ref field '%s' (%s)." % \
raise Unauthorized("User can't write Ref field '%s' (%s)." % \
(self.name, may.msg)) (self.name, may.msg))
def getCbJsInit(self, obj): def getCbJsInit(self, obj):
@ -1070,7 +1069,7 @@ class Ref(Field):
# "link_many", "unlink_many", "delete_many". As a preamble, perform # "link_many", "unlink_many", "delete_many". As a preamble, perform
# a security check once, instead of doing it on every object-level # a security check once, instead of doing it on every object-level
# operation. # operation.
obj.allows(self.writePermission, raiseError=True) obj.mayEdit(self.writePermission, raiseError=True)
# Get the (un-)checked objects from the request. # Get the (un-)checked objects from the request.
uids = rq['targetUid'].strip(',') or (); uids = rq['targetUid'].strip(',') or ();
if uids: uids = uids.split(',') if uids: uids = uids.split(',')

View file

@ -397,7 +397,7 @@ class Transition:
if not obj.isTemporary(): obj.reindex() if not obj.isTemporary(): obj.reindex()
# If we are viewing the object and if the logged user looses the # If we are viewing the object and if the logged user looses the
# permission to view it, redirect the user to its home page. # permission to view it, redirect the user to its home page.
if not obj.allows('read') and \ if not obj.mayView() and \
(obj.absolute_url_path() in rq['HTTP_REFERER']): (obj.absolute_url_path() in rq['HTTP_REFERER']):
back = tool.getHomePage() back = tool.getHomePage()
else: else:

View file

@ -276,8 +276,7 @@ class BaseMixin:
# used back/forward buttons of its browser... # used back/forward buttons of its browser...
userId = user.login userId = user.login
if (page in self.locks) and (userId != self.locks[page][0]): if (page in self.locks) and (userId != self.locks[page][0]):
from AccessControl import Unauthorized self.raiseUnauthorized('This page is locked.')
raise Unauthorized('This page is locked.')
# Set the lock # Set the lock
from DateTime import DateTime from DateTime import DateTime
self.locks[page] = (userId, DateTime()) self.locks[page] = (userId, DateTime())
@ -301,8 +300,7 @@ class BaseMixin:
if not force: if not force:
userId = self.getTool().getUser().login userId = self.getTool().getUser().login
if self.locks[page][0] != userId: if self.locks[page][0] != userId:
from AccessControl import Unauthorized self.raiseUnauthorized('This page was locked by someone else.')
raise Unauthorized('This page was locked by someone else.')
# Remove the lock # Remove the lock
del self.locks[page] del self.locks[page]
@ -444,10 +442,9 @@ class BaseMixin:
# onEdit), redirect to the main site page. # onEdit), redirect to the main site page.
if not getattr(obj.getParentNode().aq_base, obj.id, None): if not getattr(obj.getParentNode().aq_base, obj.id, None):
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 its home
# main site page. # page.
if not obj.allows('read'): if not obj.mayView(): return self.goto(tool.getHomePage(), msg)
return self.goto(tool.getSiteUrl(), msg)
if (buttonClicked == 'save') or saveConfirmed: if (buttonClicked == 'save') or saveConfirmed:
obj.say(msg) obj.say(msg)
if isNew and initiator: if isNew and initiator:
@ -520,8 +517,7 @@ class BaseMixin:
corresponding Appy wrapper and returns, as XML, its result.''' corresponding Appy wrapper and returns, as XML, its result.'''
self.REQUEST.RESPONSE.setHeader('Content-Type','text/xml;charset=utf-8') self.REQUEST.RESPONSE.setHeader('Content-Type','text/xml;charset=utf-8')
# Check if the user is allowed to consult this object # Check if the user is allowed to consult this object
if not self.allows('read'): if not self.mayView(): return XmlMarshaller().marshall('Unauthorized')
return XmlMarshaller().marshall('Unauthorized')
if not action: if not action:
marshaller = XmlMarshaller(rootTag=self.getClass().__name__, marshaller = XmlMarshaller(rootTag=self.getClass().__name__,
dumpUnicode=True) dumpUnicode=True)
@ -576,6 +572,12 @@ class BaseMixin:
if rq.get('appy', None) == '1': obj = obj.appy() if rq.get('appy', None) == '1': obj = obj.appy()
return getattr(obj, 'on'+action)() return getattr(obj, 'on'+action)()
def raiseUnauthorized(self, msg=None):
'''Raise an error "Unauthorized access".'''
from AccessControl import Unauthorized
if msg: raise Unauthorized(msg)
raise Unauthorized()
def rememberPreviousData(self, fields): def rememberPreviousData(self, fields):
'''This method is called before updating an object and remembers, for '''This method is called before updating an object and remembers, for
every historized field, the previous value. Result is a dict every historized field, the previous value. Result is a dict
@ -1131,10 +1133,11 @@ class BaseMixin:
return 'main' return 'main'
def mayAct(self): def mayAct(self):
'''May the currently logged user see column "actions" for this '''m_mayAct allows to hide the whole set of actions for an object.
object? This can be used for hiding the "edit" icon, for example: Indeed, beyond workflow security, it can be useful to hide controls
when a user may edit only a restricted set of fields on an object, like "edit" icons/buttons. For example, if a user may only edit some
we may avoid showing him the global "edit" icon.''' Ref fields with add=True on an object, when clicking on "edit", he
will see an empty edit form.'''
appyObj = self.appy() appyObj = self.appy()
if hasattr(appyObj, 'mayAct'): return appyObj.mayAct() if hasattr(appyObj, 'mayAct'): return appyObj.mayAct()
return True return True
@ -1148,14 +1151,34 @@ class BaseMixin:
if hasattr(appyObj, 'mayDelete'): return appyObj.mayDelete() if hasattr(appyObj, 'mayDelete'): return appyObj.mayDelete()
return True return True
def mayEdit(self, permission='write'): def mayEdit(self, permission='write', permOnly=False, raiseError=False):
'''May the currently logged user edit this object? p_perm can be a '''May the currently logged user edit this object? p_permission can be a
field-specific permission.''' field-specific permission. If p_permOnly is True, the specific
res = self.allows(permission) user-defined condition is not evaluated. If p_raiseError is True, if
the user may not edit p_self, an error is raised.'''
res = self.allows(permission, raiseError=raiseError)
if not res: return
if permOnly: return res
# An additional, user-defined condition, may refine the base permission.
appyObj = self.appy()
if hasattr(appyObj, 'mayEdit'):
res = appyObj.mayEdit()
if not res and raiseError: self.raiseUnauthorized()
return res
return True
def mayView(self, permission='read', raiseError=False):
'''May the currently logged user view this object? p_permission can be a
field-specific permission. If p_raiseError is True, if the user may
not view p_self, an error is raised.'''
res = self.allows(permission, raiseError=raiseError)
if not res: return if not res: return
# An additional, user-defined condition, may refine the base permission. # An additional, user-defined condition, may refine the base permission.
appyObj = self.appy() appyObj = self.appy()
if hasattr(appyObj, 'mayEdit'): return appyObj.mayEdit() if hasattr(appyObj, 'mayView'):
res = appyObj.mayView()
if not res and raiseError: self.raiseUnauthorized()
return res
return True return True
def onExecuteAction(self): def onExecuteAction(self):
@ -1512,14 +1535,12 @@ class BaseMixin:
is retrieved from the request.''' is retrieved from the request.'''
name = self.REQUEST.get('name') name = self.REQUEST.get('name')
if not name: return if not name: return
# Security check
if '_img_' not in name: if '_img_' not in name:
appyType = self.getAppyType(name) field = self.getAppyType(name)
else: else:
appyType = self.getAppyType(name.split('_img_')[0]) field = self.getAppyType(name.split('_img_')[0])
if (not appyType.isShowable(self, 'view')) and \ self.mayView(field.readPermission, raiseError=True)
(not appyType.isShowable(self, 'result')):
from zExceptions import NotFound
raise NotFound()
info = getattr(self.aq_base, name, None) info = getattr(self.aq_base, name, None)
if info: if info:
# Write the file in the HTTP response. # Write the file in the HTTP response.
@ -1556,16 +1577,13 @@ class BaseMixin:
def allows(self, permission, raiseError=False): def allows(self, permission, raiseError=False):
'''Has the logged user p_permission on p_self ?''' '''Has the logged user p_permission on p_self ?'''
res = self.getTool().getUser().has_permission(permission, self) res = self.getTool().getUser().has_permission(permission, self)
if not res and raiseError: if not res and raiseError: self.raiseUnauthorized()
from AccessControl import Unauthorized
raise Unauthorized
return res return res
def isTemporary(self): def isTemporary(self):
'''Is this object temporary ?''' '''Is this object temporary ?'''
parent = self.getParentNode() parent = self.getParentNode()
if not parent: # Is propably being created through code if not parent: return # Is probably being created through code
return False
return parent.getId() == 'temp_folder' return parent.getId() == 'temp_folder'
def onProcess(self): def onProcess(self):
@ -1575,7 +1593,7 @@ class BaseMixin:
def onCall(self): def onCall(self):
'''Calls a specific method on the corresponding wrapper.''' '''Calls a specific method on the corresponding wrapper.'''
self.allows('read', raiseError=True) self.mayView(raiseError=True)
method = self.REQUEST['method'] method = self.REQUEST['method']
obj = self.appy() obj = self.appy()
return getattr(obj, method)() return getattr(obj, method)()

View file

@ -104,7 +104,7 @@ td.search { padding-top: 8px }
.dropdown a:hover { text-decoration: underline } .dropdown a:hover { text-decoration: underline }
.list { margin-bottom: 3px } .list { margin-bottom: 3px }
.list td, .list th { border: 3px solid #ededed; color: grey; .list td, .list th { border: 3px solid #ededed; color: grey;
padding: 3px 5px 0 5px } padding: 3px 5px 3px 5px }
.list th { background-color: #e5e5e5; font-style: italic; font-weight: normal } .list th { background-color: #e5e5e5; font-style: italic; font-weight: normal }
.grid th { font-style: italic; font-weight: normal; .grid th { font-style: italic; font-weight: normal;
border-bottom: 5px solid #fdfdfd; padding: 3px 5px 0 5px } border-bottom: 5px solid #fdfdfd; padding: 3px 5px 0 5px }

View file

@ -302,7 +302,8 @@ class ToolWrapper(AbstractWrapper):
# Show on query list or grid, the field content for a given object. # Show on query list or grid, the field content for a given object.
pxQueryField = Px(''' pxQueryField = Px('''
<!-- Title --> <!-- Title -->
<x if="field.name == 'title'" <x if="field.name == 'title'">
<x if="mayView"
var2="navInfo='search.%s.%s.%d.%d' % \ var2="navInfo='search.%s.%s.%d.%d' % \
(className, searchName, startNumber+currentNumber, totalNumber); (className, searchName, startNumber+currentNumber, totalNumber);
cssClass=zobj.getCssFor('title')"> cssClass=zobj.getCssFor('title')">
@ -328,7 +329,7 @@ class ToolWrapper(AbstractWrapper):
<!-- Delete --> <!-- Delete -->
<img if="zobj.mayDelete()" class="clickable" src=":url('delete')" <img if="zobj.mayDelete()" class="clickable" src=":url('delete')"
title=":_('object_delete')" title=":_('object_delete')"
onClick=":'onDeleteObject(%s)' % q(zobj.UID())"/> onClick=":'onDeleteObject(%s)' % q(zobj.id)"/>
</td> </td>
<!-- Workflow transitions --> <!-- Workflow transitions -->
<td if="zobj.showTransitions('result')" <td if="zobj.showTransitions('result')"
@ -337,8 +338,13 @@ class ToolWrapper(AbstractWrapper):
</tr> </tr>
</table> </table>
</x> </x>
<x if="not mayView">
<img src=":url('fake')" style="margin-right: 5px"/>
<x>:_('unauthorized')</x>
</x>
</x>
<!-- Any other field --> <!-- Any other field -->
<x if="field.name != 'title'"> <x if="(field.name != 'title') and mayView">
<x var="layoutType='cell'; innerRef=True" <x var="layoutType='cell'; innerRef=True"
if="field.isShowable(zobj, 'result')">:field.pxRender</x> if="field.isShowable(zobj, 'result')">:field.pxRender</x>
</x>''') </x>''')
@ -361,7 +367,7 @@ class ToolWrapper(AbstractWrapper):
<!-- Results --> <!-- Results -->
<tr for="zobj in zobjects" id="query_row" valign="top" <tr for="zobj in zobjects" id="query_row" valign="top"
var2="currentNumber=currentNumber + 1; var2="currentNumber=currentNumber + 1;
obj=zobj.appy()" obj=zobj.appy(); mayView=zobj.mayView()"
class=":loop.zobj.odd and 'even' or 'odd'"> class=":loop.zobj.odd and 'even' or 'odd'">
<td for="column in columns" <td for="column in columns"
var2="field=column.field" id=":'field_%s' % field.name" var2="field=column.field" id=":'field_%s' % field.name"
@ -378,7 +384,8 @@ class ToolWrapper(AbstractWrapper):
rows=ztool.splitList(zobjects, cols)"> rows=ztool.splitList(zobjects, cols)">
<tr for="row in rows" valign="middle"> <tr for="row in rows" valign="middle">
<td for="zobj in row" width=":'%d%%' % (100/cols)" align="center" <td for="zobj in row" width=":'%d%%' % (100/cols)" align="center"
style="padding-top: 25px" var2="obj=zobj.appy()"> style="padding-top: 25px"
var2="obj=zobj.appy(); mayView=zobj.mayView()">
<x var="currentNumber=currentNumber + 1" <x var="currentNumber=currentNumber + 1"
for="column in columns" for="column in columns"
var2="field=column.field">:tool.pxQueryField</x> var2="field=column.field">:tool.pxQueryField</x>

View file

@ -454,6 +454,7 @@ class AbstractWrapper(object):
var="previousPage=phaseObj.getPreviousPage(page)[0]; var="previousPage=phaseObj.getPreviousPage(page)[0];
nextPage=phaseObj.getNextPage(page)[0]; nextPage=phaseObj.getNextPage(page)[0];
isEdit=layoutType == 'edit'; isEdit=layoutType == 'edit';
mayAct=not isEdit and zobj.mayAct();
pageInfo=phaseObj.pagesInfo[page]"> pageInfo=phaseObj.pagesInfo[page]">
<tr valign="top"> <tr valign="top">
<!-- Refresh --> <!-- Refresh -->
@ -486,7 +487,6 @@ class AbstractWrapper(object):
style=":'%s; %s' % (url('save', bg=True), \ style=":'%s; %s' % (url('save', bg=True), \
ztool.getButtonWidth(label))" /> ztool.getButtonWidth(label))" />
</td> </td>
<!-- Cancel --> <!-- Cancel -->
<td if="isEdit and pageInfo.showCancel"> <td if="isEdit and pageInfo.showCancel">
<input type="button" class="button" onClick="submitAppyForm('cancel')" <input type="button" class="button" onClick="submitAppyForm('cancel')"
@ -494,11 +494,10 @@ class AbstractWrapper(object):
style=":'%s; %s' % (url('cancel', bg=True), \ style=":'%s; %s' % (url('cancel', bg=True), \
ztool.getButtonWidth(label))"/> ztool.getButtonWidth(label))"/>
</td> </td>
<td if="not isEdit" <td if="not isEdit"
var2="locked=zobj.isLocked(user, page); var2="locked=zobj.isLocked(user, page);
editable=pageInfo.showOnEdit and pageInfo.showEdit and \ editable=pageInfo.showOnEdit and pageInfo.showEdit and \
zobj.mayEdit()"> mayAct and zobj.mayEdit()">
<!-- Edit --> <!-- Edit -->
<input type="button" class="button" if="editable and not locked" <input type="button" class="button" if="editable and not locked"
@ -540,7 +539,8 @@ class AbstractWrapper(object):
<!-- Workflow transitions --> <!-- Workflow transitions -->
<td var="targetObj=zobj; buttonsMode='normal'" <td var="targetObj=zobj; buttonsMode='normal'"
if="targetObj.showTransitions(layoutType)">:obj.pxTransitions</td> if="mayAct and \
targetObj.showTransitions(layoutType)">:obj.pxTransitions</td>
</tr> </tr>
</table>''') </table>''')
@ -554,7 +554,7 @@ class AbstractWrapper(object):
</table>''') </table>''')
pxView = Px(''' pxView = Px('''
<x var="x=zobj.allows('read', raiseError=True); <x var="x=zobj.mayView(raiseError=True);
errors=req.get('errors', {}); errors=req.get('errors', {});
layout=zobj.getPageLayout(layoutType); layout=zobj.getPageLayout(layoutType);
phaseObj=zobj.getAppyPhases(currentOnly=True, layoutType='view'); phaseObj=zobj.getAppyPhases(currentOnly=True, layoutType='view');
@ -570,7 +570,7 @@ class AbstractWrapper(object):
</x>''', template=pxTemplate, hook='content') </x>''', template=pxTemplate, hook='content')
pxEdit = Px(''' pxEdit = Px('''
<x var="x=zobj.allows('write', raiseError=True); <x var="x=zobj.mayEdit(raiseError=True, permOnly=zobj.isTemporary());
errors=req.get('errors', {}); errors=req.get('errors', {});
layout=zobj.getPageLayout(layoutType); layout=zobj.getPageLayout(layoutType);
cssJs={}; cssJs={};