mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-08-07 14:08:29 -04:00
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
This commit is contained in:
@@ -32,13 +32,14 @@ var greeterCmd = &cobra.Command{
|
|||||||
var (
|
var (
|
||||||
greeterConfigSyncFn = greeter.SyncDMSConfigs
|
greeterConfigSyncFn = greeter.SyncDMSConfigs
|
||||||
sharedAuthSyncFn = sharedpam.SyncAuthConfig
|
sharedAuthSyncFn = sharedpam.SyncAuthConfig
|
||||||
|
greeterIsNixOSFn = greeter.IsNixOS
|
||||||
)
|
)
|
||||||
|
|
||||||
var greeterInstallCmd = &cobra.Command{
|
var greeterInstallCmd = &cobra.Command{
|
||||||
Use: "install",
|
Use: "install",
|
||||||
Short: "Install and configure DMS greeter",
|
Short: "Install and configure DMS greeter",
|
||||||
Long: "Install greetd and configure it to use DMS as the greeter interface",
|
Long: "Install greetd and configure it to use DMS as the greeter interface",
|
||||||
PreRunE: preRunPrivileged,
|
PreRunE: preRunGreeterMutation,
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
yes, _ := cmd.Flags().GetBool("yes")
|
yes, _ := cmd.Flags().GetBool("yes")
|
||||||
term, _ := cmd.Flags().GetBool("terminal")
|
term, _ := cmd.Flags().GetBool("terminal")
|
||||||
@@ -64,6 +65,9 @@ var greeterSyncCmd = &cobra.Command{
|
|||||||
Short: "Sync DMS theme and settings with greeter",
|
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/<username>/ for multi-account greeter theme preview.\n\nUse --profile on secondary accounts to sync only your own users/<username>/ slot without sudo or greetd changes.",
|
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/<username>/ for multi-account greeter theme preview.\n\nUse --profile on secondary accounts to sync only your own users/<username>/ slot without sudo or greetd changes.",
|
||||||
PreRunE: func(cmd *cobra.Command, args []string) error {
|
PreRunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
if err := rejectNixOSGreeterMutation(cmd); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
profile, _ := cmd.Flags().GetBool("profile")
|
profile, _ := cmd.Flags().GetBool("profile")
|
||||||
if profile {
|
if profile {
|
||||||
return nil
|
return nil
|
||||||
@@ -140,7 +144,7 @@ var greeterEnableCmd = &cobra.Command{
|
|||||||
Use: "enable",
|
Use: "enable",
|
||||||
Short: "Enable DMS greeter in greetd config",
|
Short: "Enable DMS greeter in greetd config",
|
||||||
Long: "Configure greetd to use DMS as the greeter",
|
Long: "Configure greetd to use DMS as the greeter",
|
||||||
PreRunE: preRunPrivileged,
|
PreRunE: preRunGreeterMutation,
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
yes, _ := cmd.Flags().GetBool("yes")
|
yes, _ := cmd.Flags().GetBool("yes")
|
||||||
term, _ := cmd.Flags().GetBool("terminal")
|
term, _ := cmd.Flags().GetBool("terminal")
|
||||||
@@ -176,7 +180,7 @@ var greeterUninstallCmd = &cobra.Command{
|
|||||||
Use: "uninstall",
|
Use: "uninstall",
|
||||||
Short: "Remove DMS greeter configuration and restore previous display manager",
|
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",
|
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) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
yes, _ := cmd.Flags().GetBool("yes")
|
yes, _ := cmd.Flags().GetBool("yes")
|
||||||
term, _ := cmd.Flags().GetBool("terminal")
|
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)")
|
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 {
|
func syncGreeterConfigsAndAuth(dmsPath, compositor string, logFunc func(string), options sharedpam.SyncAuthOptions, beforeAuth func()) error {
|
||||||
if err := greeterConfigSyncFn(dmsPath, compositor, logFunc, ""); err != nil {
|
if err := greeterConfigSyncFn(dmsPath, compositor, logFunc, ""); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -1415,24 +1434,36 @@ func readDefaultSessionCommand(configPath string) string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
func extractGreeterCacheDirFromCommand(command string) string {
|
func explicitGreeterCacheDirFromCommand(command string) (string, bool) {
|
||||||
if command == "" {
|
|
||||||
return greeter.GreeterCacheDir
|
|
||||||
}
|
|
||||||
tokens := strings.Fields(command)
|
tokens := strings.Fields(command)
|
||||||
for i := 0; i < len(tokens); i++ {
|
for i := 0; i < len(tokens); i++ {
|
||||||
token := strings.Trim(tokens[i], "\"")
|
token := strings.Trim(tokens[i], "\"")
|
||||||
if token == "--cache-dir" && i+1 < len(tokens) {
|
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=") {
|
if strings.HasPrefix(token, "--cache-dir=") {
|
||||||
value := strings.TrimPrefix(token, "--cache-dir=")
|
value := strings.TrimPrefix(token, "--cache-dir=")
|
||||||
value = strings.Trim(value, "\"")
|
value = strings.Trim(value, "\"")
|
||||||
if 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
|
return greeter.GreeterCacheDir
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1569,6 +1600,10 @@ func checkGreeterStatus() error {
|
|||||||
fmt.Println("=== DMS Greeter Status ===")
|
fmt.Println("=== DMS Greeter Status ===")
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
|
|
||||||
|
if greeterIsNixOSFn() {
|
||||||
|
return checkNixOSGreeterStatus()
|
||||||
|
}
|
||||||
|
|
||||||
homeDir, err := os.UserHomeDir()
|
homeDir, err := os.UserHomeDir()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to get user home directory: %w", err)
|
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")
|
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.Println("\nGreeter Cache Directory:")
|
||||||
fmt.Printf(" Effective cache dir: %s\n", cacheDir)
|
fmt.Printf(" Effective cache dir: %s\n", cacheDir)
|
||||||
if cacheDir != greeter.GreeterCacheDir {
|
if cacheDir != greeter.GreeterCacheDir {
|
||||||
@@ -1895,6 +1930,85 @@ func checkGreeterStatus() error {
|
|||||||
return nil
|
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) {
|
func recentAppArmorGreeterDenials(sampleLimit int) (int, []string, error) {
|
||||||
if sampleLimit <= 0 {
|
if sampleLimit <= 0 {
|
||||||
sampleLimit = 3
|
sampleLimit = 3
|
||||||
|
|||||||
@@ -3,9 +3,11 @@ package main
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
sharedpam "github.com/AvengeMedia/DankMaterialShell/core/internal/pam"
|
sharedpam "github.com/AvengeMedia/DankMaterialShell/core/internal/pam"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestSyncGreeterConfigsAndAuthDelegatesSharedAuth(t *testing.T) {
|
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")
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user