Commit ae225038 authored by Martijn Pieters's avatar Martijn Pieters

- Backport a postonly decorator from Zope trunk's requestmethod decorator factory.

- Protect various security-setting-mutators with this decorator.
parent 47315997
...@@ -8,6 +8,10 @@ Zope Changes ...@@ -8,6 +8,10 @@ Zope Changes
Bugs fixed Bugs fixed
- Protected various security mutators with a new postonly decorator.
The decorator limits method publishing to POST requests only, and
is a backport from Zope 2.11's requestmethod decorator factory.
- Collector #2289: restored compatibility with PTProfiler - Collector #2289: restored compatibility with PTProfiler
- No longer opens a zodb connection every time a ProductDispatcher - No longer opens a zodb connection every time a ProductDispatcher
......
...@@ -22,6 +22,7 @@ from AccessControl import getSecurityManager, Unauthorized ...@@ -22,6 +22,7 @@ from AccessControl import getSecurityManager, Unauthorized
from AccessControl.Permissions import view_management_screens from AccessControl.Permissions import view_management_screens
from AccessControl.Permissions import take_ownership from AccessControl.Permissions import take_ownership
from Acquisition import aq_get, aq_parent, aq_base from Acquisition import aq_get, aq_parent, aq_base
from requestmethod import postonly
from zope.interface import implements from zope.interface import implements
from interfaces import IOwned from interfaces import IOwned
...@@ -177,6 +178,7 @@ class Owned(ExtensionClass.Base): ...@@ -177,6 +178,7 @@ class Owned(ExtensionClass.Base):
return security.checkPermission('Take ownership', self) return security.checkPermission('Take ownership', self)
security.declareProtected(take_ownership, 'manage_takeOwnership') security.declareProtected(take_ownership, 'manage_takeOwnership')
@postonly
def manage_takeOwnership(self, REQUEST, RESPONSE, recursive=0): def manage_takeOwnership(self, REQUEST, RESPONSE, recursive=0):
"""Take ownership (responsibility) for an object. """Take ownership (responsibility) for an object.
...@@ -197,6 +199,7 @@ class Owned(ExtensionClass.Base): ...@@ -197,6 +199,7 @@ class Owned(ExtensionClass.Base):
RESPONSE.redirect(REQUEST['HTTP_REFERER']) RESPONSE.redirect(REQUEST['HTTP_REFERER'])
security.declareProtected(take_ownership, 'manage_changeOwnershipType') security.declareProtected(take_ownership, 'manage_changeOwnershipType')
@postonly
def manage_changeOwnershipType(self, explicit=1, def manage_changeOwnershipType(self, explicit=1,
RESPONSE=None, REQUEST=None): RESPONSE=None, REQUEST=None):
"""Change the type (implicit or explicit) of ownership. """Change the type (implicit or explicit) of ownership.
......
...@@ -28,11 +28,14 @@ from zope.interface import implements ...@@ -28,11 +28,14 @@ from zope.interface import implements
from interfaces import IPermissionMappingSupport from interfaces import IPermissionMappingSupport
from Owned import UnownableOwner from Owned import UnownableOwner
from Permission import pname from Permission import pname
from requestmethod import postonly
class RoleManager: class RoleManager:
implements(IPermissionMappingSupport) implements(IPermissionMappingSupport)
# XXX: No security declarations?
def manage_getPermissionMapping(self): def manage_getPermissionMapping(self):
"""Return the permission mapping for the object """Return the permission mapping for the object
...@@ -58,6 +61,7 @@ class RoleManager: ...@@ -58,6 +61,7 @@ class RoleManager:
a({'permission_name': ac_perms[0], 'class_permission': p}) a({'permission_name': ac_perms[0], 'class_permission': p})
return r return r
@postonly
def manage_setPermissionMapping(self, def manage_setPermissionMapping(self,
permission_names=[], permission_names=[],
class_permissions=[], REQUEST=None): class_permissions=[], REQUEST=None):
......
...@@ -27,6 +27,7 @@ from zope.interface import implements ...@@ -27,6 +27,7 @@ from zope.interface import implements
from interfaces import IRoleManager from interfaces import IRoleManager
from Permission import Permission from Permission import Permission
from requestmethod import postonly
DEFAULTMAXLISTUSERS=250 DEFAULTMAXLISTUSERS=250
...@@ -128,6 +129,7 @@ class RoleManager(ExtensionClass.Base, PermissionMapping.RoleManager): ...@@ -128,6 +129,7 @@ class RoleManager(ExtensionClass.Base, PermissionMapping.RoleManager):
help_product='OFSP') help_product='OFSP')
security.declareProtected(change_permissions, 'manage_role') security.declareProtected(change_permissions, 'manage_role')
@postonly
def manage_role(self, role_to_manage, permissions=[], REQUEST=None): def manage_role(self, role_to_manage, permissions=[], REQUEST=None):
"""Change the permissions given to the given role. """Change the permissions given to the given role.
""" """
...@@ -146,6 +148,7 @@ class RoleManager(ExtensionClass.Base, PermissionMapping.RoleManager): ...@@ -146,6 +148,7 @@ class RoleManager(ExtensionClass.Base, PermissionMapping.RoleManager):
help_product='OFSP') help_product='OFSP')
security.declareProtected(change_permissions, 'manage_acquiredPermissions') security.declareProtected(change_permissions, 'manage_acquiredPermissions')
@postonly
def manage_acquiredPermissions(self, permissions=[], REQUEST=None): def manage_acquiredPermissions(self, permissions=[], REQUEST=None):
"""Change the permissions that acquire. """Change the permissions that acquire.
""" """
...@@ -167,6 +170,7 @@ class RoleManager(ExtensionClass.Base, PermissionMapping.RoleManager): ...@@ -167,6 +170,7 @@ class RoleManager(ExtensionClass.Base, PermissionMapping.RoleManager):
help_product='OFSP') help_product='OFSP')
security.declareProtected(change_permissions, 'manage_permission') security.declareProtected(change_permissions, 'manage_permission')
@postonly
def manage_permission(self, permission_to_manage, def manage_permission(self, permission_to_manage,
roles=[], acquire=0, REQUEST=None): roles=[], acquire=0, REQUEST=None):
"""Change the settings for the given permission. """Change the settings for the given permission.
...@@ -205,6 +209,7 @@ class RoleManager(ExtensionClass.Base, PermissionMapping.RoleManager): ...@@ -205,6 +209,7 @@ class RoleManager(ExtensionClass.Base, PermissionMapping.RoleManager):
return apply(self._normal_manage_access,(), kw) return apply(self._normal_manage_access,(), kw)
security.declareProtected(change_permissions, 'manage_changePermissions') security.declareProtected(change_permissions, 'manage_changePermissions')
@postonly
def manage_changePermissions(self, REQUEST): def manage_changePermissions(self, REQUEST):
"""Change all permissions settings, called by management screen. """Change all permissions settings, called by management screen.
""" """
...@@ -358,6 +363,7 @@ class RoleManager(ExtensionClass.Base, PermissionMapping.RoleManager): ...@@ -358,6 +363,7 @@ class RoleManager(ExtensionClass.Base, PermissionMapping.RoleManager):
return tuple(dict.get(userid, [])) return tuple(dict.get(userid, []))
security.declareProtected(change_permissions, 'manage_addLocalRoles') security.declareProtected(change_permissions, 'manage_addLocalRoles')
@postonly
def manage_addLocalRoles(self, userid, roles, REQUEST=None): def manage_addLocalRoles(self, userid, roles, REQUEST=None):
"""Set local roles for a user.""" """Set local roles for a user."""
if not roles: if not roles:
...@@ -376,6 +382,7 @@ class RoleManager(ExtensionClass.Base, PermissionMapping.RoleManager): ...@@ -376,6 +382,7 @@ class RoleManager(ExtensionClass.Base, PermissionMapping.RoleManager):
return self.manage_listLocalRoles(self, REQUEST, stat=stat) return self.manage_listLocalRoles(self, REQUEST, stat=stat)
security.declareProtected(change_permissions, 'manage_setLocalRoles') security.declareProtected(change_permissions, 'manage_setLocalRoles')
@postonly
def manage_setLocalRoles(self, userid, roles, REQUEST=None): def manage_setLocalRoles(self, userid, roles, REQUEST=None):
"""Set local roles for a user.""" """Set local roles for a user."""
if not roles: if not roles:
...@@ -390,6 +397,7 @@ class RoleManager(ExtensionClass.Base, PermissionMapping.RoleManager): ...@@ -390,6 +397,7 @@ class RoleManager(ExtensionClass.Base, PermissionMapping.RoleManager):
return self.manage_listLocalRoles(self, REQUEST, stat=stat) return self.manage_listLocalRoles(self, REQUEST, stat=stat)
security.declareProtected(change_permissions, 'manage_delLocalRoles') security.declareProtected(change_permissions, 'manage_delLocalRoles')
@postonly
def manage_delLocalRoles(self, userids, REQUEST=None): def manage_delLocalRoles(self, userids, REQUEST=None):
"""Remove all local roles for a user.""" """Remove all local roles for a user."""
dict=self.__ac_local_roles__ dict=self.__ac_local_roles__
...@@ -482,6 +490,7 @@ class RoleManager(ExtensionClass.Base, PermissionMapping.RoleManager): ...@@ -482,6 +490,7 @@ class RoleManager(ExtensionClass.Base, PermissionMapping.RoleManager):
return self.manage_access(REQUEST) return self.manage_access(REQUEST)
@postonly
def _addRole(self, role, REQUEST=None): def _addRole(self, role, REQUEST=None):
if not role: if not role:
return MessageDialog( return MessageDialog(
...@@ -499,6 +508,7 @@ class RoleManager(ExtensionClass.Base, PermissionMapping.RoleManager): ...@@ -499,6 +508,7 @@ class RoleManager(ExtensionClass.Base, PermissionMapping.RoleManager):
if REQUEST is not None: if REQUEST is not None:
return self.manage_access(REQUEST) return self.manage_access(REQUEST)
@postonly
def _delRoles(self, roles, REQUEST=None): def _delRoles(self, roles, REQUEST=None):
if not roles: if not roles:
return MessageDialog( return MessageDialog(
......
...@@ -33,6 +33,7 @@ from zope.interface import implements ...@@ -33,6 +33,7 @@ from zope.interface import implements
import AuthEncoding import AuthEncoding
import SpecialUsers import SpecialUsers
from interfaces import IStandardUserFolder from interfaces import IStandardUserFolder
from requestmethod import postonly
from PermissionRole import _what_not_even_god_should_do, rolesForPermissionOn from PermissionRole import _what_not_even_god_should_do, rolesForPermissionOn
from Role import RoleManager, DEFAULTMAXLISTUSERS from Role import RoleManager, DEFAULTMAXLISTUSERS
from SecurityManagement import getSecurityManager from SecurityManagement import getSecurityManager
...@@ -534,7 +535,9 @@ class BasicUserFolder(Implicit, Persistent, Navigation, Tabs, RoleManager, ...@@ -534,7 +535,9 @@ class BasicUserFolder(Implicit, Persistent, Navigation, Tabs, RoleManager,
# user folder subclasses already implement. # user folder subclasses already implement.
security.declareProtected(ManageUsers, 'userFolderAddUser') security.declareProtected(ManageUsers, 'userFolderAddUser')
def userFolderAddUser(self, name, password, roles, domains, **kw): @postonly
def userFolderAddUser(self, name, password, roles, domains,
REQUEST=None, **kw):
"""API method for creating a new user object. Note that not all """API method for creating a new user object. Note that not all
user folder implementations support dynamic creation of user user folder implementations support dynamic creation of user
objects.""" objects."""
...@@ -543,7 +546,9 @@ class BasicUserFolder(Implicit, Persistent, Navigation, Tabs, RoleManager, ...@@ -543,7 +546,9 @@ class BasicUserFolder(Implicit, Persistent, Navigation, Tabs, RoleManager,
raise NotImplementedError raise NotImplementedError
security.declareProtected(ManageUsers, 'userFolderEditUser') security.declareProtected(ManageUsers, 'userFolderEditUser')
def userFolderEditUser(self, name, password, roles, domains, **kw): @postonly
def userFolderEditUser(self, name, password, roles, domains,
REQUEST=None, **kw):
"""API method for changing user object attributes. Note that not """API method for changing user object attributes. Note that not
all user folder implementations support changing of user object all user folder implementations support changing of user object
attributes.""" attributes."""
...@@ -552,7 +557,8 @@ class BasicUserFolder(Implicit, Persistent, Navigation, Tabs, RoleManager, ...@@ -552,7 +557,8 @@ class BasicUserFolder(Implicit, Persistent, Navigation, Tabs, RoleManager,
raise NotImplementedError raise NotImplementedError
security.declareProtected(ManageUsers, 'userFolderDelUsers') security.declareProtected(ManageUsers, 'userFolderDelUsers')
def userFolderDelUsers(self, names): @postonly
def userFolderDelUsers(self, names, REQUEST=None):
"""API method for deleting one or more user objects. Note that not """API method for deleting one or more user objects. Note that not
all user folder implementations support deletion of user objects.""" all user folder implementations support deletion of user objects."""
if hasattr(self, '_doDelUsers'): if hasattr(self, '_doDelUsers'):
...@@ -794,6 +800,7 @@ class BasicUserFolder(Implicit, Persistent, Navigation, Tabs, RoleManager, ...@@ -794,6 +800,7 @@ class BasicUserFolder(Implicit, Persistent, Navigation, Tabs, RoleManager,
self, REQUEST, manage_tabs_message=manage_tabs_message, self, REQUEST, manage_tabs_message=manage_tabs_message,
management_view='Properties') management_view='Properties')
@postonly
def manage_setUserFolderProperties(self, encrypt_passwords=0, def manage_setUserFolderProperties(self, encrypt_passwords=0,
update_passwords=0, update_passwords=0,
maxlistusers=DEFAULTMAXLISTUSERS, maxlistusers=DEFAULTMAXLISTUSERS,
...@@ -848,7 +855,7 @@ class BasicUserFolder(Implicit, Persistent, Navigation, Tabs, RoleManager, ...@@ -848,7 +855,7 @@ class BasicUserFolder(Implicit, Persistent, Navigation, Tabs, RoleManager,
return 1 return 1
@postonly
def _addUser(self,name,password,confirm,roles,domains,REQUEST=None): def _addUser(self,name,password,confirm,roles,domains,REQUEST=None):
if not name: if not name:
return MessageDialog( return MessageDialog(
...@@ -884,7 +891,7 @@ class BasicUserFolder(Implicit, Persistent, Navigation, Tabs, RoleManager, ...@@ -884,7 +891,7 @@ class BasicUserFolder(Implicit, Persistent, Navigation, Tabs, RoleManager,
self._doAddUser(name, password, roles, domains) self._doAddUser(name, password, roles, domains)
if REQUEST: return self._mainUser(self, REQUEST) if REQUEST: return self._mainUser(self, REQUEST)
@postonly
def _changeUser(self,name,password,confirm,roles,domains,REQUEST=None): def _changeUser(self,name,password,confirm,roles,domains,REQUEST=None):
if password == 'password' and confirm == 'pconfirm': if password == 'password' and confirm == 'pconfirm':
# Protocol for editUser.dtml to indicate unchanged password # Protocol for editUser.dtml to indicate unchanged password
...@@ -922,6 +929,7 @@ class BasicUserFolder(Implicit, Persistent, Navigation, Tabs, RoleManager, ...@@ -922,6 +929,7 @@ class BasicUserFolder(Implicit, Persistent, Navigation, Tabs, RoleManager,
self._doChangeUser(name, password, roles, domains) self._doChangeUser(name, password, roles, domains)
if REQUEST: return self._mainUser(self, REQUEST) if REQUEST: return self._mainUser(self, REQUEST)
@postonly
def _delUsers(self,names,REQUEST=None): def _delUsers(self,names,REQUEST=None):
if not names: if not names:
return MessageDialog( return MessageDialog(
......
#############################################################################
#
# Copyright (c) 2007 Zope Corporation and Contributors. All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE
#
##############################################################################
import inspect
from zExceptions import Forbidden
from ZPublisher.HTTPRequest import HTTPRequest
def _buildFacade(spec, docstring):
"""Build a facade function, matching the decorated method in signature.
Note that defaults are replaced by None, and _curried will reconstruct
these to preserve mutable defaults.
"""
args = inspect.formatargspec(formatvalue=lambda v: '=None', *spec)
callargs = inspect.formatargspec(formatvalue=lambda v: '', *spec)
return 'def _facade%s:\n """%s"""\n return _curried%s' % (
args, docstring, callargs)
def postonly(callable):
"""Only allow callable when request method is POST."""
spec = inspect.getargspec(callable)
args, defaults = spec[0], spec[3]
try:
r_index = args.index('REQUEST')
except ValueError:
raise ValueError('No REQUEST parameter in callable signature')
arglen = len(args)
if defaults is not None:
defaults = zip(args[arglen - len(defaults):], defaults)
arglen -= len(defaults)
def _curried(*args, **kw):
request = None
if len(args) > r_index:
request = args[r_index]
if isinstance(request, HTTPRequest):
if request.get('REQUEST_METHOD', 'GET').upper() != 'POST':
raise Forbidden('Request must be POST')
# Reconstruct keyword arguments
if defaults is not None:
args, kwparams = args[:arglen], args[arglen:]
for positional, (key, default) in zip(kwparams, defaults):
if positional is None:
kw[key] = default
else:
kw[key] = positional
return callable(*args, **kw)
# Build a facade, with a reference to our locally-scoped _curried
facade_globs = dict(_curried=_curried)
exec _buildFacade(spec, callable.__doc__) in facade_globs
return facade_globs['_facade']
__all__ = ('postonly',)
Request method decorators
=========================
.. Note::
This is a partial backport from Zope 2.11's new request method
decorators, condensed into a postonly decorator.
Using request method decorators, you can limit functions or methods to only
be callable when the HTTP request was made using a particular method.
To limit access to a function or method to POST requests, use the postonly
decorator::
>>> from AccessControl.requestmethod import *
>>> @postonly
... def foo(bar, REQUEST):
... return bar
When this method is accessed through a request that does not use POST, the
Forbidden exception will be raised::
>>> foo('spam', GET)
Traceback (most recent call last):
...
Forbidden: Request must be POST
Only when the request was made using POST, will the call succeed::
>>> foo('spam', POST)
'spam'
It doesn't matter if REQUEST is a positional or a keyword parameter::
>>> @postonly
... def foo(bar, REQUEST=None):
... return bar
>>> foo('spam', REQUEST=GET)
Traceback (most recent call last):
...
Forbidden: Request must be POST
*Not* passing an optional REQUEST always succeeds::
>>> foo('spam')
'spam'
Note that the REQUEST parameter is a requirement for the decorator to operate,
not including it in the callable signature results in an error::
>>> @postonly
... def foo(bar):
... return bar
Traceback (most recent call last):
...
ValueError: No REQUEST parameter in callable signature
Because the Zope Publisher uses introspection to match REQUEST variables
against callable signatures, the result of the decorator must match the
original closely, and keyword parameter defaults must be preserved::
>>> import inspect
>>> mutabledefault = dict()
>>> @postonly
... def foo(bar, baz=mutabledefault, REQUEST=None, **kw):
... return bar, baz is mutabledefault, REQUEST
>>> inspect.getargspec(foo)[:3]
(['bar', 'baz', 'REQUEST'], None, 'kw')
>>> foo('spam')
('spam', True, None)
#############################################################################
#
# Copyright (c) 2007 Zope Corporation and Contributors. All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE
#
##############################################################################
from ZPublisher.HTTPRequest import HTTPRequest
def makerequest(method):
environ = dict(SERVER_NAME='foo', SERVER_PORT='80', REQUEST_METHOD=method)
return HTTPRequest(None, environ, None)
def test_suite():
from doctest import DocFileSuite
return DocFileSuite('../requestmethod.txt',
globs=dict(GET=makerequest('GET'),
POST=makerequest('POST')))
if __name__ == '__main__':
import unittest
unittest.main(defaultTest='test_suite')
...@@ -36,6 +36,7 @@ from AccessControl.Permissions import change_proxy_roles ...@@ -36,6 +36,7 @@ from AccessControl.Permissions import change_proxy_roles
from AccessControl.Permissions import view as View from AccessControl.Permissions import view as View
from AccessControl.Permissions import ftp_access from AccessControl.Permissions import ftp_access
from AccessControl.DTML import RestrictedDTML from AccessControl.DTML import RestrictedDTML
from AccessControl.requestmethod import postonly
from Cache import Cacheable from Cache import Cacheable
from zExceptions import Forbidden from zExceptions import Forbidden
from zExceptions.TracebackSupplement import PathTracebackSupplement from zExceptions.TracebackSupplement import PathTracebackSupplement
...@@ -327,6 +328,7 @@ class DTMLMethod(RestrictedDTML, HTML, Acquisition.Implicit, RoleManager, ...@@ -327,6 +328,7 @@ class DTMLMethod(RestrictedDTML, HTML, Acquisition.Implicit, RoleManager,
security.declareProtected(change_proxy_roles, 'manage_proxy') security.declareProtected(change_proxy_roles, 'manage_proxy')
@postonly
def manage_proxy(self, roles=(), REQUEST=None): def manage_proxy(self, roles=(), REQUEST=None):
"Change Proxy Roles" "Change Proxy Roles"
self._validateProxy(REQUEST, roles) self._validateProxy(REQUEST, roles)
......
...@@ -34,6 +34,7 @@ from AccessControl import getSecurityManager ...@@ -34,6 +34,7 @@ from AccessControl import getSecurityManager
from OFS.History import Historical, html_diff from OFS.History import Historical, html_diff
from OFS.Cache import Cacheable from OFS.Cache import Cacheable
from AccessControl.ZopeGuards import get_safe_globals, guarded_getattr from AccessControl.ZopeGuards import get_safe_globals, guarded_getattr
from AccessControl.requestmethod import postonly
from zExceptions import Forbidden from zExceptions import Forbidden
import Globals import Globals
...@@ -360,6 +361,7 @@ class PythonScript(Script, Historical, Cacheable): ...@@ -360,6 +361,7 @@ class PythonScript(Script, Historical, Cacheable):
'manage_proxyForm', 'manage_proxy') 'manage_proxyForm', 'manage_proxy')
manage_proxyForm = DTMLFile('www/pyScriptProxy', globals()) manage_proxyForm = DTMLFile('www/pyScriptProxy', globals())
@postonly
def manage_proxy(self, roles=(), REQUEST=None): def manage_proxy(self, roles=(), REQUEST=None):
"Change Proxy Roles" "Change Proxy Roles"
self._validateProxy(roles) self._validateProxy(roles)
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment