[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
field?'''
# Check if the user has the permission to view or edit the field
if layoutType == 'edit': perm = self.writePermission
else: perm = self.readPermission
if not obj.allows(perm): return False
perm = (layoutType == 'edit') and self.writePermission or \
self.readPermission
if not obj.allows(perm): return
# Evaluate self.show
if callable(self.show):
res = self.callMethod(obj, self.show)
@ -319,7 +319,7 @@ class Field:
if type(res) in sutils.sequenceTypes:
for r in res:
if r == layoutType: return True
return False
return
elif res in ('view', 'edit', 'result'):
return res == layoutType
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,
# via Ajax, when the user selects another month.
pxMonthView = Px('''
<div var="ajaxHookId=zobj.UID() + field.name;
<div var="ajaxHookId=zobj.id + field.name;
month=req['month'];
monthDayOne=DateTime('%s/01' % month);
today=DateTime('00:00');
@ -27,7 +27,7 @@ class Calendar(Field):
defaultDateMonth=defaultDate.strftime('%Y/%m');
previousMonth=field.getSiblingMonth(month, 'previous');
nextMonth=field.getSiblingMonth(month, 'next');
mayEdit=zobj.allows(field.writePermission);
mayEdit=zobj.mayEdit(field.writePermission);
objUrl=zobj.absolute_url();
startDate=field.getStartDate(zobj);
endDate=field.getEndDate(zobj);
@ -630,6 +630,8 @@ class Calendar(Field):
or deletion of a calendar event.'''
rq = obj.REQUEST
action = rq['actionType']
# Security check
obj.mayEdit(self.writePermission, raiseError=True)
# Get the date for this action
if action == 'createEvent':
return self.createEvent(obj, DateTime(rq['day']))

View file

@ -468,7 +468,7 @@ class Pod(Field):
# What is the action to perform?
action = rq.get('action', 'generate')
# Security check.
obj.o.allows('read', raiseError=True)
obj.o.mayView(self.readPermission, raiseError=True)
# Perform the requested action.
tool = obj.tool.o
template = rq.get('template')
@ -486,7 +486,7 @@ class Pod(Field):
res.writeResponse(rq.RESPONSE)
return
# 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'
if action == 'freeze':
# (Re-)freeze a document in the database.

View file

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

View file

@ -397,7 +397,7 @@ class Transition:
if not obj.isTemporary(): obj.reindex()
# If we are viewing the object and if the logged user looses the
# 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']):
back = tool.getHomePage()
else:

View file

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

View file

@ -104,7 +104,7 @@ td.search { padding-top: 8px }
.dropdown a:hover { text-decoration: underline }
.list { margin-bottom: 3px }
.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 }
.grid th { font-style: italic; font-weight: normal;
border-bottom: 5px solid #fdfdfd; padding: 3px 5px 0 5px }

View file

@ -302,43 +302,49 @@ class ToolWrapper(AbstractWrapper):
# Show on query list or grid, the field content for a given object.
pxQueryField = Px('''
<!-- Title -->
<x if="field.name == 'title'"
var2="navInfo='search.%s.%s.%d.%d' % \
<x if="field.name == 'title'">
<x if="mayView"
var2="navInfo='search.%s.%s.%d.%d' % \
(className, searchName, startNumber+currentNumber, totalNumber);
cssClass=zobj.getCssFor('title')">
<x>::zobj.getSupTitle(navInfo)</x>
<a href=":zobj.getUrl(nav=navInfo, page=zobj.getDefaultViewPage())"
if="enableLinks" class=":cssClass">:zobj.Title()</a><span
if="not enableLinks" class=":cssClass">:zobj.Title()</span><span
style=":showSubTitles and 'display:inline' or 'display:none'"
name="subTitle">::zobj.getSubTitle()</span>
cssClass=zobj.getCssFor('title')">
<x>::zobj.getSupTitle(navInfo)</x>
<a href=":zobj.getUrl(nav=navInfo, page=zobj.getDefaultViewPage())"
if="enableLinks" class=":cssClass">:zobj.Title()</a><span
if="not enableLinks" class=":cssClass">:zobj.Title()</span><span
style=":showSubTitles and 'display:inline' or 'display:none'"
name="subTitle">::zobj.getSubTitle()</span>
<!-- Actions -->
<table class="noStyle" if="zobj.mayAct()">
<tr>
<!-- Edit -->
<td if="zobj.mayEdit()">
<a var="navInfo='search.%s.%s.%d.%d' % \
<!-- Actions -->
<table class="noStyle" if="zobj.mayAct()">
<tr>
<!-- Edit -->
<td if="zobj.mayEdit()">
<a var="navInfo='search.%s.%s.%d.%d' % \
(className, searchName, loop.zobj.nb+1+startNumber, totalNumber)"
href=":zobj.getUrl(mode='edit', page=zobj.getDefaultEditPage(), \
nav=navInfo)">
<img src=":url('edit')" title=":_('object_edit')"/></a>
</td>
<td>
<!-- Delete -->
<img if="zobj.mayDelete()" class="clickable" src=":url('delete')"
title=":_('object_delete')"
onClick=":'onDeleteObject(%s)' % q(zobj.UID())"/>
</td>
<!-- Workflow transitions -->
<td if="zobj.showTransitions('result')"
var2="targetObj=zobj;
buttonsMode='small'">:targetObj.appy().pxTransitions</td>
</tr>
</table>
href=":zobj.getUrl(mode='edit', page=zobj.getDefaultEditPage(), \
nav=navInfo)">
<img src=":url('edit')" title=":_('object_edit')"/></a>
</td>
<td>
<!-- Delete -->
<img if="zobj.mayDelete()" class="clickable" src=":url('delete')"
title=":_('object_delete')"
onClick=":'onDeleteObject(%s)' % q(zobj.id)"/>
</td>
<!-- Workflow transitions -->
<td if="zobj.showTransitions('result')"
var2="targetObj=zobj;
buttonsMode='small'">:targetObj.appy().pxTransitions</td>
</tr>
</table>
</x>
<x if="not mayView">
<img src=":url('fake')" style="margin-right: 5px"/>
<x>:_('unauthorized')</x>
</x>
</x>
<!-- Any other field -->
<x if="field.name != 'title'">
<x if="(field.name != 'title') and mayView">
<x var="layoutType='cell'; innerRef=True"
if="field.isShowable(zobj, 'result')">:field.pxRender</x>
</x>''')
@ -361,7 +367,7 @@ class ToolWrapper(AbstractWrapper):
<!-- Results -->
<tr for="zobj in zobjects" id="query_row" valign="top"
var2="currentNumber=currentNumber + 1;
obj=zobj.appy()"
obj=zobj.appy(); mayView=zobj.mayView()"
class=":loop.zobj.odd and 'even' or 'odd'">
<td for="column in columns"
var2="field=column.field" id=":'field_%s' % field.name"
@ -378,7 +384,8 @@ class ToolWrapper(AbstractWrapper):
rows=ztool.splitList(zobjects, cols)">
<tr for="row in rows" valign="middle">
<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"
for="column in columns"
var2="field=column.field">:tool.pxQueryField</x>

View file

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