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
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
......
......@@ -22,6 +22,7 @@ from AccessControl import getSecurityManager, Unauthorized
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 @@ class Owned(ExtensionClass.Base):
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 @@ class Owned(ExtensionClass.Base):
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.
......
......@@ -28,11 +28,14 @@ from zope.interface import implements
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 @@ class RoleManager:
a({'permission_name': ac_perms[0], 'class_permission': p})
return r
@requestmethod('POST')
def manage_setPermissionMapping(self,
permission_names=[],
class_permissions=[], REQUEST=None):
......
......@@ -28,6 +28,7 @@ from zope.interface import implements
from interfaces import IRoleManager
from Permission import Permission
from requestmethod import requestmethod
DEFAULTMAXLISTUSERS=250
......@@ -129,6 +130,7 @@ class RoleManager(ExtensionClass.Base, PermissionMapping.RoleManager):
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 @@ class RoleManager(ExtensionClass.Base, PermissionMapping.RoleManager):
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 @@ class RoleManager(ExtensionClass.Base, PermissionMapping.RoleManager):
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 @@ class RoleManager(ExtensionClass.Base, PermissionMapping.RoleManager):
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 @@ class RoleManager(ExtensionClass.Base, PermissionMapping.RoleManager):
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 @@ class RoleManager(ExtensionClass.Base, PermissionMapping.RoleManager):
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 @@ class RoleManager(ExtensionClass.Base, PermissionMapping.RoleManager):
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 @@ class RoleManager(ExtensionClass.Base, PermissionMapping.RoleManager):
return self.manage_access(REQUEST)
@requestmethod('POST')
def _addRole(self, role, REQUEST=None):
if not role:
return MessageDialog(
......@@ -561,6 +570,7 @@ class RoleManager(ExtensionClass.Base, PermissionMapping.RoleManager):
if REQUEST is not None:
return self.manage_access(REQUEST)
@requestmethod('POST')
def _delRoles(self, roles, REQUEST=None):
if not roles:
return MessageDialog(
......
......@@ -33,6 +33,7 @@ from zope.interface import implements
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 @@ class BasicUserFolder(Implicit, Persistent, Navigation, Tabs, RoleManager,
# 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 @@ class BasicUserFolder(Implicit, Persistent, Navigation, Tabs, RoleManager,
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 @@ class BasicUserFolder(Implicit, Persistent, Navigation, Tabs, RoleManager,
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 @@ class BasicUserFolder(Implicit, Persistent, Navigation, Tabs, RoleManager,
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 @@ class BasicUserFolder(Implicit, Persistent, Navigation, Tabs, RoleManager,
return 1
@requestmethod('POST')
def _addUser(self,name,password,confirm,roles,domains,REQUEST=None):
if not name:
return MessageDialog(
......@@ -884,7 +891,7 @@ class BasicUserFolder(Implicit, Persistent, Navigation, Tabs, RoleManager,
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 @@ class BasicUserFolder(Implicit, Persistent, Navigation, Tabs, RoleManager,
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(
......
#############################################################################
#
# 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
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 @@ class DTMLMethod(RestrictedDTML, HTML, Acquisition.Implicit, RoleManager,
security.declareProtected(change_proxy_roles, 'manage_proxy')
@requestmethod('POST')
def manage_proxy(self, roles=(), REQUEST=None):
"Change Proxy Roles"
self._validateProxy(REQUEST, roles)
......
......@@ -34,6 +34,7 @@ from AccessControl import getSecurityManager
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 @@ class PythonScript(Script, Historical, Cacheable):
'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)
......
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