[Zope-dev] Improvements for Zope2's security

Christian Heimes lists at cheimes.de
Mon Sep 18 11:20:24 EDT 2006


Hey guys!

In the past few months I fiddled around with Zope2's security and access
control code. I analysied my own code and code from other developers to
search for common errors. Also I tried to think of ways to make the
security system easier and more verbose on coding errors

I have not yet implemented all my ideas but Zope 2.10 is on the door
steps. Here is my first set of improvements.

Issue 1
=======

Zope's security declarations have to be called with a method *name* AS
STRING. Developers are human beeings and human beeings tend to make
small errors like typos. Or they forget to change the security
declaration when they rename a method. Zope doesn't raise an error when
a developer adds a security declaration for a non existing method.

Have a look at the following example. It contains a tiny but devastating
typo::

    security.declarePrivate('chooseProtocol')
    def chooseProtocols(self, request):
        ...

These kinds or errors are extremly hard to find and may lead to big
security holes. By the way this example was taken from a well known and
well tested Zope addon!

Solution
--------

The solution was very easy to implement. I created a small helper
function checkClassHasMethod() and called it in the apply() method of
AccessControl.SecurityInfo.ClassSecurityInfo. The apply() method is
called at startup. The code doesn't slow down requests.

Issue 2
=======

Another way to introduce security breaches is to forget the
InitializeClass() call. The call is responsable for applying the
informations from a ClassSecurityInfo. Without the call all security
settings made through security.declare* are useless.

The good news is: Even if a developer forgets to call the method
explictly in his code the function is called implictly.

The implicit call was hard to find for me at first. I struggled with the
code in OFS.Image because there is an import of InitializeClass but it
is never called. Never the less the security informations are somehow
merged automagically. After some digging I found the code that is
responsible for the magic. It's ExtensionClass' __class_init__ magic and
OFS.ObjectManager.ObjectManager.__class_init__

Now the bad news: The magic doesn't work under some conditions. For
example if you class doesn't subclass ObjectManager or if you monkey
patch a class with security info object you have to call InitializeClass
explictly.

Solution
--------

Not yet finished. I've created a function checkObjectHasSecurityInfo()
and added a call to ZPublisher.BaseRequest.DefaultPublishTraverse but it
is untested.

Issue 3
=======

Developers are lazy and they like to make typos. No one likes to type
security.declarePrivate('chooseProtocol') so we are using copy & paste
which may cause even more typos. Wouldn't it be cool to get rid of
security. and typing the name of the method twice? Let's use decorators!
Here is the doc test example from my patch:

Solution
--------

Security decorators are an alternative syntax to define security
declarations on classes.

>>> from ExtensionClass import Base
>>> from AccessControl import ClassSecurityInfo
>>> from AccessControl.decorator import declarePublic
>>> from AccessControl.decorator import declarePrivate
>>> from AccessControl.decorator import declareProtected
>>> from AccessControl.Permissions import view as View
>>> from Globals import InitializeClass

>>> class DecoratorExample(Base):
...     '''decorator example'''
...
...     security = ClassSecurityInfo()
...
...     @declarePublic
...     def publicMethod(self):
...         "public method"
...
...     @declarePrivate
...     def privateMethod(self):
...         "private method"
...
...     @declareProtected(View)
...     def protectedByView(self):
...         "method protected by View"
...
>>> InitializeClass(DecoratorExample)


With the new syntax you have to type only 15 letters instead of 41!

Issue 4
=======

Some methods shouldn't be callable from certain types of request
methods. For example there is no need that the webdav DELETE and PROP*
methods should be callable from an ordinary HTTP GET request. I don't
want to go into details. Some people know why :)

Solution
--------

Only a small subset is implemented. There are two methods in my patch to
either whitelist or blacklist a request method. An older version of my
patch contained even code to distinguish between request types (ftp,
http, ftp, xml-rpc) but Jim told me in a private mail it's kind of YAGNI.

At the moment blacklistRequestMethod() and whitelistRequestMethod() have
to be called explictly inside a method. There is no way to protect
methods via security.blacklistRequestMethod() or @blacklistRequestMethod.

I have two ideas to implement such security declarations but both ways
are complicated and hard to implement. An ordinary decorator doesn't
work because it messes up with ZPublisher.mapply.

