[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