[Zope-Checkins] SVN: Zope/trunk/ - Add a request method decorator
to AccessControl,
creating decorators that limit a method to one request method only.
Martijn Pieters
mj at zopatista.com
Tue Mar 20 04:50:26 EDT 2007
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)
More information about the Zope-Checkins
mailing list