Log message for revision 73386: - Add a request method decorator to AccessControl, creating decorators that limit a method to one request method only. - Protect various security-setting-mutators with a POST-only decorator.
Changed: U Zope/trunk/doc/CHANGES.txt U Zope/trunk/lib/python/AccessControl/Owned.py U Zope/trunk/lib/python/AccessControl/PermissionMapping.py U Zope/trunk/lib/python/AccessControl/Role.py U Zope/trunk/lib/python/AccessControl/User.py A Zope/trunk/lib/python/AccessControl/requestmethod.py A Zope/trunk/lib/python/AccessControl/requestmethod.txt A Zope/trunk/lib/python/AccessControl/tests/test_requestmethod.py U Zope/trunk/lib/python/OFS/DTMLMethod.py U Zope/trunk/lib/python/Products/PythonScripts/PythonScript.py -=- Modified: Zope/trunk/doc/CHANGES.txt =================================================================== --- Zope/trunk/doc/CHANGES.txt 2007-03-20 08:07:42 UTC (rev 73385) +++ Zope/trunk/doc/CHANGES.txt 2007-03-20 08:50:24 UTC (rev 73386) @@ -51,6 +51,12 @@ Features added + - A new module, AccessControl.requestmethod, provides a decorator + factory that limits decorated methods to one request method only. + For example, marking a method with @requestmethod('POST') limits + that method to POST requests only when published. Several + security-related methods have been limited to POST only. + - PythonScripts: allow usage of Python's 'sets' module - added 'fast_listen' directive to http-server and webdav-source-server Modified: Zope/trunk/lib/python/AccessControl/Owned.py =================================================================== --- Zope/trunk/lib/python/AccessControl/Owned.py 2007-03-20 08:07:42 UTC (rev 73385) +++ Zope/trunk/lib/python/AccessControl/Owned.py 2007-03-20 08:50:24 UTC (rev 73386) @@ -22,6 +22,7 @@ from AccessControl.Permissions import view_management_screens from AccessControl.Permissions import take_ownership from Acquisition import aq_get, aq_parent, aq_base +from requestmethod import requestmethod from zope.interface import implements from interfaces import IOwned @@ -177,6 +178,7 @@ return security.checkPermission('Take ownership', self) security.declareProtected(take_ownership, 'manage_takeOwnership') + @requestmethod('POST') def manage_takeOwnership(self, REQUEST, RESPONSE, recursive=0): """Take ownership (responsibility) for an object. @@ -197,6 +199,7 @@ RESPONSE.redirect(REQUEST['HTTP_REFERER']) security.declareProtected(take_ownership, 'manage_changeOwnershipType') + @requestmethod('POST') def manage_changeOwnershipType(self, explicit=1, RESPONSE=None, REQUEST=None): """Change the type (implicit or explicit) of ownership. Modified: Zope/trunk/lib/python/AccessControl/PermissionMapping.py =================================================================== --- Zope/trunk/lib/python/AccessControl/PermissionMapping.py 2007-03-20 08:07:42 UTC (rev 73385) +++ Zope/trunk/lib/python/AccessControl/PermissionMapping.py 2007-03-20 08:50:24 UTC (rev 73386) @@ -28,11 +28,14 @@ from interfaces import IPermissionMappingSupport from Owned import UnownableOwner from Permission import pname +from requestmethod import requestmethod class RoleManager: implements(IPermissionMappingSupport) + + # XXX: No security declarations? def manage_getPermissionMapping(self): """Return the permission mapping for the object @@ -58,6 +61,7 @@ a({'permission_name': ac_perms[0], 'class_permission': p}) return r + @requestmethod('POST') def manage_setPermissionMapping(self, permission_names=[], class_permissions=[], REQUEST=None): Modified: Zope/trunk/lib/python/AccessControl/Role.py =================================================================== --- Zope/trunk/lib/python/AccessControl/Role.py 2007-03-20 08:07:42 UTC (rev 73385) +++ Zope/trunk/lib/python/AccessControl/Role.py 2007-03-20 08:50:24 UTC (rev 73386) @@ -28,6 +28,7 @@ from interfaces import IRoleManager from Permission import Permission +from requestmethod import requestmethod DEFAULTMAXLISTUSERS=250 @@ -129,6 +130,7 @@ help_product='OFSP') security.declareProtected(change_permissions, 'manage_role') + @requestmethod('POST') def manage_role(self, role_to_manage, permissions=[], REQUEST=None): """Change the permissions given to the given role. """ @@ -147,6 +149,7 @@ help_product='OFSP') security.declareProtected(change_permissions, 'manage_acquiredPermissions') + @requestmethod('POST') def manage_acquiredPermissions(self, permissions=[], REQUEST=None): """Change the permissions that acquire. """ @@ -228,6 +231,7 @@ help_product='OFSP') security.declareProtected(change_permissions, 'manage_permission') + @requestmethod('POST') def manage_permission(self, permission_to_manage, roles=[], acquire=0, REQUEST=None): """Change the settings for the given permission. @@ -267,6 +271,7 @@ return apply(self._normal_manage_access,(), kw) security.declareProtected(change_permissions, 'manage_changePermissions') + @requestmethod('POST') def manage_changePermissions(self, REQUEST): """Change all permissions settings, called by management screen. """ @@ -420,6 +425,7 @@ return tuple(dict.get(userid, [])) security.declareProtected(change_permissions, 'manage_addLocalRoles') + @requestmethod('POST') def manage_addLocalRoles(self, userid, roles, REQUEST=None): """Set local roles for a user.""" if not roles: @@ -438,6 +444,7 @@ return self.manage_listLocalRoles(self, REQUEST, stat=stat) security.declareProtected(change_permissions, 'manage_setLocalRoles') + @requestmethod('POST') def manage_setLocalRoles(self, userid, roles, REQUEST=None): """Set local roles for a user.""" if not roles: @@ -452,6 +459,7 @@ return self.manage_listLocalRoles(self, REQUEST, stat=stat) security.declareProtected(change_permissions, 'manage_delLocalRoles') + @requestmethod('POST') def manage_delLocalRoles(self, userids, REQUEST=None): """Remove all local roles for a user.""" dict=self.__ac_local_roles__ @@ -544,6 +552,7 @@ return self.manage_access(REQUEST) + @requestmethod('POST') def _addRole(self, role, REQUEST=None): if not role: return MessageDialog( @@ -561,6 +570,7 @@ if REQUEST is not None: return self.manage_access(REQUEST) + @requestmethod('POST') def _delRoles(self, roles, REQUEST=None): if not roles: return MessageDialog( Modified: Zope/trunk/lib/python/AccessControl/User.py =================================================================== --- Zope/trunk/lib/python/AccessControl/User.py 2007-03-20 08:07:42 UTC (rev 73385) +++ Zope/trunk/lib/python/AccessControl/User.py 2007-03-20 08:50:24 UTC (rev 73386) @@ -33,6 +33,7 @@ import AuthEncoding import SpecialUsers from interfaces import IStandardUserFolder +from requestmethod import requestmethod from PermissionRole import _what_not_even_god_should_do, rolesForPermissionOn from Role import RoleManager, DEFAULTMAXLISTUSERS from SecurityManagement import getSecurityManager @@ -534,7 +535,9 @@ # user folder subclasses already implement. security.declareProtected(ManageUsers, 'userFolderAddUser') - def userFolderAddUser(self, name, password, roles, domains, **kw): + @requestmethod('POST') + def userFolderAddUser(self, name, password, roles, domains, + REQUEST=None, **kw): """API method for creating a new user object. Note that not all user folder implementations support dynamic creation of user objects.""" @@ -543,7 +546,9 @@ raise NotImplementedError security.declareProtected(ManageUsers, 'userFolderEditUser') - def userFolderEditUser(self, name, password, roles, domains, **kw): + @requestmethod('POST') + def userFolderEditUser(self, name, password, roles, domains, + REQUEST=None, **kw): """API method for changing user object attributes. Note that not all user folder implementations support changing of user object attributes.""" @@ -552,7 +557,8 @@ raise NotImplementedError security.declareProtected(ManageUsers, 'userFolderDelUsers') - def userFolderDelUsers(self, names): + @requestmethod('POST') + def userFolderDelUsers(self, names, REQUEST=None): """API method for deleting one or more user objects. Note that not all user folder implementations support deletion of user objects.""" if hasattr(self, '_doDelUsers'): @@ -794,6 +800,7 @@ self, REQUEST, manage_tabs_message=manage_tabs_message, management_view='Properties') + @requestmethod('POST') def manage_setUserFolderProperties(self, encrypt_passwords=0, update_passwords=0, maxlistusers=DEFAULTMAXLISTUSERS, @@ -848,7 +855,7 @@ return 1 - + @requestmethod('POST') def _addUser(self,name,password,confirm,roles,domains,REQUEST=None): if not name: return MessageDialog( @@ -884,7 +891,7 @@ self._doAddUser(name, password, roles, domains) if REQUEST: return self._mainUser(self, REQUEST) - + @requestmethod('POST') def _changeUser(self,name,password,confirm,roles,domains,REQUEST=None): if password == 'password' and confirm == 'pconfirm': # Protocol for editUser.dtml to indicate unchanged password @@ -922,6 +929,7 @@ self._doChangeUser(name, password, roles, domains) if REQUEST: return self._mainUser(self, REQUEST) + @requestmethod('POST') def _delUsers(self,names,REQUEST=None): if not names: return MessageDialog( Added: Zope/trunk/lib/python/AccessControl/requestmethod.py =================================================================== --- Zope/trunk/lib/python/AccessControl/requestmethod.py 2007-03-20 08:07:42 UTC (rev 73385) +++ Zope/trunk/lib/python/AccessControl/requestmethod.py 2007-03-20 08:50:24 UTC (rev 73386) @@ -0,0 +1,75 @@ +############################################################################# +# +# 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 zope.publisher.interfaces.browser import IBrowserRequest + +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 requestmethod(method): + """Create a request method specific decorator""" + method = method.upper() + + def _methodtest(callable): + """Only allow callable when request method is %s.""" % method + 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 IBrowserRequest.providedBy(request): + if request.method != method: + raise Forbidden('Request must be %s' % method) + + # 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'] + + return _methodtest + +__all__ = ('requestmethod',) Added: Zope/trunk/lib/python/AccessControl/requestmethod.txt =================================================================== --- Zope/trunk/lib/python/AccessControl/requestmethod.txt 2007-03-20 08:07:42 UTC (rev 73385) +++ Zope/trunk/lib/python/AccessControl/requestmethod.txt 2007-03-20 08:50:24 UTC (rev 73386) @@ -0,0 +1,76 @@ +Request method decorators +========================= + +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 requestmethod +decorator factory:: + + >>> from AccessControl.requestmethod import requestmethod + >>> @requestmethod('POST') + ... 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:: + + >>> @requestmethod('POST') + ... 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:: + + >>> @requestmethod('POST') + ... 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() + >>> @requestmethod('POST') + ... 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) + +The requestmethod decorator factory can be used for any request method, simply +pass in the desired request method:: + + >>> @requestmethod('PUT') + ... def foo(bar, REQUEST=None): + ... return bar + >>> foo('spam', GET) + Traceback (most recent call last): + ... + Forbidden: Request must be PUT Added: Zope/trunk/lib/python/AccessControl/tests/test_requestmethod.py =================================================================== --- Zope/trunk/lib/python/AccessControl/tests/test_requestmethod.py 2007-03-20 08:07:42 UTC (rev 73385) +++ Zope/trunk/lib/python/AccessControl/tests/test_requestmethod.py 2007-03-20 08:50:24 UTC (rev 73386) @@ -0,0 +1,31 @@ +############################################################################# +# +# 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 zope.interface import implements +from zope.publisher.interfaces.browser import IBrowserRequest + +class DummyRequest: + implements(IBrowserRequest) + + def __init__(self, method): + self.method = method + +def test_suite(): + from doctest import DocFileSuite + return DocFileSuite('../requestmethod.txt', + globs=dict(GET=DummyRequest('GET'), + POST=DummyRequest('POST'))) + +if __name__ == '__main__': + import unittest + unittest.main(defaultTest='test_suite') Modified: Zope/trunk/lib/python/OFS/DTMLMethod.py =================================================================== --- Zope/trunk/lib/python/OFS/DTMLMethod.py 2007-03-20 08:07:42 UTC (rev 73385) +++ Zope/trunk/lib/python/OFS/DTMLMethod.py 2007-03-20 08:50:24 UTC (rev 73386) @@ -36,6 +36,7 @@ from AccessControl.Permissions import view as View from AccessControl.Permissions import ftp_access from AccessControl.DTML import RestrictedDTML +from AccessControl.requestmethod import requestmethod from Cache import Cacheable from zExceptions import Forbidden from zExceptions.TracebackSupplement import PathTracebackSupplement @@ -327,6 +328,7 @@ security.declareProtected(change_proxy_roles, 'manage_proxy') + @requestmethod('POST') def manage_proxy(self, roles=(), REQUEST=None): "Change Proxy Roles" self._validateProxy(REQUEST, roles) Modified: Zope/trunk/lib/python/Products/PythonScripts/PythonScript.py =================================================================== --- Zope/trunk/lib/python/Products/PythonScripts/PythonScript.py 2007-03-20 08:07:42 UTC (rev 73385) +++ Zope/trunk/lib/python/Products/PythonScripts/PythonScript.py 2007-03-20 08:50:24 UTC (rev 73386) @@ -34,6 +34,7 @@ from OFS.History import Historical, html_diff from OFS.Cache import Cacheable from AccessControl.ZopeGuards import get_safe_globals, guarded_getattr +from AccessControl.requestmethod import requestmethod from zExceptions import Forbidden import Globals @@ -360,6 +361,7 @@ 'manage_proxyForm', 'manage_proxy') manage_proxyForm = DTMLFile('www/pyScriptProxy', globals()) + @requestmethod('POST') def manage_proxy(self, roles=(), REQUEST=None): "Change Proxy Roles" self._validateProxy(roles) _______________________________________________ Zope-Checkins maillist - Zope-Checkins@zope.org http://mail.zope.org/mailman/listinfo/zope-checkins