mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-04-03 20:32:07 -04:00
fix(Greeter): Multi-distro reliability updates
- Merge duplicate niri input/output KDL nodes instead of appending. Allows more overrides - Guard AppArmor install/uninstall behind IsAppArmorEnabled() check
This commit is contained in:
@@ -227,9 +227,11 @@ func installGreeter(nonInteractive bool) error {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Println("\nConfiguring AppArmor profile...")
|
||||
if err := greeter.InstallAppArmorProfile(logFunc, ""); err != nil {
|
||||
logFunc(fmt.Sprintf("⚠ AppArmor profile setup failed: %v", err))
|
||||
if greeter.IsAppArmorEnabled() {
|
||||
fmt.Println("\nConfiguring AppArmor profile...")
|
||||
if err := greeter.InstallAppArmorProfile(logFunc, ""); err != nil {
|
||||
logFunc(fmt.Sprintf("⚠ AppArmor profile setup failed: %v", err))
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Println("\nConfiguring greetd...")
|
||||
@@ -575,12 +577,13 @@ func syncGreeter(nonInteractive bool, forceAuth bool, local bool) error {
|
||||
}
|
||||
}
|
||||
|
||||
if greeter.IsGreeterPackaged() && greeter.HasLegacyLocalGreeterWrapper() {
|
||||
return fmt.Errorf("legacy manual wrapper detected at /usr/local/bin/dms-greeter; remove it before using packaged dms-greeter: sudo rm -f /usr/local/bin/dms-greeter")
|
||||
}
|
||||
|
||||
cacheDir := greeter.GreeterCacheDir
|
||||
if _, err := os.Stat(cacheDir); os.IsNotExist(err) {
|
||||
logFunc("Cache directory not found — attempting to create it...")
|
||||
if createErr := greeter.EnsureGreeterCacheDir(logFunc, ""); createErr != nil {
|
||||
return fmt.Errorf("greeter cache directory not found at %s and could not be created: %w\nRun: sudo mkdir -p %s && sudo chown greeter:greeter %s", cacheDir, createErr, cacheDir, cacheDir)
|
||||
}
|
||||
}
|
||||
|
||||
greeterGroup := greeter.DetectGreeterGroup()
|
||||
@@ -600,27 +603,28 @@ func syncGreeter(nonInteractive bool, forceAuth bool, local bool) error {
|
||||
inGreeterGroup := strings.Contains(string(groupsOutput), greeterGroup)
|
||||
if !inGreeterGroup {
|
||||
if nonInteractive {
|
||||
return fmt.Errorf("user must be in the %s group; run 'dms greeter sync' from a terminal to add", greeterGroup)
|
||||
}
|
||||
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.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 %s group: %w", greeterGroup, err)
|
||||
}
|
||||
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")
|
||||
logFunc(fmt.Sprintf("⚠ Not yet in %s group — will be added during sync (logout/login required to take effect).", greeterGroup))
|
||||
} else {
|
||||
return fmt.Errorf("aborted: user must be in the greeter group before syncing")
|
||||
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.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 %s group: %w", greeterGroup, err)
|
||||
}
|
||||
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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -694,18 +698,25 @@ func syncGreeter(nonInteractive bool, forceAuth bool, local bool) error {
|
||||
}
|
||||
|
||||
fmt.Println("\nSetting up permissions and ACLs...")
|
||||
greeter.RemediateStaleACLs(logFunc, "")
|
||||
greeter.RemediateStaleAppArmor(logFunc, "")
|
||||
if err := greeter.SetupDMSGroup(logFunc, ""); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := greeter.EnsureGreeterCacheDir(logFunc, ""); err != nil {
|
||||
return fmt.Errorf("failed to ensure greeter cache directory at %s: %w\nRun: sudo mkdir -p %s && sudo chown root:%s %s && sudo chmod 2770 %s", cacheDir, err, cacheDir, greeterGroup, cacheDir, cacheDir)
|
||||
}
|
||||
|
||||
fmt.Println("\nSynchronizing DMS configurations...")
|
||||
if err := greeter.SyncDMSConfigs(dmsPath, compositor, logFunc, "", forceAuth); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Println("\nConfiguring AppArmor profile...")
|
||||
if err := greeter.InstallAppArmorProfile(logFunc, ""); err != nil {
|
||||
logFunc(fmt.Sprintf("⚠ AppArmor profile setup failed: %v", err))
|
||||
if greeter.IsAppArmorEnabled() {
|
||||
fmt.Println("\nConfiguring AppArmor profile...")
|
||||
if err := greeter.InstallAppArmorProfile(logFunc, ""); err != nil {
|
||||
logFunc(fmt.Sprintf("⚠ AppArmor profile setup failed: %v", err))
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Println("\n=== Sync Complete ===")
|
||||
@@ -1021,6 +1032,7 @@ func enableGreeter(nonInteractive bool) error {
|
||||
logFunc := func(msg string) {
|
||||
fmt.Println(msg)
|
||||
}
|
||||
greeterGroup := greeter.DetectGreeterGroup()
|
||||
|
||||
if configAlreadyCorrect {
|
||||
fmt.Println("✓ Greeter is already configured with dms-greeter")
|
||||
@@ -1028,8 +1040,12 @@ func enableGreeter(nonInteractive bool) error {
|
||||
fmt.Printf("✓ Configured compositor: %s\n", configuredCompositor)
|
||||
}
|
||||
|
||||
fmt.Println("\nSetting up dms-greeter group and permissions...")
|
||||
if err := greeter.SetupDMSGroup(logFunc, ""); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := greeter.EnsureGreeterCacheDir(logFunc, ""); err != nil {
|
||||
fmt.Printf("⚠ Could not create cache directory: %v\n Run: sudo mkdir -p %s && sudo chown greeter:greeter %s\n", err, greeter.GreeterCacheDir, greeter.GreeterCacheDir)
|
||||
fmt.Printf("⚠ Could not ensure cache directory: %v\n Run: sudo mkdir -p %s && sudo chown root:%s %s && sudo chmod 2770 %s\n", err, greeter.GreeterCacheDir, greeterGroup, greeter.GreeterCacheDir, greeter.GreeterCacheDir)
|
||||
}
|
||||
|
||||
if err := ensureGraphicalTarget(); err != nil {
|
||||
@@ -1100,12 +1116,18 @@ func enableGreeter(nonInteractive bool) error {
|
||||
return fmt.Errorf("failed to configure greetd: %w", err)
|
||||
}
|
||||
|
||||
fmt.Println("\nSetting up dms-greeter group and permissions...")
|
||||
if err := greeter.SetupDMSGroup(logFunc, ""); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := greeter.EnsureGreeterCacheDir(logFunc, ""); err != nil {
|
||||
fmt.Printf("⚠ Could not create cache directory: %v\n Run: sudo mkdir -p %s && sudo chown greeter:greeter %s\n", err, greeter.GreeterCacheDir, greeter.GreeterCacheDir)
|
||||
fmt.Printf("⚠ Could not ensure cache directory: %v\n Run: sudo mkdir -p %s && sudo chown root:%s %s && sudo chmod 2770 %s\n", err, greeter.GreeterCacheDir, greeterGroup, greeter.GreeterCacheDir, greeter.GreeterCacheDir)
|
||||
}
|
||||
|
||||
if err := greeter.InstallAppArmorProfile(logFunc, ""); err != nil {
|
||||
logFunc(fmt.Sprintf("⚠ AppArmor profile setup failed: %v", err))
|
||||
if greeter.IsAppArmorEnabled() {
|
||||
if err := greeter.InstallAppArmorProfile(logFunc, ""); err != nil {
|
||||
logFunc(fmt.Sprintf("⚠ AppArmor profile setup failed: %v", err))
|
||||
}
|
||||
}
|
||||
|
||||
if err := ensureGraphicalTarget(); err != nil {
|
||||
@@ -1540,30 +1562,33 @@ func checkGreeterStatus() error {
|
||||
fmt.Println(" - security key (U2F): disabled")
|
||||
}
|
||||
} else {
|
||||
fmt.Println(" ℹ No managed auth block present (fingerprint/U2F disabled for greeter)")
|
||||
fmt.Println(" ℹ No managed auth block present (DMS-managed fingerprint/U2F lines are disabled)")
|
||||
}
|
||||
if legacyManaged {
|
||||
fmt.Println(" ⚠ Legacy unmanaged DMS PAM lines detected. Run 'dms greeter sync' to normalize.")
|
||||
allGood = false
|
||||
}
|
||||
includedFprintFile := greeter.DetectIncludedPamModule(string(pamData), "pam_fprintd.so")
|
||||
showIncludedFprintNotice := false
|
||||
if includedFprintFile != "" {
|
||||
if enableFprint, _, settingsErr := greeter.ReadGreeterAuthToggles(homeDir); settingsErr == nil && enableFprint {
|
||||
showIncludedFprintNotice = greeter.FingerprintAuthAvailableForCurrentUser()
|
||||
}
|
||||
}
|
||||
if managedFprint {
|
||||
if includedFprintFile != "" {
|
||||
fmt.Printf(" ⚠ pam_fprintd found in both DMS managed block and %s.\n", includedFprintFile)
|
||||
fmt.Println(" Double fingerprint auth detected — run 'dms greeter sync' to resolve.")
|
||||
allGood = false
|
||||
}
|
||||
} else if includedFprintFile != "" {
|
||||
} else if includedFprintFile != "" && showIncludedFprintNotice {
|
||||
fmt.Printf(" ℹ Fingerprint auth is enabled via included %s.\n", includedFprintFile)
|
||||
fmt.Println(" The DMS toggle only controls the managed block; disable fingerprint in authselect/pam-auth-update for password-only greeter login.")
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Println("\nSecurity (AppArmor):")
|
||||
appArmorEnabled, appArmorErr := isAppArmorEnabled()
|
||||
if appArmorErr != nil {
|
||||
fmt.Printf(" ℹ Could not determine AppArmor status: %v\n", appArmorErr)
|
||||
} else if !appArmorEnabled {
|
||||
if !greeter.IsAppArmorEnabled() {
|
||||
fmt.Println(" ℹ AppArmor not enabled")
|
||||
} else {
|
||||
fmt.Println(" ℹ AppArmor is enabled")
|
||||
@@ -1612,18 +1637,6 @@ func checkGreeterStatus() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func isAppArmorEnabled() (bool, error) {
|
||||
data, err := os.ReadFile("/sys/module/apparmor/parameters/enabled")
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return false, nil
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
value := strings.TrimSpace(strings.ToLower(string(data)))
|
||||
return strings.HasPrefix(value, "y"), nil
|
||||
}
|
||||
|
||||
func recentAppArmorGreeterDenials(sampleLimit int) (int, []string, error) {
|
||||
if sampleLimit <= 0 {
|
||||
sampleLimit = 3
|
||||
@@ -1712,8 +1725,7 @@ func isGreeterRelatedAppArmorDenial(line string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// appArmorProfileMode returns "complain", "enforce", or "" (unknown) for a named AppArmor
|
||||
// profile by reading /sys/kernel/security/apparmor/profiles.
|
||||
// appArmorProfileMode returns "complain", "enforce", or "" for a named AppArmor profile.
|
||||
func appArmorProfileMode(profileName string) string {
|
||||
data, err := os.ReadFile("/sys/kernel/security/apparmor/profiles")
|
||||
if err != nil {
|
||||
|
||||
@@ -208,7 +208,7 @@ func DetectGreeterUser() string {
|
||||
}
|
||||
}
|
||||
|
||||
if user, found := findPasswdUser(passwdContent, "greeter", "_greeter", "greetd"); found {
|
||||
if user, found := findPasswdUser(passwdContent, "greeter", "greetd", "_greeter"); found {
|
||||
return user
|
||||
}
|
||||
} else {
|
||||
@@ -230,6 +230,16 @@ func resolveGreeterWrapperPath() string {
|
||||
return override
|
||||
}
|
||||
|
||||
// Packaged installs only use the official wrapper; never fall back to /usr/local/bin.
|
||||
if IsGreeterPackaged() {
|
||||
packagedWrapper := "/usr/bin/dms-greeter"
|
||||
if info, err := os.Stat(packagedWrapper); err == nil && !info.IsDir() && (info.Mode()&0o111) != 0 {
|
||||
return packagedWrapper
|
||||
}
|
||||
fmt.Fprintln(os.Stderr, "⚠ Warning: packaged dms-greeter detected, but /usr/bin/dms-greeter is missing or not executable")
|
||||
return packagedWrapper
|
||||
}
|
||||
|
||||
for _, candidate := range []string{"/usr/bin/dms-greeter", "/usr/local/bin/dms-greeter"} {
|
||||
if info, err := os.Stat(candidate); err == nil && !info.IsDir() && (info.Mode()&0o111) != 0 {
|
||||
return candidate
|
||||
@@ -558,54 +568,124 @@ func CopyGreeterFiles(dmsPath, compositor string, logFunc func(string), sudoPass
|
||||
}
|
||||
|
||||
// EnsureGreeterCacheDir creates /var/cache/dms-greeter with correct ownership if it does not exist.
|
||||
// It is safe to call multiple times (idempotent).
|
||||
// It is safe to call multiple times (idempotent) and will repair ownership/mode
|
||||
// when the directory already exists with stale permissions.
|
||||
func EnsureGreeterCacheDir(logFunc func(string), sudoPassword string) error {
|
||||
cacheDir := GreeterCacheDir
|
||||
if _, err := os.Stat(cacheDir); err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := runSudoCmd(sudoPassword, "mkdir", "-p", cacheDir); err != nil {
|
||||
return fmt.Errorf("failed to create cache directory: %w", err)
|
||||
created := false
|
||||
if info, err := os.Stat(cacheDir); err != nil {
|
||||
if !os.IsNotExist(err) {
|
||||
return fmt.Errorf("failed to stat cache directory: %w", err)
|
||||
}
|
||||
if err := runSudoCmd(sudoPassword, "mkdir", "-p", cacheDir); err != nil {
|
||||
return fmt.Errorf("failed to create cache directory: %w", err)
|
||||
}
|
||||
created = true
|
||||
} else if !info.IsDir() {
|
||||
return fmt.Errorf("cache path exists but is not a directory: %s", cacheDir)
|
||||
}
|
||||
|
||||
group := DetectGreeterGroup()
|
||||
owner := fmt.Sprintf("%s:%s", group, group)
|
||||
|
||||
daemonUser := DetectGreeterUser()
|
||||
preferredOwner := fmt.Sprintf("%s:%s", daemonUser, group)
|
||||
owner := preferredOwner
|
||||
if err := runSudoCmd(sudoPassword, "chown", owner, cacheDir); err != nil {
|
||||
return fmt.Errorf("failed to set cache directory owner: %w", err)
|
||||
// Some setups may not have a matching daemon user at this moment; fall back
|
||||
// to root:<group> while still allowing group-writable greeter runtime access.
|
||||
fallbackOwner := fmt.Sprintf("root:%s", group)
|
||||
if fallbackErr := runSudoCmd(sudoPassword, "chown", fallbackOwner, cacheDir); fallbackErr != nil {
|
||||
return fmt.Errorf("failed to set cache directory owner (preferred %s: %v; fallback %s: %w)", preferredOwner, err, fallbackOwner, fallbackErr)
|
||||
}
|
||||
owner = fallbackOwner
|
||||
}
|
||||
|
||||
if err := runSudoCmd(sudoPassword, "chmod", "750", cacheDir); err != nil {
|
||||
if err := runSudoCmd(sudoPassword, "chmod", "2770", cacheDir); err != nil {
|
||||
return fmt.Errorf("failed to set cache directory permissions: %w", err)
|
||||
}
|
||||
logFunc(fmt.Sprintf("✓ Created cache directory %s (owner: %s, mode: 750)", cacheDir, owner))
|
||||
|
||||
runtimeDirs := []string{
|
||||
filepath.Join(cacheDir, ".local"),
|
||||
filepath.Join(cacheDir, ".local", "state"),
|
||||
filepath.Join(cacheDir, ".local", "share"),
|
||||
filepath.Join(cacheDir, ".cache"),
|
||||
}
|
||||
for _, dir := range runtimeDirs {
|
||||
if err := runSudoCmd(sudoPassword, "mkdir", "-p", dir); err != nil {
|
||||
return fmt.Errorf("failed to create cache runtime directory %s: %w", dir, err)
|
||||
}
|
||||
if err := runSudoCmd(sudoPassword, "chown", owner, dir); err != nil {
|
||||
return fmt.Errorf("failed to set owner for cache runtime directory %s: %w", dir, err)
|
||||
}
|
||||
if err := runSudoCmd(sudoPassword, "chmod", "2770", dir); err != nil {
|
||||
return fmt.Errorf("failed to set permissions for cache runtime directory %s: %w", dir, err)
|
||||
}
|
||||
}
|
||||
|
||||
legacyMemoryPath := filepath.Join(cacheDir, "memory.json")
|
||||
stateMemoryPath := filepath.Join(cacheDir, ".local", "state", "memory.json")
|
||||
if err := ensureGreeterMemoryCompatLink(logFunc, sudoPassword, legacyMemoryPath, stateMemoryPath); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if isSELinuxEnforcing() && utils.CommandExists("restorecon") {
|
||||
if err := runSudoCmd(sudoPassword, "restorecon", "-Rv", cacheDir); err != nil {
|
||||
logFunc(fmt.Sprintf("⚠ Warning: Failed to restore SELinux context for %s: %v", cacheDir, err))
|
||||
}
|
||||
}
|
||||
|
||||
if created {
|
||||
logFunc(fmt.Sprintf("✓ Created cache directory %s (owner: %s, mode: 2770)", cacheDir, owner))
|
||||
} else {
|
||||
logFunc(fmt.Sprintf("✓ Ensured cache directory %s permissions (owner: %s, mode: 2770)", cacheDir, owner))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// InstallAppArmorProfile writes the bundled AppArmor profile for dms-greeter and reloads
|
||||
// it with apparmor_parser. It is safe to call multiple times (idempotent reload).
|
||||
//
|
||||
// Skipped silently when:
|
||||
// - AppArmor kernel module is absent (/sys/module/apparmor does not exist)
|
||||
// - Running on NixOS (profiles are managed via security.apparmor.policies)
|
||||
// - SELinux is active (/sys/fs/selinux/enforce exists and equals "1") — Fedora/RHEL
|
||||
func isSELinuxEnforcing() bool {
|
||||
data, err := os.ReadFile("/sys/fs/selinux/enforce")
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return strings.TrimSpace(string(data)) == "1"
|
||||
}
|
||||
|
||||
func ensureGreeterMemoryCompatLink(logFunc func(string), sudoPassword, legacyPath, statePath string) error {
|
||||
info, err := os.Lstat(legacyPath)
|
||||
if err == nil && info.Mode().IsRegular() {
|
||||
if _, stateErr := os.Stat(statePath); os.IsNotExist(stateErr) {
|
||||
if copyErr := runSudoCmd(sudoPassword, "cp", "-f", legacyPath, statePath); copyErr != nil {
|
||||
logFunc(fmt.Sprintf("⚠ Warning: Failed to migrate legacy greeter memory file to %s: %v", statePath, copyErr))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := runSudoCmd(sudoPassword, "ln", "-sfn", statePath, legacyPath); err != nil {
|
||||
return fmt.Errorf("failed to create greeter memory compatibility symlink %s -> %s: %w", legacyPath, statePath, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsAppArmorEnabled reports whether AppArmor is active on the running kernel.
|
||||
func IsAppArmorEnabled() bool {
|
||||
data, err := os.ReadFile("/sys/module/apparmor/parameters/enabled")
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return strings.HasPrefix(strings.TrimSpace(strings.ToLower(string(data))), "y")
|
||||
}
|
||||
|
||||
// InstallAppArmorProfile installs the bundled AppArmor profile and reloads it. No-op on NixOS or non-AppArmor systems.
|
||||
func InstallAppArmorProfile(logFunc func(string), sudoPassword string) error {
|
||||
if IsNixOS() {
|
||||
logFunc(" ℹ Skipping AppArmor profile on NixOS (manage via security.apparmor.policies)")
|
||||
return nil
|
||||
}
|
||||
|
||||
if _, err := os.Stat("/sys/module/apparmor"); os.IsNotExist(err) {
|
||||
if !IsAppArmorEnabled() {
|
||||
return nil
|
||||
}
|
||||
|
||||
if data, err := os.ReadFile("/sys/fs/selinux/enforce"); err == nil {
|
||||
if strings.TrimSpace(string(data)) == "1" {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
if err := runSudoCmd(sudoPassword, "mkdir", "-p", "/etc/apparmor.d"); err != nil {
|
||||
return fmt.Errorf("failed to create /etc/apparmor.d: %w", err)
|
||||
}
|
||||
@@ -814,7 +894,7 @@ func SetupParentDirectoryACLs(logFunc func(string), sudoPassword string) error {
|
||||
{filepath.Join(homeDir, ".local", "share"), ".local/share directory"},
|
||||
}
|
||||
|
||||
owner := DetectGreeterGroup()
|
||||
group := DetectGreeterGroup()
|
||||
|
||||
logFunc("\nSetting up parent directory ACLs for greeter user access...")
|
||||
|
||||
@@ -826,9 +906,10 @@ func SetupParentDirectoryACLs(logFunc func(string), sudoPassword string) error {
|
||||
}
|
||||
}
|
||||
|
||||
if err := runSudoCmd(sudoPassword, "setfacl", "-m", fmt.Sprintf("u:%s:rx", owner), dir.path); err != nil {
|
||||
// Group ACL covers daemon users regardless of username (e.g. greetd ≠ greeter on Fedora).
|
||||
if err := runSudoCmd(sudoPassword, "setfacl", "-m", fmt.Sprintf("g:%s:rX", group), 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:%s:x %s", owner, dir.path))
|
||||
logFunc(fmt.Sprintf(" You may need to run manually: setfacl -m g:%s:rX %s", group, dir.path))
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -838,6 +919,73 @@ func SetupParentDirectoryACLs(logFunc func(string), sudoPassword string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemediateStaleACLs removes user-based ACLs left by older binary versions. Best-effort.
|
||||
func RemediateStaleACLs(logFunc func(string), sudoPassword string) {
|
||||
if !utils.CommandExists("setfacl") {
|
||||
return
|
||||
}
|
||||
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
passwdData, err := os.ReadFile("/etc/passwd")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
dirs := []string{
|
||||
homeDir,
|
||||
filepath.Join(homeDir, ".config"),
|
||||
filepath.Join(homeDir, ".config", "DankMaterialShell"),
|
||||
filepath.Join(homeDir, ".cache"),
|
||||
filepath.Join(homeDir, ".cache", "DankMaterialShell"),
|
||||
filepath.Join(homeDir, ".local"),
|
||||
filepath.Join(homeDir, ".local", "state"),
|
||||
filepath.Join(homeDir, ".local", "share"),
|
||||
}
|
||||
|
||||
passwdContent := string(passwdData)
|
||||
staleUsers := []string{"greeter", "greetd", "_greeter"}
|
||||
existingUsers := make([]string, 0, len(staleUsers))
|
||||
for _, user := range staleUsers {
|
||||
if hasPasswdUser(passwdContent, user) {
|
||||
existingUsers = append(existingUsers, user)
|
||||
}
|
||||
}
|
||||
if len(existingUsers) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
cleaned := false
|
||||
for _, dir := range dirs {
|
||||
if _, err := os.Stat(dir); err != nil {
|
||||
continue
|
||||
}
|
||||
for _, user := range existingUsers {
|
||||
_ = runSudoCmd(sudoPassword, "setfacl", "-x", fmt.Sprintf("u:%s", user), dir)
|
||||
cleaned = true
|
||||
}
|
||||
}
|
||||
if cleaned {
|
||||
logFunc("✓ Cleaned up stale user-based ACLs from previous versions")
|
||||
}
|
||||
}
|
||||
|
||||
// RemediateStaleAppArmor removes any AppArmor profile installed by an older binary on
|
||||
// systems where AppArmor is not active.
|
||||
func RemediateStaleAppArmor(logFunc func(string), sudoPassword string) {
|
||||
if IsAppArmorEnabled() {
|
||||
return
|
||||
}
|
||||
if _, err := os.Stat(appArmorProfileDest); os.IsNotExist(err) {
|
||||
return
|
||||
}
|
||||
logFunc("ℹ Removing stale AppArmor profile (AppArmor is not active on this system)")
|
||||
_ = UninstallAppArmorProfile(logFunc, sudoPassword)
|
||||
}
|
||||
|
||||
func SetupDMSGroup(logFunc func(string), sudoPassword string) error {
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
@@ -854,6 +1002,14 @@ func SetupDMSGroup(logFunc func(string), sudoPassword string) error {
|
||||
|
||||
group := DetectGreeterGroup()
|
||||
|
||||
// Create the group if it doesn't exist yet (e.g. before greetd package is installed).
|
||||
if !utils.HasGroup(group) {
|
||||
if err := runSudoCmd(sudoPassword, "groupadd", "-r", group); err != nil {
|
||||
return fmt.Errorf("failed to create %s group: %w", group, err)
|
||||
}
|
||||
logFunc(fmt.Sprintf("✓ Created system group %s", group))
|
||||
}
|
||||
|
||||
groupsCmd := exec.Command("groups", currentUser)
|
||||
groupsOutput, err := groupsCmd.Output()
|
||||
if err == nil && strings.Contains(string(groupsOutput), group) {
|
||||
@@ -865,6 +1021,24 @@ func SetupDMSGroup(logFunc func(string), sudoPassword string) error {
|
||||
logFunc(fmt.Sprintf("✓ Added %s to %s group (logout/login required for changes to take effect)", currentUser, group))
|
||||
}
|
||||
|
||||
// Also add the daemon user (e.g. greetd on Fedora) so group ACLs apply to the running process.
|
||||
daemonUser := DetectGreeterUser()
|
||||
if daemonUser != currentUser {
|
||||
daemonGroupsCmd := exec.Command("groups", daemonUser)
|
||||
daemonGroupsOutput, daemonGroupsErr := daemonGroupsCmd.Output()
|
||||
if daemonGroupsErr == nil {
|
||||
if strings.Contains(string(daemonGroupsOutput), group) {
|
||||
logFunc(fmt.Sprintf("✓ Greeter daemon user %s is already in %s group", daemonUser, group))
|
||||
} else {
|
||||
if err := runSudoCmd(sudoPassword, "usermod", "-aG", group, daemonUser); err != nil {
|
||||
logFunc(fmt.Sprintf("⚠ Warning: could not add %s to %s group: %v", daemonUser, group, err))
|
||||
} else {
|
||||
logFunc(fmt.Sprintf("✓ Added greeter daemon user %s to %s group", daemonUser, group))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
configDirs := []struct {
|
||||
path string
|
||||
desc string
|
||||
@@ -1018,8 +1192,11 @@ func syncGreeterWallpaperOverride(homeDir, cacheDir string, logFunc func(string)
|
||||
return fmt.Errorf("failed to copy override wallpaper to %s: %w", destPath, err)
|
||||
}
|
||||
greeterGroup := DetectGreeterGroup()
|
||||
if err := runSudoCmd(sudoPassword, "chown", "greeter:"+greeterGroup, destPath); err != nil {
|
||||
return fmt.Errorf("failed to set override ownership on %s: %w", destPath, err)
|
||||
daemonUser := DetectGreeterUser()
|
||||
if err := runSudoCmd(sudoPassword, "chown", daemonUser+":"+greeterGroup, destPath); err != nil {
|
||||
if fallbackErr := runSudoCmd(sudoPassword, "chown", "root:"+greeterGroup, destPath); fallbackErr != nil {
|
||||
return fmt.Errorf("failed to set override ownership on %s: %w", destPath, err)
|
||||
}
|
||||
}
|
||||
if err := runSudoCmd(sudoPassword, "chmod", "644", destPath); err != nil {
|
||||
return fmt.Errorf("failed to set override permissions on %s: %w", destPath, err)
|
||||
@@ -1158,28 +1335,107 @@ func DetectIncludedPamModule(pamText, module string) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
type greeterAuthSettings struct {
|
||||
GreeterEnableFprint bool `json:"greeterEnableFprint"`
|
||||
GreeterEnableU2f bool `json:"greeterEnableU2f"`
|
||||
}
|
||||
|
||||
func readGreeterAuthSettings(homeDir string) (greeterAuthSettings, error) {
|
||||
settingsPath := filepath.Join(homeDir, ".config", "DankMaterialShell", "settings.json")
|
||||
data, err := os.ReadFile(settingsPath)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return greeterAuthSettings{}, nil
|
||||
}
|
||||
return greeterAuthSettings{}, fmt.Errorf("failed to read settings at %s: %w", settingsPath, err)
|
||||
}
|
||||
if strings.TrimSpace(string(data)) == "" {
|
||||
return greeterAuthSettings{}, nil
|
||||
}
|
||||
var settings greeterAuthSettings
|
||||
if err := json.Unmarshal(data, &settings); err != nil {
|
||||
return greeterAuthSettings{}, fmt.Errorf("failed to parse settings at %s: %w", settingsPath, err)
|
||||
}
|
||||
return settings, nil
|
||||
}
|
||||
|
||||
func ReadGreeterAuthToggles(homeDir string) (enableFprint bool, enableU2f bool, err error) {
|
||||
settings, err := readGreeterAuthSettings(homeDir)
|
||||
if err != nil {
|
||||
return false, false, err
|
||||
}
|
||||
return settings.GreeterEnableFprint, settings.GreeterEnableU2f, nil
|
||||
}
|
||||
|
||||
func hasEnrolledFingerprintOutput(output string) bool {
|
||||
lower := strings.ToLower(output)
|
||||
if strings.Contains(lower, "no fingers enrolled") ||
|
||||
strings.Contains(lower, "no fingerprints enrolled") ||
|
||||
strings.Contains(lower, "no prints enrolled") {
|
||||
return false
|
||||
}
|
||||
if strings.Contains(lower, "has fingers enrolled") ||
|
||||
strings.Contains(lower, "has fingerprints enrolled") {
|
||||
return true
|
||||
}
|
||||
for _, line := range strings.Split(lower, "\n") {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if strings.HasPrefix(trimmed, "finger:") {
|
||||
return true
|
||||
}
|
||||
if strings.HasPrefix(trimmed, "- ") && strings.Contains(trimmed, "finger") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func FingerprintAuthAvailableForUser(username string) bool {
|
||||
username = strings.TrimSpace(username)
|
||||
if username == "" {
|
||||
return false
|
||||
}
|
||||
if !pamModuleExists("pam_fprintd.so") {
|
||||
return false
|
||||
}
|
||||
if _, err := exec.LookPath("fprintd-list"); err != nil {
|
||||
return false
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||
defer cancel()
|
||||
out, err := exec.CommandContext(ctx, "fprintd-list", username).CombinedOutput()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return hasEnrolledFingerprintOutput(string(out))
|
||||
}
|
||||
|
||||
func FingerprintAuthAvailableForCurrentUser() bool {
|
||||
username := strings.TrimSpace(os.Getenv("SUDO_USER"))
|
||||
if username == "" {
|
||||
username = strings.TrimSpace(os.Getenv("USER"))
|
||||
}
|
||||
if username == "" {
|
||||
out, err := exec.Command("id", "-un").Output()
|
||||
if err == nil {
|
||||
username = strings.TrimSpace(string(out))
|
||||
}
|
||||
}
|
||||
return FingerprintAuthAvailableForUser(username)
|
||||
}
|
||||
|
||||
func syncGreeterPamConfig(homeDir string, logFunc func(string), sudoPassword string, forceAuth bool) error {
|
||||
var wantFprint, wantU2f bool
|
||||
fprintToggleEnabled := forceAuth
|
||||
if forceAuth {
|
||||
wantFprint = pamModuleExists("pam_fprintd.so")
|
||||
wantU2f = pamModuleExists("pam_u2f.so")
|
||||
} else {
|
||||
settingsPath := filepath.Join(homeDir, ".config", "DankMaterialShell", "settings.json")
|
||||
data, err := os.ReadFile(settingsPath)
|
||||
settings, err := readGreeterAuthSettings(homeDir)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
data = []byte("{}")
|
||||
} else {
|
||||
return fmt.Errorf("failed to read settings at %s: %w", settingsPath, err)
|
||||
}
|
||||
}
|
||||
var settings struct {
|
||||
GreeterEnableFprint bool `json:"greeterEnableFprint"`
|
||||
GreeterEnableU2f bool `json:"greeterEnableU2f"`
|
||||
}
|
||||
if err := json.Unmarshal(data, &settings); err != nil {
|
||||
return fmt.Errorf("failed to parse settings at %s: %w", settingsPath, err)
|
||||
return err
|
||||
}
|
||||
fprintToggleEnabled = settings.GreeterEnableFprint
|
||||
fprintModule := pamModuleExists("pam_fprintd.so")
|
||||
u2fModule := pamModuleExists("pam_u2f.so")
|
||||
wantFprint = settings.GreeterEnableFprint && fprintModule
|
||||
@@ -1212,7 +1468,8 @@ func syncGreeterPamConfig(homeDir string, logFunc func(string), sudoPassword str
|
||||
logFunc("⚠ pam_fprintd already present in included " + includedFprintFile + " (managed by authselect/pam-auth-update). Skipping DMS fprint block to avoid double-fingerprint auth.")
|
||||
wantFprint = false
|
||||
}
|
||||
if !wantFprint && includedFprintFile != "" {
|
||||
showIncludedFprintNotice := fprintToggleEnabled && FingerprintAuthAvailableForCurrentUser()
|
||||
if !wantFprint && includedFprintFile != "" && showIncludedFprintNotice {
|
||||
logFunc("ℹ Fingerprint auth is still enabled via included " + includedFprintFile + ".")
|
||||
logFunc(" Disable fingerprint in your system PAM manager (authselect/pam-auth-update) to force password-only greeter login.")
|
||||
}
|
||||
@@ -1272,6 +1529,8 @@ type niriGreeterSync struct {
|
||||
cursorCount int
|
||||
debugCount int
|
||||
cursorNode *document.Node
|
||||
inputNode *document.Node
|
||||
outputNodes map[string]*document.Node
|
||||
}
|
||||
|
||||
func syncNiriGreeterConfig(logFunc func(string), sudoPassword string) error {
|
||||
@@ -1289,7 +1548,8 @@ func syncNiriGreeterConfig(logFunc func(string), sudoPassword string) error {
|
||||
}
|
||||
|
||||
extractor := &niriGreeterSync{
|
||||
processed: make(map[string]bool),
|
||||
processed: make(map[string]bool),
|
||||
outputNodes: make(map[string]*document.Node),
|
||||
}
|
||||
|
||||
if err := extractor.processFile(configPath); err != nil {
|
||||
@@ -1488,10 +1748,22 @@ func (s *niriGreeterSync) processFile(filePath string) error {
|
||||
return err
|
||||
}
|
||||
case "input":
|
||||
s.nodes = append(s.nodes, node)
|
||||
if s.inputNode == nil {
|
||||
s.inputNode = node
|
||||
s.inputNode.Children = dedupeCursorChildren(s.inputNode.Children)
|
||||
s.nodes = append(s.nodes, node)
|
||||
} else if len(node.Children) > 0 {
|
||||
s.inputNode.Children = mergeInputChildren(s.inputNode.Children, node.Children)
|
||||
}
|
||||
s.inputCount++
|
||||
case "output":
|
||||
s.nodes = append(s.nodes, node)
|
||||
key := outputNodeKey(node)
|
||||
if existing, ok := s.outputNodes[key]; ok {
|
||||
*existing = *node
|
||||
} else {
|
||||
s.outputNodes[key] = node
|
||||
s.nodes = append(s.nodes, node)
|
||||
}
|
||||
s.outputCount++
|
||||
case "cursor":
|
||||
if s.cursorNode == nil {
|
||||
@@ -1554,6 +1826,36 @@ func dedupeCursorChildren(children []*document.Node) []*document.Node {
|
||||
return result
|
||||
}
|
||||
|
||||
func mergeInputChildren(existing []*document.Node, incoming []*document.Node) []*document.Node {
|
||||
if len(incoming) == 0 {
|
||||
return existing
|
||||
}
|
||||
|
||||
indexByName := make(map[string]int, len(existing))
|
||||
for i, child := range existing {
|
||||
indexByName[child.Name.String()] = i
|
||||
}
|
||||
|
||||
for _, child := range incoming {
|
||||
name := child.Name.String()
|
||||
if idx, ok := indexByName[name]; ok {
|
||||
existing[idx] = child
|
||||
continue
|
||||
}
|
||||
indexByName[name] = len(existing)
|
||||
existing = append(existing, child)
|
||||
}
|
||||
|
||||
return existing
|
||||
}
|
||||
|
||||
func outputNodeKey(node *document.Node) string {
|
||||
if len(node.Arguments) > 0 {
|
||||
return strings.Trim(node.Arguments[0].String(), "\"")
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (s *niriGreeterSync) handleInclude(node *document.Node, baseDir string) error {
|
||||
if len(node.Arguments) == 0 {
|
||||
return nil
|
||||
|
||||
Reference in New Issue
Block a user