diff --git a/ipykernel/kernelspec.py b/ipykernel/kernelspec.py index bce8051e0..f160e0d78 100644 --- a/ipykernel/kernelspec.py +++ b/ipykernel/kernelspec.py @@ -61,17 +61,27 @@ def get_kernel_dict(extra_arguments=None): def write_kernel_spec(path=None, overrides=None, extra_arguments=None): """Write a kernel spec directory to `path` - + If `path` is not specified, a temporary directory is created. If `overrides` is given, the kernelspec JSON is updated before writing. - + if 'resources' is given, copies resources from non-standard location. + The path to the kernelspec is always returned. """ if path is None: path = os.path.join(tempfile.mkdtemp(suffix='_kernels'), KERNEL_NAME) - + # stage resources shutil.copytree(RESOURCES, path) + + # change permission if resources directory is not read-write able + if not os.access(path, os.W_OK | os.R_OK): + # changes permissions only for owner, do not touch group/other + os.chmod(path, os.stat(path).st_mode | 0o700) + for f in os.listdir(path): + file_path = os.path.join(path, f) + os.chmod(file_path, os.stat(file_path).st_mode | 0o600) + # write kernel.json kernel_dict = get_kernel_dict(extra_arguments) @@ -79,17 +89,17 @@ def write_kernel_spec(path=None, overrides=None, extra_arguments=None): kernel_dict.update(overrides) with open(pjoin(path, 'kernel.json'), 'w') as f: json.dump(kernel_dict, f, indent=1) - + return path def install(kernel_spec_manager=None, user=False, kernel_name=KERNEL_NAME, display_name=None, prefix=None, profile=None): """Install the IPython kernelspec for Jupyter - + Parameters ---------- - + kernel_spec_manager: KernelSpecManager [optional] A KernelSpecManager to use for installation. If none provided, a default instance will be created. @@ -108,7 +118,7 @@ def install(kernel_spec_manager=None, user=False, kernel_name=KERNEL_NAME, displ Returns ------- - + The path where the kernelspec was installed. """ if kernel_spec_manager is None: @@ -143,12 +153,12 @@ def install(kernel_spec_manager=None, user=False, kernel_name=KERNEL_NAME, displ class InstallIPythonKernelSpecApp(Application): """Dummy app wrapping argparse""" name = 'ipython-kernel-install' - + def initialize(self, argv=None): if argv is None: argv = sys.argv[1:] self.argv = argv - + def start(self): import argparse parser = argparse.ArgumentParser(prog=self.name, diff --git a/ipykernel/tests/test_kernelspec.py b/ipykernel/tests/test_kernelspec.py index a3b9771c7..264f2fea8 100644 --- a/ipykernel/tests/test_kernelspec.py +++ b/ipykernel/tests/test_kernelspec.py @@ -13,6 +13,8 @@ except ImportError: import mock # py2 +import pytest + from jupyter_core.paths import jupyter_data_dir from ipykernel.kernelspec import ( @@ -88,10 +90,41 @@ def test_write_kernel_spec_path(): shutil.rmtree(path) +@pytest.mark.skipif(sys.platform == 'win32', + reason="does not run on windows") +def test_write_kernel_spec_permissions(tmp_path): + read_only_resources = os.path.join(tmp_path, "_RESOURCES") + shutil.copytree(RESOURCES, read_only_resources) + + # file used to check that the correct (mocked) resource directory was used + with open(read_only_resources + '/touch', 'w') as f: + pass + + # make copy of resources directory read-only + os.chmod(read_only_resources, 0o500) + for f in os.listdir(read_only_resources): + os.chmod(os.path.join(read_only_resources, f), 0o400) + + with mock.patch('ipykernel.kernelspec.RESOURCES', read_only_resources): + path = write_kernel_spec() + + assert_is_spec(path) + + # if `touch` is missing then the wrong resources directory was copied by + # `write_kernel_spec`, `RESOURCES` mocking didn't work? + assert os.path.isfile(path + '/touch') + + # ensure permissions are not loosened too much, original permission was + # 0o500, so the 'fixed' one should be 0o700, still no rw for group/other + assert os.stat(path).st_mode & 0o77 == 0o00 + + shutil.rmtree(path) + + def test_install_kernelspec(): path = tempfile.mkdtemp() - try: + try: test = InstallIPythonKernelSpecApp.launch_instance(argv=['--prefix', path]) assert_is_spec(os.path.join( path, 'share', 'jupyter', 'kernels', KERNEL_NAME)) @@ -101,21 +134,21 @@ def test_install_kernelspec(): def test_install_user(): tmp = tempfile.mkdtemp() - + with mock.patch.dict(os.environ, {'HOME': tmp}): install(user=True) data_dir = jupyter_data_dir() - + assert_is_spec(os.path.join(data_dir, 'kernels', KERNEL_NAME)) def test_install(): system_jupyter_dir = tempfile.mkdtemp() - + with mock.patch('jupyter_client.kernelspec.SYSTEM_JUPYTER_PATH', [system_jupyter_dir]): install() - + assert_is_spec(os.path.join(system_jupyter_dir, 'kernels', KERNEL_NAME))