From 2b6d1f511c74df80095f40c3864aa26942b48f6a Mon Sep 17 00:00:00 2001 From: Blair Bonnett Date: Tue, 11 Jul 2023 12:51:51 +0200 Subject: [PATCH 1/2] Add dynamic_user_statedir configuration option. This allows setting the StateDirectory option for a dynamic user to a subdirectory of /var/lib, allowing for easy backups or persistence between containers. --- README.md | 25 ++++++++++++++-- systemdspawner/systemdspawner.py | 50 ++++++++++++++++++++++++++++---- 2 files changed, 67 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 78f0502..83c1fe7 100644 --- a/README.md +++ b/README.md @@ -155,6 +155,7 @@ in your `jupyterhub_config.py` file: - **[`readonly_paths`](#readonly_paths)** - **[`readwrite_paths`](#readwrite_paths)** - **[`dynamic_users`](#dynamic_users)** +- **[`dynamic_user_statedir`](#dynamic_user_statedir)** ### `mem_limit` @@ -377,14 +378,32 @@ Defaults to `None` which disables this feature. Allocate system users dynamically for each user. -Uses the DynamicUser= feature of Systemd to make a new system user -for each hub user dynamically. Their home directories are set up -under /var/lib/{USERNAME}, and persist over time. The system user +Uses the DynamicUser= feature of Systemd to make a new system user for each hub +user dynamically. Their home directories are set up in the directories +configured by `dynamic_user_statedir`, and persist over time. The system user is deallocated whenever the user's server is not running. See http://0pointer.net/blog/dynamic-users-with-systemd.html for more information. +### `dynamic_user_statedir` + +The name of the state directory for a dynamic user. This will correspond to a +directory under /var/lib (this base path is enforced by systemd) which will +persist between uses. The home and working directory of the user's server will +also be set to this state directory. + +`{USERNAME}` and `{USERID}` will be expanded to the appropriate values for the +user being spawned. The default is {USERNAME}, corresponding to a filesystem +path /var/lib/{USERNAME}. + +Note that systemd will create the state directories in /var/lib/private and +symlink them into /var/lib (see +http://0pointer.net/blog/dynamic-users-with-systemd.html for more information). +Any desired backups should therefore be taken from the /var/lib/private paths. + +This value is ignored if `dynamic_users` is set to False. + ### `slice` Run the spawned notebook in a given systemd slice. This allows aggregate configuration that diff --git a/systemdspawner/systemdspawner.py b/systemdspawner/systemdspawner.py index 06b3c72..ad803cf 100644 --- a/systemdspawner/systemdspawner.py +++ b/systemdspawner/systemdspawner.py @@ -129,14 +129,30 @@ class SystemdSpawner(Spawner): Uses the DynamicUser= feature of Systemd to make a new system user for each hub user dynamically. Their home directories are set up - under /var/lib/{USERNAME}, and persist over time. The system user - is deallocated whenever the user's server is not running. + in the state directories configured by the dynamic_user_statedir + option, and persist over time. The system user is deallocated whenever + the user's server is not running. See http://0pointer.net/blog/dynamic-users-with-systemd.html for more information. """, ).tag(config=True) + dynamic_user_statedir = Unicode( + "{USERNAME}", + help=""" + The state directory for dynamic users. This is the name of a persistent + directory under /var/lib. + + {USERNAME} and {USERID} are expanded. + + Defaults to {USERNAME}, corresponding to a filesystem directory + /var/lib/{USERNAME}. + + Ignored if dynamic_users is set to False. + """ + ).tag(config=True) + slice = Unicode( None, allow_none=True, @@ -255,10 +271,34 @@ async def start(self): if self.dynamic_users: properties["DynamicUser"] = "yes" - properties["StateDirectory"] = self._expand_user_vars("{USERNAME}") - # HOME is not set by default otherwise - env["HOME"] = self._expand_user_vars("/var/lib/{USERNAME}") + # Expand the state directory for the unit. Perform some basic checks on the + # directory name so we can give a more obvious error than systemd would. + statedir = self._expand_user_vars(self.dynamic_user_statedir) + if os.path.isabs(statedir): + self.log.error( + "User %s: StateDirectory (%s) cannot be absolute", + self.user.name, + statedir + ) + raise Exception(f"StateDirectory ({statedir}) cannot be absolute") + + testpath = statedir + while testpath: + testpath, component = os.path.split(testpath) + if component == "..": + self.log.error( + "User %s: StateDirectory (%s) cannot contain ..", + self.user.name, + statedir + ) + raise Exception(f"StateDirectory ({statedir}) cannot contain ..") + + properties["StateDirectory"] = statedir + + # HOME is not set by default otherwise. Systemd places the state + # directory under /var/lib. + env["HOME"] = f"/var/lib/{statedir}" # Set working directory to $HOME too working_dir = env["HOME"] # Set uid, gid = None so we don't set them From 0953039a1a96438969a3ec49dec69bae723785c9 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 11 Jul 2023 11:03:41 +0000 Subject: [PATCH 2/2] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- README.md | 2 +- systemdspawner/systemdspawner.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 83c1fe7..caec85e 100644 --- a/README.md +++ b/README.md @@ -380,7 +380,7 @@ Allocate system users dynamically for each user. Uses the DynamicUser= feature of Systemd to make a new system user for each hub user dynamically. Their home directories are set up in the directories -configured by `dynamic_user_statedir`, and persist over time. The system user +configured by `dynamic_user_statedir`, and persist over time. The system user is deallocated whenever the user's server is not running. See http://0pointer.net/blog/dynamic-users-with-systemd.html for more diff --git a/systemdspawner/systemdspawner.py b/systemdspawner/systemdspawner.py index ad803cf..e13d153 100644 --- a/systemdspawner/systemdspawner.py +++ b/systemdspawner/systemdspawner.py @@ -150,7 +150,7 @@ class SystemdSpawner(Spawner): /var/lib/{USERNAME}. Ignored if dynamic_users is set to False. - """ + """, ).tag(config=True) slice = Unicode( @@ -279,7 +279,7 @@ async def start(self): self.log.error( "User %s: StateDirectory (%s) cannot be absolute", self.user.name, - statedir + statedir, ) raise Exception(f"StateDirectory ({statedir}) cannot be absolute") @@ -290,7 +290,7 @@ async def start(self): self.log.error( "User %s: StateDirectory (%s) cannot contain ..", self.user.name, - statedir + statedir, ) raise Exception(f"StateDirectory ({statedir}) cannot contain ..")