Skip to content

Commit

Permalink
Fixed non-root-user access
Browse files Browse the repository at this point in the history
  • Loading branch information
ckunki committed Mar 5, 2024
1 parent faa3213 commit 5bcfb49
Show file tree
Hide file tree
Showing 8 changed files with 141 additions and 14 deletions.
2 changes: 1 addition & 1 deletion doc/changes/changes_1.1.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ n/a

## Bug Fixes

n/a
* #241: Fixed non-root-user access

## Documentation

Expand Down
4 changes: 3 additions & 1 deletion exasol/ds/sandbox/lib/dss_docker/create_image.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,11 @@ def jupyter():
return []
port = get_fact(facts, "jupyter", "port")
user_name = get_fact(facts, "jupyter", "user")
user_home = get_fact(facts, "jupyter", "home")
password = get_fact(facts, "jupyter", "password")
logfile = get_fact(facts, "jupyter", "logfile")
return [
"--home", user_home,
"--jupyter-server", command,
"--port", port,
"--user", user_name,
Expand Down Expand Up @@ -162,7 +164,7 @@ def _commit_container(
notebook_folder_initial = get_fact(facts, "notebook_folder", "initial")
conf = {
"Entrypoint": entrypoint(facts),
"User": get_fact(facts, "jupyter", "user"),
# "User": get_fact(facts, "jupyter", "user"),
"Cmd": [],
"Volumes": {notebook_folder_final: {}, },
"ExposedPorts": {f"{port}/tcp": {}},
Expand Down
1 change: 1 addition & 0 deletions exasol/ds/sandbox/runtime/ansible/general_setup_tasks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
command: "{{ jupyter_virtualenv }}/bin/jupyter-lab"
port: "49494"
user: "{{ user_name }}"
home: "{{ user_home }}"
password: "{{ lookup('ansible.builtin.env', 'JUPYTER_LAB_PASSWORD', default='ailab') }}"
logfile: "{{ user_home }}/jupyter-server.log"
notebook_folder:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import argparse
import logging
import os
import pwd
import re
import resource
import shutil
Expand All @@ -10,6 +11,7 @@

from inspect import cleandoc
from pathlib import Path
from typing import List, TextIO


_logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -39,6 +41,10 @@ def arg_parser():
"--user", type=str,
help="user name for running jupyter server",
)
parser.add_argument(
"--home", type=str,
help="home directory of user running jupyter server",
)
parser.add_argument(
"--password", type=str,
help="initial default password for Jupyter server",
Expand All @@ -54,7 +60,20 @@ def arg_parser():
return parser


def start_subprocess_as_user(
command_line: List[str],
logfile: str,
home_directory: str,
) -> subprocess.Popen:
env = os.environ.copy()
env["HOME"] = home_directory
with open(logfile, "w") as f:
p = subprocess.Popen(command_line, stdout=f, stderr=f, env=env)
return p


def start_jupyter_server(
home_directory: str,
binary_path: str,
port: int,
notebook_dir: str,
Expand All @@ -81,10 +100,13 @@ def exit_on_error(rc):
binary_path,
f"--notebook-dir={notebook_dir}",
"--no-browser",
"--allow-root",
"--allow-root"
]

env = os.environ.copy()
env["HOME"] = home_directory
with open(logfile, "w") as f:
p = subprocess.Popen(command_line, stdout=f, stderr=f)
p = subprocess.Popen(command_line, stdout=f, stderr=f, env=env)

url = "http://<host>:<port>"
localhost_url = url.replace("<host>", "localhost").replace("<port>", str(port))
Expand Down Expand Up @@ -153,27 +175,58 @@ def disable_core_dumps():
resource.setrlimit(resource.RLIMIT_CORE, (0, 0))


def sleep_inifinity():
def sleep_infinity():
while True:
time.sleep(1)


class User:
def __init__(self, name: str):
self.name = name
self._id = None

@property
def id(self):
if self._id is None:
self._id = pwd.getpwnam(self.name).pw_uid
return self._id

def own(self, path: str):
if Path(path).exists():
unchanged_gid = -1
os.chown(path, self.id, unchanged_gid)
return self

def switch_to(self):
uid = self.id
os.setresuid(uid, uid, uid)
return self


def main():
args = arg_parser().parse_args()
if args.user:
(
User(args.user)
.own("/var/run/docker.sock")
.switch_to()
)
if args.notebook_defaults and args.notebooks:
copy_rec(
args.notebook_defaults,
args.notebooks,
args.warning_as_error,
)
disable_core_dumps()
if (args.jupyter_server
if (args.user
and args.jupyter_server
and args.notebooks
and args.jupyter_logfile
and args.user
and args.home
and args.password
):
start_jupyter_server(
args.home,
args.jupyter_server,
args.port,
args.notebooks,
Expand All @@ -182,7 +235,7 @@ def main():
args.password,
)
else:
sleep_inifinity()
sleep_infinity()


if __name__ == "__main__":
Expand Down
35 changes: 29 additions & 6 deletions test/unit/entrypoint/test_main.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import pytest
from unittest.mock import MagicMock, create_autospec

from exasol.ds.sandbox.runtime.ansible.roles.entrypoint.files import entrypoint
from pathlib import Path
Expand All @@ -13,9 +14,21 @@ def entrypoint_method(name: str) -> str:

def test_no_args(mocker):
mocker.patch("sys.argv", ["app"])
mocker.patch(entrypoint_method("sleep_inifinity"))
mocker.patch(entrypoint_method("sleep_infinity"))
entrypoint.main()
assert entrypoint.sleep_inifinity.called
assert entrypoint.sleep_infinity.called


def test_user_arg(mocker):
mocker.patch("sys.argv", ["app", "--user", "jennifer"])
user = create_autospec(entrypoint.User)
user.own.return_value = user
mocker.patch(entrypoint_method("User"), return_value=user)
mocker.patch(entrypoint_method("sleep_infinity"))
entrypoint.main()
assert user.own.called
assert user.own.call_args == mocker.call("/var/run/docker.sock")
assert user.switch_to.called


@pytest.mark.parametrize("warning_as_error", [True, False])
Expand All @@ -27,7 +40,7 @@ def test_copy_args_valid(mocker, warning_as_error ):
"--notebooks", "destination",
] + extra_args)
mocker.patch(entrypoint_method("copy_rec"))
mocker.patch(entrypoint_method("sleep_inifinity"))
mocker.patch(entrypoint_method("sleep_infinity"))
entrypoint.main()
assert entrypoint.copy_rec.called
expected = mocker.call(Path("source"), Path("destination"), warning_as_error)
Expand All @@ -41,17 +54,27 @@ def test_jupyter(mocker):
logfile = Path("/root/jupyter-server.log")
mocker.patch("sys.argv", [
"app",
"--home", "home-directory",
"--notebooks", str(notebook_folder),
"--jupyter-server", jupyter,
"--port", port,
"--user", "usr",
"--password", "pwd",
"--jupyter-logfile", str(logfile),
])
mocker.patch(entrypoint_method("User"))
mocker.patch(entrypoint_method("start_jupyter_server"))
mocker.patch(entrypoint_method("sleep_inifinity"))
mocker.patch(entrypoint_method("sleep_infinity"))
entrypoint.main()
assert entrypoint.start_jupyter_server.called
expected = mocker.call(jupyter, int(port), notebook_folder, logfile, "usr", "pwd")
expected = mocker.call(
"home-directory",
jupyter,
int(port),
notebook_folder,
logfile,
"usr",
"pwd",
)
assert entrypoint.start_jupyter_server.call_args == expected
assert not entrypoint.sleep_inifinity.called
assert not entrypoint.sleep_infinity.called
1 change: 1 addition & 0 deletions test/unit/entrypoint/test_start_jupyter_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ def create_script(self, before="", after=""):

def run(self):
entrypoint.start_jupyter_server(
"home",
self.script,
"port",
"notebooks",
Expand Down
44 changes: 44 additions & 0 deletions test/unit/entrypoint/test_user_class.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import os
import pwd
import pytest

from unittest.mock import MagicMock
from exasol.ds.sandbox.runtime.ansible.roles.entrypoint.files import entrypoint


@pytest.fixture
def user():
return entrypoint.User("jennifer")


@pytest.fixture
def user_with_id(mocker, user):
mocker.patch("pwd.getpwnam", return_value=MagicMock(pw_uid=123))
return user


def test_id(mocker, user):
mocker.patch("pwd.getpwnam")
user.id
assert pwd.getpwnam.called
assert pwd.getpwnam.call_args == mocker.call("jennifer")


def test_chown_file_absent(mocker, user):
mocker.patch("os.chown")
user.own("/non/existing/path")
assert not os.chown.called


def test_chown_file_exists(mocker, tmp_path, user_with_id):
mocker.patch("os.chown")
user_with_id.own(tmp_path)
assert os.chown.called
assert os.chown.call_args == mocker.call(tmp_path, 123, -1)


def test_switch_to(mocker, user_with_id):
mocker.patch("os.setresuid")
user_with_id.switch_to()
assert os.setresuid.called
assert os.setresuid.call_args == mocker.call(123, 123, 123)
3 changes: 3 additions & 0 deletions test/unit/test_dss_docker_image.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ def test_entrypoint_with_copy_args():
initial = "/path/to/initial"
final = "/path/to/final"
user = "jupyter-user-name"
home = "/home/user"
password = "jupyter-default-password"
logfile = "/path/to/jupyter-server.log"
facts = {
Expand All @@ -89,6 +90,7 @@ def test_entrypoint_with_copy_args():
"command": jupyter,
"port": port,
"user": user,
"home": home,
"password": password,
"logfile": logfile,
},
Expand All @@ -102,6 +104,7 @@ def test_entrypoint_with_copy_args():
entrypoint,
"--notebook-defaults", initial,
"--notebooks", final,
"--home", home,
"--jupyter-server", jupyter,
"--port", port,
"--user", user,
Expand Down

0 comments on commit 5bcfb49

Please sign in to comment.