[Checkins] SVN: grok/trunk/src/grok/ Added security for XML-RPC via the @grok.require decorator (turned grok.require

Philipp von Weitershausen philikon at philikon.de
Sun Jan 7 10:31:28 EST 2007


Log message for revision 71772:
  Added security for XML-RPC via the @grok.require decorator (turned grok.require
  into a hybrid of class-level directive and a decorator).
  
  Refactored directive base classes during the process.
  
  Added various safety belt tests.
  

Changed:
  U   grok/trunk/src/grok/directive.py
  U   grok/trunk/src/grok/ftests/security/require.py
  A   grok/trunk/src/grok/ftests/security/xmlrpc.py
  U   grok/trunk/src/grok/grokker.py
  U   grok/trunk/src/grok/meta.py
  U   grok/trunk/src/grok/publication.py
  A   grok/trunk/src/grok/tests/security/
  A   grok/trunk/src/grok/tests/security/__init__.py
  A   grok/trunk/src/grok/tests/security/missing_permission.py
  A   grok/trunk/src/grok/tests/security/multiple_require.py
  A   grok/trunk/src/grok/tests/security/multiple_require_xmlrpc.py
  A   grok/trunk/src/grok/tests/security/view_decorator.py
  U   grok/trunk/src/grok/tests/test_grok.py
  U   grok/trunk/src/grok/util.py

-=-
Modified: grok/trunk/src/grok/directive.py
===================================================================
--- grok/trunk/src/grok/directive.py	2007-01-07 15:25:52 UTC (rev 71771)
+++ grok/trunk/src/grok/directive.py	2007-01-07 15:31:27 UTC (rev 71772)
@@ -16,7 +16,7 @@
 
 import sys
 import inspect
-from zope import interface
+from zope import interface, component
 from zope.interface.interfaces import IInterface
 
 import grok
@@ -83,7 +83,7 @@
         self.check_directive_context(frame)
 
         value = self.value_factory(*args, **kw)
-        self.store(frame, value)
+        return self.store(frame, value)
 
     def check_arguments(self, *args, **kw):
         raise NotImplementedError
@@ -121,25 +121,23 @@
                                      self.directive_context.description))
         frame.f_locals[self.local_name] = value
 
-class SingleValueOnceDirective(OnceDirective):
-    def check_arguments(self, value):
-        raise NotImplementedError
+class MultipleTimesDirective(Directive):
+    def store(self, frame, value):
+        values = frame.f_locals.get(self.local_name, [])
+        values.append(value)
+        frame.f_locals[self.local_name] = values
 
+class SingleValue(object):
+
     # Even though the value_factory is called with (*args, **kw), we're safe
     # since check_arguments would have bailed out with a TypeError if the number
     # arguments we were called with was not what we expect here.
     def value_factory(self, value):
         return value
 
-class MultipleTimesDirective(Directive):
-    def store(self, frame, value):
-        values = frame.f_locals.get(self.local_name, [])
-        values.append(value)
-        frame.f_locals[self.local_name] = values
-
-class TextDirective(SingleValueOnceDirective):
+class BaseTextDirective(object):
     """
-    Directive that only accepts unicode/ASCII values.
+    Base directive that only accepts unicode/ASCII values.
     """
 
     def check_arguments(self, value):
@@ -147,8 +145,19 @@
             raise GrokImportError("You can only pass unicode or ASCII to "
                                   "%s." % self.name)
 
-class InterfaceOrClassDirective(SingleValueOnceDirective):
+class SingleTextDirective(BaseTextDirective, SingleValue, OnceDirective):
     """
+    Directive that accepts a single unicode/ASCII value, only once.
+    """
+
+class MultipleTextDirective(BaseTextDirective, SingleValue,
+                            MultipleTimesDirective):
+    """
+    Directive that accepts a single unicode/ASCII value, multiple times.
+    """
+
+class InterfaceOrClassDirective(SingleValue, OnceDirective):
+    """
     Directive that only accepts classes or interface values.
     """
 
@@ -157,7 +166,7 @@
             raise GrokImportError("You can only pass classes or interfaces to "
                                   "%s." % self.name)
 
-class InterfaceDirective(SingleValueOnceDirective):
+class InterfaceDirective(SingleValue, OnceDirective):
     """
     Directive that only accepts interface values.
     """
@@ -210,17 +219,33 @@
         self.hide = hide
         self.name_in_container = name_in_container
 
