Commit 032ee39c 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 5e67108b
...@@ -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 #2288: @ and + should not be quoted when forming - Collector #2288: @ and + should not be quoted when forming
request URLs in BaseRequest and HTTPRequest request URLs in BaseRequest and HTTPRequest
......
...@@ -18,6 +18,7 @@ $Id$ ...@@ -18,6 +18,7 @@ $Id$
import Globals, urlparse, SpecialUsers, ExtensionClass import Globals, urlparse, SpecialUsers, ExtensionClass
from AccessControl import getSecurityManager, Unauthorized from AccessControl import getSecurityManager, Unauthorized
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
......
...@@ -28,12 +28,15 @@ from zope.interface import implements ...@@ -28,12 +28,15 @@ 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):
......
...@@ -24,6 +24,7 @@ from zope.interface import implements ...@@ -24,6 +24,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
...@@ -135,6 +136,7 @@ class RoleManager(ExtensionClass.Base, PermissionMapping.RoleManager): ...@@ -135,6 +136,7 @@ class RoleManager(ExtensionClass.Base, PermissionMapping.RoleManager):
help_topic='Security_Manage-Role.stx', help_topic='Security_Manage-Role.stx',
help_product='OFSP') help_product='OFSP')
@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.
""" """
...@@ -151,6 +153,7 @@ class RoleManager(ExtensionClass.Base, PermissionMapping.RoleManager): ...@@ -151,6 +153,7 @@ class RoleManager(ExtensionClass.Base, PermissionMapping.RoleManager):
help_topic='Security_Manage-Acquisition.stx', help_topic='Security_Manage-Acquisition.stx',
help_product='OFSP') help_product='OFSP')
@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.
""" """
...@@ -170,6 +173,7 @@ class RoleManager(ExtensionClass.Base, PermissionMapping.RoleManager): ...@@ -170,6 +173,7 @@ class RoleManager(ExtensionClass.Base, PermissionMapping.RoleManager):
help_topic='Security_Manage-Permission.stx', help_topic='Security_Manage-Permission.stx',
help_product='OFSP') help_product='OFSP')
@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.
...@@ -206,6 +210,7 @@ class RoleManager(ExtensionClass.Base, PermissionMapping.RoleManager): ...@@ -206,6 +210,7 @@ class RoleManager(ExtensionClass.Base, PermissionMapping.RoleManager):
else: else:
return apply(self._normal_manage_access,(), kw) return apply(self._normal_manage_access,(), kw)
@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.
""" """
...@@ -353,6 +358,7 @@ class RoleManager(ExtensionClass.Base, PermissionMapping.RoleManager): ...@@ -353,6 +358,7 @@ class RoleManager(ExtensionClass.Base, PermissionMapping.RoleManager):
dict=self.__ac_local_roles__ or {} dict=self.__ac_local_roles__ or {}
return tuple(dict.get(userid, [])) return tuple(dict.get(userid, []))
@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:
...@@ -370,6 +376,7 @@ class RoleManager(ExtensionClass.Base, PermissionMapping.RoleManager): ...@@ -370,6 +376,7 @@ class RoleManager(ExtensionClass.Base, PermissionMapping.RoleManager):
stat='Your changes have been saved.' stat='Your changes have been saved.'
return self.manage_listLocalRoles(self, REQUEST, stat=stat) return self.manage_listLocalRoles(self, REQUEST, stat=stat)
@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:
...@@ -383,6 +390,7 @@ class RoleManager(ExtensionClass.Base, PermissionMapping.RoleManager): ...@@ -383,6 +390,7 @@ class RoleManager(ExtensionClass.Base, PermissionMapping.RoleManager):
stat='Your changes have been saved.' stat='Your changes have been saved.'
return self.manage_listLocalRoles(self, REQUEST, stat=stat) return self.manage_listLocalRoles(self, REQUEST, stat=stat)
@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__
...@@ -473,6 +481,7 @@ class RoleManager(ExtensionClass.Base, PermissionMapping.RoleManager): ...@@ -473,6 +481,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(
...@@ -490,6 +499,7 @@ class RoleManager(ExtensionClass.Base, PermissionMapping.RoleManager): ...@@ -490,6 +499,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(
......
...@@ -31,6 +31,7 @@ from zope.interface import implements ...@@ -31,6 +31,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,
# Authors of custom user folders don't need to do anything special to # Authors of custom user folders don't need to do anything special to
# support these - they will just call the appropriate '_' methods that # support these - they will just call the appropriate '_' methods that
# user folder subclasses already implement. # user folder subclasses already implement.
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."""
...@@ -542,7 +545,9 @@ class BasicUserFolder(Implicit, Persistent, Navigation, Tabs, RoleManager, ...@@ -542,7 +545,9 @@ class BasicUserFolder(Implicit, Persistent, Navigation, Tabs, RoleManager,
return self._doAddUser(name, password, roles, domains, **kw) return self._doAddUser(name, password, roles, domains, **kw)
raise NotImplementedError raise NotImplementedError
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."""
...@@ -550,7 +555,8 @@ class BasicUserFolder(Implicit, Persistent, Navigation, Tabs, RoleManager, ...@@ -550,7 +555,8 @@ class BasicUserFolder(Implicit, Persistent, Navigation, Tabs, RoleManager,
return self._doChangeUser(name, password, roles, domains, **kw) return self._doChangeUser(name, password, roles, domains, **kw)
raise NotImplementedError raise NotImplementedError
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'):
...@@ -792,6 +798,7 @@ class BasicUserFolder(Implicit, Persistent, Navigation, Tabs, RoleManager, ...@@ -792,6 +798,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,
...@@ -846,7 +853,7 @@ class BasicUserFolder(Implicit, Persistent, Navigation, Tabs, RoleManager, ...@@ -846,7 +853,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(
...@@ -882,7 +889,7 @@ class BasicUserFolder(Implicit, Persistent, Navigation, Tabs, RoleManager, ...@@ -882,7 +889,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
...@@ -920,6 +927,7 @@ class BasicUserFolder(Implicit, Persistent, Navigation, Tabs, RoleManager, ...@@ -920,6 +927,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')
...@@ -29,6 +29,7 @@ from urllib import quote ...@@ -29,6 +29,7 @@ from urllib import quote
import Globals, sys, Acquisition import Globals, sys, Acquisition
from AccessControl import getSecurityManager from AccessControl import getSecurityManager
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
...@@ -315,6 +316,7 @@ class DTMLMethod(RestrictedDTML, HTML, Acquisition.Implicit, RoleManager, ...@@ -315,6 +316,7 @@ class DTMLMethod(RestrictedDTML, HTML, Acquisition.Implicit, RoleManager,
'do not have proxy roles.\n<!--%s, %s-->' % (self.__name__, u, roles)) 'do not have proxy roles.\n<!--%s, %s-->' % (self.__name__, u, roles))
@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
...@@ -359,6 +360,7 @@ class PythonScript(Script, Historical, Cacheable): ...@@ -359,6 +360,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