diff --git a/nixos/modules/services/system/userborn.nix b/nixos/modules/services/system/userborn.nix index f9595fe72654..5649b438965b 100644 --- a/nixos/modules/services/system/userborn.nix +++ b/nixos/modules/services/system/userborn.nix @@ -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); }; }; }; diff --git a/nixos/tests/userborn-mutable-users.nix b/nixos/tests/userborn-mutable-users.nix index 92b952d397d3..8f23afce9b9a 100644 --- a/nixos/tests/userborn-mutable-users.nix +++ b/nixos/tests/userborn-mutable-users.nix @@ -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") ''; }