From fd6660cb9182fde5e593246e311dc3c2dd4b9d13 Mon Sep 17 00:00:00 2001 From: Michael Hoang Date: Thu, 24 Oct 2024 23:55:25 +1100 Subject: [PATCH 01/10] tests: fix negative asserts with `grep` not working Using `grep -v` without `-z` will return 0 even if there is a match found as all the non-matching lines will be matched. Instead of using `grep -vqz`, `(! grep ...)` is more readable. The brackets are necessary as `! grep` will not trigger `set -e`[0], so we run it inside a subshell to use its non-zero exit code. [0]: https://www.gnu.org/savannah-checkouts/gnu/bash/manual/bash.html#The-Set-Builtin --- pkgs/darwin-installer/default.nix | 2 +- tests/programs-zsh.nix | 2 +- tests/users-groups.nix | 18 +++++++++--------- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/pkgs/darwin-installer/default.nix b/pkgs/darwin-installer/default.nix index 36643a36..37a391c0 100644 --- a/pkgs/darwin-installer/default.nix +++ b/pkgs/darwin-installer/default.nix @@ -118,7 +118,7 @@ stdenv.mkDerivation { test -e /etc/static echo >&2 "checking profile" cat /etc/profile - grep -v nix-daemon.sh /etc/profile + (! grep nix-daemon.sh /etc/profile) echo >&2 "checking /run/current-system" readlink /run test -e /run diff --git a/tests/programs-zsh.nix b/tests/programs-zsh.nix index 9c98c335..18680a5d 100644 --- a/tests/programs-zsh.nix +++ b/tests/programs-zsh.nix @@ -34,7 +34,7 @@ echo >&2 "checking compinit in /etc/zshrc" grep 'autoload -U compinit && compinit' ${config.out}/etc/zshrc echo >&2 "checking bashcompinit in /etc/zshrc" - grep -vq 'bashcompinit' ${config.out}/etc/zshrc + (! grep 'bashcompinit' ${config.out}/etc/zshrc) echo >&2 "checking zprofile.d in /etc/zprofile" grep 'source /etc/zprofile.d/\*.conf' ${config.out}/etc/zprofile diff --git a/tests/users-groups.nix b/tests/users-groups.nix index 7df92ba8..5b4f1ae0 100644 --- a/tests/users-groups.nix +++ b/tests/users-groups.nix @@ -28,37 +28,37 @@ grep "dscl . -create ${lib.escapeShellArg "/Groups/foo"} PrimaryGroupID 42000" ${config.out}/activate grep "dscl . -create ${lib.escapeShellArg "/Groups/foo"} RealName ${lib.escapeShellArg "Foo group"}" ${config.out}/activate grep "dscl . -create ${lib.escapeShellArg "/Groups/created.group"} PrimaryGroupID 42001" ${config.out}/activate - grep -qv "dscl . -delete ${lib.escapeShellArg "/Groups/created.group"}" ${config.out}/activate + (! grep "dscl . -delete ${lib.escapeShellArg "/Groups/created.group"}" ${config.out}/activate) # checking group deletion in /activate grep "dscl . -delete ${lib.escapeShellArg "/Groups/deleted.group"}" ${config.out}/activate - grep -qv "dscl . -create ${lib.escapeShellArg "/Groups/deleted.group"}" ${config.out}/activate + (! grep "dscl . -create ${lib.escapeShellArg "/Groups/deleted.group"}" ${config.out}/activate) echo "checking group membership in /activate" >&2 grep "dscl . -create ${lib.escapeShellArg "/Groups/foo"} GroupMembership ${lib.escapeShellArgs [ "admin" "foo" ]}" ${config.out}/activate grep "dscl . -create ${lib.escapeShellArg "/Groups/created.group"} GroupMembership" ${config.out}/activate # checking unknown group in /activate - grep -qv "dscl . -create ${lib.escapeShellArg "/Groups/unknown.group"}" ${config.out}/activate - grep -qv "dscl . -delete ${lib.escapeShellArg "/Groups/unknown.group"}" ${config.out}/activate + (! grep "dscl . -create ${lib.escapeShellArg "/Groups/unknown.group"}" ${config.out}/activate) + (! grep "dscl . -delete ${lib.escapeShellArg "/Groups/unknown.group"}" ${config.out}/activate) # checking user creation in /activate grep "sysadminctl -addUser ${lib.escapeShellArgs [ "foo" "-UID" 42000 "-GID" 42000 "-fullName" "Foo user" "-home" "/Users/foo" "-shell" "/run/current-system/sw/bin/bash" ]}" ${config.out}/activate grep "createhomedir -cu ${lib.escapeShellArg "foo"}" ${config.out}/activate grep "sysadminctl -addUser ${lib.escapeShellArgs [ "created.user" "-UID" 42001 ]} .* ${lib.escapeShellArgs [ "-shell" "/sbin/nologin" ]}" ${config.out}/activate - grep -qv "deleteUser ${lib.escapeShellArg "created.user"}" ${config.out}/activate - grep -qv "deleteUser ${lib.escapeShellArg "created.user"}" ${config.out}/activate + (! grep "deleteUser ${lib.escapeShellArg "created.user"}" ${config.out}/activate) + (! grep "deleteUser ${lib.escapeShellArg "created.user"}" ${config.out}/activate) # checking user properties always get updated in /activate grep "dscl . -create ${lib.escapeShellArg "/Users/foo"} UserShell ${lib.escapeShellArg "/run/current-system/sw/bin/bash"}" ${config.out}/activate # checking user deletion in /activate grep "deleteUser ${lib.escapeShellArg "deleted.user"}" ${config.out}/activate - grep -qv "sysadminctl -addUser ${lib.escapeShellArg "deleted.user"}" ${config.out}/activate + (! grep "sysadminctl -addUser ${lib.escapeShellArg "deleted.user"}" ${config.out}/activate) # checking unknown user in /activate - grep -qv "sysadminctl -addUser ${lib.escapeShellArg "unknown.user"}" ${config.out}/activate - grep -qv "deleteUser ${lib.escapeShellArg "unknown.user"}" ${config.out}/activate + (! grep "sysadminctl -addUser ${lib.escapeShellArg "unknown.user"}" ${config.out}/activate) + (! grep "deleteUser ${lib.escapeShellArg "unknown.user"}" ${config.out}/activate) set +v ''; From 13816f682d1f604271651fec193961ee76610670 Mon Sep 17 00:00:00 2001 From: Michael Hoang Date: Fri, 25 Oct 2024 01:07:34 +1100 Subject: [PATCH 02/10] tests: fix old test getting messed up in refactor https://github.com/LnL7/nix-darwin/commit/2788e4fa981566e34fa40938705cd7f595f05e74#diff-0642dcb4e551dcf07032904ee7f6b7ea645db36939f159908ccb2b85a2bbd1b8L53 --- tests/users-groups.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/users-groups.nix b/tests/users-groups.nix index 5b4f1ae0..fa6dcc30 100644 --- a/tests/users-groups.nix +++ b/tests/users-groups.nix @@ -47,7 +47,7 @@ grep "createhomedir -cu ${lib.escapeShellArg "foo"}" ${config.out}/activate grep "sysadminctl -addUser ${lib.escapeShellArgs [ "created.user" "-UID" 42001 ]} .* ${lib.escapeShellArgs [ "-shell" "/sbin/nologin" ]}" ${config.out}/activate (! grep "deleteUser ${lib.escapeShellArg "created.user"}" ${config.out}/activate) - (! grep "deleteUser ${lib.escapeShellArg "created.user"}" ${config.out}/activate) + (! grep "dscl . -delete ${lib.escapeShellArg "/Groups/created.user"}" ${config.out}/activate) # checking user properties always get updated in /activate grep "dscl . -create ${lib.escapeShellArg "/Users/foo"} UserShell ${lib.escapeShellArg "/run/current-system/sw/bin/bash"}" ${config.out}/activate From c9af5c2d1394d1bc34f4722998bcd51714ccd68c Mon Sep 17 00:00:00 2001 From: Michael Hoang Date: Thu, 24 Oct 2024 22:58:35 +1100 Subject: [PATCH 03/10] users: update properties on known users --- modules/users/default.nix | 5 ++++- tests/users-groups.nix | 13 ++++++++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/modules/users/default.nix b/modules/users/default.nix index b636d6fe..f293f779 100644 --- a/modules/users/default.nix +++ b/modules/users/default.nix @@ -247,7 +247,10 @@ in dscl . -create ${dsclUser} IsHidden ${if v.isHidden then "1" else "0"} ${optionalString v.createHome "createhomedir -cu ${name}"} fi - # Always set the shell path, in case it was updated + + # Update properties on known users to keep them inline with configuration + dscl . -create ${dsclUser} PrimaryGroupID ${toString v.gid} + ${optionalString (v.description != null) "dscl . -create ${dsclUser} RealName ${lib.escapeShellArg v.description}"} dscl . -create ${dsclUser} UserShell ${lib.escapeShellArg (shellPath v.shell)} fi '') createdUsers} diff --git a/tests/users-groups.nix b/tests/users-groups.nix index fa6dcc30..d06eedd8 100644 --- a/tests/users-groups.nix +++ b/tests/users-groups.nix @@ -19,6 +19,8 @@ users.users.foo.shell = pkgs.bashInteractive; users.users."created.user".uid = 42001; + users.users."created.user".description = null; + users.users."unknown.user".uid = 42002; test = '' @@ -39,6 +41,7 @@ grep "dscl . -create ${lib.escapeShellArg "/Groups/created.group"} GroupMembership" ${config.out}/activate # checking unknown group in /activate + # checking groups not in knownGroups don't appear in /activate (! grep "dscl . -create ${lib.escapeShellArg "/Groups/unknown.group"}" ${config.out}/activate) (! grep "dscl . -delete ${lib.escapeShellArg "/Groups/unknown.group"}" ${config.out}/activate) @@ -50,15 +53,23 @@ (! grep "dscl . -delete ${lib.escapeShellArg "/Groups/created.user"}" ${config.out}/activate) # checking user properties always get updated in /activate + grep "dscl . -create ${lib.escapeShellArg "/Users/foo"} PrimaryGroupID 42000" ${config.out}/activate + grep "dscl . -create ${lib.escapeShellArg "/Users/foo"} RealName ${lib.escapeShellArg "Foo user"}" ${config.out}/activate + grep "createhomedir -cu ${lib.escapeShellArg "foo"}" ${config.out}/activate grep "dscl . -create ${lib.escapeShellArg "/Users/foo"} UserShell ${lib.escapeShellArg "/run/current-system/sw/bin/bash"}" ${config.out}/activate + grep "dscl . -create ${lib.escapeShellArg "/Users/foo"} IsHidden 0" ${config.out}/activate + + # checking user properties that are null don't get updated in /activate + (! grep "dscl . -create ${lib.escapeShellArg "/Users/created.user"} RealName" ${config.out}/activate) # checking user deletion in /activate grep "deleteUser ${lib.escapeShellArg "deleted.user"}" ${config.out}/activate (! grep "sysadminctl -addUser ${lib.escapeShellArg "deleted.user"}" ${config.out}/activate) - # checking unknown user in /activate + # checking that users not specified in knownUsers doesn't get changed in /activate (! grep "sysadminctl -addUser ${lib.escapeShellArg "unknown.user"}" ${config.out}/activate) (! grep "deleteUser ${lib.escapeShellArg "unknown.user"}" ${config.out}/activate) + (! grep "dscl . -create ${lib.escapeShellArg "/Users/unknown.user"}" ${config.out}/activate) set +v ''; From bd161d61d6f322e1c16543b67b1dbd13934e763c Mon Sep 17 00:00:00 2001 From: Michael Hoang Date: Thu, 24 Oct 2024 23:19:27 +1100 Subject: [PATCH 04/10] users: allow `home` to be managed by macOS --- modules/users/default.nix | 13 +++++++++++-- modules/users/user.nix | 14 +++++++++++--- tests/users-groups.nix | 2 ++ 3 files changed, 24 insertions(+), 5 deletions(-) diff --git a/modules/users/default.nix b/modules/users/default.nix index f293f779..0b2ffd91 100644 --- a/modules/users/default.nix +++ b/modules/users/default.nix @@ -236,7 +236,13 @@ in requireFDA ${name} "created" - sysadminctl -addUser ${lib.escapeShellArgs ([ v.name "-UID" v.uid "-GID" v.gid ] ++ (lib.optionals (v.description != null) [ "-fullName" v.description ]) ++ [ "-home" v.home "-shell" (shellPath v.shell) ])} 2> /dev/null + sysadminctl -addUser ${lib.escapeShellArgs ([ + v.name + "-UID" v.uid + "-GID" v.gid ] + ++ (lib.optionals (v.description != null) [ "-fullName" v.description ]) + ++ (lib.optionals (v.home != null) [ "-home" v.home ]) + ++ [ "-shell" (shellPath v.shell) ])} 2> /dev/null # We need to check as `sysadminctl -addUser` still exits with exit code 0 when there's an error if ! id ${name} &> /dev/null; then @@ -245,7 +251,10 @@ in fi dscl . -create ${dsclUser} IsHidden ${if v.isHidden then "1" else "0"} - ${optionalString v.createHome "createhomedir -cu ${name}"} + + # `sysadminctl -addUser` won't create the home directory if we use the `-home` + # flag so we need to do it ourselves + ${optionalString (v.home != null && v.createHome) "createhomedir -cu ${name} > /dev/null"} fi # Update properties on known users to keep them inline with configuration diff --git a/modules/users/user.nix b/modules/users/user.nix index a0c8aab5..b9c97997 100644 --- a/modules/users/user.nix +++ b/modules/users/user.nix @@ -53,9 +53,17 @@ # }; home = mkOption { - type = types.path; - default = "/var/empty"; - description = "The user's home directory."; + type = types.nullOr types.path; + default = null; + description = '' + The user's home directory. This defaults to `null`. + + When this is set to `null`, the value is managed by macOS instead of + `nix-darwin`. This means if the user has not been created yet, + `sysadminctl` will be called without the `-home` flag which means the + user will have a default home directory of `/Users/` which will + be created by `sysadminctl`. + ''; }; createHome = mkOption { diff --git a/tests/users-groups.nix b/tests/users-groups.nix index d06eedd8..8fc435ae 100644 --- a/tests/users-groups.nix +++ b/tests/users-groups.nix @@ -20,6 +20,7 @@ users.users."created.user".uid = 42001; users.users."created.user".description = null; + users.users."created.user".home = null; users.users."unknown.user".uid = 42002; @@ -49,6 +50,7 @@ grep "sysadminctl -addUser ${lib.escapeShellArgs [ "foo" "-UID" 42000 "-GID" 42000 "-fullName" "Foo user" "-home" "/Users/foo" "-shell" "/run/current-system/sw/bin/bash" ]}" ${config.out}/activate grep "createhomedir -cu ${lib.escapeShellArg "foo"}" ${config.out}/activate grep "sysadminctl -addUser ${lib.escapeShellArgs [ "created.user" "-UID" 42001 ]} .* ${lib.escapeShellArgs [ "-shell" "/sbin/nologin" ]}" ${config.out}/activate + (! grep "sysadminctl -addUser ${lib.escapeShellArg "created.user"} .* -home" ${config.out}/activate) (! grep "deleteUser ${lib.escapeShellArg "created.user"}" ${config.out}/activate) (! grep "dscl . -delete ${lib.escapeShellArg "/Groups/created.user"}" ${config.out}/activate) From 3712ff78ccacd65c819435a310fe8b1a8a2de2ee Mon Sep 17 00:00:00 2001 From: Michael Hoang Date: Sat, 26 Oct 2024 11:35:34 +1100 Subject: [PATCH 05/10] users: change default shell to `/usr/bin/false` to match macOS --- modules/users/user.nix | 2 +- tests/users-groups.nix | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/users/user.nix b/modules/users/user.nix index b9c97997..281b7e65 100644 --- a/modules/users/user.nix +++ b/modules/users/user.nix @@ -74,7 +74,7 @@ shell = mkOption { type = types.either types.shellPackage types.path; - default = "/sbin/nologin"; + default = "/usr/bin/false"; example = literalExpression "pkgs.bashInteractive"; description = "The user's shell."; }; diff --git a/tests/users-groups.nix b/tests/users-groups.nix index 8fc435ae..290b94b5 100644 --- a/tests/users-groups.nix +++ b/tests/users-groups.nix @@ -49,7 +49,7 @@ # checking user creation in /activate grep "sysadminctl -addUser ${lib.escapeShellArgs [ "foo" "-UID" 42000 "-GID" 42000 "-fullName" "Foo user" "-home" "/Users/foo" "-shell" "/run/current-system/sw/bin/bash" ]}" ${config.out}/activate grep "createhomedir -cu ${lib.escapeShellArg "foo"}" ${config.out}/activate - grep "sysadminctl -addUser ${lib.escapeShellArgs [ "created.user" "-UID" 42001 ]} .* ${lib.escapeShellArgs [ "-shell" "/sbin/nologin" ]}" ${config.out}/activate + grep "sysadminctl -addUser ${lib.escapeShellArgs [ "created.user" "-UID" 42001 ]} .* ${lib.escapeShellArgs [ "-shell" "/usr/bin/false" ]}" ${config.out}/activate (! grep "sysadminctl -addUser ${lib.escapeShellArg "created.user"} .* -home" ${config.out}/activate) (! grep "deleteUser ${lib.escapeShellArg "created.user"}" ${config.out}/activate) (! grep "dscl . -delete ${lib.escapeShellArg "/Groups/created.user"}" ${config.out}/activate) From dc6f754fe5d3b0d1ee6b033495c87ec3199a7f68 Mon Sep 17 00:00:00 2001 From: Michael Hoang Date: Fri, 25 Oct 2024 01:16:19 +1100 Subject: [PATCH 06/10] users: allow `shell` to be managed by macOS --- modules/system/shells.nix | 12 +++++++++--- modules/users/default.nix | 4 ++-- modules/users/user.nix | 13 ++++++++++--- tests/users-groups.nix | 4 +++- 4 files changed, 24 insertions(+), 9 deletions(-) diff --git a/modules/system/shells.nix b/modules/system/shells.nix index 0b599d98..025936d4 100644 --- a/modules/system/shells.nix +++ b/modules/system/shells.nix @@ -14,9 +14,15 @@ in example = literalExpression "[ pkgs.bashInteractive pkgs.zsh ]"; description = '' A list of permissible login shells for user accounts. - No need to mention `/bin/sh` - and other shells that are available by default on - macOS. + + The default macOS shells will be automatically included: + - /bin/bash + - /bin/csh + - /bin/dash + - /bin/ksh + - /bin/sh + - /bin/tcsh + - /bin/zsh ''; apply = map (v: if types.shellPackage.check v then "/run/current-system/sw${v.shellPath}" else v); }; diff --git a/modules/users/default.nix b/modules/users/default.nix index 0b2ffd91..aee8fecc 100644 --- a/modules/users/default.nix +++ b/modules/users/default.nix @@ -242,7 +242,7 @@ in "-GID" v.gid ] ++ (lib.optionals (v.description != null) [ "-fullName" v.description ]) ++ (lib.optionals (v.home != null) [ "-home" v.home ]) - ++ [ "-shell" (shellPath v.shell) ])} 2> /dev/null + ++ [ "-shell" (if v.shell != null then shellPath v.shell else "/usr/bin/false") ])} 2> /dev/null # We need to check as `sysadminctl -addUser` still exits with exit code 0 when there's an error if ! id ${name} &> /dev/null; then @@ -260,7 +260,7 @@ in # Update properties on known users to keep them inline with configuration dscl . -create ${dsclUser} PrimaryGroupID ${toString v.gid} ${optionalString (v.description != null) "dscl . -create ${dsclUser} RealName ${lib.escapeShellArg v.description}"} - dscl . -create ${dsclUser} UserShell ${lib.escapeShellArg (shellPath v.shell)} + ${optionalString (v.shell != null) "dscl . -create ${dsclUser} UserShell ${lib.escapeShellArg (shellPath v.shell)}"} fi '') createdUsers} diff --git a/modules/users/user.nix b/modules/users/user.nix index 281b7e65..72ae07b8 100644 --- a/modules/users/user.nix +++ b/modules/users/user.nix @@ -73,10 +73,17 @@ }; shell = mkOption { - type = types.either types.shellPackage types.path; - default = "/usr/bin/false"; + type = types.nullOr (types.either types.shellPackage types.path); + default = null; example = literalExpression "pkgs.bashInteractive"; - description = "The user's shell."; + description = '' + The user's shell. This defaults to `null`. + + When this is set to `null`, if the user has not been created yet, + they will be created with the shell `/usr/bin/false` to prevent + interactive login. If the user already exists, the value is + considered managed by macOS and `nix-darwin` will not change it. + ''; }; packages = mkOption { diff --git a/tests/users-groups.nix b/tests/users-groups.nix index 290b94b5..cf2f0084 100644 --- a/tests/users-groups.nix +++ b/tests/users-groups.nix @@ -21,6 +21,7 @@ users.users."created.user".uid = 42001; users.users."created.user".description = null; users.users."created.user".home = null; + users.users."created.user".shell = null; users.users."unknown.user".uid = 42002; @@ -49,7 +50,7 @@ # checking user creation in /activate grep "sysadminctl -addUser ${lib.escapeShellArgs [ "foo" "-UID" 42000 "-GID" 42000 "-fullName" "Foo user" "-home" "/Users/foo" "-shell" "/run/current-system/sw/bin/bash" ]}" ${config.out}/activate grep "createhomedir -cu ${lib.escapeShellArg "foo"}" ${config.out}/activate - grep "sysadminctl -addUser ${lib.escapeShellArgs [ "created.user" "-UID" 42001 ]} .* ${lib.escapeShellArgs [ "-shell" "/usr/bin/false" ]}" ${config.out}/activate + grep "sysadminctl -addUser ${lib.escapeShellArgs [ "created.user" "-UID" 42001 ]} .* ${lib.escapeShellArgs [ "-shell" "/usr/bin/false" ] }" ${config.out}/activate (! grep "sysadminctl -addUser ${lib.escapeShellArg "created.user"} .* -home" ${config.out}/activate) (! grep "deleteUser ${lib.escapeShellArg "created.user"}" ${config.out}/activate) (! grep "dscl . -delete ${lib.escapeShellArg "/Groups/created.user"}" ${config.out}/activate) @@ -63,6 +64,7 @@ # checking user properties that are null don't get updated in /activate (! grep "dscl . -create ${lib.escapeShellArg "/Users/created.user"} RealName" ${config.out}/activate) + (! grep "dscl . -create ${lib.escapeShellArg "/Users/created.user"} UserShell" ${config.out}/activate) # checking user deletion in /activate grep "deleteUser ${lib.escapeShellArg "deleted.user"}" ${config.out}/activate From 55be3e1a5f9c816f30baf0d9de8ba77c954847dd Mon Sep 17 00:00:00 2001 From: Michael Hoang Date: Sat, 26 Oct 2024 12:31:53 +1100 Subject: [PATCH 07/10] users: move checks to `system.checks` --- modules/system/activation-scripts.nix | 1 + modules/users/default.nix | 120 +++++++++++++++++--------- 2 files changed, 79 insertions(+), 42 deletions(-) diff --git a/modules/system/activation-scripts.nix b/modules/system/activation-scripts.nix index da8eb5c9..5f8916cc 100644 --- a/modules/system/activation-scripts.nix +++ b/modules/system/activation-scripts.nix @@ -86,6 +86,7 @@ in exit $_status ''; + # FIXME: activationScripts.checks should be system level system.activationScripts.userScript.text = '' #! ${stdenv.shell} set -e diff --git a/modules/users/default.nix b/modules/users/default.nix index aee8fecc..a618792b 100644 --- a/modules/users/default.nix +++ b/modules/users/default.nix @@ -98,6 +98,84 @@ in users.gids = mkMerge gids; users.uids = mkMerge uids; + # NOTE: We put this in `system.checks` as we want this to run first to avoid partial activations + # however currently that runs at user level activation as that runs before system level activation + # TODO: replace `$USER` with `$SUDO_USER` when system.checks runs from system level + system.checks.text = lib.mkAfter '' + requireFDA() { + fullDiskAccess=false + + if cat /Library/Preferences/com.apple.TimeMachine.plist > /dev/null 2>&1; then + fullDiskAccess=true + fi + + if [[ "$fullDiskAccess" != true ]]; then + printf >&2 '\e[1;31merror: users cannot be %s without Full Disk Access, aborting activation\e[0m\n' "$2" + printf >&2 'The user %s could not be %s as `darwin-rebuild` was not executed with Full Disk Access.\n' "$1" "$2" + printf >&2 '\n' + printf >&2 'Opening "Privacy & Security" > "Full Disk Access" in System Settings\n' + printf >&2 '\n' + # This command will fail if run as root and System Settings is already running + # even if System Settings was launched by root. + open "x-apple.systempreferences:com.apple.preference.security?Privacy_AllFiles" + + if [[ -n "$SSH_CONNECTION" ]]; then + printf >&2 'Please enable Full Disk Access for programs over SSH by flipping\n' + printf >&2 'the switch for `sshd-keygen-wrapper`.\n' + else + printf >&2 'Please enable Full Disk Access for your terminal emulator by flipping\n' + printf >&2 'the switch in System Settings.\n' + fi + + exit 1 + fi + } + + ensureDeletable() { + # TODO: add `darwin.primaryUser` as well + if [[ "$1" == "$USER" ]]; then + printf >&2 '\e[1;31merror: refusing to delete the user calling `darwin-rebuild` (%s), aborting activation\e[0m\n', "$1" + exit 1 + elif [[ "$1" == "root" ]]; then + printf >&2 '\e[1;31merror: refusing to delete `root`, aborting activation\e[0m\n' + exit 1 + fi + + requireFDA "$1" deleted + } + + ${concatMapStringsSep "\n" (v: let + name = lib.escapeShellArg v.name; + dsclUser = lib.escapeShellArg "/Users/${v.name}"; + in '' + ${optionalString cfg.forceRecreate '' + u=$(id -u ${name} 2> /dev/null) || true + if [[ "$u" -eq ${toString v.uid} ]]; then + # TODO: add `darwin.primaryUser` as well + if [[ ${name} != "$USER" && ${name} != "root" ]]; then + ensureDeletable ${name} + fi + fi + ''} + + u=$(id -u ${name} 2> /dev/null) || true + if ! [[ -n "$u" && "$u" -ne "${toString v.uid}" ]]; then + if [ -z "$u" ]; then + requireFDA ${name} created + fi + fi + '') createdUsers} + + ${concatMapStringsSep "\n" (name: '' + u=$(id -u ${lib.escapeShellArg name} 2> /dev/null) || true + if [ -n "$u" ]; then + if [ "$u" -gt 501 ]; then + ensureDeletable ${lib.escapeShellArg name} + fi + fi + '') deletedUsers} + ''; + system.activationScripts.groups.text = mkIf (cfg.knownGroups != []) '' echo "setting up groups..." >&2 @@ -154,47 +232,7 @@ in system.activationScripts.users.text = mkIf (cfg.knownUsers != []) '' echo "setting up users..." >&2 - requireFDA() { - fullDiskAccess=false - - if cat /Library/Preferences/com.apple.TimeMachine.plist > /dev/null 2>&1; then - fullDiskAccess=true - fi - - if [[ "$fullDiskAccess" != true ]]; then - printf >&2 '\e[1;31merror: users cannot be %s without Full Disk Access, aborting activation\e[0m\n' "$2" - printf >&2 'The user %s could not be %s as `darwin-rebuild` was not executed with Full Disk Access.\n' "$1" "$2" - printf >&2 '\n' - printf >&2 'Opening "Privacy & Security" > "Full Disk Access" in System Settings\n' - printf >&2 '\n' - # This command will fail if run as root and System Settings is already running - # even if System Settings was launched by root. - sudo -u $SUDO_USER open "x-apple.systempreferences:com.apple.preference.security?Privacy_AllFiles" - - if [[ -n "$SSH_CONNECTION" ]]; then - printf >&2 'Please enable Full Disk Access for programs over SSH by flipping\n' - printf >&2 'the switch for `sshd-keygen-wrapper`.\n' - else - printf >&2 'Please enable Full Disk Access for your terminal emulator by flipping\n' - printf >&2 'the switch in System Settings.\n' - fi - - exit 1 - fi - } - deleteUser() { - # FIXME: add `darwin.primaryUser` as well - if [[ "$1" == "$SUDO_USER" ]]; then - printf >&2 '\e[1;31merror: refusing to delete the user calling `darwin-rebuild` (%s), aborting activation\e[0m\n', "$1" - exit 1 - elif [[ "$1" == "root" ]]; then - printf >&2 '\e[1;31merror: refusing to delete `root`, aborting activation\e[0m\n', "$1" - exit 1 - fi - - requireFDA "$1" deleted - dscl . -delete "/Users/$1" 2> /dev/null # `dscl . -delete` should exit with a non-zero exit code when there's an error, but we'll leave @@ -234,8 +272,6 @@ in if [ -z "$u" ]; then echo "creating user ${v.name}..." >&2 - requireFDA ${name} "created" - sysadminctl -addUser ${lib.escapeShellArgs ([ v.name "-UID" v.uid From 9cd3976486fd0d189cbb3ad3e71c345502a3b1f5 Mon Sep 17 00:00:00 2001 From: Michael Hoang Date: Sat, 26 Oct 2024 12:31:53 +1100 Subject: [PATCH 08/10] users: ensure all users' home directories in the config are correct --- modules/users/default.nix | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/modules/users/default.nix b/modules/users/default.nix index a618792b..3f614c69 100644 --- a/modules/users/default.nix +++ b/modules/users/default.nix @@ -94,6 +94,14 @@ in }; config = { + assertions = [ + { + # We don't check `root` like the rest of the users as on some systems `root`'s + # home directory is set to `/var/root /private/var/root` + assertion = cfg.users ? root -> (cfg.users.root.home == null || cfg.users.root.home == "/var/root"); + message = "`users.users.root.home` must be set to either `null` or `/var/root`."; + } + ]; users.gids = mkMerge gids; users.uids = mkMerge uids; @@ -163,6 +171,22 @@ in if [ -z "$u" ]; then requireFDA ${name} created fi + + ${optionalString (v.home != null && v.name != "root") '' + homeDirectory=$(dscl . -read ${dsclUser} NFSHomeDirectory) + homeDirectory=''${homeDirectory#NFSHomeDirectory: } + if [[ ${lib.escapeShellArg v.home} != "$homeDirectory" ]]; then + printf >&2 '\e[1;31merror: config contains the wrong home directory for %s, aborting activation\e[0m\n' ${name} + printf >&2 'nix-darwin does not support changing the home directory of existing users. + printf >&2 '\n' + printf >&2 'Please set:\n' + printf >&2 '\n' + printf >&2 ' users.users.%s.home = "%s";\n' ${name} "$homeDirectory" + printf >&2 '\n' + printf >&2 'or remove it from your configuration.\n' + exit 1 + fi + ''} fi '') createdUsers} From 32f0cf2140af6a852f8c8b6c8f15e4855d461b87 Mon Sep 17 00:00:00 2001 From: Michael Hoang Date: Mon, 28 Oct 2024 00:37:55 +1100 Subject: [PATCH 09/10] users: replace FDA check with more fine grained permissions check --- modules/users/default.nix | 78 +++++++++++++++++++-------------------- tests/users-groups.nix | 6 +-- 2 files changed, 42 insertions(+), 42 deletions(-) diff --git a/modules/users/default.nix b/modules/users/default.nix index 3f614c69..c6c66f35 100644 --- a/modules/users/default.nix +++ b/modules/users/default.nix @@ -110,32 +110,44 @@ in # however currently that runs at user level activation as that runs before system level activation # TODO: replace `$USER` with `$SUDO_USER` when system.checks runs from system level system.checks.text = lib.mkAfter '' - requireFDA() { - fullDiskAccess=false - - if cat /Library/Preferences/com.apple.TimeMachine.plist > /dev/null 2>&1; then - fullDiskAccess=true - fi - - if [[ "$fullDiskAccess" != true ]]; then - printf >&2 '\e[1;31merror: users cannot be %s without Full Disk Access, aborting activation\e[0m\n' "$2" - printf >&2 'The user %s could not be %s as `darwin-rebuild` was not executed with Full Disk Access.\n' "$1" "$2" - printf >&2 '\n' - printf >&2 'Opening "Privacy & Security" > "Full Disk Access" in System Settings\n' - printf >&2 '\n' - # This command will fail if run as root and System Settings is already running - # even if System Settings was launched by root. - open "x-apple.systempreferences:com.apple.preference.security?Privacy_AllFiles" + ensurePerms() { + homeDirectory=$(dscl . -read /Users/nobody NFSHomeDirectory) + homeDirectory=''${homeDirectory#NFSHomeDirectory: } + if ! sudo dscl . -change /Users/nobody NFSHomeDirectory "$homeDirectory" "$homeDirectory" &> /dev/null; then if [[ -n "$SSH_CONNECTION" ]]; then - printf >&2 'Please enable Full Disk Access for programs over SSH by flipping\n' - printf >&2 'the switch for `sshd-keygen-wrapper`.\n' + printf >&2 '\e[1;31merror: users cannot be %s over SSH without Full Disk Access, aborting activation\e[0m\n' "$2" + printf >&2 'The user %s could not be %s as `darwin-rebuild` was not executed with Full Disk Access over SSH.\n' "$1" "$2" + printf >&2 'You can either:\n' + printf >&2 '\n' + printf >&2 ' grant Full Disk Access to all programs run over SSH\n' + printf >&2 '\n' + printf >&2 'or\n' + printf >&2 '\n' + printf >&2 ' run `darwin-rebuild` in a graphical session.\n' + printf >&2 '\n' + printf >&2 'The option "Allow full disk access for remote users" can be found by\n' + printf >&2 'navigating to System Settings > General > Sharing > Remote Login\n' + printf >&2 'and then pressing on the i icon next to the switch.\n' + exit 1 else - printf >&2 'Please enable Full Disk Access for your terminal emulator by flipping\n' - printf >&2 'the switch in System Settings.\n' + # The TCC service required to change home directories is `kTCCServiceSystemPolicySysAdminFiles` + # and we can reset it to ensure the user gets another prompt + tccutil reset SystemPolicySysAdminFiles > /dev/null + + if ! sudo dscl . -change /Users/nobody NFSHomeDirectory "$homeDirectory" "$homeDirectory" &> /dev/null; then + printf >&2 '\e[1;31merror: permission denied when trying to %s user %s, aborting activation\e[0m\n' "$2" "$1" + printf >&2 '`darwin-rebuild` requires permissions to administrate your computer,\n' "$1" "$2" + printf >&2 'please accept the dialog that pops up.\n' + printf >&2 '\n' + printf >&2 'If you do not wish to be prompted every time `darwin-rebuild updates your users,\n' + printf >&2 'you can grant Full Disk Access to your terminal emulator in System Settings.\n' + printf >&2 '\n' + printf >&2 'This can be found in System Settings > Privacy & Security > Full Disk Access.\n' + exit 1 + fi fi - exit 1 fi } @@ -149,7 +161,7 @@ in exit 1 fi - requireFDA "$1" deleted + ensurePerms "$1" delete } ${concatMapStringsSep "\n" (v: let @@ -169,7 +181,7 @@ in u=$(id -u ${name} 2> /dev/null) || true if ! [[ -n "$u" && "$u" -ne "${toString v.uid}" ]]; then if [ -z "$u" ]; then - requireFDA ${name} created + ensurePerms ${name} create fi ${optionalString (v.home != null && v.name != "root") '' @@ -211,7 +223,7 @@ in g=''${g#PrimaryGroupID: } if [[ "$g" -eq ${toString v.gid} ]]; then echo "deleting group ${v.name}..." >&2 - dscl . -delete ${dsclGroup} 2> /dev/null + dscl . -delete ${dsclGroup} else echo "warning: existing group '${v.name}' has unexpected gid $g, skipping..." >&2 fi @@ -245,7 +257,7 @@ in if [ -n "$g" ]; then if [ "$g" -gt 501 ]; then echo "deleting group ${name}..." >&2 - dscl . -delete ${dsclGroup} 2> /dev/null + dscl . -delete ${dsclGroup} else echo "warning: existing group '${name}' has unexpected gid $g, skipping..." >&2 fi @@ -256,18 +268,6 @@ in system.activationScripts.users.text = mkIf (cfg.knownUsers != []) '' echo "setting up users..." >&2 - deleteUser() { - dscl . -delete "/Users/$1" 2> /dev/null - - # `dscl . -delete` should exit with a non-zero exit code when there's an error, but we'll leave - # this code here just in case and for when we switch to `sysadminctl -deleteUser` - # We need to check as `sysadminctl -deleteUser` still exits with exit code 0 when there's an error - if id "$1" &> /dev/null; then - printf >&2 '\e[1;31merror: failed to delete user %s, aborting activation\e[0m\n', "$1" - exit 1 - fi - } - ${concatMapStringsSep "\n" (v: let name = lib.escapeShellArg v.name; dsclUser = lib.escapeShellArg "/Users/${v.name}"; @@ -282,7 +282,7 @@ in printf >&2 'warning: not going to recreate root, skipping...\n' else printf >&2 'deleting user ${v.name}...\n' - deleteUser ${name} + dscl . -delete ${dsclUser} fi else echo "warning: existing user '${v.name}' has unexpected uid $u, skipping..." >&2 @@ -329,7 +329,7 @@ in if [ -n "$u" ]; then if [ "$u" -gt 501 ]; then echo "deleting user ${name}..." >&2 - deleteUser ${lib.escapeShellArg name} + dscl . -delete ${lib.escapeShellArg "/Users/${name}"} else echo "warning: existing user '${name}' has unexpected uid $u, skipping..." >&2 fi diff --git a/tests/users-groups.nix b/tests/users-groups.nix index cf2f0084..34ee5c24 100644 --- a/tests/users-groups.nix +++ b/tests/users-groups.nix @@ -52,7 +52,7 @@ grep "createhomedir -cu ${lib.escapeShellArg "foo"}" ${config.out}/activate grep "sysadminctl -addUser ${lib.escapeShellArgs [ "created.user" "-UID" 42001 ]} .* ${lib.escapeShellArgs [ "-shell" "/usr/bin/false" ] }" ${config.out}/activate (! grep "sysadminctl -addUser ${lib.escapeShellArg "created.user"} .* -home" ${config.out}/activate) - (! grep "deleteUser ${lib.escapeShellArg "created.user"}" ${config.out}/activate) + (! grep "dscl . -delete ${lib.escapeShellArg "/Users/created.user"}" ${config.out}/activate) (! grep "dscl . -delete ${lib.escapeShellArg "/Groups/created.user"}" ${config.out}/activate) # checking user properties always get updated in /activate @@ -67,12 +67,12 @@ (! grep "dscl . -create ${lib.escapeShellArg "/Users/created.user"} UserShell" ${config.out}/activate) # checking user deletion in /activate - grep "deleteUser ${lib.escapeShellArg "deleted.user"}" ${config.out}/activate + grep "dscl . -delete ${lib.escapeShellArg "/Users/deleted.user"}" ${config.out}/activate (! grep "sysadminctl -addUser ${lib.escapeShellArg "deleted.user"}" ${config.out}/activate) # checking that users not specified in knownUsers doesn't get changed in /activate (! grep "sysadminctl -addUser ${lib.escapeShellArg "unknown.user"}" ${config.out}/activate) - (! grep "deleteUser ${lib.escapeShellArg "unknown.user"}" ${config.out}/activate) + (! grep "dscl . -delete ${lib.escapeShellArg "/Users/unknown.user"}" ${config.out}/activate) (! grep "dscl . -create ${lib.escapeShellArg "/Users/unknown.user"}" ${config.out}/activate) set +v From febc3b3f514d1e3d46182975430737d0232e6af0 Mon Sep 17 00:00:00 2001 From: Michael Hoang Date: Sat, 26 Oct 2024 16:13:23 +1100 Subject: [PATCH 10/10] users: remove `with lib;` --- modules/users/default.nix | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/modules/users/default.nix b/modules/users/default.nix index c6c66f35..a23251dc 100644 --- a/modules/users/default.nix +++ b/modules/users/default.nix @@ -1,8 +1,9 @@ { config, lib, pkgs, ... }: -with lib; - let + inherit (lib) concatStringsSep concatMapStringsSep elem filter filterAttrs + mapAttrs' mapAttrsToList mkIf mkMerge mkOption mkOrder optionalString types; + cfg = config.users; group = import ./group.nix;