Commit 12032013 authored by Jérome Perrin's avatar Jérome Perrin

Restricted: bug fixes, support generator and collections module

Add some features to restricted python and fix problems revealed by running Zope's test suites.

# Bug fixes

 - Disallow access to old style classes without security declarations. This is not allowed in vanilla zope, but we allowed this accidentally about two years ago. This branch includes some fixes for cases where we accessed not protected classes in a way that should not have been allowed - ERP5 test suite pass, but there might be more cases in code not covered by ERP5 test suite.
 - Fix iterating on `reversed(iterable)` which was unauthorized, maybe since python 2.7
 - Disallow new style classes in container access (iteration, `{}.get` etc). Only classes had this problem, not instances, so this probably has no impact for us, but it allows running AccessControl test suite.
 - Disallow attribute names ending in `__roles__` in class name. This probably does not impact us either, but also for AccessControl tests suite.


# New features

 - Allow iterating on a generator. It's still not possible to use `yield` statement in restricted python, but iterating is now possible
 - Allow `cStringIO.StringIO("initial value")`, only `cStringIO.StringIO()` was allowed
 - Enable `collections.namedtuple` and add a few tests for other members of `collections` ( not `collections.deque` because we never used it so far )

See merge request !1090
parents 24d45f1c 5b995163
......@@ -25,12 +25,18 @@
#
##############################################################################
import os.path
import tempfile
import textwrap
import unittest
import uuid
from Products.ERP5Type.tests.ERP5TypeTestCase import ERP5TypeTestCase
from Products.ERP5Type.tests.utils import createZODBPythonScript
from Products.ERP5Type.tests.utils import removeZODBPythonScript
from Products.ERP5Type.patches.Restricted import allow_class_attribute
from AccessControl import Unauthorized
import uuid
class TestRestrictedPythonSecurity(ERP5TypeTestCase):
"""
......@@ -40,9 +46,9 @@ class TestRestrictedPythonSecurity(ERP5TypeTestCase):
def getTitle(self):
return "Restricted Python Security Test"
def runScript(self, container, name):
def runScript(self, container, name, kwargs):
func = getattr(self.portal, name)
return func()
return func(**kwargs)
def createAndRunScript(self, *args, **kwargs):
# we do not care the script name for security test thus use uuid1
......@@ -53,9 +59,9 @@ class TestRestrictedPythonSecurity(ERP5TypeTestCase):
try:
createZODBPythonScript(script_container, name, '**kw', code)
if expected:
self.assertEqual(self.runScript(script_container, name), expected)
self.assertEqual(self.runScript(script_container, name, kwargs.get('kwargs', {})), expected)
else:
self.runScript(script_container, name)
self.runScript(script_container, name, kwargs.get('kwargs', {}))
finally:
removeZODBPythonScript(script_container, name)
......@@ -136,4 +142,398 @@ class TestRestrictedPythonSecurity(ERP5TypeTestCase):
self.assertRaises(Unauthorized,
self.createAndRunScript, 'import os',
'return os.system')
self.assertRaises(Unauthorized,
self.createAndRunScript, 'from os import system')
def test_set(self):
self.createAndRunScript(
textwrap.dedent('''\
s = set()
s.add(1)
s.clear()
s.copy()
s = set([1, 2])
s.difference([1])
s.difference_update([1])
s.discard(1)
s.intersection([1])
s.intersection_update([1])
s.isdisjoint([1])
s.issubset([1])
s.issuperset([1])
s.add(1)
s.pop()
s.add(1)
s.remove(1)
s.symmetric_difference([1])
s.symmetric_difference_update([1])
s.union([1])
s.update()
'''),
)
def test_frozenset(self):
self.createAndRunScript(
textwrap.dedent('''\
s = frozenset([1, 2])
s.copy()
s.difference([1])
s.intersection([1])
s.isdisjoint([1])
s.issubset([1])
s.issuperset([1])
s.symmetric_difference([1])
s.union([1])
'''),
)
def test_sorted(self):
self.createAndRunScript(
textwrap.dedent('''\
returned = []
for i in sorted([2, 3, 1]):
returned.append(i)
return returned
'''),
expected=[1, 2, 3],
)
def test_reversed(self):
self.createAndRunScript(
textwrap.dedent('''\
returned = []
for i in reversed(('3', '2', '1')):
returned.append(i)
return returned
'''),
expected=['1', '2', '3'],
)
self.createAndRunScript(
textwrap.dedent('''\
returned = []
for i in reversed([3, 2, 1]):
returned.append(i)
return returned
'''),
expected=[1, 2, 3],
)
self.createAndRunScript(
textwrap.dedent('''\
returned = []
for i in reversed('321'):
returned.append(i)
return returned
'''),
expected=['1', '2', '3'],
)
def test_enumerate(self):
self.createAndRunScript(
textwrap.dedent('''\
returned = []
for i in enumerate(["zero", "one", "two",]):
returned.append(i)
return returned
'''),
expected=[(0, "zero"), (1, "one"), (2, "two"), ],
)
# with start= argument
self.createAndRunScript(
textwrap.dedent('''\
returned = []
for i in enumerate(["one", "two", "three"], start=1):
returned.append(i)
return returned
'''),
expected=[(1, "one"), (2, "two"), (3, "three")],
)
def test_generator_iteration(self):
generator_iteration_script = textwrap.dedent(
'''\
result = []
for elem in kw['generator']:
result.append(elem)
return result
''')
class AllowedObject:
__allow_access_to_unprotected_subobjects__ = 1
allowed_object = AllowedObject()
class NotAllowedObject:
__roles__ = ()
not_allowed_object = NotAllowedObject()
def generator_with_allowed_objects():
yield 1
yield "two"
yield allowed_object
self.createAndRunScript(
generator_iteration_script,
kwargs={'generator': generator_with_allowed_objects()},
expected=[1, "two", allowed_object],
)
# generator expression
self.createAndRunScript(
generator_iteration_script,
kwargs={'generator': (x for x in [1, "two", allowed_object])},
expected=[1, "two", allowed_object],
)
def generator_with_not_allowed_objects():
yield "one"
yield not_allowed_object
yield 2
self.assertRaises(
Unauthorized,
self.createAndRunScript,
generator_iteration_script,
kwargs={'generator': generator_with_not_allowed_objects()},
)
self.createAndRunScript(
textwrap.dedent('''\
result = []
i = iter(kw['generator'])
for _ in range(100): # prevent infinite loop
try:
result.append(next(i))
except StopIteration:
break
except Exception as e:
result.append(repr(e))
return result
'''),
kwargs={'generator': generator_with_not_allowed_objects()},
expected=["one", "Unauthorized()", 2],
)
def test_json(self):
self.createAndRunScript(
textwrap.dedent('''\
import json
return json.loads(json.dumps({"ok": [True]}))
'''),
expected={"ok": [True]}
)
def test_calendar(self):
self.createAndRunScript(
textwrap.dedent('''\
import calendar
calendar.IllegalMonthError
calendar.IllegalWeekdayError
calendar.calendar(2020)
calendar.firstweekday()
calendar.isleap(2020)
calendar.leapdays(200, 2020)
calendar.month(2020, 1)
calendar.monthcalendar(2020, 1)
calendar.monthrange(2020, 1)
calendar.setfirstweekday(1)
calendar.timegm((2020, 1, 1, 0, 0, 0))
calendar.weekday(2020, 1, 1)
calendar.Calendar().getfirstweekday()
calendar.HTMLCalendar().getfirstweekday()
'''),
)
def test_collections_Counter(self):
self.createAndRunScript(
textwrap.dedent('''\
from collections import Counter
c = Counter(["a", "b"])
c["a"] = c["a"] + 1
del c["b"]
c.update({"a": 1})
return c.most_common(1)
'''),
expected=[('a', 3)]
)
def test_collections_defaultdict(self):
self.createAndRunScript(
textwrap.dedent('''\
from collections import defaultdict
d = defaultdict(list)
d["x"].append(1)
d["y"] = 2
del d["y"]
return d
'''),
expected={"x": [1]}
)
def test_collections_namedtuple(self):
self.createAndRunScript(
textwrap.dedent('''\
from collections import namedtuple
Object = namedtuple("Object", ["a", "b", "c"])
return Object(a=1, b=2, c=3).a
'''),
expected=1
)
# also make sure we can iterate on nametuples
self.createAndRunScript(
textwrap.dedent('''\
from collections import namedtuple
Object = namedtuple("Object", ["a", "b", "c"])
returned = []
for x in Object(a=1, b=2, c=3):
returned.append(x)
return returned
'''),
expected=[1, 2, 3]
)
def test_collections_OrderedDict(self):
self.createAndRunScript(
textwrap.dedent('''\
from collections import OrderedDict
d = OrderedDict()
d["a"] = 1
d["b"] = 2
d["c"] = 3
del d["c"]
return list(d.items())
'''),
expected=[("a", 1), ("b", 2)]
)
def test_lax_name(self):
self.createAndRunScript(
textwrap.dedent('''\
def _function():
pass
class SimpleObject:
def __init__(self):
self.attribute = 1
def _method(self):
_variable = 1
return SimpleObject().attribute
'''),
expected=1
)
def test_StringIO(self):
self.createAndRunScript(
textwrap.dedent('''\
import StringIO
s = StringIO.StringIO()
s.write("ok")
return s.getvalue()
'''),
expected="ok"
)
self.createAndRunScript(
textwrap.dedent('''\
import StringIO
return StringIO.StringIO("ok").getvalue()
'''),
expected="ok"
)
def test_cStringIO(self):
self.createAndRunScript(
textwrap.dedent('''\
import cStringIO
s = cStringIO.StringIO()
s.write("ok")
return s.getvalue()
'''),
expected="ok"
)
self.createAndRunScript(
textwrap.dedent('''\
import cStringIO
return cStringIO.StringIO("ok").getvalue()
'''),
expected="ok"
)
def test_suite():
suite = unittest.TestSuite()
suite.addTest(unittest.makeSuite(TestRestrictedPythonSecurity))
# Also run original tests of RestrictedPython, to confirm that our patches did not break
# original functionality
import RestrictedPython.tests.testCompile
suite.addTest(RestrictedPython.tests.testCompile.test_suite())
import RestrictedPython.tests.testUtiliities
suite.addTest(RestrictedPython.tests.testUtiliities.test_suite())
import RestrictedPython.tests.testREADME
suite.addTest(RestrictedPython.tests.testREADME.test_suite())
import RestrictedPython.tests.testRestrictions
suite.addTest(RestrictedPython.tests.testRestrictions.test_suite())
import AccessControl.tests.test_requestmethod
suite.addTest(AccessControl.tests.test_requestmethod.test_suite())
import AccessControl.tests.test_safeiter
suite.addTest(AccessControl.tests.test_safeiter.test_suite())
import AccessControl.tests.test_tainted
suite.addTest(AccessControl.tests.test_tainted.test_suite())
import AccessControl.tests.test_formatter
suite.addTest(unittest.makeSuite(AccessControl.tests.test_formatter.FormatterTest))
import AccessControl.tests.test_userfolder
suite.addTest(AccessControl.tests.test_userfolder.test_suite())
import AccessControl.tests.test_users
suite.addTest(AccessControl.tests.test_users.test_suite())
import AccessControl.tests.testClassSecurityInfo
suite.addTest(AccessControl.tests.testClassSecurityInfo.test_suite())
import AccessControl.tests.testImplementation
suite.addTest(AccessControl.tests.testImplementation.test_suite())
import AccessControl.tests.testModuleSecurity
# we allow part of os module, so adjust this test for another not allowed module
def test_unprotected_module(self):
self.assertUnauth('subprocess', ())
AccessControl.tests.testModuleSecurity.ModuleSecurityTests.test_unprotected_module = test_unprotected_module
suite.addTest(AccessControl.tests.testModuleSecurity.test_suite())
import AccessControl.tests.testOwned
suite.addTest(AccessControl.tests.testOwned.test_suite())
import AccessControl.tests.testPasswordDigest
suite.addTest(AccessControl.tests.testPasswordDigest.test_suite())
import AccessControl.tests.testPermissionMapping
suite.addTest(AccessControl.tests.testPermissionMapping.test_suite())
import AccessControl.tests.testPermissionRole
suite.addTest(AccessControl.tests.testPermissionRole.test_suite())
import AccessControl.tests.testRole
suite.addTest(AccessControl.tests.testRole.test_suite())
import AccessControl.tests.testSecurityManager
suite.addTest(AccessControl.tests.testSecurityManager.test_suite())
import AccessControl.tests.testZCML
suite.addTest(AccessControl.tests.testZCML.test_suite())
import AccessControl.tests.testZopeGuards
# patch so that AccessControl.tests.testZopeGuards.TestActualPython.testPython
# also exercise our additions. This test checks that all safe builtins are tested
TestActualPython_compile = AccessControl.tests.testZopeGuards.TestActualPython._compile
def _compile(self, fname):
if fname == 'actual_python.py':
with open(os.path.join(
os.path.dirname(AccessControl.tests.testZopeGuards.__file__),
fname
)) as infile:
with tempfile.NamedTemporaryFile(suffix='.py', mode='w') as outfile:
outfile.write(
infile.read() + textwrap.dedent(
'''\
def erp5_patch():
assert next(iter([True, ])) == True
assert list(sorted([3,2,1])) == [1, 2, 3]
assert list(reversed([3,2,1])) == [1, 2, 3]
erp5_patch()
'''
))
outfile.flush()
return TestActualPython_compile(self, outfile.name)
return TestActualPython_compile(self, fname)
AccessControl.tests.testZopeGuards.TestActualPython._compile = _compile
suite.addTest(AccessControl.tests.testZopeGuards.test_suite())
import AccessControl.tests.testZopeSecurityPolicy
suite.addTest(AccessControl.tests.testZopeSecurityPolicy.test_suite())
return suite
\ No newline at end of file
......@@ -25,10 +25,10 @@
#
##############################################################################
import email
import mock
import time
from Products.ERP5Type.tests.ERP5TypeLiveTestCase import ERP5TypeTestCase
from Products.ERP5Type.tests.utils import createZODBPythonScript, removeZODBPythonScript
from Products.ERP5Type.tests.Sequence import SequenceList
from Products.ZSQLCatalog.SQLCatalog import SimpleQuery
from DateTime import DateTime
......@@ -82,14 +82,6 @@ class TestInterfacePost(ERP5TypeTestCase):
module = getattr(self.portal, module_id)
module.manage_delObjects(list(module.objectIds()))
custom_skin = self.portal.portal_skins.custom
if 'Entity_sendEmail' in custom_skin.objectIds():
removeZODBPythonScript(
custom_skin,
'Entity_sendEmail',
)
self.commit()
def _portal_catalog(self, **kw):
result_list = self.portal.portal_catalog(**kw)
uid_list = [x.uid for x in result_list]
......@@ -342,20 +334,19 @@ class TestInterfacePost(ERP5TypeTestCase):
pdf_document, = pdf_document_list
self.assertEqual(2, int(pdf_document.getContentInformation()['Pages']))
def stepMakeEntitySendEmailFailOnce(self, sequence=None):
createZODBPythonScript(
self.portal.portal_skins.custom,
'Entity_sendEmail',
self.portal.Entity_sendEmail.params(),
"""portal = context.getPortalObject()
for activity in portal.portal_activities.getMessageList():
if activity.method_id == script.id:
if activity.retry == 0:
raise ValueError('Failure on purpose')
else:
return context.skinSuper('custom', script.id)(%s)""" % (self.portal.Entity_sendEmail.params(),)
)
def Entity_sendEmail(*args, **kw):
self.Entity_sendEmail_patcher.stop()
raise ValueError("Fail on first execution")
self.Entity_sendEmail_patcher = mock.patch(
'erp5.portal_type.Person.Entity_sendEmail',
create=True,
side_effect=Entity_sendEmail)
self.Entity_sendEmail_mock = self.Entity_sendEmail_patcher.start()
self.addCleanup(self.Entity_sendEmail_patcher.stop)
def stepCheckEntitySendEmailCalled(self, sequence=None):
self.Entity_sendEmail_mock.assert_called()
def test_emailSendingIsPilotedByInternetMessagePost(self):
"""
......@@ -433,6 +424,7 @@ for activity in portal.portal_activities.getMessageList():
stepCheckInternetMessagePostCreated
stepCheckOnlyOneMessageHasBeenSentFromMailHost
stepCheckLatestMessageListFromMailHost
stepCheckEntitySendEmailCalled
"""
sequence_list.addSequenceString(sequence_string)
sequence_list.play(self)
......
......@@ -42,6 +42,7 @@ except ImportError:
warnings.warn("Please install unidiff, it is needed by Diff Tool",
DeprecationWarning)
from AccessControl import ClassSecurityInfo
from Acquisition import Explicit
from Products.ERP5Type.patches.diff import DeepDiff
from Products.ERP5Type import Permissions
from Products.ERP5Type.Globals import InitializeClass
......@@ -71,7 +72,7 @@ class DiffTool(BaseTool):
path -- optional path to specify which property to diff
patch_format -- optional format (rfc6902 or deepdiff)
"""
return PortalPatch(old_value, new_value, path, patch_format)
return PortalPatch(old_value, new_value, path, patch_format).__of__(self)
security.declarePrivate('patchPortalObject')
def patchPortalObject(self, old, diff_list):
......@@ -89,7 +90,8 @@ class DiffTool(BaseTool):
return new_obj
class PortalPatch:
class PortalPatch(Explicit):
"""
Provides an abstraction to a patch that
depends on the patch format.
......@@ -282,4 +284,5 @@ class PortalPatch:
return obj_dict
InitializeClass(DiffTool)
\ No newline at end of file
InitializeClass(DiffTool)
InitializeClass(PortalPatch)
\ No newline at end of file
##############################################################################
#
# Copyright (c) 2020 Nexedi SA and Contributors. All Rights Reserved.
#
# WARNING: This program as such is intended to be used by professional
# programmers who take the whole responsability of assessing all potential
# consequences resulting from its eventual inadequacies and bugs
# End users who are looking for a ready-to-use solution with commercial
# garantees and support are strongly adviced to contract a Free Software
# Service Company
#
# This program is Free Software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
#
##############################################################################
"""
Restricted collections module.
From restricted python, use "import collections" (see patches/Restricted.py).
"""
from collections import (
Counter, defaultdict, deque, OrderedDict, namedtuple as _namedtuple)
def namedtuple(typename, field_names, verbose=False, rename=False):
ret = _namedtuple(typename, field_names, verbose, rename)
ret.__allow_access_to_unprotected_subobjects__ = 1
return ret
......@@ -11,14 +11,30 @@
#
##############################################################################
import copy
import sys
import types
from RestrictedPython.RestrictionMutator import RestrictionMutator
_MARKER = []
def checkNameLax(self, node, name=_MARKER):
"""Verifies that a name being assigned is safe.
# Unsafe attributes on protected objects are already disallowed at execution
# and we don't want to maintain a duplicated list of exceptions.
RestrictionMutator.checkName = RestrictionMutator.checkAttrName = \
lambda *args, **kw: None
In ERP5 we are much more lax that than in Zope's original restricted
python and allow to using names starting with _, because we rely on
runtime checks to prevent access to forbidden attributes from objects.
We don't allow defining attributes ending with __roles__ though.
"""
if name is _MARKER:
# we use same implementation for checkName and checkAttrName which access
# the name in different ways ( see RestrictionMutator 3.6.0 )
name = node.attrname
if name.endswith('__roles__'):
self.error(node, '"%s" is an invalid variable name because '
'it ends with "__roles__".' % name)
RestrictionMutator.checkName = RestrictionMutator.checkAttrName = checkNameLax
from Acquisition import aq_acquire
......@@ -79,36 +95,48 @@ def allow_class_attribute(klass, access=1):
assert(inspect.isclass(klass))
_safe_class_attribute_dict[klass] = access
def _check_type_access(name, v):
class TypeAccessChecker:
"""Check Access for class instances (whose type() is `type`).
"""
Create a method which checks the access if the context type is <type 'type'>s.
def __call__(self, name, v):
"""
Create a callable which checks the access if the context type is <type 'type'>s.
Since the 'type' can be any types of classes, we support the three ways
defined in AccessControl/SimpleObjectPolicies. We implement this
as "a method which returing a method" because we can not know what is the
type until it is actually called. So the three ways are simulated the
returning method inide this method.
"""
def factory(inst, name):
function returned by this method.
"""
Check function used with ContainerAssetions checked by cAccessControl.
"""
access = _safe_class_attribute_dict.get(inst, 0)
# The next 'dict' only checks the access configuration type
if access == 1 or (isinstance(access, dict) and access.get(name, 0) == 1):
pass
elif isinstance(access, dict) and callable(access.get(name, 0)):
guarded_method = access.get(name)
return guarded_method(inst, name)
elif callable(access):
# Only check whether the access configuration raise error or not
access(inst, name)
else:
# fallback to default security
aq_acquire(inst, name, aq_validate, getSecurityManager().validate)
return v
return factory
ContainerAssertions[type] = _check_type_access
def factory(inst, name):
"""
Check function used with ContainerAssertions checked by cAccessControl.
"""
access = _safe_class_attribute_dict.get(inst, 0)
# The next 'dict' only checks the access configuration type
if access == 1 or (isinstance(access, dict) and access.get(name, 0) == 1):
pass
elif isinstance(access, dict) and callable(access.get(name, 0)):
guarded_method = access.get(name)
return guarded_method(inst, name)
elif callable(access):
# Only check whether the access configuration raise error or not
access(inst, name)
else:
# fallback to default security
aq_acquire(inst, name, aq_validate, getSecurityManager().validate)
return v
return factory
def __nonzero__(self):
# If Containers(type(x)) is true, ZopeGuard checks will short circuit,
# thinking it's a simple type, but we don't want this for type, because
# type(x) is type for classes, being trueish would skip security check on
# classes.
return False
ContainerAssertions[type] = TypeAccessChecker()
class SafeIterItems(SafeIter):
......@@ -133,6 +161,9 @@ safe_builtins['sorted'] = guarded_sorted
def guarded_reversed(seq):
return SafeIter(reversed(seq))
safe_builtins['reversed'] = guarded_reversed
ContainerAssertions[reversed] = 1
# listreverseiterator is a special type, returned by list.__reversed__
ContainerAssertions[type(reversed([]))] = 1
def guarded_enumerate(seq, start=0):
return NullIter(enumerate(guarded_iter(seq), start=start))
......@@ -175,12 +206,17 @@ ContainerAssertions[set] = _check_access_wrapper(set, _set_white_dict)
ContainerAssertions[frozenset] = 1
ContainerAssertions[types.GeneratorType] = 1
from collections import OrderedDict
ModuleSecurityInfo('collections').declarePublic('OrderedDict')
from collections import defaultdict
ModuleSecurityInfo('collections').declarePublic('defaultdict')
from collections import Counter
ModuleSecurityInfo('collections').declarePublic('Counter')
from AccessControl.ZopeGuards import _dict_white_list
# Attributes cannot be set on defaultdict, thus modify 'safetype' dict
......@@ -195,6 +231,14 @@ ContainerAssertions[OrderedDict] = _check_access_wrapper(OrderedDict, _dict_whit
OrderedDict.__guarded_setitem__ = OrderedDict.__setitem__.__func__
OrderedDict.__guarded_delitem__ = OrderedDict.__delitem__.__func__
_counter_white_list = copy.copy(_dict_white_list)
_counter_white_list['most_common'] = 1
ContainerAssertions[Counter] = _check_access_wrapper(Counter, _counter_white_list)
Counter.__guarded_setitem__ = dict.__setitem__
Counter.__guarded_delitem__ = dict.__delitem__
ModuleSecurityInfo('collections').declarePublic('namedtuple')
# given as example in Products.PythonScripts.module_access_examples
allow_module('base64')
allow_module('binascii')
......@@ -214,13 +258,14 @@ allow_type(type(re.compile('')))
allow_type(type(re.match('x','x')))
allow_type(type(re.finditer('x','x')))
import cStringIO, StringIO
f_cStringIO = cStringIO.StringIO()
f_StringIO = StringIO.StringIO()
allow_module('cStringIO')
allow_module('StringIO')
allow_type(type(f_cStringIO))
allow_type(type(f_StringIO))
import StringIO
StringIO.StringIO.__allow_access_to_unprotected_subobjects__ = 1
allow_module('cStringIO')
import cStringIO
allow_type(cStringIO.InputType)
allow_type(cStringIO.OutputType)
ModuleSecurityInfo('cgi').declarePublic('escape', 'parse_header')
allow_module('datetime')
......@@ -286,6 +331,7 @@ ModuleSecurityInfo('email.mime.text').declarePublic('MIMEText')
MNAME_MAP = {
'zipfile': 'Products.ERP5Type.ZipFile',
'calendar': 'Products.ERP5Type.Calendar',
'collections': 'Products.ERP5Type.Collections',
}
for alias, real in MNAME_MAP.items():
assert '.' not in alias, alias # TODO: support this
......
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