+class RequireDirective(BaseTextDirective, SingleValue, MultipleTimesDirective):
+
+    def store(self, frame, value):
+        super(RequireDirective, self).store(frame, value)
+        values = frame.f_locals.get(self.local_name, [])
+
+        # grok.require can be used both as a class-level directive and
+        # as a decorator for methods.  Therefore we return a decorator
+        # here, which may be used for methods, or simply ignored when
+        # used as a directive.
+        def decorator(func):
+            permission = values.pop()
+            func.__grok_require__ = permission
+            return func
+        return decorator
+
 # Define grok directives
-name = TextDirective('grok.name', ClassDirectiveContext())
-template = TextDirective('grok.template', ClassDirectiveContext())
+name = SingleTextDirective('grok.name', ClassDirectiveContext())
+template = SingleTextDirective('grok.template', ClassDirectiveContext())
 context = InterfaceOrClassDirective('grok.context',
                                     ClassOrModuleDirectiveContext())
-templatedir = TextDirective('grok.templatedir', ModuleDirectiveContext())
+templatedir = SingleTextDirective('grok.templatedir', ModuleDirectiveContext())
 provides = InterfaceDirective('grok.provides', ClassDirectiveContext())
 global_utility = GlobalUtilityDirective('grok.global_utility',
                                         ModuleDirectiveContext())
 local_utility = LocalUtilityDirective('grok.local_utility',
                                       ClassDirectiveContext())
-define_permission = TextDirective('grok.define_permission',
-                                  ModuleDirectiveContext())
-require = TextDirective('grok.require', ClassDirectiveContext())
+define_permission = MultipleTextDirective('grok.define_permission',
+                                          ModuleDirectiveContext())
+require = RequireDirective('grok.require', ClassDirectiveContext())

Modified: grok/trunk/src/grok/ftests/security/require.py
===================================================================
--- grok/trunk/src/grok/ftests/security/require.py	2007-01-07 15:25:52 UTC (rev 71771)
+++ grok/trunk/src/grok/ftests/security/require.py	2007-01-07 15:31:27 UTC (rev 71772)
@@ -22,6 +22,12 @@
   >>> print browser.contents
   What a beautiful painting.
 