The following code does NOT work with POST requests because mapply is
using introspection to get the names and default values of a method. The
decorator has a different signatur.

def blacklistRequestMethod(*dargs):
    """Blacklists the available request methods
    """
    __security_decorator__ = True
    def wrapper(method):
        name = _getFunctionName(method)
        def decorator(self, *oargs, **okwargs):
            _blacklistMethod(self, blacklist=dargs)
            return method(self, *oargs, **okwargs)
        decorator.__doc__ = method.__doc__
        decorator.func_name = name
        return decorator
    return wrapper

The problem could be solved by either creating a decorator with a
correct signatur using eval/compile/exec (ugly!) or altering the mapply
magic.

My first approach stored some informations in the class similar to
methodname__roles__ and some explicit checks in the request broker and
DefaultPublishTraverse.

Comments? :)

The attached patch is a svn diff against the 2.10 tree.

Christian
-------------- next part --------------
Index: lib/python/AccessControl/__init__.py
===================================================================
--- lib/python/AccessControl/__init__.py	(revision 70214)
+++ lib/python/AccessControl/__init__.py	(working copy)
@@ -26,6 +26,8 @@
 from ZopeGuards import full_write_guard, safe_builtins
 
 ModuleSecurityInfo('AccessControl').declarePublic('getSecurityManager')
+ModuleSecurityInfo('AccessControl.requestsecurity').declarePublic('blacklistRequestMethod')
+ModuleSecurityInfo('AccessControl.requestsecurity').declarePublic('whitelistRequestMethod')
 
 import DTML
 del DTML
