[Checkins] SVN: Zope/branches/2.10/ - Backport a postonly decorator from Zope trunk's requestmethod decorator factory.

Martijn Pieters mj at zopatista.com
Tue Mar 20 05:02:29 EDT 2007


Log message for revision 73388:
  - Backport a postonly decorator from Zope trunk's requestmethod decorator factory.
  - Protect various security-setting-mutators with this decorator.

Changed:
  U   Zope/branches/2.10/doc/CHANGES.txt
  U   Zope/branches/2.10/lib/python/AccessControl/Owned.py
  U   Zope/branches/2.10/lib/python/AccessControl/PermissionMapping.py
  U   Zope/branches/2.10/lib/python/AccessControl/Role.py
  U   Zope/branches/2.10/lib/python/AccessControl/User.py
  A   Zope/branches/2.10/lib/python/AccessControl/requestmethod.py
  A   Zope/branches/2.10/lib/python/AccessControl/requestmethod.txt
  A   Zope/branches/2.10/lib/python/AccessControl/tests/test_requestmethod.py
  U   Zope/branches/2.10/lib/python/OFS/DTMLMethod.py
  U   Zope/branches/2.10/lib/python/Products/PythonScripts/PythonScript.py

-=-
Modified: Zope/branches/2.10/doc/CHANGES.txt
===================================================================
--- Zope/branches/2.10/doc/CHANGES.txt	2007-03-20 08:56:31 UTC (rev 73387)
+++ Zope/branches/2.10/doc/CHANGES.txt	2007-03-20 09:02:28 UTC (rev 73388)
@@ -8,6 +8,10 @@
 
     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 #2289: restored compatibility with PTProfiler
     
       - No longer opens a zodb connection every time a ProductDispatcher

Modified: Zope/branches/2.10/lib/python/AccessControl/Owned.py
===================================================================
--- Zope/branches/2.10/lib/python/AccessControl/Owned.py	2007-03-20 08:56:31 UTC (rev 73387)
+++ Zope/branches/2.10/lib/python/AccessControl/Owned.py	2007-03-20 09:02:28 UTC (rev 73388)
@@ -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 postonly
 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')
+    @postonly
     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')
+    @postonly
     def manage_changeOwnershipType(self, explicit=1,
                                    RESPONSE=None, REQUEST=None):
         """Change the type (implicit or explicit) of ownership.

Modified: Zope/branches/2.10/lib/python/AccessControl/PermissionMapping.py
===================================================================
--- Zope/branches/2.10/lib/python/AccessControl/PermissionMapping.py	2007-03-20 08:56:31 UTC (rev 73387)
+++ Zope/branches/2.10/lib/python/AccessControl/PermissionMapping.py	2007-03-20 09:02:28 UTC (rev 73388)
@@ -28,11 +28,14 @@
 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 @@
             a({'permission_name': ac_perms[0], 'class_permission': p})
         return r
 
+    @postonly
     def manage_setPermissionMapping(self,
                                     permission_names=[],
                                     class_permissions=[], REQUEST=None):

Modified: Zope/branches/2.10/lib/python/AccessControl/Role.py
===================================================================
--- Zope/branches/2.10/lib/python/AccessControl/Role.py	2007-03-20 08:56:31 UTC (rev 73387)
+++ Zope/branches/2.10/lib/python/AccessControl/Role.py	2007-03-20 09:02:28 UTC (rev 73388)
@@ -27,6 +27,7 @@
 
 from interfaces import IRoleManager
 from Permission import Permission
+from requestmethod import postonly
 
 
 DEFAULTMAXLISTUSERS=250
@@ -128,6 +129,7 @@
                              help_product='OFSP')
 
     security.declareProtected(change_permissions, 'manage_role')
+    @postonly
     def manage_role(self, role_to_manage, permissions=[], REQUEST=None):
         """Change the permissions given to the given role.
         """
@@ -146,6 +148,7 @@
                                  help_product='OFSP')
 
     security.declareProtected(change_permissions, 'manage_acquiredPermissions')
+    @postonly
     def manage_acquiredPermissions(self, permissions=[], REQUEST=None):
         """Change the permissions that acquire.
         """
@@ -167,6 +170,7 @@
                                    help_product='OFSP')
 
     security.declareProtected(change_permissions, 'manage_permission')
+    @postonly
     def manage_permission(self, permission_to_manage,
                           roles=[], acquire=0, REQUEST=None):
         """Change the settings for the given permission.
@@ -205,6 +209,7 @@
             return apply(self._normal_manage_access,(), kw)
 
     security.declareProtected(change_permissions, 'manage_changePermissions')
+    @postonly
     def manage_changePermissions(self, REQUEST):
         """Change all permissions settings, called by management screen.
         """
@@ -358,6 +363,7 @@
         return tuple(dict.get(userid, []))
 
     security.declareProtected(change_permissions, 'manage_addLocalRoles')
+    @postonly
     def manage_addLocalRoles(self, userid, roles, REQUEST=None):
         """Set local roles for a user."""
         if not roles:
@@ -376,6 +382,7 @@
             return self.manage_listLocalRoles(self, REQUEST, stat=stat)
 
     security.declareProtected(change_permissions, 'manage_setLocalRoles')
+    @postonly
     def manage_setLocalRoles(self, userid, roles, REQUEST=None):
         """Set local roles for a user."""
         if not roles:
