From e423e17807207bba779f7e63a2367bd07fd6ddac Mon Sep 17 00:00:00 2001 From: purian23 Date: Thu, 2 Jul 2026 15:18:53 -0400 Subject: [PATCH] fix(greeter): update NixOS declarative configuration - Report NixOS greeter state from `var/lib/dms-greeter` - Allows `dms greeter status` use - Prevent imperative greeter changes on module-managed NixOS systems - Preserve immutable-distro sync policy - Closes #2728 --- core/cmd/dms/commands_greeter.go | 134 ++++++++++++++++++++++++-- core/cmd/dms/commands_greeter_test.go | 56 +++++++++++ core/cmd/dms/immutable_policy_test.go | 24 +++++ 3 files changed, 204 insertions(+), 10 deletions(-) create mode 100644 core/cmd/dms/immutable_policy_test.go diff --git a/core/cmd/dms/commands_greeter.go b/core/cmd/dms/commands_greeter.go index 2f4cabffd..2a002f3c6 100644 --- a/core/cmd/dms/commands_greeter.go +++ b/core/cmd/dms/commands_greeter.go @@ -32,13 +32,14 @@ var greeterCmd = &cobra.Command{ var ( greeterConfigSyncFn = greeter.SyncDMSConfigs sharedAuthSyncFn = sharedpam.SyncAuthConfig + greeterIsNixOSFn = greeter.IsNixOS ) var greeterInstallCmd = &cobra.Command{ Use: "install", Short: "Install and configure DMS greeter", Long: "Install greetd and configure it to use DMS as the greeter interface", - PreRunE: preRunPrivileged, + PreRunE: preRunGreeterMutation, Run: func(cmd *cobra.Command, args []string) { yes, _ := cmd.Flags().GetBool("yes") term, _ := cmd.Flags().GetBool("terminal") @@ -64,6 +65,9 @@ var greeterSyncCmd = &cobra.Command{ Short: "Sync DMS theme and settings with greeter", Long: "Synchronize your current user's DMS theme, settings, and wallpaper configuration with the login greeter screen. Also updates a per-user cache slot at users// for multi-account greeter theme preview.\n\nUse --profile on secondary accounts to sync only your own users// slot without sudo or greetd changes.", PreRunE: func(cmd *cobra.Command, args []string) error { + if err := rejectNixOSGreeterMutation(cmd); err != nil { + return err + } profile, _ := cmd.Flags().GetBool("profile") if profile { return nil @@ -140,7 +144,7 @@ var greeterEnableCmd = &cobra.Command{ Use: "enable", Short: "Enable DMS greeter in greetd config", Long: "Configure greetd to use DMS as the greeter", - PreRunE: preRunPrivileged, + PreRunE: preRunGreeterMutation, Run: func(cmd *cobra.Command, args []string) { yes, _ := cmd.Flags().GetBool("yes") term, _ := cmd.Flags().GetBool("terminal") @@ -176,7 +180,7 @@ var greeterUninstallCmd = &cobra.Command{ Use: "uninstall", Short: "Remove DMS greeter configuration and restore previous display manager", Long: "Disable greetd, remove DMS managed configs, and restore the system to its pre-DMS-greeter state", - PreRunE: preRunPrivileged, + PreRunE: preRunGreeterMutation, Run: func(cmd *cobra.Command, args []string) { yes, _ := cmd.Flags().GetBool("yes") term, _ := cmd.Flags().GetBool("terminal") @@ -206,6 +210,21 @@ func init() { greeterUninstallCmd.Flags().BoolP("terminal", "t", false, "Run in a new terminal (for entering sudo password)") } +func rejectNixOSGreeterMutation(cmd *cobra.Command) error { + if !greeterIsNixOSFn() { + return nil + } + + return fmt.Errorf("dms %s is disabled on NixOS because the greeter is managed declaratively\nConfigure the DMS greeter in your NixOS module, then apply the change with your normal nixos-rebuild workflow", normalizeCommandSpec(cmd.CommandPath())) +} + +func preRunGreeterMutation(cmd *cobra.Command, args []string) error { + if err := rejectNixOSGreeterMutation(cmd); err != nil { + return err + } + return preRunPrivileged(cmd, args) +} + func syncGreeterConfigsAndAuth(dmsPath, compositor string, logFunc func(string), options sharedpam.SyncAuthOptions, beforeAuth func()) error { if err := greeterConfigSyncFn(dmsPath, compositor, logFunc, ""); err != nil { return err @@ -1415,24 +1434,36 @@ func readDefaultSessionCommand(configPath string) string { return "" } -func extractGreeterCacheDirFromCommand(command string) string { - if command == "" { - return greeter.GreeterCacheDir - } +func explicitGreeterCacheDirFromCommand(command string) (string, bool) { tokens := strings.Fields(command) for i := 0; i < len(tokens); i++ { token := strings.Trim(tokens[i], "\"") if token == "--cache-dir" && i+1 < len(tokens) { - return strings.Trim(tokens[i+1], "\"") + value := strings.Trim(tokens[i+1], "\"") + if value != "" { + return value, true + } } if strings.HasPrefix(token, "--cache-dir=") { value := strings.TrimPrefix(token, "--cache-dir=") value = strings.Trim(value, "\"") if value != "" { - return value + return value, true } } } + return "", false +} + +const nixOSGreeterStateDir = "/var/lib/dms-greeter" + +func greeterStatusStateDir(command string, isNixOS bool) string { + if cacheDir, ok := explicitGreeterCacheDirFromCommand(command); ok { + return cacheDir + } + if isNixOS { + return nixOSGreeterStateDir + } return greeter.GreeterCacheDir } @@ -1569,6 +1600,10 @@ func checkGreeterStatus() error { fmt.Println("=== DMS Greeter Status ===") fmt.Println() + if greeterIsNixOSFn() { + return checkNixOSGreeterStatus() + } + homeDir, err := os.UserHomeDir() if err != nil { return fmt.Errorf("failed to get user home directory: %w", err) @@ -1632,7 +1667,7 @@ func checkGreeterStatus() error { fmt.Println(" Run 'dms greeter sync' to set up group membership and permissions") } - cacheDir := extractGreeterCacheDirFromCommand(configuredCommand) + cacheDir := greeterStatusStateDir(configuredCommand, false) fmt.Println("\nGreeter Cache Directory:") fmt.Printf(" Effective cache dir: %s\n", cacheDir) if cacheDir != greeter.GreeterCacheDir { @@ -1895,6 +1930,85 @@ func checkGreeterStatus() error { return nil } +func checkNixOSGreeterStatus() error { + const configPath = "/etc/greetd/config.toml" + + configuredCommand := readDefaultSessionCommand(configPath) + allGood := true + + fmt.Println("Greeter Configuration:") + switch { + case strings.Contains(configuredCommand, "dms-greeter"): + fmt.Println(" ✓ DMS greeter command found") + if wrapper := extractGreeterWrapperFromCommand(configuredCommand); wrapper != "" { + fmt.Printf(" Wrapper: %s\n", wrapper) + } + case configuredCommand != "": + fmt.Println(" ⚠ greetd default session does not reference dms-greeter") + allGood = false + default: + fmt.Printf(" ℹ No readable DMS command found in %s\n", configPath) + } + fmt.Println(" ℹ NixOS manages greeter configuration declaratively; apply changes through your NixOS module.") + + stateDir := greeterStatusStateDir(configuredCommand, true) + fmt.Println("\nGreeter State Directory:") + fmt.Printf(" Effective state dir: %s\n", stateDir) + if stateDir == nixOSGreeterStateDir { + fmt.Println(" ✓ Using the NixOS module state path") + } + if stat, err := os.Stat(stateDir); err == nil && stat.IsDir() { + fmt.Printf(" ✓ %s exists\n", stateDir) + } else if os.IsNotExist(err) { + fmt.Printf(" ✗ %s not found\n", stateDir) + fmt.Println(" Rebuild your NixOS configuration after enabling the DMS greeter module.") + allGood = false + } else if err != nil { + fmt.Printf(" ✗ Could not inspect %s: %v\n", stateDir, err) + allGood = false + } else { + fmt.Printf(" ✗ %s is not a directory\n", stateDir) + allGood = false + } + + fmt.Println("\nDeclarative Configuration Files:") + configFiles := []struct { + name string + path string + }{ + {name: "Settings", path: filepath.Join(stateDir, "settings.json")}, + {name: "Session state", path: filepath.Join(stateDir, "session.json")}, + {name: "Color theme", path: filepath.Join(stateDir, "colors.json")}, + } + for _, configFile := range configFiles { + if stat, err := os.Stat(configFile.path); err == nil && !stat.IsDir() { + fmt.Printf(" ✓ %s: %s\n", configFile.name, configFile.path) + } else if os.IsNotExist(err) { + fmt.Printf(" ℹ %s not present (optional; configure configHome/configFiles in the NixOS module)\n", configFile.name) + } else if err != nil { + fmt.Printf(" ⚠ %s could not be inspected: %v\n", configFile.name, err) + } else { + fmt.Printf(" ⚠ %s path is not a regular file: %s\n", configFile.name, configFile.path) + } + } + + fmt.Println("\nGroup Membership:") + fmt.Println(" ℹ User group membership is managed by NixOS and is not required for declarative theme copies.") + + fmt.Println("\nGreeter PAM Authentication:") + fmt.Println(" ℹ PAM is managed by NixOS modules.") + fmt.Println(" Configure fingerprint/U2F through security.pam.services.greetd.") + + fmt.Println() + if allGood { + fmt.Println("✓ NixOS greeter state looks healthy and is managed declaratively.") + } else { + fmt.Println("⚠ Some issues detected. Update the DMS greeter module and rebuild NixOS; do not run 'dms greeter sync'.") + } + + return nil +} + func recentAppArmorGreeterDenials(sampleLimit int) (int, []string, error) { if sampleLimit <= 0 { sampleLimit = 3 diff --git a/core/cmd/dms/commands_greeter_test.go b/core/cmd/dms/commands_greeter_test.go index 775149fcf..11489612c 100644 --- a/core/cmd/dms/commands_greeter_test.go +++ b/core/cmd/dms/commands_greeter_test.go @@ -3,9 +3,11 @@ package main import ( "errors" "reflect" + "strings" "testing" sharedpam "github.com/AvengeMedia/DankMaterialShell/core/internal/pam" + "github.com/spf13/cobra" ) func TestSyncGreeterConfigsAndAuthDelegatesSharedAuth(t *testing.T) { @@ -85,3 +87,57 @@ func TestSyncGreeterConfigsAndAuthStopsOnConfigError(t *testing.T) { t.Fatal("expected auth sync not to run after config sync failure") } } + +func TestGreeterStatusStateDirUsesNixOSDefault(t *testing.T) { + if got := greeterStatusStateDir("", true); got != nixOSGreeterStateDir { + t.Fatalf("greeterStatusStateDir() = %q, want %q", got, nixOSGreeterStateDir) + } +} + +func TestGreeterStatusStateDirHonorsExplicitOverrideOnNixOS(t *testing.T) { + command := "dms-greeter --cache-dir /srv/dms-greeter --command niri" + if got := greeterStatusStateDir(command, true); got != "/srv/dms-greeter" { + t.Fatalf("greeterStatusStateDir() = %q, want %q", got, "/srv/dms-greeter") + } +} + +func TestRejectNixOSGreeterMutationBlocksImperativeCommands(t *testing.T) { + origGreeterIsNixOSFn := greeterIsNixOSFn + greeterIsNixOSFn = func() bool { return true } + t.Cleanup(func() { + greeterIsNixOSFn = origGreeterIsNixOSFn + }) + + for _, commandName := range []string{"install", "enable", "sync", "uninstall"} { + t.Run(commandName, func(t *testing.T) { + root := &cobra.Command{Use: "dms"} + greeterCommand := &cobra.Command{Use: "greeter"} + mutationCommand := &cobra.Command{Use: commandName} + root.AddCommand(greeterCommand) + greeterCommand.AddCommand(mutationCommand) + + err := rejectNixOSGreeterMutation(mutationCommand) + if err == nil { + t.Fatalf("expected NixOS greeter %s to be rejected", commandName) + } + if !strings.Contains(err.Error(), "dms greeter "+commandName+" is disabled on NixOS") { + t.Fatalf("unexpected error: %v", err) + } + if strings.Contains(err.Error(), "/var/cache/dms-greeter") { + t.Fatalf("NixOS remediation should not recommend the non-NixOS cache path: %v", err) + } + }) + } +} + +func TestRejectNixOSGreeterMutationAllowsOtherDistros(t *testing.T) { + origGreeterIsNixOSFn := greeterIsNixOSFn + greeterIsNixOSFn = func() bool { return false } + t.Cleanup(func() { + greeterIsNixOSFn = origGreeterIsNixOSFn + }) + + if err := rejectNixOSGreeterMutation(&cobra.Command{Use: "sync"}); err != nil { + t.Fatalf("expected non-NixOS greeter command to be allowed, got %v", err) + } +} diff --git a/core/cmd/dms/immutable_policy_test.go b/core/cmd/dms/immutable_policy_test.go new file mode 100644 index 000000000..d95dc18d2 --- /dev/null +++ b/core/cmd/dms/immutable_policy_test.go @@ -0,0 +1,24 @@ +package main + +import ( + "encoding/json" + "testing" +) + +func TestDefaultImmutablePolicyAllowsSyncButBlocksEnable(t *testing.T) { + var policyFile cliPolicyFile + if err := json.Unmarshal(defaultCLIPolicyJSON, &policyFile); err != nil { + t.Fatalf("failed to parse embedded CLI policy: %v", err) + } + if policyFile.BlockedCommands == nil { + t.Fatal("embedded CLI policy has no blocked_commands") + } + + blocked := normalizeBlockedCommands(*policyFile.BlockedCommands) + if !commandBlockedByPolicy("greeter enable", blocked) { + t.Fatal("expected greeter enable to remain blocked on immutable/image-based systems") + } + if commandBlockedByPolicy("greeter sync", blocked) { + t.Fatal("expected greeter sync to remain available on immutable/image-based systems") + } +}