Index: lib/python/AccessControl/checker.py
===================================================================
--- lib/python/AccessControl/checker.py	(revision 0)
+++ lib/python/AccessControl/checker.py	(revision 0)
@@ -0,0 +1,164 @@
+##############################################################################
+#
+# Copyright (c) 2006 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.
+#
+##############################################################################
+"""Misc security related check functions
+
+$Id: $
+"""
+from logging import getLogger
+
+from Acquisition import aq_base
+from ExtensionClass import Base as ExtensionClassBase
+from AccessControl.decorator import _getFunctionName
+
+LOG = getLogger('SecurityCheck')
+
+def checkClassHasMethod(classobj, name='', log=True):
+    """Check if a class has a given method
+    
+    This checker is used to find security declarations for methods that don't
+    exist. Per definition an empty name  always exists since an empty name 
+    has a special meaning.
+    
+    >>> from ExtensionClass import Base
+    >>> class Foo(Base):
+    ...     def method(self):
+    ...         pass
+
+    >>> checkClassHasMethod(Foo, '', log=False)
+    True
+    >>> checkClassHasMethod(Foo, 'method', log=False)
+    True
+    >>> checkClassHasMethod(Foo, 'nomethod', log=False)
+    False
+    """
+    if not name or hasattr(classobj, name):
+        return True
+
+    if log:
+        LOG.warn("Class '%s' has a security setting for a non "
+            "existing method '%s'" % (_dottedName(classobj), name))
+    return False
+        
+
+def checkObjectHasSecurityInfo(obj, log=True, paranoid=False):
+    """Check if the class of an object has a security info object
+    
+    obj can be an instance, class or a bound method.
+    
+    Under some rare circumstances a class can still have a security info
+    object. In most cases InitializeClass() is called either explictly by the
+    programmer or implictly by the ExtensionClass's __class_init__() magic if
+    the class is a subclass of OFS.ObjectManager.
+    Edge cases are for example monkey patching with a security info object or
+    classes that don't subclass from ObjectManager.
+    
+    >>> from ExtensionClass import Base
+    >>> class FakeSecurityInfo(Base):
+    ...     __security_info__ = 1
+    ... 
+
+    Test class with no security info
+    >>> class NoSecurity(Base):
+    ...     def method(self):
+    ...         pass
+    >>> nosecurity = NoSecurity()
+    >>> checkObjectHasSecurityInfo(NoSecurity, log=False)
+    False
+    >>> checkObjectHasSecurityInfo(NoSecurity.method, log=False)
+    False
+    >>> checkObjectHasSecurityInfo(nosecurity, log=False)
+    False
+    >>> checkObjectHasSecurityInfo(nosecurity.method, log=False)
+    False
+    
+    Test class with a security info object
+    >>> class WithSecurity(NoSecurity):
+    ...     security = FakeSecurityInfo()
+    >>> withsecurity = WithSecurity()
+    >>> checkObjectHasSecurityInfo(WithSecurity, log=False)
+    True
+    >>> checkObjectHasSecurityInfo(WithSecurity.method, log=False)
+    True
+    >>> checkObjectHasSecurityInfo(withsecurity, log=False)
+    True
+    >>> checkObjectHasSecurityInfo(withsecurity.method, log=False)
+    True
+
+    Test class with a security info object not named security
+    >>> class HiddenSecurity(NoSecurity):
+    ...     hidden = FakeSecurityInfo()
+    >>> hiddensecurity = HiddenSecurity()
+    >>> checkObjectHasSecurityInfo(hiddensecurity, log=False)
+    False
+    >>> checkObjectHasSecurityInfo(hiddensecurity.method, log=False)
+    False
+    >>> checkObjectHasSecurityInfo(hiddensecurity, log=False, paranoid=True)
+    True
+    >>> checkObjectHasSecurityInfo(hiddensecurity.method, log=False, paranoid=True)
+    True
+    
+    checkObjectHasSecurityInfo() shouldn't bark and fail with other object
+    >>> checkObjectHasSecurityInfo(None, log=False, paranoid=True)
+    False
+    >>> checkObjectHasSecurityInfo(1, log=False, paranoid=True)
+    False
+    >>> checkObjectHasSecurityInfo('1', log=False, paranoid=True)
+    False
+    >>> checkObjectHasSecurityInfo(object(), log=False, paranoid=True)
+    False
+    """
+    target = aq_base(obj)
+    if hasattr(target, 'im_class'):
+        target = target.im_class
+        name = _dottedName(target) + '.' + _getFunctionName(obj)
+    elif isinstance(target, ExtensionClassBase):
+        target = target.__class__
+        name = _dottedName(target)
+    else:
+        name = _dottedName(target)
+
+    found = []
+    if paranoid:
+        if not hasattr(target, '__dict__'):
+            return False
+        for key, value in target.__dict__.items():
+            if hasattr(value, '__security_info__'):
+                found.append(key)
+        found = ','.join(found)
+    else:
+        security = getattr(target, 'security', None)
+        if security is not None and hasattr(security, '__security_info__'):
+            found = 'security'
+    
+    if not found:
+        return False
+    
+    if log:
+        LOG.warn("Object %s has a security info object %s. This problem "
+            "is probably caused by a missing InitializeClass() call and may "
+            "introduce severe security breaches!" % (name, found))
+    return True
+
+# internal helper functions
+def _dottedName(obj, method=None):
+    """Get the dotted name of an object
+    """
+    modname = getattr(obj, '__module__', 'UNKNOWN')
+    try:
+        clsname = getattr(obj, '__name__', None)
+        if clsname is None:
+            clsname = obj.__class__.__name__
+    except AttributeError:
+        clsname = 'UNKNOWN'
+    return modname + '.' + clsname
Index: lib/python/AccessControl/tests/test_checker.py
===================================================================
--- lib/python/AccessControl/tests/test_checker.py	(revision 0)
+++ lib/python/AccessControl/tests/test_checker.py	(revision 0)
@@ -0,0 +1,25 @@
+##############################################################################
+#
+# Copyright (c) 2002 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
+#
+##############################################################################
+"""Unit tests for AccessControl.checker
+"""
+
+import unittest
+from zope.testing.doctest import DocTestSuite
+
+def test_suite():
+    suite = unittest.TestSuite()
+    suite.addTest(DocTestSuite('AccessControl.checker'))
+    return suite
+
+if __name__ == '__main__':
+    unittest.main(defaultTest='test_suite')
Index: lib/python/AccessControl/tests/testClassSecurityInfo.py
===================================================================
--- lib/python/AccessControl/tests/testClassSecurityInfo.py	(revision 70214)
+++ lib/python/AccessControl/tests/testClassSecurityInfo.py	(working copy)
@@ -10,66 +10,79 @@
 # FOR A PARTICULAR PURPOSE
 #
 ##############################################################################
