nixos/userborn: properly implement mutable users

For this to work properly we need a new Userborn version in Nixpkgs.
This commit is contained in:
nikstur
2026-01-25 17:39:38 +01:00
parent 8b80ecd252
commit 2b71179e9f
2 changed files with 39 additions and 12 deletions

View File

@@ -38,6 +38,7 @@ let
userbornStaticFiles =
pkgs.runCommand "static-userborn" { }
"mkdir -p $out; ${lib.getExe cfg.package} ${userbornConfigJson} $out";
previousConfigPath = "/var/lib/userborn/previous-userborn.json";
immutableEtc = config.system.etc.overlay.enable && !config.system.etc.overlay.mutable;
# The filenames created by userborn.
@@ -155,6 +156,10 @@ in
# This way we don't have to re-declare all the dependencies to other
# services again.
aliases = [ "systemd-sysusers.service" ];
environment = {
USERBORN_MUTABLE_USERS = lib.boolToString userCfg.mutableUsers;
USERBORN_PREVIOUS_CONFIG = lib.mkIf userCfg.mutableUsers previousConfigPath;
};
unitConfig = {
Description = "Manage Users and Groups";
@@ -165,6 +170,7 @@ in
Type = "oneshot";
RemainAfterExit = true;
TimeoutSec = "90s";
StateDirectory = "userborn";
ExecStart = "${lib.getExe cfg.package} ${userbornConfigJson} ${cfg.passwordFilesLocation}";
@@ -179,13 +185,18 @@ in
))
];
# Make the source files read-only after userborn has finished.
ExecStartPost = lib.mkIf (!userCfg.mutableUsers) (
lib.map (
file:
"${pkgs.util-linux}/bin/mount --bind -o ro ${cfg.passwordFilesLocation}/${file} ${cfg.passwordFilesLocation}/${file}"
) passwordFiles
);
ExecStartPost =
if userCfg.mutableUsers then
# Store the config somewhere for the next invocation
[
"${pkgs.coreutils}/bin/ln -sf ${userbornConfigJson} ${previousConfigPath}"
]
else
# Make the source files read-only after userborn has finished.
(lib.map (
file:
"${pkgs.util-linux}/bin/mount --bind -o ro ${cfg.passwordFilesLocation}/${file} ${cfg.passwordFilesLocation}/${file}"
) passwordFiles);
};
};
};

View File

@@ -42,6 +42,10 @@ in
new-normalo = {
isNormalUser = true;
};
mutable-to-declarative = {
isNormalUser = true;
description = "I'm now declaratively managed";
};
};
};
};
@@ -54,16 +58,25 @@ in
assert 1000 == int(machine.succeed("id --user normalo")), "normalo user doesn't have UID 1000"
assert "${normaloHashedPassword}" in machine.succeed("getent shadow normalo"), "normalo user password is not correct"
with subtest("Add new user manually"):
with subtest("Add new user manual-normalo manually"):
machine.succeed("useradd manual-normalo")
assert 1001 == int(machine.succeed("id --user manual-normalo")), "manual-normalo user doesn't have UID 1001"
with subtest("Delete manual--normalo user manually"):
machine.succeed("userdel manual-normalo")
with subtest("Add new user mutable-to-declarative manually"):
machine.succeed("useradd --comment 'I was created imperatively' mutable-to-declarative")
assert 1002 == int(machine.succeed("id --user mutable-to-declarative")), "mutable-to-declarative user doesn't have UID 1002"
machine.succeed("/run/current-system/specialisation/new-generation/bin/switch-to-configuration switch")
with subtest("manual-normalo user is still enabled"):
manual_normalo_shadow = machine.succeed("getent shadow manual-normalo")
print(manual_normalo_shadow)
t.assertNotIn("!*", manual_normalo_shadow, "manual-normalo user is falsely disabled")
with subtest("mutable-to-declarative user description has changed"):
mutable_to_declarative_passwd = machine.succeed("getent passwd mutable-to-declarative")
print(mutable_to_declarative_passwd)
t.assertIn("I'm now declaratively managed", mutable_to_declarative_passwd, "mutable-to-declarative user description is unchanged")
with subtest("normalo user is disabled"):
print(machine.succeed("getent shadow normalo"))
@@ -71,6 +84,9 @@ in
with subtest("new-normalo user is created after switching to new generation"):
print(machine.succeed("getent passwd new-normalo"))
assert 1001 == int(machine.succeed("id --user new-normalo")), "new-normalo user doesn't have UID 1001"
assert 1003 == int(machine.succeed("id --user new-normalo")), "new-normalo user doesn't have UID 1003"
with subtest("Delete manual-normalo user manually"):
machine.succeed("userdel manual-normalo")
'';
}