+A view protected with 'zope.Public' is always accessible:
+
+  >>> browser = Browser()
+  >>> browser.open("http://localhost/@@publicnudity")
+  >>> print browser.contents
+  Everybody can see this.
 """
 
 import grok
@@ -36,3 +42,11 @@
 
     def render(self):
         return 'What a beautiful painting.'
+
+class PublicNudity(grok.View):
+
+    grok.context(zope.interface.Interface)
+    grok.require('zope.Public')
+
+    def render(self):
+        return 'Everybody can see this.'

Added: grok/trunk/src/grok/ftests/security/xmlrpc.py
===================================================================
--- grok/trunk/src/grok/ftests/security/xmlrpc.py	2007-01-07 15:25:52 UTC (rev 71771)
+++ grok/trunk/src/grok/ftests/security/xmlrpc.py	2007-01-07 15:31:27 UTC (rev 71772)
@@ -0,0 +1,67 @@
+"""
+  >>> import grok
+  >>> grok.grok('grok.ftests.security.xmlrpc')
+
+  >>> from grok.ftests.xmlrpc_helper import ServerProxy
+
+  >>> server = ServerProxy("http://localhost/")
+  >>> mgr_server = ServerProxy("http://mgr:mgrpw@localhost/")
+
+We can access a public method just fine, but a protected method will
+raise Unauthorized:
+
+  >>> print server.stomp()
+  Manfred stomped.
+
+  >>> print server.dance()
+  Traceback (most recent call last):
+  ProtocolError: <ProtocolError for localhost/: 401 401 Unauthorized>
+
+With manager privileges, the protected method is accessible, however:
+
+  >>> print mgr_server.dance()  
+  Manfred doesn't like to dance.
+
+The same applies when a default permission is defined for all XML-RPC
+methods in a class:
+
+  >>> print server.hunt()
+  Traceback (most recent call last):
+  ProtocolError: <ProtocolError for localhost/: 401 401 Unauthorized>
+
+  >>> print mgr_server.hunt()
+  ME GROK LIKE MAMMOTH!
+
+  >>> print server.eat()
+  MMM, MANFRED TASTE GOOD!
+
+  >>> print server.rest()
+  ME GROK TIRED!
+"""
+import grok
+import zope.interface
+
+class MammothRPC(grok.XMLRPC):
+    grok.context(zope.interface.Interface)
+
+    def stomp(self):
+        return 'Manfred stomped.'
+
+    @grok.require('zope.ManageContent')
+    def dance(self):
+        return 'Manfred doesn\'t like to dance.'
+
+class CavemanRPC(grok.XMLRPC):
+    grok.context(zope.interface.Interface)
+    grok.require('zope.ManageContent')
+
+    def hunt(self):
+        return 'ME GROK LIKE MAMMOTH!'
+
+    @grok.require('zope.View')
+    def eat(self):
+        return 'MMM, MANFRED TASTE GOOD!'
+
+    @grok.require('zope.Public')
+    def rest(self):
+        return 'ME GROK TIRED!'


Property changes on: grok/trunk/src/grok/ftests/security/xmlrpc.py
___________________________________________________________________
Name: svn:eol-style
   + native

Modified: grok/trunk/src/grok/grokker.py
===================================================================
--- grok/trunk/src/grok/grokker.py	2007-01-07 15:25:52 UTC (rev 71771)
+++ grok/trunk/src/grok/grokker.py	2007-01-07 15:31:27 UTC (rev 71772)
@@ -42,6 +42,7 @@
             # lookup?
             for grokker in grokkers:
                 if grokker.match(obj):
+                    from grok.meta import XMLRPCGrokker
                     components[grokker.component_class].append((name, obj))
                     if not grokker.continue_scanning:
                         break

Modified: grok/trunk/src/grok/meta.py
===================================================================
--- grok/trunk/src/grok/meta.py	2007-01-07 15:25:52 UTC (rev 71771)
+++ grok/trunk/src/grok/meta.py	2007-01-07 15:31:27 UTC (rev 71772)
@@ -1,5 +1,4 @@
 import os
-import inspect
 
 import zope.component.interface
 from zope import interface, component
@@ -9,6 +8,7 @@
 from zope.publisher.interfaces.xmlrpc import IXMLRPCRequest
 from zope.security.checker import NamesChecker, defineChecker
 from zope.security.permission import Permission
+from zope.security.interfaces import IPermission
 
 from zope.app.publisher.xmlrpc import MethodPublisher
 from zope.app.container.interfaces import INameChooser
@@ -17,6 +17,8 @@
 from grok import util, components, formlib
 from grok.error import GrokError
 
+
+
 class ModelGrokker(grok.ClassGrokker):
     component_class = grok.Model
 
@@ -69,9 +71,22 @@
 
     def register(self, context, name, factory, module_info, templates):
         view_context = util.determine_class_context(factory, context)
-        candidates = [getattr(factory, name) for name in dir(factory)]
-        methods = [c for c in candidates if inspect.ismethod(c)]
+        # XXX We should really not make __FOO__ methods available to
+        # the outside -- need to discuss how to restrict such things.
+        methods = util.methods_from_class(factory)
 
+        # Determine the default permission for the XMLRPC methods.
+        # There can only be 0 or 1 of those.
+        permissions = util.class_annotation(factory, 'grok.require', [])
+        if not permissions:
+            default_permission = None
+        elif len(permissions) == 1:
+            default_permission = permissions[0]
+        else:
+            raise GrokError('grok.require was called multiple times in '
+                            '%r. It may only be called once on class level.'
+                            % factory, factory)
+
         for method in methods:
             # Make sure that the class inherits MethodPublisher, so that the
             # views have a location
@@ -84,7 +99,14 @@
                 interface.Interface,
                 name=method.__name__)
 
-            checker = NamesChecker(['__call__'])
+            # Protect method_view with either the permission that was
+            # set on the method, the default permission from the class
+            # level or zope.Public.
+            permission = getattr(method, '__grok_require__', default_permission)
+            if permission is None or permission == 'zope.Public':
+                checker = NamesChecker(['__call__'])
+            else:
+                checker = NamesChecker(['__call__'], permission)
             defineChecker(method_view, checker)
 
 class ViewGrokker(grok.ClassGrokker):
@@ -166,13 +188,35 @@
                                  name=view_name)
 
         # protect view, public by default
-        permission = util.class_annotation(factory, 'grok.require', None)
-        if permission is None:
+        permissions = util.class_annotation(factory, 'grok.require', [])
+        if not permissions:
             checker = NamesChecker(['__call__'])
+        elif len(permissions) > 1:
+            raise GrokError('grok.require was called multiple times in view '
+                            '%r. It may only be called once.' % factory,
+                            factory)
+        elif permissions[0] == 'zope.Public':
+            checker = NamesChecker(['__call__'])
         else:
-            checker = NamesChecker(['__call__'], permission)
+            perm = permissions[0]
+            if component.queryUtility(IPermission, name=perm) is None:
+                raise GrokError('Undefined permission %r in view %r. Use '
+                                'grok.define_permission first.'
+                                % (perm, factory), factory)
+            checker = NamesChecker(['__call__'], permissions[0])
+
         defineChecker(factory, checker)
 
+        # safety belt: make sure that the programmer didn't use
+        # @grok.require on any of the view's methods.
+        methods = util.methods_from_class(factory)
+        for method in methods:
+            if getattr(method, '__grok_require__', None) is not None:
+                raise GrokError('The @grok.require decorator is used for '
+                                'method %r in view %r. It may only be used '
+                                'for XML-RPC methods.'
+                                % (method.__name__, factory), factory)
+
 class TraverserGrokker(grok.ClassGrokker):
     component_class = grok.Traverser
 
@@ -322,6 +366,8 @@
 
 class DefinePermissionGrokker(grok.ModuleGrokker):
 
+    priority = 1500
+
     def register(self, context, module_info, templates):
         permissions = module_info.getAnnotation('grok.define_permission', [])
         for permission in permissions:

Modified: grok/trunk/src/grok/publication.py
===================================================================
--- grok/trunk/src/grok/publication.py	2007-01-07 15:25:52 UTC (rev 71771)
+++ grok/trunk/src/grok/publication.py	2007-01-07 15:31:27 UTC (rev 71772)
@@ -37,7 +37,6 @@
     def callObject(self, request, ob):
         checker = selectChecker(ob)
         if checker is not None:
-            #import pdb; pdb.set_trace()
             checker.check(ob, '__call__')
         return super(ZopePublicationSansProxy, self).callObject(request, ob)
 

Added: grok/trunk/src/grok/tests/security/__init__.py
===================================================================
--- grok/trunk/src/grok/tests/security/__init__.py	2007-01-07 15:25:52 UTC (rev 71771)
+++ grok/trunk/src/grok/tests/security/__init__.py	2007-01-07 15:31:27 UTC (rev 71772)
@@ -0,0 +1 @@
+# make this directory a package


Property changes on: grok/trunk/src/grok/tests/security/__init__.py
___________________________________________________________________
Name: svn:eol-style
   + native

Added: grok/trunk/src/grok/tests/security/missing_permission.py
===================================================================
--- grok/trunk/src/grok/tests/security/missing_permission.py	2007-01-07 15:25:52 UTC (rev 71771)
+++ grok/trunk/src/grok/tests/security/missing_permission.py	2007-01-07 15:31:27 UTC (rev 71772)
@@ -0,0 +1,19 @@
+"""
+A permission has to be defined first (using grok.define_permission for
+example) before it can be used in grok.require().
+
+  >>> grok.grok(__name__)
+  Traceback (most recent call last):
+  GrokError: Undefined permission 'doesnt.exist' in view <class 'grok.tests.security.missing_permission.MissingPermission'>. Use grok.define_permission first.
+
+"""
+
+import grok
+import zope.interface
+
+class MissingPermission(grok.View):
+    grok.context(zope.interface.Interface)
+    grok.require('doesnt.exist')
+
+    def render(self):
+        pass


Property changes on: grok/trunk/src/grok/tests/security/missing_permission.py
___________________________________________________________________
Name: svn:eol-style
   + native

Added: grok/trunk/src/grok/tests/security/multiple_require.py
===================================================================
--- grok/trunk/src/grok/tests/security/multiple_require.py	2007-01-07 15:25:52 UTC (rev 71771)
+++ grok/trunk/src/grok/tests/security/multiple_require.py	2007-01-07 15:31:27 UTC (rev 71772)
@@ -0,0 +1,21 @@
+"""
+Multiple calls of grok.require in one class are not allowed.
+
+  >>> grok.grok(__name__)
+  Traceback (most recent call last):
+  GrokError: grok.require was called multiple times in view <class 'grok.tests.security.multiple_require.MultipleView'>. It may only be called once.
+
+"""
+import grok
+import zope.interface
+
+grok.define_permission('permission.1')
+grok.define_permission('permission.2')
+
+class MultipleView(grok.View):
+    grok.context(zope.interface.Interface)
+    grok.require('permission.1')
+    grok.require('permission.2')
+
+    def render(self):
+        pass


Property changes on: grok/trunk/src/grok/tests/security/multiple_require.py
___________________________________________________________________
Name: svn:eol-style
   + native

Added: grok/trunk/src/grok/tests/security/multiple_require_xmlrpc.py
===================================================================
--- grok/trunk/src/grok/tests/security/multiple_require_xmlrpc.py	2007-01-07 15:25:52 UTC (rev 71771)
+++ grok/trunk/src/grok/tests/security/multiple_require_xmlrpc.py	2007-01-07 15:31:27 UTC (rev 71772)
@@ -0,0 +1,21 @@
+"""
+Multiple calls of grok.require in one class are not allowed.
+
+  >>> grok.grok(__name__)
+  Traceback (most recent call last):
+  GrokError: grok.require was called multiple times in <class 'grok.tests.security.multiple_require_xmlrpc.MultipleXMLRPC'>. It may only be called once on class level.
+
+"""
+import grok
+import zope.interface
+
+grok.define_permission('permission.1')
+grok.define_permission('permission.2')
+
+class MultipleXMLRPC(grok.XMLRPC):
+    grok.context(zope.interface.Interface)
+    grok.require('permission.1')
+    grok.require('permission.2')
+
+    def render(self):
+        pass


Property changes on: grok/trunk/src/grok/tests/security/multiple_require_xmlrpc.py
___________________________________________________________________
Name: svn:eol-style
   + native

Added: grok/trunk/src/grok/tests/security/view_decorator.py
===================================================================
--- grok/trunk/src/grok/tests/security/view_decorator.py	2007-01-07 15:25:52 UTC (rev 71771)
+++ grok/trunk/src/grok/tests/security/view_decorator.py	2007-01-07 15:31:27 UTC (rev 71772)
@@ -0,0 +1,21 @@
+"""
+Using the @grok.require decorator in a view class is not allowed.
+
+  >>> grok.grok(__name__)
+  Traceback (most recent call last):
+  GrokError: The @grok.require decorator is used for method 'render' in view <class 'grok.tests.security.view_decorator.BogusView'>. It may only be used for XML-RPC methods.
+
+
+"""
+
+import grok
+import zope.interface
+
+grok.define_permission('bogus.perm')
+
+class BogusView(grok.View):
+    grok.context(zope.interface.Interface)
+
+    @grok.require('bogus.perm')
+    def render(self):
+        pass


Property changes on: grok/trunk/src/grok/tests/security/view_decorator.py
___________________________________________________________________
Name: svn:eol-style
   + native

Modified: grok/trunk/src/grok/tests/test_grok.py
===================================================================
--- grok/trunk/src/grok/tests/test_grok.py	2007-01-07 15:25:52 UTC (rev 71771)
+++ grok/trunk/src/grok/tests/test_grok.py	2007-01-07 15:31:27 UTC (rev 71772)
@@ -32,7 +32,7 @@
 
 def test_suite():
     suite = unittest.TestSuite()
-    for name in ['adapter', 'error', 'view', 'scan', 'event',
+    for name in ['adapter', 'error', 'view', 'scan', 'event', 'security',
                  'zcml', 'static', 'utility', 'xmlrpc', 'container',
                  'traversal', 'form', 'site', 'grokker', 'directive']:
         suite.addTest(suiteFromPackage(name))

Modified: grok/trunk/src/grok/util.py
===================================================================
--- grok/trunk/src/grok/util.py	2007-01-07 15:25:52 UTC (rev 71771)
+++ grok/trunk/src/grok/util.py	2007-01-07 15:31:27 UTC (rev 71772)
@@ -17,6 +17,7 @@
 import re
 import types
 import sys
+import inspect
 
 from zope import component
 from zope import interface
@@ -110,3 +111,12 @@
     context = class_annotation(class_, 'grok.context', module_context)
     check_context(class_, context)
     return context
+
+
+def methods_from_class(class_):
+    # XXX Problem with zope.interface here that makes us special-case
+    # __provides__.
+    candidates = [getattr(class_, name) for name in dir(class_)
+                  if name != '__provides__' ]
+    methods = [c for c in candidates if inspect.ismethod(c)]
+    return methods



More information about the Checkins mailing list