Commit eabcb211 authored by Arnaud Fontaine's avatar Arnaud Fontaine

ZODB Components: ModuleSecurityInfo() should also apply to the alias module,...

ZODB Components: ModuleSecurityInfo() should also apply to the alias module, not only to the versioned one.

This fixes an Unauthorized error when GitLoginError is imported from
erp5.component.module.Git defining ModuleSecurityInfo() for __name__
(erp5.component.module.erp5_version.Git).

Note that when migrating the content of a Product, ModuleSecurityInfo() first
parameter must be __name__.

Traceback (innermost last):
  [...]
  Module script, line 1, in BusinessTemplate_handleException
   - <PythonScript at /erp5/BusinessTemplate_handleException used for /erp5/portal_templates/832>
   - Line 1
    from erp5.component.module.Git import GitLoginError
  Module Products.ERP5Type.patches.Restricted, line 305, in guarded_import
    return orig_guarded_import(mname, globals, locals, fromlist, level)
  Module AccessControl.ZopeGuards, line 305, in guarded_import
    raise Unauthorized("import of '%s' is unauthorized" % mname)
Unauthorized: import of 'erp5.component.module.Git' is unauthorized
parent 223d4818
Pipeline #6594 failed with stage
in 0 seconds
......@@ -40,6 +40,8 @@ from types import ModuleType
from zLOG import LOG, BLATHER, WARNING
from Acquisition import aq_base
from importlib import import_module
from Products.ERP5Type.patches.Restricted import MNAME_MAP
from AccessControl.SecurityInfo import _moduleSecurity, _appliedModuleSecurity
class ComponentVersionPackage(ModuleType):
"""
......@@ -265,6 +267,7 @@ class ComponentDynamicPackage(ModuleType):
else:
setattr(self, name, module)
sys.modules[module_fullname_alias] = module
MNAME_MAP[module_fullname_alias] = module.__name__
return module
component = getattr(site.portal_components, component_id)
......@@ -315,6 +318,7 @@ class ComponentDynamicPackage(ModuleType):
setattr(version_package, name, module)
if module_fullname_alias:
setattr(self, name, module)
MNAME_MAP[module_fullname_alias] = module_fullname
import erp5.component
erp5.component.ref_manager.add_module(module)
......@@ -398,6 +402,16 @@ class ComponentDynamicPackage(ModuleType):
self.__fullname_source_code_dict.clear()
package = self
# Force reload of ModuleSecurityInfo() as it may have been changed in
# the source code
for modsec_dict in _moduleSecurity, _appliedModuleSecurity:
for k in modsec_dict.keys():
if k.startswith(self._namespace):
del modsec_dict[k]
for k in MNAME_MAP.keys():
if k.startswith(self._namespace):
del MNAME_MAP[k]
for name, module in package.__dict__.items():
if name[0] == '_' or not isinstance(module, ModuleType):
continue
......
......@@ -297,6 +297,22 @@ def guarded_import(mname, globals=None, locals=None, fromlist=None,
for fromname in fromlist or ():
if fromname[:1] == '_':
raise Unauthorized(fromname)
# ZODB Components must be imported beforehand as ModuleSecurityInfo() may be
# called there and AccessControl secureModule() expects to find the module
# in _moduleSecurity dict. Also, import loader will fill MNAME_MAP.
if mname.startswith('erp5.component.'):
# Call find_load_module() to log errors as this will always raise
# Unauthorized error without details
#
# XXX: pkgutil.get_loader() only works with '__path__'
import erp5.component
_, _, package_name, module_name = mname.split('.', 3)
try:
component_package = getattr(erp5.component, package_name)
except AttributeError:
raise Unauthorized(mname)
if component_package.find_load_module(module_name) is None:
raise Unauthorized(mname)
if mname in MNAME_MAP:
mname = MNAME_MAP[mname]
if not fromlist:
......
......@@ -2331,6 +2331,120 @@ namedtuple('NamedTuple', 'foo bar')(1, 2)
self.assertEqual(component.getTextContentWarningMessageList(), [])
self.assertEqual(component.getTextContentErrorMessageList(), [])
def testModuleSecurityInfo(self):
"""
AccessControl.SecurityInfo.ModuleSecurityInfo() function allows to declare
public/private classes and functions at Module level.
When called, an entry is added to AccessControl.SecurityInfo._moduleSecurity
dict (mapping module name to _ModuleSecurityInfo class instance). Later on,
when this module is imported from 'Restricted Code', securities will be
applied to the Module and then be moved to from _moduleSecurity to
AccessControl.SecurityInfo._appliedModuleSecurity dict.
For ZODB Components, we have the versioned Module and its alias. This test
ensures that securities are also properly defined for the alias to be
importable (and not raising an 'Unauthorized' exception).
"""
from AccessControl.SecurityInfo import (_moduleSecurity,
_appliedModuleSecurity)
from Products.ERP5Type.patches.Restricted import MNAME_MAP
reference = self._generateReference('TestModuleSecurityInfo')
component = self._newComponent(reference)
version_package = (self._document_class._getDynamicModuleNamespace() +
'.erp5_version')
module = self._getComponentFullModuleName(reference)
module_versioned = self._getComponentFullModuleName(reference,
version='erp5')
# __name__ == erp5.component.XXX.erp5_version.TestModuleSecurityInfo
# (erp5.component.XXX.TestModuleSecurityInfo is just an alias)
component.setTextContent("""
class TestModuleSecurityInfoException(Exception):
pass
from AccessControl.SecurityInfo import ModuleSecurityInfo
ModuleSecurityInfo(__name__).declarePublic('TestModuleSecurityInfoException')
""" + component.getTextContent())
self.portal.portal_workflow.doActionFor(component, 'validate_action')
self.tic()
self.assertEqual(component.getValidationState(), 'validated')
self.assertEqual(component.getTextContentErrorMessageList(), [])
self.assertEqual(component.getTextContentWarningMessageList(), [])
self.assertModuleImportable(reference,
expected_default_version='erp5_version',
reset=True)
self.assertFalse(module_versioned in _moduleSecurity)
self.assertFalse(module_versioned in _appliedModuleSecurity)
# Define another ZODB Component to check that not importing the module
# beforehand works
reference2 = self._generateReference('TestModuleSecurityInfo2')
component2 = self._newComponent(reference2)
module2 = self._getComponentFullModuleName(reference2)
module_versioned2 = self._getComponentFullModuleName(reference2,
version='erp5')
# __name__ == erp5.component.XXX.erp5_version.TestModuleSecurityInfo2
# (erp5.component.XXX.TestModuleSecurityInfo2 is just an alias)
component2.setTextContent("""
class TestModuleSecurityInfoException2(Exception):
pass
from AccessControl.SecurityInfo import ModuleSecurityInfo
ModuleSecurityInfo(__name__).declarePublic('TestModuleSecurityInfoException2')
""" + component2.getTextContent())
self.portal.portal_workflow.doActionFor(component2, 'validate_action')
self.tic()
self.assertEqual(component2.getValidationState(), 'validated')
self.assertEqual(component2.getTextContentErrorMessageList(), [])
self.assertEqual(component2.getTextContentWarningMessageList(), [])
self.assertModuleImportable(reference2,
expected_default_version='erp5_version',
reset=True)
## Import module from non-'Restricted Code': it must be only in
## _moduleSecurity
self._importModule('erp5_version.%s' % reference)
self.assertFalse(module in MNAME_MAP)
self.assertTrue(module_versioned in _moduleSecurity)
self.assertFalse(module in _appliedModuleSecurity)
self.assertFalse(module2 in MNAME_MAP)
self.assertFalse(module2 in _moduleSecurity)
self.assertFalse(module2 in _appliedModuleSecurity)
## Import module from 'Restricted Code': it must be in
## _appliedModuleSecurity and no longer in _moduleSecurity
createZODBPythonScript(self.portal.portal_skins.custom,
'TestModuleSecurityInfoPythonScript', '',
"""
from %s import TestModuleSecurityInfoException
from %s import TestModuleSecurityInfoException
from %s import TestModuleSecurityInfoException2
from %s import TestModuleSecurityInfoException2
return 'OK'
""" % (module_versioned, module,
module_versioned2, module2))
self.assertEqual(self.portal.TestModuleSecurityInfoPythonScript(), 'OK')
self.assertEqual(MNAME_MAP.get(module), module_versioned)
self.assertFalse(module_versioned in _moduleSecurity)
self.assertTrue(module_versioned in _appliedModuleSecurity)
self.assertEqual(MNAME_MAP.get(module2), module_versioned2)
self.assertFalse(module_versioned2 in _moduleSecurity)
self.assertTrue(module_versioned2 in _appliedModuleSecurity)
## Reset must clear everything including the version package as this is
## dynamic (no need to clear erp5.component.XXX though)...
self._component_tool.reset(force=True,
reset_portal_type_at_transaction_boundary=True)
self.tic()
self.assertFalse(version_package in _moduleSecurity)
self.assertFalse(version_package in _appliedModuleSecurity)
self.assertFalse(module in MNAME_MAP)
self.assertFalse(module_versioned in _moduleSecurity)
self.assertFalse(module_versioned in _appliedModuleSecurity)
self.assertFalse(module2 in MNAME_MAP)
self.assertFalse(module_versioned2 in _moduleSecurity)
self.assertFalse(module_versioned2 in _appliedModuleSecurity)
from Products.ERP5Type.Core.ExtensionComponent import ExtensionComponent
class TestZodbExtensionComponent(_TestZodbComponent):
......
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