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
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
request URLs in BaseRequest and HTTPRequest
......
......@@ -18,6 +18,7 @@ $Id$
import Globals, urlparse, SpecialUsers, ExtensionClass
from AccessControl import getSecurityManager, Unauthorized
from Acquisition import aq_get, aq_parent, aq_base
from requestmethod import postonly
from zope.interface import implements
from interfaces import IOwned
......
......@@ -28,11 +28,14 @@ from zope.interface import implements
from interfaces import IPermissionMappingSupport
from Owned import UnownableOwner
from Permission import pname
from requestmethod import postonly
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
@postonly
def manage_setPermissionMapping(self,
permission_names=[],
class_permissions=[], REQUEST=None):
......
......@@ -24,6 +24,7 @@ from zope.interface import implements
from interfaces import IRoleManager
from Permission import Permission
from requestmethod import postonly
DEFAULTMAXLISTUSERS=250
......@@ -135,6 +136,7 @@ class RoleManager(ExtensionClass.Base, PermissionMapping.RoleManager):
help_topic='Security_Manage-Role.stx',
help_product='OFSP')
@postonly
def manage_role(self, role_to_manage, permissions=[], REQUEST=None):
"""Change the permissions given to the given role.
"""
......@@ -151,6 +153,7 @@ class RoleManager(ExtensionClass.Base, PermissionMapping.RoleManager):
help_topic='Security_Manage-Acquisition.stx',
help_product='OFSP')
@postonly
def manage_acquiredPermissions(self, permissions=[], REQUEST=None):
"""Change the permissions that acquire.
"""
......@@ -170,6 +173,7 @@ class RoleManager(ExtensionClass.Base, PermissionMapping.RoleManager):
help_topic='Security_Manage-Permission.stx',
help_product='OFSP')
@postonly
def manage_permission(self, permission_to_manage,
roles=[], acquire=0, REQUEST=None):
"""Change the settings for the given permission.
......@@ -206,6 +210,7 @@ class RoleManager(ExtensionClass.Base, PermissionMapping.RoleManager):
else:
return apply(self._normal_manage_access,(), kw)
@postonly
def manage_changePermissions(self, REQUEST):
"""Change all permissions settings, called by management screen.
"""
......@@ -353,6 +358,7 @@ class RoleManager(ExtensionClass.Base, PermissionMapping.RoleManager):
dict=self.__ac_local_roles__ or {}
return tuple(dict.get(userid, []))
@postonly
def manage_addLocalRoles(self, userid, roles, REQUEST=None):
"""Set local roles for a user."""
if not roles:
......@@ -370,6 +376,7 @@ class RoleManager(ExtensionClass.Base, PermissionMapping.RoleManager):
stat='Your changes have been saved.'
return self.manage_listLocalRoles(self, REQUEST, stat=stat)
@postonly
def manage_setLocalRoles(self, userid, roles, REQUEST=None):
"""Set local roles for a user."""
if not roles:
......@@ -383,6 +390,7 @@ class RoleManager(ExtensionClass.Base, PermissionMapping.RoleManager):
stat='Your changes have been saved.'
return self.manage_listLocalRoles(self, REQUEST, stat=stat)
@postonly
def manage_delLocalRoles(self, userids, REQUEST=None):
"""Remove all local roles for a user."""
dict=self.__ac_local_roles__
......@@ -473,6 +481,7 @@ class RoleManager(ExtensionClass.Base, PermissionMapping.RoleManager):
return self.manage_access(REQUEST)
@postonly
def _addRole(self, role, REQUEST=None):
if not role:
return MessageDialog(
......@@ -490,6 +499,7 @@ class RoleManager(ExtensionClass.Base, PermissionMapping.RoleManager):
if REQUEST is not None:
return self.manage_access(REQUEST)
@postonly
def _delRoles(self, roles, REQUEST=None):
if not roles:
return MessageDialog(
......
......@@ -31,6 +31,7 @@ from zope.interface import implements
import AuthEncoding
import SpecialUsers
from interfaces import IStandardUserFolder
from requestmethod import postonly
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,
# Authors of custom user folders don't need to do anything special to
# support these - they will just call the appropriate '_' methods that
# 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
user folder implementations support dynamic creation of user
objects."""
......@@ -542,7 +545,9 @@ class BasicUserFolder(Implicit, Persistent, Navigation, Tabs, RoleManager,
return self._doAddUser(name, password, roles, domains, **kw)
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
all user folder implementations support changing of user object
attributes."""
......@@ -550,7 +555,8 @@ class BasicUserFolder(Implicit, Persistent, Navigation, Tabs, RoleManager,
return self._doChangeUser(name, password, roles, domains, **kw)
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
all user folder implementations support deletion of user objects."""
if hasattr(self, '_doDelUsers'):
......@@ -792,6 +798,7 @@ class BasicUserFolder(Implicit, Persistent, Navigation, Tabs, RoleManager,
self, REQUEST, manage_tabs_message=manage_tabs_message,
management_view='Properties')
@postonly
def manage_setUserFolderProperties(self, encrypt_passwords=0,
update_passwords=0,
maxlistusers=DEFAULTMAXLISTUSERS,
......@@ -846,7 +853,7 @@ class BasicUserFolder(Implicit, Persistent, Navigation, Tabs, RoleManager,
return 1
@postonly
def _addUser(self,name,password,confirm,roles,domains,REQUEST=None):
if not name:
return MessageDialog(
......@@ -882,7 +889,7 @@ class BasicUserFolder(Implicit, Persistent, Navigation, Tabs, RoleManager,
self._doAddUser(name, password, roles, domains)
if REQUEST: return self._mainUser(self, REQUEST)
@postonly
def _changeUser(self,name,password,confirm,roles,domains,REQUEST=None):
if password == 'password' and confirm == 'pconfirm':
# Protocol for editUser.dtml to indicate unchanged password
......@@ -920,6 +927,7 @@ class BasicUserFolder(Implicit, Persistent, Navigation, Tabs, RoleManager,
self._doChangeUser(name, password, roles, domains)
if REQUEST: return self._mainUser(self, REQUEST)
@postonly
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 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
import Globals, sys, Acquisition
from AccessControl import getSecurityManager
from AccessControl.DTML import RestrictedDTML
from AccessControl.requestmethod import postonly
from Cache import Cacheable
from zExceptions import Forbidden
from zExceptions.TracebackSupplement import PathTracebackSupplement
......@@ -315,6 +316,7 @@ class DTMLMethod(RestrictedDTML, HTML, Acquisition.Implicit, RoleManager,
'do not have proxy roles.\n<!--%s, %s-->' % (self.__name__, u, roles))
@postonly
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 postonly
from zExceptions import Forbidden
import Globals
......@@ -359,6 +360,7 @@ class PythonScript(Script, Historical, Cacheable):
'manage_proxyForm', 'manage_proxy')
manage_proxyForm = DTMLFile('www/pyScriptProxy', globals())
@postonly
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