diff --git a/nixos/doc/manual/release-notes/rl-2505.section.md b/nixos/doc/manual/release-notes/rl-2505.section.md index 82f3d6fa59d4..16902e41564e 100644 --- a/nixos/doc/manual/release-notes/rl-2505.section.md +++ b/nixos/doc/manual/release-notes/rl-2505.section.md @@ -555,6 +555,8 @@ - `gerbera` now has wavpack support. +- A toggle has been added under `users.users..enable` to allow toggling individual users conditionally. If set to false, the user account will not be created. + - `ddclient` was updated from 3.11.2 to 4.0.0 [Release notes](https://github.com/ddclient/ddclient/releases/tag/v4.0.0) diff --git a/nixos/modules/config/users-groups.nix b/nixos/modules/config/users-groups.nix index 0fe5cc357b87..4e4d88546af2 100644 --- a/nixos/modules/config/users-groups.nix +++ b/nixos/modules/config/users-groups.nix @@ -124,6 +124,16 @@ let options = { + enable = mkOption { + type = types.bool; + default = true; + example = false; + description = '' + If set to false, the user account will not be created. This is useful for when you wish to conditionally + disable user accounts. + ''; + }; + name = mkOption { type = types.passwdEntry types.str; apply = x: assert (stringLength x < 32 || abort "Username '${x}' is longer than 31 characters which is not allowed!"); x; @@ -557,7 +567,7 @@ let autoSubUidGidRange subUidRanges subGidRanges initialPassword initialHashedPassword expires; shell = utils.toShellPath u.shell; - }) cfg.users; + }) (filterAttrs (_: u: u.enable) cfg.users); groups = attrValues cfg.groups; }); diff --git a/nixos/modules/services/system/userborn.nix b/nixos/modules/services/system/userborn.nix index 4d72229573ee..96c0e6f65ae7 100644 --- a/nixos/modules/services/system/userborn.nix +++ b/nixos/modules/services/system/userborn.nix @@ -31,7 +31,7 @@ let ; isNormal = opts.isNormalUser; shell = utils.toShellPath opts.shell; - }) config.users.users; + }) (lib.filterAttrs (_: u: u.enable) config.users.users); }; userbornConfigJson = pkgs.writeText "userborn.json" (builtins.toJSON userbornConfig); @@ -95,16 +95,23 @@ in # Create home directories, do not create /var/empty even if that's a user's # home. - tmpfiles.settings.home-directories = lib.mapAttrs' ( - username: opts: - lib.nameValuePair (toString opts.home) { - d = { - mode = opts.homeMode; - user = opts.name; - inherit (opts) group; - }; - } - ) (lib.filterAttrs (_username: opts: opts.createHome && opts.home != "/var/empty") userCfg.users); + tmpfiles.settings.home-directories = + lib.mapAttrs' + ( + username: opts: + lib.nameValuePair (toString opts.home) { + d = { + mode = opts.homeMode; + user = opts.name; + inherit (opts) group; + }; + } + ) + ( + lib.filterAttrs ( + _username: opts: opts.enable && opts.createHome && opts.home != "/var/empty" + ) userCfg.users + ); services.userborn = { wantedBy = [ "sysinit.target" ]; diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index f919e5f5a141..50fa8cba2d8f 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -1201,6 +1201,7 @@ in { userborn-mutable-etc = runTest ./userborn-mutable-etc.nix; userborn-immutable-etc = runTest ./userborn-immutable-etc.nix; user-activation-scripts = handleTest ./user-activation-scripts.nix {}; + user-enable-option = runTest ./user-enable-option.nix; user-expiry = runTest ./user-expiry.nix; user-home-mode = handleTest ./user-home-mode.nix {}; ustreamer = handleTest ./ustreamer.nix {}; diff --git a/nixos/tests/user-enable-option.nix b/nixos/tests/user-enable-option.nix new file mode 100644 index 000000000000..304777fd6a55 --- /dev/null +++ b/nixos/tests/user-enable-option.nix @@ -0,0 +1,82 @@ +let + normal-enabled = "username-normal-enabled"; + normal-disabled = "username-normal-disabled"; + system-enabled = "username-system-enabled"; + system-disabled = "username-system-disabled"; + passwd = "enableOptionPasswd"; +in +{ + name = "user-enable-option"; + + nodes.machine = { + users = { + groups.test-group = { }; + users = { + # User is enabled (default behaviour). + ${normal-enabled} = { + enable = true; + isNormalUser = true; + initialPassword = passwd; + }; + + # User is disabled. + ${normal-disabled} = { + enable = false; + isNormalUser = true; + initialPassword = passwd; + }; + + # User is a system user, and is enabled. + ${system-enabled} = { + enable = true; + isSystemUser = true; + initialPassword = passwd; + group = "test-group"; + }; + + # User is a system user, and is disabled. + ${system-disabled} = { + enable = false; + isSystemUser = true; + initialPassword = passwd; + group = "test-group"; + }; + }; + }; + }; + + testScript = '' + def switch_to_tty(tty_number): + machine.fail(f"pgrep -f 'agetty.*tty{tty_number}'") + machine.send_key(f"alt-f{tty_number}") + machine.wait_until_succeeds(f"[ $(fgconsole) = {tty_number} ]") + machine.wait_for_unit(f"getty@tty{tty_number}.service") + machine.wait_until_succeeds(f"pgrep -f 'agetty.*tty{tty_number}'") + + machine.wait_for_unit("multi-user.target") + machine.wait_for_unit("getty@tty1.service") + + with subtest("${normal-enabled} exists"): + check_fn = f"id ${normal-enabled}" + machine.succeed(check_fn) + machine.wait_until_tty_matches("1", "login: ") + machine.send_chars("${normal-enabled}\n") + machine.wait_until_tty_matches("1", "Password: ") + machine.send_chars("${passwd}\n") + + with subtest("${normal-disabled} does not exist"): + switch_to_tty(2) + check_fn = f"id ${normal-disabled}" + machine.fail(check_fn) + + with subtest("${system-enabled} exists"): + switch_to_tty(3) + check_fn = f"id ${system-enabled}" + machine.succeed(check_fn) + + with subtest("${system-disabled} does not exist"): + switch_to_tty(4) + check_fn = f"id ${system-disabled}" + machine.fail(check_fn) + ''; +} diff --git a/nixos/tests/userborn.nix b/nixos/tests/userborn.nix index 2c4f44b93ca5..7932a73b7c9b 100644 --- a/nixos/tests/userborn.nix +++ b/nixos/tests/userborn.nix @@ -66,6 +66,10 @@ in isNormalUser = true; hashedPassword = newNormaloHashedPassword; }; + normalo-disabled = { + enable = false; + isNormalUser = true; + }; }; groups = { new-group = { }; @@ -96,6 +100,11 @@ in assert 1000 > int(machine.succeed("id --user sysuser")), "sysuser user doesn't have a system UID" assert "${sysuserInitialHashedPassword}" in machine.succeed("getent shadow sysuser"), "system user password is not correct" + with subtest("normalo-disabled is NOT created"): + machine.fail("id normalo-disabled") + # Check if user's home has been created + machine.fail("[ -d '/home/normalo-disabled' ]") + with subtest("sysusers group is created"): print(machine.succeed("getent group sysusers"))