-""" Unit tests for ClassSecurityInfo.
+"""Unit tests for ClassSecurityInfo and some decorator stuff
 """
 
 import unittest
 
+import Globals
+from App.class_init import default__class_init__
+from ExtensionClass import Base
+from AccessControl.SecurityInfo import ClassSecurityInfo
+from AccessControl.SecurityInfo import ACCESS_PRIVATE
+from AccessControl.SecurityInfo import ACCESS_PUBLIC
+from AccessControl.decorator import declareProtected
+from AccessControl.decorator import declarePrivate
+from AccessControl.decorator import declarePublic
 
-class ClassSecurityInfoTests(unittest.TestCase):
+# Setup a test class with default role -> permission decls.
+class Test(Base):
+    """Test class
+    """
+    __ac_roles__ = ('Role A', 'Role B', 'Role C')
 
+    meta_type = "Test"
 
-    def _getTargetClass(self):
+    security = ClassSecurityInfo()
 
-        from AccessControl.SecurityInfo import ClassSecurityInfo
-        return ClassSecurityInfo
+    security.setPermissionDefault('Make food', ('Chef',))
 
-    def test_SetPermissionDefault(self):
+    security.setPermissionDefault(
+        'Test permission',
+        ('Manager', 'Role A', 'Role B', 'Role C')
+        )
 
-        # Test setting default roles for permissions.
+    security.declareProtected('Test permission', 'foo')
+    def foo(self, REQUEST=None):
+        """ """
+        pass
+    
+    @declareProtected('Test permission')
+    def protected(self, REQUEST=None):
+        """ """
 
-        import Globals  # XXX: avoiding import cycle
-        from App.class_init import default__class_init__
-        from ExtensionClass import Base
+    @declarePrivate
+    def private(self, REQUEST=None):
+        """ """
 
-        ClassSecurityInfo = self._getTargetClass()
+    @declarePublic
+    def public(self, REQUEST=None):
+        """ """
 
-        # Setup a test class with default role -> permission decls.
-        class Test(Base):
-            """Test class
-            """
-            __ac_roles__ = ('Role A', 'Role B', 'Role C')
+# Do class initialization.
+default__class_init__(Test)
 
-            meta_type = "Test"
+class ClassSecurityInfoTests(unittest.TestCase):
 
-            security = ClassSecurityInfo()
+  
+    def _checkPerm(self, func__roles__):
+        imPermissionRole = [r for r in func__roles__
+                            if not r.endswith('_Permission')]
+        self.failUnless(len(imPermissionRole) == 4)
 
-            security.setPermissionDefault('Make food', ('Chef',))
+        for item in ('Manager', 'Role A', 'Role B', 'Role C'):
+            self.failUnless(item in imPermissionRole)
 
-            security.setPermissionDefault(
-                'Test permission',
-                ('Manager', 'Role A', 'Role B', 'Role C')
-                )
-
-            security.declareProtected('Test permission', 'foo')
-            def foo(self, REQUEST=None):
-                """ """
-                pass
-
-        # Do class initialization.
-        default__class_init__(Test)
-
+    def test_SetPermissionDefault(self):
         # Now check the resulting class to see if the mapping was made
         # correctly. Note that this uses carnal knowledge of the internal
         # structures used to store this information!
         object = Test()
-        imPermissionRole = [r for r in object.foo__roles__
-                            if not r.endswith('_Permission')]
-        self.failUnless(len(imPermissionRole) == 4)
-
-        for item in ('Manager', 'Role A', 'Role B', 'Role C'):
-            self.failUnless(item in imPermissionRole)
-
+        self._checkPerm(object.foo__roles__)
+        self._checkPerm(object.protected__roles__)
+        self.failUnless(object.private__roles__ == ACCESS_PRIVATE)
+        self.failUnless(object.public__roles__ == ACCESS_PUBLIC)
+        
         # Make sure that a permission defined without accompanying method
         # is still reflected in __ac_permissions__
         self.assertEquals([t for t in Test.__ac_permissions__ if not t[1]],
Index: lib/python/AccessControl/tests/test_requestsecurity.py
===================================================================
--- lib/python/AccessControl/tests/test_requestsecurity.py	(revision 0)
+++ lib/python/AccessControl/tests/test_requestsecurity.py	(revision 0)
@@ -0,0 +1,25 @@
+##############################################################################
+#
+# Copyright (c) 2002 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
+#
+##############################################################################
+"""Unit tests for AccessControl.requestsecurity
+"""
+
+import unittest
+from zope.testing.doctest import DocTestSuite
+
+def test_suite():
+    suite = unittest.TestSuite()
+    suite.addTest(DocTestSuite('AccessControl.requestsecurity'))
+    return suite
+
+if __name__ == '__main__':
+    unittest.main(defaultTest='test_suite')
Index: lib/python/AccessControl/requestsecurity.py
===================================================================
--- lib/python/AccessControl/requestsecurity.py	(revision 0)
+++ lib/python/AccessControl/requestsecurity.py	(revision 0)
@@ -0,0 +1,141 @@
+##############################################################################
+#
+# Copyright (c) 2006 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.
+#
+##############################################################################
+"""Request method based access control
+
+The requestsecurity module contains helper functions to restrict the access
+to resources based on the request method. For example you can deny a GET
+request to a method that should only be available for forms over POST
+requests.
+
+The restrictions are available in two flavors: whitelist and blacklist:
+
+ o A blacklist contains a single or more methods that are FORBIDDEN. Every
+   method that is NOT listed is ALLOWED.
+
+ o A whitelist contains a single or more methods that are ALLOWED. Every
+   method that is NOT listed is DENIED.
+   
+In general a whitelist is more secure than a blacklist but harder to define.
+
+The blacklistRequestMethod and whitelistRequestMethod are available in
+restricted python code like scripts, too.
+
+>>> from AccessControl.ZopeGuards import guarded_import
+>>> mod = guarded_import('AccessControl.requestsecurity',
+...                fromlist=('blacklistRequestMethod', ))
+>>> mod = guarded_import('AccessControl.requestsecurity',
+...                fromlist=('whitelistRequestMethod', ))
+
+Examples:
+
+boiler plate
+>>> context = object()
+>>> name = 'testobject'
+>>> request = {'REQUEST_METHOD' : 'GET',
+...            'URL' : '/testobject' }
+
+Black list tests
+>>> blacklistRequestMethod(context, 'GET', request=request, name=name)
+Traceback (most recent call last):
+...
+Unauthorized: GET request to 'testobject' is not allowed.
+
+>>> blacklistRequestMethod(context, 'POST', request=request, name=name)
+
+>>> blacklistRequestMethod(context, ('POST', 'GET'), request=request)
+Traceback (most recent call last):
+...
+Unauthorized: GET request to '/testobject' is not allowed.
+ 
+White lists are working the opposite way
+>>> whitelistRequestMethod(context, 'GET', request=request, name=name)
+
+>>> whitelistRequestMethod(context, 'POST', request=request, name=name)
+Traceback (most recent call last):
+...
+Unauthorized: GET request to 'testobject' is not allowed.
+
+>>> whitelistRequestMethod(context, ('POST', 'GET'), request=request, name=name)
+
+$Id: $
+"""
+from AccessControl.unauthorized import Unauthorized
+
+
+def blacklistRequestMethod(context, blacklist='GET', request=None, name=None):
+    """Deny access with forbidden request methods
+    """
+    return _restrictRequestMethod(context, blacklist=blacklist, 
+                                  request=request, name=name)
+    
+def whitelistRequestMethod(context, whitelist='POST', request=None, name=None):
+    """Deny access except for allowed request methods
+    """
+    return _restrictRequestMethod(context, whitelist=whitelist, 
+                                  request=request, name=name)
+
+__all__ = ('blacklistRequestMethod', 'whitelistRequestMethod',)
+
+# internal stuff
+def _restrictRequestMethod(context, blacklist=None, whitelist=None,
+                           request=None, name=None):
+    """Helper method for white or blacklisting request methods
+    
+    context should be the requested method or object with an acquistion
+    context.
+    
+    whitelist and blacklist can either be a string or a sequence. You can't
+    define both.
+    
+    request is the request object. If no request is given then the request is
+    acquired from the context
+    
+    name is used for the error message. If no name is given then the URL of
+    the context or repr(context) is used.
+    """
+    if request is None:
+        request = getattr(context, 'REQUEST', None)
+        if request is None:
+            # XXX no request found, what shall I do?
+            return
+    
+    if not name:
+        try:
+            name = request.get('URL', None)
+            if not name:
+                name = context.absolute_url()
+        except AttributeError:
+            name = repr(context)
+    
+    req_method = request.get('REQUEST_METHOD', 'GET').upper()
+    
+    if not bool(blacklist) ^ bool(whitelist):
+        raise ValueError("You have to specify either blacklist or whitelist")
+    
+    if blacklist:
+        if isinstance(blacklist, basestring):
+            blacklist = (blacklist, )
+        if req_method in blacklist:
+            raise Unauthorized("%s request to '%s' is not allowed."
+                                % (req_method, name))
+        return
+    
+    if whitelist:
+        if isinstance(whitelist, basestring):
+            whitelist = (whitelist, )
+        if req_method not in whitelist:
+            raise Unauthorized("%s request to '%s' is not allowed."
+                                % (req_method, name))
+        return
+
Index: lib/python/AccessControl/SecurityInfo.py
===================================================================
--- lib/python/AccessControl/SecurityInfo.py	(revision 70214)
+++ lib/python/AccessControl/SecurityInfo.py	(working copy)
@@ -42,8 +42,8 @@
 from logging import getLogger
 
 import Acquisition
-
 from AccessControl.ImplPython import _what_not_even_god_should_do
+from AccessControl.checker import checkClassHasMethod
 
 LOG = getLogger('SecurityInfo')
 
@@ -160,6 +160,7 @@
         # Collect protected attribute names in ac_permissions.
         ac_permissions = {}
         for name, access in self.names.items():
+            checkClassHasMethod(classobj, name, log=True)
             if access in (ACCESS_PRIVATE, ACCESS_PUBLIC, ACCESS_NONE):
                 setattr(classobj, '%s__roles__' % name, access)
             else:
Index: lib/python/AccessControl/decorator.py
===================================================================
--- lib/python/AccessControl/decorator.py	(revision 0)
+++ lib/python/AccessControl/decorator.py	(revision 0)
@@ -0,0 +1,144 @@
+##############################################################################
+#
+# Copyright (c) 2006 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.
+#
+##############################################################################
+"""Security decorators
+
+Security decorators are an alternative syntax to define security declarations
+on classes.
+
+>>> from ExtensionClass import Base
+>>> from AccessControl import ClassSecurityInfo
+>>> from AccessControl.decorator import declarePublic
+>>> from AccessControl.decorator import declarePrivate
+>>> from AccessControl.decorator import declareProtected
+>>> from AccessControl.Permissions import view as View
+>>> from Globals import InitializeClass
+
+>>> class DecoratorExample(Base):
+...     '''decorator example'''
+...
+...     security = ClassSecurityInfo()
+...
+...     @declarePublic
+...     def publicMethod(self):
+...         "public method"
+...
+...     @declarePrivate
+...     def privateMethod(self):
+...         "private method"
+...
+...     @declareProtected(View)
+...     def protectedByView(self):
+...         "method protected by View"
+... 
+>>> InitializeClass(DecoratorExample)
+
+$Id: $
+"""
+import sys
+
+def declarePublic(method):
+    """Declare names to be publicly accessible.
+    
+    Returns the same method object
+    """
+    __security_decorator__ = True
+    security = _getSecurityInfoFromStack()
+    name = _getFunctionName(method)
+    security.declarePublic(name)
+    return method
+
+def declarePrivate(method):
+    """Declare names to be inaccessible to restricted code.
+        
+    Returns the same method object
+    """
+    __security_decorator__ = True
+    security = _getSecurityInfoFromStack()
+    name = _getFunctionName(method)
+    security.declarePrivate(name)
+    return method
+
+def declareProtected(permission_name):
+    """Declare names to be associated with a permission.
+        
+    Returns the same method object
+    """
+    __security_decorator__ = True
+    security = _getSecurityInfoFromStack()
+    def wrapper(method):
+        name = _getFunctionName(method)
+        security.declareProtected(permission_name, name)
+        return method
+    return wrapper
+
+__all__ = ('declarePublic', 'declarePrivate', 'declareProtected')
+
+# internal helper functions
+
+def _getSecurityInfoFromStack():
+    """Get the security object from the caller stack of a decorator
+
+    There is no direct way to access class variables from a decorator method.
+    This method tries to get the security var from the caller stack. Yeah it
+    is a bit hacky but even zope.interface's implements() is using the same
+    trick.
+
+    If the local namespace contains a var __security_decorator__ then the
+    functions is trying to get the security object from the next stack level.
+    This allows the stacking of two or more decorators. The 
+    __security__decorator__ var minimizes the risk to acquire a different
+    security object from the stack.
+    """
+    # frame 1 is the decorator function, 2 is the class unless you use more
+    # than one decorator.
+    index = 2
+    while True:
+        frame = sys._getframe(index)
+        loc = frame.f_locals
+        security = loc.get('security', None)
+        sd = loc.has_key('__security_decorator__')
+        del loc # delete frame and locals to avoid memory leaks
+        del frame
+        if security and hasattr(security, '__security_info__'):
+            return security
+        if sd:
+            index+=1
+            continue
+        raise TypeError('A security decorator must be defined on a method '
+            'of a class with a security information object!')
+
+def _getFunctionName(func):
+    """Get function name from a method, function or classfunction
+
+    >>> def function(): pass
+    >>> class Foo(object):
+    ...     def method(self): pass
+    ...     @classmethod
+    ...     def clsmethod(cls): pass
+    >>> foo = Foo()
+
+    >>> _getFunctionName(function)
+    'function'
+    >>> _getFunctionName(Foo.method)
+    'method'
+    >>> _getFunctionName(Foo.clsmethod)
+    'clsmethod'
+    >>> _getFunctionName(foo.method)
+    'method'
+    >>> _getFunctionName(foo.clsmethod)
+    'clsmethod'
+    """
+    func = getattr(func, 'im_func', func)
+    return func.func_name
+
Index: lib/python/ZPublisher/BaseRequest.py
===================================================================
--- lib/python/ZPublisher/BaseRequest.py	(revision 70214)
+++ lib/python/ZPublisher/BaseRequest.py	(working copy)
@@ -18,6 +18,7 @@
 import xmlrpc
 from zExceptions import Forbidden, Unauthorized, NotFound
 from Acquisition import aq_base
+from AccessControl.checker import checkObjectHasSecurityInfo
 
 from zope.interface import implements, providedBy, Interface
 from zope.component import queryMultiAdapter
@@ -126,6 +127,10 @@
                 "published." % URL
                 )
 
+        #from Globals import DevelopmentMode
+        #if DevelopmentMode:
+        #    checkObjectHasSecurityInfo(subobject, log=True)
+
         # Hack for security: in Python 2.2.2, most built-in types
         # gained docstrings that they didn't have before. That caused
         # certain mutable types (dicts, lists) to become publishable
Index: lib/python/Shared/DC/ZRDB/DA.py
===================================================================
--- lib/python/Shared/DC/ZRDB/DA.py	(revision 70214)
+++ lib/python/Shared/DC/ZRDB/DA.py	(working copy)
@@ -113,7 +113,7 @@
     security.declareProtected(view_management_screens, 'manage_advancedForm')
     manage_advancedForm=DTMLFile('dtml/advanced', globals())
 
-    security.declarePublic('test_url')
+    security.declarePublic('test_url_')
     def test_url_(self):
         'Method for testing server connection information'
         return 'PING'
Index: lib/python/webdav/Lockable.py
===================================================================
--- lib/python/webdav/Lockable.py	(revision 70214)
+++ lib/python/webdav/Lockable.py	(working copy)
@@ -43,7 +43,7 @@
     # Protect methods using declarative security
     security = ClassSecurityInfo()
     security.declarePrivate('wl_lockmapping')
-    security.declarePublic('wl_isLocked', 'wl_getLock', 'wl_isLockedByUser',
+    security.declarePublic('wl_isLocked', 'wl_getLock', 
                            'wl_lockItems', 'wl_lockValues', 'wl_lockTokens',)
     security.declareProtected('WebDAV Lock items', 'wl_setLock')
     security.declareProtected('WebDAV Unlock items', 'wl_delLock')


More information about the Zope-Dev mailing list