Commit ff96a50c by Ned Batchelder

Refactor to move assumed_imports into capa, so that code_jail is more pure.

parent ef328415
"""A module proxy for delayed importing of modules.
From http://barnesc.blogspot.com/2006/06/automatic-python-imports-with-autoimp.html,
in the public domain.
"""
import sys
class LazyModule(object):
"""A lazy module proxy."""
def __init__(self, modname):
self.__dict__['__name__'] = modname
self._set_mod(None)
def _set_mod(self, mod):
if mod is not None:
self.__dict__ = mod.__dict__
self.__dict__['_lazymod_mod'] = mod
def _load_mod(self):
__import__(self.__name__)
self._set_mod(sys.modules[self.__name__])
def __getattr__(self, name):
if self.__dict__['_lazymod_mod'] is None:
self._load_mod()
mod = self.__dict__['_lazymod_mod']
if hasattr(mod, name):
return getattr(mod, name)
else:
try:
subname = '%s.%s' % (self.__name__, name)
__import__(subname)
submod = getattr(mod, name)
except ImportError:
raise AttributeError("'module' object has no attribute %r" % name)
self.__dict__[name] = LazyModule(subname, submod)
return self.__dict__[name]
...@@ -6,40 +6,17 @@ import shutil ...@@ -6,40 +6,17 @@ import shutil
import sys import sys
import textwrap import textwrap
import lazymod
import jailpy import jailpy
from util import temp_directory, change_directory, TempDirectory from util import temp_directory, change_directory
# We'll need the code from lazymod.py for use in jailpy, so read it now. def safe_exec(code, globals_dict, files=None, python_path=None):
lazymod_py_file = lazymod.__file__
if lazymod_py_file.endswith("c"):
lazymod_py_file = lazymod_py_file[:-1]
lazymod_py = open(lazymod_py_file).read()
def names_and_modules(assumed_imports):
"""Get uniform names and modules from assumed_imports."""
for modname in assumed_imports:
if isinstance(modname, tuple):
yield modname
else:
yield modname, modname
def safe_exec(code, globals_dict, assumed_imports=None, files=None, python_path=None):
"""Execute code as "exec" does, but safely. """Execute code as "exec" does, but safely.
`code` is a string of Python code. `globals_dict` is used as the globals `code` is a string of Python code. `globals_dict` is used as the globals
during execution. Modifications the code makes to `globals_dict` are during execution. Modifications the code makes to `globals_dict` are
reflected in the dictionary on return. reflected in the dictionary on return.
`assumed_imports` is a list of modules to make available as implicit
imports for the code. Entries are either a name, "mod", which makes
"import mod" part of the code, or a pair, ("f", "fooey"), which makes
"import fooey as f" part of the code. The module name can be dotted.
Returns None. Changes made by `code` are visible in `globals_dict`. Returns None. Changes made by `code` are visible in `globals_dict`.
""" """
...@@ -72,11 +49,6 @@ def safe_exec(code, globals_dict, assumed_imports=None, files=None, python_path= ...@@ -72,11 +49,6 @@ def safe_exec(code, globals_dict, assumed_imports=None, files=None, python_path=
the_code.append("sys.path.append(%r)\n" % pybase) the_code.append("sys.path.append(%r)\n" % pybase)
files.append(pydir) files.append(pydir)
if assumed_imports:
the_code.append(lazymod_py)
for name, modname in names_and_modules(assumed_imports):
the_code.append("g_dict['{}'] = LazyModule('{}')\n".format(name, modname))
the_code.append(textwrap.dedent( the_code.append(textwrap.dedent(
# Execute the sandboxed code. # Execute the sandboxed code.
""" """
...@@ -140,7 +112,7 @@ def json_safe(d): ...@@ -140,7 +112,7 @@ def json_safe(d):
return json.loads(json.dumps(jd)) return json.loads(json.dumps(jd))
def not_safe_exec(code, globals_dict, assumed_imports=None, files=None, python_path=None): def not_safe_exec(code, globals_dict, files=None, python_path=None):
"""Another implementation of `safe_exec`, but not safe. """Another implementation of `safe_exec`, but not safe.
This can be swapped in for debugging problems in sandboxed Python code. This can be swapped in for debugging problems in sandboxed Python code.
...@@ -151,9 +123,6 @@ def not_safe_exec(code, globals_dict, assumed_imports=None, files=None, python_p ...@@ -151,9 +123,6 @@ def not_safe_exec(code, globals_dict, assumed_imports=None, files=None, python_p
""" """
g_dict = json_safe(globals_dict) g_dict = json_safe(globals_dict)
for name, modname in names_and_modules(assumed_imports or ()):
g_dict[name] = lazymod.LazyModule(modname)
with temp_directory(delete_when_done=True) as tmpdir: with temp_directory(delete_when_done=True) as tmpdir:
with change_directory(tmpdir): with change_directory(tmpdir):
# Copy the files here. # Copy the files here.
......
"""Test lazymod.py"""
import sys
import unittest
from codejail.lazymod import LazyModule
from codejail.util import ModuleIsolation
class TestLazyMod(unittest.TestCase):
def setUp(self):
# Each test will remove modules that it imported.
self.addCleanup(ModuleIsolation().clean_up)
def test_simple(self):
# Import some stdlib module that has not been imported before
self.assertNotIn("colorsys", sys.modules)
colorsys = LazyModule("colorsys")
hsv = colorsys.rgb_to_hsv(.3, .4, .2)
self.assertEqual(hsv[0], 0.25)
def test_dotted(self):
self.assertNotIn("email.utils", sys.modules)
email_utils = LazyModule("email.utils")
self.assertEqual(email_utils.quote('"hi"'), r'\"hi\"')
...@@ -14,19 +14,6 @@ class SafeExecTests(object): ...@@ -14,19 +14,6 @@ class SafeExecTests(object):
self.safe_exec("a = 17", g) self.safe_exec("a = 17", g)
self.assertEqual(g['a'], 17) self.assertEqual(g['a'], 17)
def test_assumed_imports(self):
g = {}
# Using string without importing it is bad.
with self.assertRaises(Exception):
self.safe_exec("a = string.ascii_lowercase[0]", g)
# Using string with an assumed import is fine.
self.safe_exec("a = string.ascii_lowercase[0]", g, assumed_imports=["string"])
self.assertEqual(g['a'], 'a')
# Can also import with a shorthand.
self.safe_exec("a = op.join('x', 'y')", g, assumed_imports=[("op", "os.path")])
self.assertEqual(g['a'][0], 'x')
self.assertEqual(g['a'][-1], 'y')
def test_files_are_copied(self): def test_files_are_copied(self):
g = {} g = {}
self.safe_exec( self.safe_exec(
......
...@@ -32,25 +32,6 @@ def temp_directory(delete_when_done=True): ...@@ -32,25 +32,6 @@ def temp_directory(delete_when_done=True):
tmp.clean_up() tmp.clean_up()
class ModuleIsolation(object):
"""
Manage changes to sys.modules so that we can roll back imported modules.
Create this object, it will snapshot the currently imported modules. When
you call `clean_up()`, it will delete any module imported since its creation.
"""
def __init__(self):
# Save all the names of all the imported modules.
self.mods = set(sys.modules)
def clean_up(self):
# Get a list of modules that didn't exist when we were created
new_mods = [m for m in sys.modules if m not in self.mods]
# and delete them all so another import will run code for real again.
for m in new_mods:
del sys.modules[m]
class ChangeDirectory(object): class ChangeDirectory(object):
def __init__(self, new_dir): def __init__(self, new_dir):
self.old_dir = os.getcwd() self.old_dir = os.getcwd()
......
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