Commit da9a1115 authored by Martijn Pieters's avatar Martijn Pieters

- Add a request method decorator to AccessControl, creating decorators that...

- 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.
parent e5140039
...@@ -51,6 +51,12 @@ Zope Changes ...@@ -51,6 +51,12 @@ Zope Changes
Features added 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 - PythonScripts: allow usage of Python's 'sets' module
- added 'fast_listen' directive to http-server and webdav-source-server - added 'fast_listen' directive to http-server and webdav-source-server
......
...@@ -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 requestmethod
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')
@requestmethod('POST')
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')
@requestmethod('POST')
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 requestmethod
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
@requestmethod('POST')
def manage_setPermissionMapping(self, def manage_setPermissionMapping(self,
permission_names=[], permission_names=[],
class_permissions=[], REQUEST=None): class_permissions=[], REQUEST=None):
......
...@@ -28,6 +28,7 @@ from zope.interface import implements ...@@ -28,6 +28,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 requestmethod
DEFAULTMAXLISTUSERS=250 DEFAULTMAXLISTUSERS=250
...@@ -129,6 +130,7 @@ class RoleManager(ExtensionClass.Base, PermissionMapping.RoleManager): ...@@ -129,6 +130,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')
@requestmethod('POST')
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.
""" """
...@@ -147,6 +149,7 @@ class RoleManager(ExtensionClass.Base, PermissionMapping.RoleManager): ...@@ -147,6 +149,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')
@requestmethod('POST')
def manage_acquiredPermissions(self, permissions=[], REQUEST=None): def manage_acquiredPermissions(self, permissions=[], REQUEST=None):
"""Change the permissions that acquire. """Change the permissions that acquire.
""" """
...@@ -228,6 +231,7 @@ class RoleManager(ExtensionClass.Base, PermissionMapping.RoleManager): ...@@ -228,6 +231,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')
@requestmethod('POST')
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.
...@@ -267,6 +271,7 @@ class RoleManager(ExtensionClass.Base, PermissionMapping.RoleManager): ...@@ -267,6 +271,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')
@requestmethod('POST')
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.
""" """
...@@ -420,6 +425,7 @@ class RoleManager(ExtensionClass.Base, PermissionMapping.RoleManager): ...@@ -420,6 +425,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')
@requestmethod('POST')
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:
...@@ -438,6 +444,7 @@ class RoleManager(ExtensionClass.Base, PermissionMapping.RoleManager): ...@@ -438,6 +444,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')
@requestmethod('POST')
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:
...@@ -452,6 +459,7 @@ class RoleManager(ExtensionClass.Base, PermissionMapping.RoleManager): ...@@ -452,6 +459,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')
@requestmethod('POST')
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__
...@@ -544,6 +552,7 @@ class RoleManager(ExtensionClass.Base, PermissionMapping.RoleManager): ...@@ -544,6 +552,7 @@ class RoleManager(ExtensionClass.Base, PermissionMapping.RoleManager):
return self.manage_access(REQUEST) return self.manage_access(REQUEST)
@requestmethod('POST')
def _addRole(self, role, REQUEST=None): def _addRole(self, role, REQUEST=None):
if not role: if not role:
return MessageDialog( return MessageDialog(
...@@ -561,6 +570,7 @@ class RoleManager(ExtensionClass.Base, PermissionMapping.RoleManager): ...@@ -561,6 +570,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)
@requestmethod('POST')
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 requestmethod
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): @requestmethod('POST')
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): @requestmethod('POST')
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): @requestmethod('POST')
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')
@requestmethod('POST')
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
@requestmethod('POST')
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)
@requestmethod('POST')
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)
@requestmethod('POST')
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 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',)
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
#############################################################################
#
# 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')
...@@ -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 requestmethod
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')
@requestmethod('POST')
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 requestmethod
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())
@requestmethod('POST')
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