From 81bce7461243c6da17f216273dd8424bc6135159 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristoffer=20Gr=C3=B6nlund?= Date: Mon, 16 Feb 2026 06:36:58 +0100 Subject: [PATCH] greeter: Add support for Debian greetd user/group name (#1685) * greeter: Detect user and group used by greetd On most distros greetd runs as user and group "greeter", but on Debian the user and group "_greetd" are used. * greeter: Use correct group in sync command * greeter: more generic group detection --------- Co-authored-by: bbedward --- core/cmd/dms/commands_greeter.go | 32 ++----- core/internal/greeter/installer.go | 61 +++++++++---- core/internal/utils/group.go | 37 ++++++++ core/internal/utils/group_test.go | 142 +++++++++++++++++++++++++++++ 4 files changed, 231 insertions(+), 41 deletions(-) create mode 100644 core/internal/utils/group.go create mode 100644 core/internal/utils/group_test.go diff --git a/core/cmd/dms/commands_greeter.go b/core/cmd/dms/commands_greeter.go index ac596304..e9de1c02 100644 --- a/core/cmd/dms/commands_greeter.go +++ b/core/cmd/dms/commands_greeter.go @@ -169,7 +169,8 @@ func syncGreeter() error { return fmt.Errorf("greeter cache directory not found at %s\nPlease install the greeter first", cacheDir) } - greeterGroupExists := checkGroupExists("greeter") + greeterGroup := greeter.DetectGreeterGroup() + greeterGroupExists := utils.HasGroup(greeterGroup) if greeterGroupExists { currentUser, err := user.Current() if err != nil { @@ -182,24 +183,24 @@ func syncGreeter() error { return fmt.Errorf("failed to check groups: %w", err) } - inGreeterGroup := strings.Contains(string(groupsOutput), "greeter") + inGreeterGroup := strings.Contains(string(groupsOutput), greeterGroup) if !inGreeterGroup { - fmt.Println("\n⚠ Warning: You are not in the greeter group.") - fmt.Print("Would you like to add your user to the greeter group? (Y/n): ") + fmt.Printf("\n⚠ Warning: You are not in the %s group.\n", greeterGroup) + fmt.Printf("Would you like to add your user to the %s group? (Y/n): ", greeterGroup) var response string fmt.Scanln(&response) response = strings.ToLower(strings.TrimSpace(response)) if response != "n" && response != "no" { - fmt.Println("\nAdding user to greeter group...") - addUserCmd := exec.Command("sudo", "usermod", "-aG", "greeter", currentUser.Username) + fmt.Printf("\nAdding user to %s group...\n", greeterGroup) + addUserCmd := exec.Command("sudo", "usermod", "-aG", greeterGroup, currentUser.Username) addUserCmd.Stdout = os.Stdout addUserCmd.Stderr = os.Stderr if err := addUserCmd.Run(); err != nil { - return fmt.Errorf("failed to add user to greeter group: %w", err) + return fmt.Errorf("failed to add user to %s group: %w", greeterGroup, err) } - fmt.Println("✓ User added to greeter group") + fmt.Printf("✓ User added to %s group\n", greeterGroup) fmt.Println("⚠ You will need to log out and back in for the group change to take effect") } else { return fmt.Errorf("aborted: user must be in the greeter group before syncing") @@ -245,21 +246,6 @@ func syncGreeter() error { return nil } -func checkGroupExists(groupName string) bool { - data, err := os.ReadFile("/etc/group") - if err != nil { - return false - } - - lines := strings.SplitSeq(string(data), "\n") - for line := range lines { - if strings.HasPrefix(line, groupName+":") { - return true - } - } - return false -} - func disableDisplayManager(dmName string) (bool, error) { state, err := getSystemdServiceState(dmName) if err != nil { diff --git a/core/internal/greeter/installer.go b/core/internal/greeter/installer.go index 9800b89a..ecef0345 100644 --- a/core/internal/greeter/installer.go +++ b/core/internal/greeter/installer.go @@ -22,6 +22,21 @@ func DetectDMSPath() (string, error) { return config.LocateDMSConfig() } +func DetectGreeterGroup() string { + data, err := os.ReadFile("/etc/group") + if err != nil { + fmt.Fprintln(os.Stderr, "⚠ Warning: could not read /etc/group, defaulting to greeter") + return "greeter" + } + + if group, found := utils.FindGroupData(string(data), "greeter", "greetd", "_greeter"); found { + return group + } + + fmt.Fprintln(os.Stderr, "⚠ Warning: no greeter group found in /etc/group, defaulting to greeter") + return "greeter" +} + // DetectCompositors checks which compositors are installed func DetectCompositors() []string { var compositors []string @@ -194,14 +209,17 @@ func CopyGreeterFiles(dmsPath, compositor string, logFunc func(string), sudoPass return fmt.Errorf("failed to create cache directory: %w", err) } - if err := runSudoCmd(sudoPassword, "chown", "greeter:greeter", cacheDir); err != nil { + group := DetectGreeterGroup() + owner := fmt.Sprintf("%s:%s", group, group) + + if err := runSudoCmd(sudoPassword, "chown", owner, cacheDir); err != nil { return fmt.Errorf("failed to set cache directory owner: %w", err) } if err := runSudoCmd(sudoPassword, "chmod", "755", cacheDir); err != nil { return fmt.Errorf("failed to set cache directory permissions: %w", err) } - logFunc(fmt.Sprintf("✓ Created cache directory %s (owner: greeter:greeter, permissions: 755)", cacheDir)) + logFunc(fmt.Sprintf("✓ Created cache directory %s (owner: %s, permissions: 755)", cacheDir, owner)) return nil } @@ -234,6 +252,8 @@ func SetupParentDirectoryACLs(logFunc func(string), sudoPassword string) error { {filepath.Join(homeDir, ".local", "share"), ".local/share directory"}, } + owner := DetectGreeterGroup() + logFunc("\nSetting up parent directory ACLs for greeter user access...") for _, dir := range parentDirs { @@ -245,9 +265,9 @@ func SetupParentDirectoryACLs(logFunc func(string), sudoPassword string) error { } // Set ACL to allow greeter user read+execute permission (for session discovery) - if err := runSudoCmd(sudoPassword, "setfacl", "-m", "u:greeter:rx", dir.path); err != nil { + if err := runSudoCmd(sudoPassword, "setfacl", "-m", fmt.Sprintf("u:%s:rx", owner), dir.path); err != nil { logFunc(fmt.Sprintf("⚠ Warning: Failed to set ACL on %s: %v", dir.desc, err)) - logFunc(fmt.Sprintf(" You may need to run manually: setfacl -m u:greeter:x %s", dir.path)) + logFunc(fmt.Sprintf(" You may need to run manually: setfacl -m u:%s:x %s", owner, dir.path)) continue } @@ -271,17 +291,19 @@ func SetupDMSGroup(logFunc func(string), sudoPassword string) error { return fmt.Errorf("failed to determine current user") } + group := DetectGreeterGroup() + // Check if user is already in greeter group groupsCmd := exec.Command("groups", currentUser) groupsOutput, err := groupsCmd.Output() - if err == nil && strings.Contains(string(groupsOutput), "greeter") { - logFunc(fmt.Sprintf("✓ %s is already in greeter group", currentUser)) + if err == nil && strings.Contains(string(groupsOutput), group) { + logFunc(fmt.Sprintf("✓ %s is already in %s group", currentUser, group)) } else { // Add current user to greeter group for file access permissions - if err := runSudoCmd(sudoPassword, "usermod", "-aG", "greeter", currentUser); err != nil { - return fmt.Errorf("failed to add %s to greeter group: %w", currentUser, err) + if err := runSudoCmd(sudoPassword, "usermod", "-aG", group, currentUser); err != nil { + return fmt.Errorf("failed to add %s to %s group: %w", currentUser, group, err) } - logFunc(fmt.Sprintf("✓ Added %s to greeter group (logout/login required for changes to take effect)", currentUser)) + logFunc(fmt.Sprintf("✓ Added %s to %s group (logout/login required for changes to take effect)", currentUser, group)) } configDirs := []struct { @@ -304,7 +326,7 @@ func SetupDMSGroup(logFunc func(string), sudoPassword string) error { } } - if err := runSudoCmd(sudoPassword, "chgrp", "-R", "greeter", dir.path); err != nil { + if err := runSudoCmd(sudoPassword, "chgrp", "-R", group, dir.path); err != nil { logFunc(fmt.Sprintf("⚠ Warning: Failed to set group for %s: %v", dir.desc, err)) continue } @@ -436,10 +458,11 @@ func syncNiriGreeterConfig(logFunc func(string), sudoPassword string) error { } greeterDir := "/etc/greetd/niri" + greeterGroup := DetectGreeterGroup() if err := runSudoCmd(sudoPassword, "mkdir", "-p", greeterDir); err != nil { return fmt.Errorf("failed to create greetd niri directory: %w", err) } - if err := runSudoCmd(sudoPassword, "chown", "root:greeter", greeterDir); err != nil { + if err := runSudoCmd(sudoPassword, "chown", fmt.Sprintf("root:%s", greeterGroup), greeterDir); err != nil { return fmt.Errorf("failed to set greetd niri directory ownership: %w", err) } if err := runSudoCmd(sudoPassword, "chmod", "755", greeterDir); err != nil { @@ -464,7 +487,7 @@ func syncNiriGreeterConfig(logFunc func(string), sudoPassword string) error { if err := backupFileIfExists(sudoPassword, dmsPath, ".backup"); err != nil { return fmt.Errorf("failed to backup %s: %w", dmsPath, err) } - if err := runSudoCmd(sudoPassword, "install", "-o", "root", "-g", "greeter", "-m", "0644", dmsTemp.Name(), dmsPath); err != nil { + if err := runSudoCmd(sudoPassword, "install", "-o", "root", "-g", greeterGroup, "-m", "0644", dmsTemp.Name(), dmsPath); err != nil { return fmt.Errorf("failed to install greetd niri dms config: %w", err) } @@ -487,7 +510,7 @@ func syncNiriGreeterConfig(logFunc func(string), sudoPassword string) error { if err := backupFileIfExists(sudoPassword, mainPath, ".backup"); err != nil { return fmt.Errorf("failed to backup %s: %w", mainPath, err) } - if err := runSudoCmd(sudoPassword, "install", "-o", "root", "-g", "greeter", "-m", "0644", mainTemp.Name(), mainPath); err != nil { + if err := runSudoCmd(sudoPassword, "install", "-o", "root", "-g", greeterGroup, "-m", "0644", mainTemp.Name(), mainPath); err != nil { return fmt.Errorf("failed to install greetd niri main config: %w", err) } @@ -736,17 +759,19 @@ func ConfigureGreetd(dmsPath, compositor string, logFunc func(string), sudoPassw logFunc(fmt.Sprintf("✓ Backed up existing config to %s", backupPath)) } + greeterUser := DetectGreeterGroup() + var configContent string if data, err := os.ReadFile(configPath); err == nil { configContent = string(data) } else { - configContent = `[terminal] + configContent = fmt.Sprintf(`[terminal] vt = 1 [default_session] -user = "greeter" -` +user = "%s" +`, greeterUser) } lines := strings.Split(configContent, "\n") @@ -755,7 +780,7 @@ user = "greeter" trimmed := strings.TrimSpace(line) if !strings.HasPrefix(trimmed, "command =") && !strings.HasPrefix(trimmed, "command=") { if strings.HasPrefix(trimmed, "user =") || strings.HasPrefix(trimmed, "user=") { - newLines = append(newLines, `user = "greeter"`) + newLines = append(newLines, fmt.Sprintf(`user = "%s"`, greeterUser)) } else { newLines = append(newLines, line) } @@ -807,7 +832,7 @@ user = "greeter" return fmt.Errorf("failed to move config to /etc/greetd: %w", err) } - logFunc(fmt.Sprintf("✓ Updated greetd configuration (user: greeter, command: %s --command %s -p %s)", wrapperCmd, compositorLower, dmsPath)) + logFunc(fmt.Sprintf("✓ Updated greetd configuration (user: %s, command: %s --command %s -p %s)", greeterUser, wrapperCmd, compositorLower, dmsPath)) return nil } diff --git a/core/internal/utils/group.go b/core/internal/utils/group.go new file mode 100644 index 00000000..1a0a9504 --- /dev/null +++ b/core/internal/utils/group.go @@ -0,0 +1,37 @@ +package utils + +import ( + "os" + "strings" +) + +func HasGroup(groupName string) bool { + return HasGroupIn(groupName, "/etc/group") +} + +func HasGroupIn(groupName, path string) bool { + data, err := os.ReadFile(path) + if err != nil { + return false + } + return HasGroupData(groupName, string(data)) +} + +func HasGroupData(groupName, data string) bool { + prefix := groupName + ":" + for line := range strings.SplitSeq(data, "\n") { + if strings.HasPrefix(line, prefix) { + return true + } + } + return false +} + +func FindGroupData(data string, candidates ...string) (string, bool) { + for _, candidate := range candidates { + if HasGroupData(candidate, data) { + return candidate, true + } + } + return "", false +} diff --git a/core/internal/utils/group_test.go b/core/internal/utils/group_test.go new file mode 100644 index 00000000..4b401dc7 --- /dev/null +++ b/core/internal/utils/group_test.go @@ -0,0 +1,142 @@ +package utils + +import "testing" + +const testGroupData = `root:x:0:brltty,root +sys:x:3:bin,testuser +mem:x:8: +ftp:x:11: +mail:x:12: +log:x:19: +smmsp:x:25: +proc:x:26: +games:x:50: +lock:x:54: +network:x:90: +floppy:x:94: +scanner:x:96: +power:x:98: +nobody:x:65534: +adm:x:999:daemon +wheel:x:998:testuser +utmp:x:997: +audio:x:996:brltty +disk:x:995: +input:x:994:brltty,testuser,greeter +kmem:x:993: +kvm:x:992:libvirt-qemu,qemu,testuser +lp:x:991:cups,testuser +optical:x:990: +render:x:989: +sgx:x:988: +storage:x:987: +tty:x:5:brltty +uucp:x:986:brltty +video:x:985:cosmic-greeter,greeter,testuser +users:x:984: +groups:x:983: +systemd-journal:x:982: +rfkill:x:981: +bin:x:1:daemon +daemon:x:2:bin +http:x:33: +dbus:x:81: +systemd-coredump:x:980: +systemd-network:x:979: +systemd-oom:x:978: +systemd-journal-remote:x:977: +systemd-resolve:x:976: +systemd-timesync:x:975: +tss:x:974: +uuidd:x:973: +alpm:x:972: +polkitd:x:102: +testuser:x:1000: +avahi:x:971: +git:x:970: +nvidia-persistenced:x:143: +i2c:x:969:testuser +seat:x:968: +rtkit:x:133: +brlapi:x:967:brltty +gdm:x:120: +brltty:x:966: +colord:x:965: +flatpak:x:964: +geoclue:x:963:testuser +gnome-remote-desktop:x:962: +saned:x:961: +usbmux:x:140: +cosmic-greeter:x:960: +greeter:x:959:testuser +openvpn:x:958: +nm-openvpn:x:957: +named:x:40: +_talkd:x:956: +keyd:x:955: +cups:x:209:testuser +docker:x:954:testuser +mysql:x:953: +radicale:x:952: +onepassword:x:1001: +nixbld:x:951:nixbld01,nixbld02,nixbld03,nixbld04,nixbld05,nixbld06,nixbld07,nixbld08,nixbld09,nixbld10 +virtlogin:x:940: +libvirt:x:939:testuser +libvirt-qemu:x:938: +qemu:x:937: +dnsmasq:x:936: +clock:x:935: +dms-greeter:x:1002:greeter,testuser +pcscd:x:934: +test:x:1003: +empower:x:933: +` + +func TestHasGroupData(t *testing.T) { + tests := []struct { + group string + want bool + }{ + {"greeter", true}, + {"root", true}, + {"docker", true}, + {"cosmic-greeter", true}, + {"dms-greeter", true}, + {"nonexistent", false}, + {"greet", false}, + } + + for _, tt := range tests { + if got := HasGroupData(tt.group, testGroupData); got != tt.want { + t.Errorf("HasGroupData(%q) = %v, want %v", tt.group, got, tt.want) + } + } +} + +func TestFindGroupData(t *testing.T) { + tests := []struct { + name string + candidates []string + wantGroup string + wantFound bool + }{ + {"first match wins", []string{"greeter", "greetd", "_greeter"}, "greeter", true}, + {"fallback to second", []string{"greetd", "greeter"}, "greeter", true}, + {"none found", []string{"_greetd", "greetd"}, "", false}, + {"single match", []string{"docker"}, "docker", true}, + } + + for _, tt := range tests { + got, found := FindGroupData(testGroupData, tt.candidates...) + if got != tt.wantGroup || found != tt.wantFound { + t.Errorf("%s: FindGroupData(%v) = (%q, %v), want (%q, %v)", + tt.name, tt.candidates, got, found, tt.wantGroup, tt.wantFound) + } + } +} + +func TestHasGroupDataEmpty(t *testing.T) { + if HasGroupData("greeter", "") { + t.Error("expected false for empty data") + } +}