[Zope-dev] Improvements for Zope2's security
Chris McDonough
chrism at plope.com
Mon Sep 18 20:50:57 EDT 2006
I think it's great that you did this... nice job! I have some
specific disagreements (while I think it's a reasonable constraint,
and I think something should enforce it, I don't believe it's the job
of something that we call a *security policy* to enforce whether a
method is called, e.g. via POST rather than GET), I think you did a
wonderful job analyzing this.. +1 on the developer-helper tools
portion of this that finds declarations that have no corresponding
method.
Thanks!
- C
On Sep 18, 2006, at 11:20 AM, Christian Heimes wrote:
> 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
> 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')
> _______________________________________________
> Zope-Dev maillist - Zope-Dev at zope.org
> http://mail.zope.org/mailman/listinfo/zope-dev
> ** No cross posts or HTML encoding! **
> (Related lists -
> http://mail.zope.org/mailman/listinfo/zope-announce
> http://mail.zope.org/mailman/listinfo/zope )
More information about the Zope-Dev
mailing list