Commit bd048ce1 authored by Tres Seaver's avatar Tres Seaver

CatalogBrains' 'getObject' now raises by default.

o Old "return None" behavior can be restored with a deprecated ZConfig
  option.
parent 82599e57
...@@ -28,6 +28,15 @@ Zope Changes ...@@ -28,6 +28,15 @@ Zope Changes
Features added Features added
- ZCatalog.CatalogBrains: 'getObject' now raises errors, rather than
returning None, in cases where the path points either to a nonexistent
object (in which case it raises NotFound) or to one which the user
cannot access (raising Unauthorized). Sites which rely on the old
behavior can restore setting a new zope.conf option,
'catalog-getObject-raises', to "off".
This compatibility option will be removed in Zope 2.10.
- PluginIndexes: the ZCatalog's "Indexes" tab now show the number of - PluginIndexes: the ZCatalog's "Indexes" tab now show the number of
distinct values indexed by each index instead of a mixture of indexed distinct values indexed by each index instead of a mixture of indexed
objects versus number of distinct values. Indexes derived from UnIndex objects versus number of distinct values. Indexes derived from UnIndex
...@@ -63,6 +72,8 @@ Zope Changes ...@@ -63,6 +72,8 @@ Zope Changes
Bugs fixed Bugs fixed
- OFS.Traversable still used a string 'NotFound' exception.
- ZPublisher would fail to recognize a XML-RPC request if the - ZPublisher would fail to recognize a XML-RPC request if the
content-type header included a 'charset' parameter. content-type header included a 'charset' parameter.
......
...@@ -21,7 +21,7 @@ from AccessControl.ZopeGuards import guarded_getattr ...@@ -21,7 +21,7 @@ from AccessControl.ZopeGuards import guarded_getattr
from ZODB.POSException import ConflictError from ZODB.POSException import ConflictError
from urllib import quote from urllib import quote
NotFound = 'NotFound' from zExceptions import NotFound
_marker = object() _marker = object()
......
...@@ -14,6 +14,13 @@ ...@@ -14,6 +14,13 @@
__version__ = "$Revision$"[11:-2] __version__ = "$Revision$"[11:-2]
import Acquisition, Record import Acquisition, Record
from zExceptions import NotFound
from zExceptions import Unauthorized
from ZODB.POSException import ConflictError
# Switch for new behavior, raise NotFound instead of returning None.
# Use 'catalog-getOb-raises off' in zope.conf to restore old behavior.
GETOBJECT_RAISES = True
class AbstractCatalogBrain(Record.Record, Acquisition.Implicit): class AbstractCatalogBrain(Record.Record, Acquisition.Implicit):
"""Abstract base brain that handles looking up attributes as """Abstract base brain that handles looking up attributes as
...@@ -54,10 +61,25 @@ class AbstractCatalogBrain(Record.Record, Acquisition.Implicit): ...@@ -54,10 +61,25 @@ class AbstractCatalogBrain(Record.Record, Acquisition.Implicit):
return None return None
parent = self.aq_parent parent = self.aq_parent
if len(path) > 1: if len(path) > 1:
parent = parent.unrestrictedTraverse('/'.join(path[:-1]), None) try:
if parent is None: parent = parent.unrestrictedTraverse(path[:-1])
except ConflictError:
raise
except:
if GETOBJECT_RAISES:
raise
return None
try:
target = parent.restrictedTraverse(path[-1])
except ConflictError:
raise
except:
if GETOBJECT_RAISES:
raise
return None return None
return parent.restrictedTraverse(path[-1], None)
return target
def getRID(self): def getRID(self):
"""Return the record ID for this object.""" """Return the record ID for this object."""
......
...@@ -56,7 +56,8 @@ class DummyCatalog(Acquisition.Implicit): ...@@ -56,7 +56,8 @@ class DummyCatalog(Acquisition.Implicit):
# This is sooooo ugly # This is sooooo ugly
def unrestrictedTraverse(self, path, default=None): def unrestrictedTraverse(self, path, default=None):
assert path == '' # for these tests... # for these tests...
assert path == '' or path == ('') or path == [''], path
return self return self
def restrictedTraverse(self, path, default=_marker): def restrictedTraverse(self, path, default=_marker):
...@@ -66,7 +67,7 @@ class DummyCatalog(Acquisition.Implicit): ...@@ -66,7 +67,7 @@ class DummyCatalog(Acquisition.Implicit):
ob = self._objs[path].__of__(self) ob = self._objs[path].__of__(self)
ob.check() ob.check()
return ob return ob
except (KeyError, Unauthorized): except KeyError:
if default is not _marker: if default is not _marker:
return default return default
raise raise
...@@ -86,60 +87,99 @@ class ConflictingCatalog(DummyCatalog): ...@@ -86,60 +87,99 @@ class ConflictingCatalog(DummyCatalog):
def getpath(self, rid): def getpath(self, rid):
raise ConflictError raise ConflictError
class TestBrains(unittest.TestCase): class BrainsTestBase:
_old_flag = None
def setUp(self): def setUp(self):
self.cat = DummyCatalog() self.cat = DummyCatalog()
self.cat.REQUEST = DummyRequest() self.cat.REQUEST = DummyRequest()
self._init_getOb_flag()
def tearDown(self):
if self._old_flag is not None:
self._restore_getOb_flag()
def _init_getOb_flag(self):
from Products.ZCatalog import CatalogBrains
self._old_flag = CatalogBrains.GETOBJECT_RAISES
CatalogBrains.GETOBJECT_RAISES = self._flag_value()
def _restore_getOb_flag(self):
from Products.ZCatalog import CatalogBrains
CatalogBrains.GETOBJECT_RAISES = self._old_flag
def makeBrain(self, rid): def _makeBrain(self, rid):
from Products.ZCatalog.CatalogBrains import AbstractCatalogBrain from Products.ZCatalog.CatalogBrains import AbstractCatalogBrain
class Brain(AbstractCatalogBrain): class Brain(AbstractCatalogBrain):
__record_schema__ = {'test_field': 0, 'data_record_id_':1} __record_schema__ = {'test_field': 0, 'data_record_id_':1}
return Brain(('test', rid)).__of__(self.cat) return Brain(('test', rid)).__of__(self.cat)
def testHasKey(self): def testHasKey(self):
b = self.makeBrain(1) b = self._makeBrain(1)
self.failUnless(b.has_key('test_field')) self.failUnless(b.has_key('test_field'))
self.failUnless(b.has_key('data_record_id_')) self.failUnless(b.has_key('data_record_id_'))
self.failIf(b.has_key('godel')) self.failIf(b.has_key('godel'))
def testGetPath(self): def testGetPath(self):
b = [self.makeBrain(rid) for rid in range(3)] b = [self._makeBrain(rid) for rid in range(3)]
self.assertEqual(b[0].getPath(), '/conflicter') self.assertEqual(b[0].getPath(), '/conflicter')
self.assertEqual(b[1].getPath(), '/happy') self.assertEqual(b[1].getPath(), '/happy')
self.assertEqual(b[2].getPath(), '/secret') self.assertEqual(b[2].getPath(), '/secret')
def testGetPathPropagatesConflictErrors(self): def testGetPathPropagatesConflictErrors(self):
self.cat = ConflictingCatalog() self.cat = ConflictingCatalog()
b = self.makeBrain(0) b = self._makeBrain(0)
self.assertRaises(ConflictError, b.getPath) self.assertRaises(ConflictError, b.getPath)
def testGetURL(self): def testGetURL(self):
b = self.makeBrain(0) b = self._makeBrain(0)
self.assertEqual(b.getURL(), 'http://superbad.com/conflicter') self.assertEqual(b.getURL(), 'http://superbad.com/conflicter')
def testGetRID(self): def testGetRID(self):
b = self.makeBrain(42) b = self._makeBrain(42)
self.assertEqual(b.getRID(), 42) self.assertEqual(b.getRID(), 42)
def testGetObjectHappy(self): def testGetObjectHappy(self):
b = self.makeBrain(1) b = self._makeBrain(1)
self.assertEqual(b.getPath(), '/happy') self.assertEqual(b.getPath(), '/happy')
self.failUnless(b.getObject().aq_base is self.cat.getobject(1).aq_base) self.failUnless(b.getObject().aq_base is self.cat.getobject(1).aq_base)
def testGetObjectPropagatesConflictErrors(self): def testGetObjectPropagatesConflictErrors(self):
b = self.makeBrain(0) b = self._makeBrain(0)
self.assertEqual(b.getPath(), '/conflicter') self.assertEqual(b.getPath(), '/conflicter')
self.assertRaises(ConflictError, b.getObject) self.assertRaises(ConflictError, b.getObject)
class TestBrains(BrainsTestBase, unittest.TestCase):
def _flag_value(self):
return True
def testGetObjectRaisesUnauthorized(self):
from zExceptions import Unauthorized
b = self._makeBrain(2)
self.assertEqual(b.getPath(), '/secret')
self.assertRaises(Unauthorized, b.getObject)
def testGetObjectRaisesNotFoundForMissing(self):
from zExceptions import NotFound
b = self._makeBrain(3)
self.assertEqual(b.getPath(), '/zonked')
self.assertRaises(KeyError, self.cat.getobject, 3)
self.assertRaises((NotFound, AttributeError, KeyError), b.getObject)
class TestBrainsOldBehavior(BrainsTestBase, unittest.TestCase):
def _flag_value(self):
return False
def testGetObjectReturnsNoneForUnauthorized(self): def testGetObjectReturnsNoneForUnauthorized(self):
b = self.makeBrain(2) b = self._makeBrain(2)
self.assertEqual(b.getPath(), '/secret') self.assertEqual(b.getPath(), '/secret')
self.assertEqual(b.getObject(), None) self.assertEqual(b.getObject(), None)
def testGetObjectReturnsNoneForMissing(self): def testGetObjectReturnsNoneForMissing(self):
b = self.makeBrain(3) b = self._makeBrain(3)
self.assertEqual(b.getPath(), '/zonked') self.assertEqual(b.getPath(), '/zonked')
self.assertRaises(KeyError, self.cat.getobject, 3) self.assertRaises(KeyError, self.cat.getobject, 3)
self.assertEqual(b.getObject(), None) self.assertEqual(b.getObject(), None)
...@@ -147,6 +187,7 @@ class TestBrains(unittest.TestCase): ...@@ -147,6 +187,7 @@ class TestBrains(unittest.TestCase):
def test_suite(): def test_suite():
suite = unittest.TestSuite() suite = unittest.TestSuite()
suite.addTest(unittest.makeSuite(TestBrains)) suite.addTest(unittest.makeSuite(TestBrains))
suite.addTest(unittest.makeSuite(TestBrainsOldBehavior))
return suite return suite
if __name__ == '__main__': if __name__ == '__main__':
......
...@@ -590,6 +590,8 @@ class PickySecurityManager: ...@@ -590,6 +590,8 @@ class PickySecurityManager:
class TestZCatalogGetObject(unittest.TestCase): class TestZCatalogGetObject(unittest.TestCase):
# Test what objects are returned by brain.getObject() # Test what objects are returned by brain.getObject()
_old_flag = None
def setUp(self): def setUp(self):
from Products.ZCatalog.ZCatalog import ZCatalog from Products.ZCatalog.ZCatalog import ZCatalog
catalog = ZCatalog('catalog') catalog = ZCatalog('catalog')
...@@ -601,6 +603,17 @@ class TestZCatalogGetObject(unittest.TestCase): ...@@ -601,6 +603,17 @@ class TestZCatalogGetObject(unittest.TestCase):
def tearDown(self): def tearDown(self):
noSecurityManager() noSecurityManager()
if self._old_flag is not None:
self._restore_getObject_flag()
def _init_getObject_flag(self, flag):
from Products.ZCatalog import CatalogBrains
self._old_flag = CatalogBrains.GETOBJECT_RAISES
CatalogBrains.GETOBJECT_RAISES = flag
def _restore_getObject_flag(self):
from Products.ZCatalog import CatalogBrains
CatalogBrains.GETOBJECT_RAISES = self._old_flag
def test_getObject_found(self): def test_getObject_found(self):
# Check normal traversal # Check normal traversal
...@@ -612,8 +625,47 @@ class TestZCatalogGetObject(unittest.TestCase): ...@@ -612,8 +625,47 @@ class TestZCatalogGetObject(unittest.TestCase):
self.assertEqual(brain.getPath(), '/ob') self.assertEqual(brain.getPath(), '/ob')
self.assertEqual(brain.getObject().getId(), 'ob') self.assertEqual(brain.getObject().getId(), 'ob')
def test_getObject_missing(self): def test_getObject_missing_raises_NotFound(self):
# Check that if the object is missing None is returned
from zExceptions import NotFound
self._init_getObject_flag(True)
root = self.root
catalog = root.catalog
root.ob = Folder('ob')
catalog.catalog_object(root.ob)
brain = catalog.searchResults()[0]
del root.ob
self.assertRaises((NotFound, AttributeError, KeyError), brain.getObject)
def test_getObject_restricted_raises_Unauthorized(self):
# Check that if the object's security does not allow traversal,
# None is returned
from zExceptions import NotFound
self._init_getObject_flag(True)
root = self.root
catalog = root.catalog
root.fold = Folder('fold')
root.fold.ob = Folder('ob')
catalog.catalog_object(root.fold.ob)
brain = catalog.searchResults()[0]
# allow all accesses
pickySecurityManager = PickySecurityManager()
setSecurityManager(pickySecurityManager)
self.assertEqual(brain.getObject().getId(), 'ob')
# disallow just 'ob' access
pickySecurityManager = PickySecurityManager(['ob'])
setSecurityManager(pickySecurityManager)
self.assertRaises(Unauthorized, brain.getObject)
# disallow just 'fold' access
pickySecurityManager = PickySecurityManager(['fold'])
setSecurityManager(pickySecurityManager)
ob = brain.getObject()
self.failIf(ob is None)
self.assertEqual(ob.getId(), 'ob')
def test_getObject_missing_returns_None(self):
# Check that if the object is missing None is returned # Check that if the object is missing None is returned
self._init_getObject_flag(False)
root = self.root root = self.root
catalog = root.catalog catalog = root.catalog
root.ob = Folder('ob') root.ob = Folder('ob')
...@@ -622,9 +674,10 @@ class TestZCatalogGetObject(unittest.TestCase): ...@@ -622,9 +674,10 @@ class TestZCatalogGetObject(unittest.TestCase):
del root.ob del root.ob
self.assertEqual(brain.getObject(), None) self.assertEqual(brain.getObject(), None)
def test_getObject_restricted(self): def test_getObject_restricted_returns_None(self):
# Check that if the object's security does not allow traversal, # Check that if the object's security does not allow traversal,
# None is returned # None is returned
self._init_getObject_flag(False)
root = self.root root = self.root
catalog = root.catalog catalog = root.catalog
root.fold = Folder('fold') root.fold = Folder('fold')
......
...@@ -124,6 +124,20 @@ def cgi_maxlen(value): ...@@ -124,6 +124,20 @@ def cgi_maxlen(value):
def http_header_max_length(value): def http_header_max_length(value):
return value return value
def catalog_getObject_raises(value):
if value is not None:
import warnings
warnings.warn(
"'catalog-getObject-raises' option will be removed in Zope 2.10:\n",
DeprecationWarning)
from Products.ZCatalog import CatalogBrains
CatalogBrains.GETOBJECT_RAISES = bool(value)
return value
# server handlers # server handlers
def root_handler(config): def root_handler(config):
......
...@@ -768,6 +768,17 @@ ...@@ -768,6 +768,17 @@
</description> </description>
</key> </key>
<key name="catalog-getObject-raises" datatype="boolean"
handler="catalog_getObject_raises">
<description>
If this directive is set to "on" (the deafult), ZCatalog brains objects
will raise NotFound exceptions from 'getObject' for unreachable objects,
and Unauthorized for disallowed objects. If the option is "off", they
will return None in such cases (which was the old behavior)
</description>
<metadefault>off</metadefault>
</key>
<multisection type="ZServer.server" name="*" attribute="servers"/> <multisection type="ZServer.server" name="*" attribute="servers"/>
<key name="port-base" datatype="integer" default="0"> <key name="port-base" datatype="integer" default="0">
<description> <description>
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment