Commit 9d71c913 authored by Tatuya Kamada's avatar Tatuya Kamada

Restricted: Allow datetime module class attributes.

Restricted: Allow datetime module class attributes.
    
To allow the class attributes, we add a helper method ``allow_class_attribute''.
 Why addng this is because allow_module can allow instance methods but not class
 attributes.
 For example, datetime.datetime.now(), datetime.datetime.max, dict.fromkeys(),
 Decimal.from_float(), epoll.fromfd(), itertools.chain.from_iterable() are class
 attributes in Python 2.7.
    
 Also, add a special in-advance-import for datetime.datetime.strptime().
 It is because datetime.datetime.strptime() imports _strptime by C function
 PyImport_Import which calls
 __import__(name, globals, locals, fromlist=['__doc__'], level=0).
 The "level=0" is not supported by AccessControl in Zope2. At the same time,
 the dummy from '__doc__'  is neither allowed in it by default.
 Therefore we import _strptime in advance in this monkey patch file.
 This prevents both importing _strptime with level=0, and accessing __doc__,
 when calling datetime.datetime.strptime() in Restricted environment.

/cc @vpelletier @jm @klaus

/reviewed-on nexedi/erp5!275
parent c985321c
......@@ -66,9 +66,43 @@ def guarded_next(iterator, default=_marker):
return default
add_builtins(next=guarded_next)
_safe_class_attribute_dict = {}
import inspect
def allow_class_attribute(klass, access=1):
"""
Allow class methods, static methods, and class properties in the class
klass -- the class
access -- a dict, a callable, or a truth value that represents access control
(defined in SimpleObjectPolicies)
"""
assert(inspect.isclass(klass))
_safe_class_attribute_dict[klass] = access
def _check_type_access(name, v):
"""
Create a method 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):
if not (name == 'fromkeys' and type(inst) is dict):
"""
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
......@@ -196,6 +230,28 @@ ContainerAssertions[datetime.time] = 1
ContainerAssertions[datetime.date] = 1
ContainerAssertions[datetime.timedelta] = 1
ContainerAssertions[datetime.tzinfo] = 1
# ContainerAssertions allows instance methods but not class attributes,
# so allowing them by allow_class_attribute(cls).
# Ex: datetime.datetime.now(), datetime.datetime.max are class attributes.
allow_class_attribute(datetime.datetime)
allow_class_attribute(datetime.date)
allow_class_attribute(datetime.time)
allow_class_attribute(datetime.timedelta)
allow_class_attribute(datetime.tzinfo)
# We need special care for datetime.datetime.strptime() in Python 2.7.
# It is because datetime.datetime.strptime() imports _strptime by C function
# PyImport_Import which calls
# __import__(name, globals, locals, fromlist=['__doc__'], level=0).
# The "level=0" is not supported by AccessControl in Zope2. At the same time,
# the dummy from '__doc__' is neither allowed in it by default.
# Therefore we import _strptime in advance in this file.
# This prevents both importing _strptime with level=0, and accessing __doc__,
# when calling datetime.datetime.strptime().
import _strptime
# Allow dict.fromkeys, Only this method is a class method in dict module.
allow_class_attribute(dict, {'fromkeys': 1})
allow_module('difflib')
allow_module('hashlib')
import hashlib
......
##############################################################################
#
# Copyright (c) 2017 Nexedi KK 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.
#
##############################################################################
import unittest
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):
"""
Test Restricted Python Security that is monkey patched by ERP5.
"""
def getTitle(self):
return "Restricted Python Security Test"
def runScript(self, container, name):
func = getattr(self.portal, name)
return func()
def createAndRunScript(self, *args, **kwargs):
# we do not care the script name for security test thus use uuid1
name = str(uuid.uuid1())
code = '\n'.join(args)
expected = kwargs.get('expected', None)
script_container = self.portal.portal_skins.custom
try:
createZODBPythonScript(script_container, name, '**kw', code)
if expected:
self.assertEqual(self.runScript(script_container, name), expected)
else:
self.runScript(script_container, name)
finally:
removeZODBPythonScript(script_container, name)
def testDateTimeModuleAllowance(self):
"""
Make sure the security configuration with creating the Python(Script),
and running the Script.
"""
self.createAndRunScript('import datetime')
self.createAndRunScript('import datetime', 'return datetime.datetime.now()')
self.createAndRunScript('import datetime', 'return datetime.time.max')
self.createAndRunScript('import datetime', 'return datetime.date.today()')
self.createAndRunScript('import datetime', 'return datetime.timedelta.min')
self.createAndRunScript('import datetime', 'return datetime.tzinfo')
self.createAndRunScript('import datetime',
"return datetime.datetime.strptime('', '')")
def testDictClassMethod(self):
# This is intended to be allowed from the beggining
self.createAndRunScript("return dict.fromkeys(['a', 'b', 'c'])")
def testDecimalClassMethod(self):
# Now it is not allowed
self.assertRaises(Unauthorized,
self.createAndRunScript, 'import decimal',
'return decimal.Decimal.from_float(3.3)')
# allow it only in this test class to check
import decimal
allow_class_attribute(decimal.Decimal, {"from_float":1})
# make sure now we can run without raising Unauthorized
self.createAndRunScript('import decimal',
'return decimal.Decimal.from_float(3.3)')
def test_suite():
suite = unittest.TestSuite()
suite.addTest(unittest.makeSuite(TestRestrictedPythonSecurity))
return suite
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