@@ -390,6 +397,7 @@
             return self.manage_listLocalRoles(self, REQUEST, stat=stat)
 
     security.declareProtected(change_permissions, 'manage_delLocalRoles')
+    @postonly
     def manage_delLocalRoles(self, userids, REQUEST=None):
         """Remove all local roles for a user."""
         dict=self.__ac_local_roles__
@@ -482,6 +490,7 @@
 
         return self.manage_access(REQUEST)
 
+    @postonly
     def _addRole(self, role, REQUEST=None):
         if not role:
             return MessageDialog(
@@ -499,6 +508,7 @@
         if REQUEST is not None:
             return self.manage_access(REQUEST)
 
+    @postonly
     def _delRoles(self, roles, REQUEST=None):
         if not roles:
             return MessageDialog(

Modified: Zope/branches/2.10/lib/python/AccessControl/User.py
===================================================================
--- Zope/branches/2.10/lib/python/AccessControl/User.py	2007-03-20 08:56:31 UTC (rev 73387)
+++ Zope/branches/2.10/lib/python/AccessControl/User.py	2007-03-20 09:02:28 UTC (rev 73388)
@@ -33,6 +33,7 @@
 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 @@
     # user folder subclasses already implement.
 
     security.declareProtected(ManageUsers, 'userFolderAddUser')
-    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."""
@@ -543,7 +546,9 @@
         raise NotImplementedError
 
     security.declareProtected(ManageUsers, 'userFolderEditUser')
-    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."""
@@ -552,7 +557,8 @@
         raise NotImplementedError
 
     security.declareProtected(ManageUsers, 'userFolderDelUsers')
-    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'):
@@ -794,6 +800,7 @@
             self, REQUEST, manage_tabs_message=manage_tabs_message,
             management_view='Properties')
 
+    @postonly
     def manage_setUserFolderProperties(self, encrypt_passwords=0,
                                        update_passwords=0,
                                        maxlistusers=DEFAULTMAXLISTUSERS,
@@ -848,7 +855,7 @@
 
         return 1
 
-
+    @postonly
     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)
 
-
+    @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
@@ -922,6 +929,7 @@
         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(

Added: Zope/branches/2.10/lib/python/AccessControl/requestmethod.py
===================================================================
--- Zope/branches/2.10/lib/python/AccessControl/requestmethod.py	2007-03-20 08:56:31 UTC (rev 73387)
+++ Zope/branches/2.10/lib/python/AccessControl/requestmethod.py	2007-03-20 09:02:28 UTC (rev 73388)
@@ -0,0 +1,70 @@
+#############################################################################
+#
+# 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',)

Added: Zope/branches/2.10/lib/python/AccessControl/requestmethod.txt
===================================================================
--- Zope/branches/2.10/lib/python/AccessControl/requestmethod.txt	2007-03-20 08:56:31 UTC (rev 73387)
+++ Zope/branches/2.10/lib/python/AccessControl/requestmethod.txt	2007-03-20 09:02:28 UTC (rev 73388)
@@ -0,0 +1,69 @@
+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)

Added: Zope/branches/2.10/lib/python/AccessControl/tests/test_requestmethod.py
===================================================================
--- Zope/branches/2.10/lib/python/AccessControl/tests/test_requestmethod.py	2007-03-20 08:56:31 UTC (rev 73387)
+++ Zope/branches/2.10/lib/python/AccessControl/tests/test_requestmethod.py	2007-03-20 09:02:28 UTC (rev 73388)
@@ -0,0 +1,28 @@
+#############################################################################
+#
+# 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')

Modified: Zope/branches/2.10/lib/python/OFS/DTMLMethod.py
===================================================================
--- Zope/branches/2.10/lib/python/OFS/DTMLMethod.py	2007-03-20 08:56:31 UTC (rev 73387)
+++ Zope/branches/2.10/lib/python/OFS/DTMLMethod.py	2007-03-20 09:02:28 UTC (rev 73388)
@@ -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 postonly
 from Cache import Cacheable
 from zExceptions import Forbidden
 from zExceptions.TracebackSupplement import PathTracebackSupplement
@@ -327,6 +328,7 @@
 
 
     security.declareProtected(change_proxy_roles, 'manage_proxy')
+    @postonly
     def manage_proxy(self, roles=(), REQUEST=None):
         "Change Proxy Roles"
         self._validateProxy(REQUEST, roles)

Modified: Zope/branches/2.10/lib/python/Products/PythonScripts/PythonScript.py
===================================================================
--- Zope/branches/2.10/lib/python/Products/PythonScripts/PythonScript.py	2007-03-20 08:56:31 UTC (rev 73387)
+++ Zope/branches/2.10/lib/python/Products/PythonScripts/PythonScript.py	2007-03-20 09:02:28 UTC (rev 73388)
@@ -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 postonly
 
 from zExceptions import Forbidden
 import Globals
@@ -360,6 +361,7 @@
       'manage_proxyForm', 'manage_proxy')
 
     manage_proxyForm = DTMLFile('www/pyScriptProxy', globals())
+    @postonly
     def manage_proxy(self, roles=(), REQUEST=None):
         "Change Proxy Roles"
         self._validateProxy(roles)



More information about the Checkins mailing list