diff options
Diffstat (limited to 'raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/pam')
| -rw-r--r-- | raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/pam/pam.go | 892 | ||||
| -rw-r--r-- | raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/pam/pam_test.go | 671 |
2 files changed, 0 insertions, 1563 deletions
diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/pam/pam.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/pam/pam.go deleted file mode 100644 index 16ca3f9..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/pam/pam.go +++ /dev/null @@ -1,892 +0,0 @@ -package pam - -import ( - "context" - "encoding/json" - "fmt" - "os" - "os/exec" - "path/filepath" - "strings" - "time" - - "github.com/AvengeMedia/DankMaterialShell/core/internal/distros" -) - -const ( - GreeterPamManagedBlockStart = "# BEGIN DMS GREETER AUTH (managed by dms greeter sync)" - GreeterPamManagedBlockEnd = "# END DMS GREETER AUTH" - - LockscreenPamManagedBlockStart = "# BEGIN DMS LOCKSCREEN AUTH (managed by dms greeter sync)" - LockscreenPamManagedBlockEnd = "# END DMS LOCKSCREEN AUTH" - - LockscreenU2FPamManagedBlockStart = "# BEGIN DMS LOCKSCREEN U2F AUTH (managed by dms auth sync)" - LockscreenU2FPamManagedBlockEnd = "# END DMS LOCKSCREEN U2F AUTH" - - legacyGreeterPamFprintComment = "# DMS greeter fingerprint" - legacyGreeterPamU2FComment = "# DMS greeter U2F" - - GreetdPamPath = "/etc/pam.d/greetd" - DankshellPamPath = "/etc/pam.d/dankshell" - DankshellU2FPamPath = "/etc/pam.d/dankshell-u2f" -) - -var includedPamAuthFiles = []string{ - "system-auth", - "common-auth", - "password-auth", - "system-login", - "system-local-login", - "common-auth-pc", - "login", -} - -type AuthSettings struct { - EnableFprint bool `json:"enableFprint"` - EnableU2f bool `json:"enableU2f"` - GreeterEnableFprint bool `json:"greeterEnableFprint"` - GreeterEnableU2f bool `json:"greeterEnableU2f"` -} - -type SyncAuthOptions struct { - HomeDir string - ForceGreeterAuth bool -} - -type syncDeps struct { - pamDir string - greetdPath string - dankshellPath string - dankshellU2fPath string - isNixOS func() bool - readFile func(string) ([]byte, error) - stat func(string) (os.FileInfo, error) - createTemp func(string, string) (*os.File, error) - removeFile func(string) error - runSudoCmd func(string, string, ...string) error - pamModuleExists func(string) bool - fingerprintAvailableForCurrentUser func() bool -} - -type lockscreenPamIncludeDirective struct { - target string - filterType string -} - -type lockscreenPamResolver struct { - pamDir string - readFile func(string) ([]byte, error) -} - -func defaultSyncDeps() syncDeps { - return syncDeps{ - pamDir: "/etc/pam.d", - greetdPath: GreetdPamPath, - dankshellPath: DankshellPamPath, - dankshellU2fPath: DankshellU2FPamPath, - isNixOS: IsNixOS, - readFile: os.ReadFile, - stat: os.Stat, - createTemp: os.CreateTemp, - removeFile: os.Remove, - runSudoCmd: runSudoCmd, - pamModuleExists: pamModuleExists, - fingerprintAvailableForCurrentUser: FingerprintAuthAvailableForCurrentUser, - } -} - -func IsNixOS() bool { - _, err := os.Stat("/etc/NIXOS") - return err == nil -} - -func ReadAuthSettings(homeDir string) (AuthSettings, error) { - settingsPath := filepath.Join(homeDir, ".config", "DankMaterialShell", "settings.json") - data, err := os.ReadFile(settingsPath) - if err != nil { - if os.IsNotExist(err) { - return AuthSettings{}, nil - } - return AuthSettings{}, fmt.Errorf("failed to read settings at %s: %w", settingsPath, err) - } - if strings.TrimSpace(string(data)) == "" { - return AuthSettings{}, nil - } - - var settings AuthSettings - if err := json.Unmarshal(data, &settings); err != nil { - return AuthSettings{}, 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 := ReadAuthSettings(homeDir) - if err != nil { - return false, false, err - } - return settings.GreeterEnableFprint, settings.GreeterEnableU2f, nil -} - -func SyncAuthConfig(logFunc func(string), sudoPassword string, options SyncAuthOptions) error { - return syncAuthConfigWithDeps(logFunc, sudoPassword, options, defaultSyncDeps()) -} - -func RemoveManagedGreeterPamBlock(logFunc func(string), sudoPassword string) error { - return removeManagedGreeterPamBlockWithDeps(logFunc, sudoPassword, defaultSyncDeps()) -} - -func syncAuthConfigWithDeps(logFunc func(string), sudoPassword string, options SyncAuthOptions, deps syncDeps) error { - homeDir := strings.TrimSpace(options.HomeDir) - if homeDir == "" { - var err error - homeDir, err = os.UserHomeDir() - if err != nil { - return fmt.Errorf("failed to get user home directory: %w", err) - } - } - - settings, err := ReadAuthSettings(homeDir) - if err != nil { - return err - } - - if err := syncLockscreenPamConfigWithDeps(logFunc, sudoPassword, deps); err != nil { - return err - } - if err := syncLockscreenU2FPamConfigWithDeps(logFunc, sudoPassword, settings.EnableU2f, deps); err != nil { - return err - } - - if _, err := deps.stat(deps.greetdPath); err != nil { - if os.IsNotExist(err) { - logFunc("ℹ /etc/pam.d/greetd not found. Skipping greeter PAM sync.") - return nil - } - return fmt.Errorf("failed to inspect %s: %w", deps.greetdPath, err) - } - - if err := syncGreeterPamConfigWithDeps(logFunc, sudoPassword, settings, options.ForceGreeterAuth, deps); err != nil { - return err - } - - return nil -} - -func removeManagedGreeterPamBlockWithDeps(logFunc func(string), sudoPassword string, deps syncDeps) error { - if deps.isNixOS() { - return nil - } - - data, err := deps.readFile(deps.greetdPath) - if err != nil { - if os.IsNotExist(err) { - return nil - } - return fmt.Errorf("failed to read %s: %w", deps.greetdPath, err) - } - - originalContent := string(data) - stripped, removed := stripManagedGreeterPamBlock(originalContent) - strippedAgain, removedLegacy := stripLegacyGreeterPamLines(stripped) - if !removed && !removedLegacy { - return nil - } - - if err := writeManagedPamFile(strippedAgain, deps.greetdPath, sudoPassword, deps); err != nil { - return fmt.Errorf("failed to write %s: %w", deps.greetdPath, err) - } - - logFunc("✓ Removed DMS managed PAM block from " + deps.greetdPath) - return nil -} - -func ParseManagedGreeterPamAuth(pamText string) (managed bool, fingerprint bool, u2f bool, legacy bool) { - if pamText == "" { - return false, false, false, false - } - - lines := strings.Split(pamText, "\n") - inManaged := false - for _, line := range lines { - trimmed := strings.TrimSpace(line) - switch trimmed { - case GreeterPamManagedBlockStart: - managed = true - inManaged = true - continue - case GreeterPamManagedBlockEnd: - inManaged = false - continue - } - - if strings.HasPrefix(trimmed, legacyGreeterPamFprintComment) || strings.HasPrefix(trimmed, legacyGreeterPamU2FComment) { - legacy = true - } - if !inManaged { - continue - } - if strings.Contains(trimmed, "pam_fprintd") { - fingerprint = true - } - if strings.Contains(trimmed, "pam_u2f") { - u2f = true - } - } - - return managed, fingerprint, u2f, legacy -} - -func StripManagedGreeterPamContent(pamText string) (string, bool) { - stripped, removed := stripManagedGreeterPamBlock(pamText) - stripped, removedLegacy := stripLegacyGreeterPamLines(stripped) - return stripped, removed || removedLegacy -} - -func PamTextIncludesFile(pamText, filename string) bool { - lines := strings.Split(pamText, "\n") - for _, line := range lines { - trimmed := strings.TrimSpace(line) - if trimmed == "" || strings.HasPrefix(trimmed, "#") { - continue - } - if strings.Contains(trimmed, filename) && - (strings.Contains(trimmed, "include") || strings.Contains(trimmed, "substack") || strings.HasPrefix(trimmed, "@include")) { - return true - } - } - return false -} - -func PamFileHasModule(pamFilePath, module string) bool { - data, err := os.ReadFile(pamFilePath) - if err != nil { - return false - } - return pamContentHasModule(string(data), module) -} - -func DetectIncludedPamModule(pamText, module string) string { - return detectIncludedPamModule(pamText, module, defaultSyncDeps()) -} - -func detectIncludedPamModule(pamText, module string, deps syncDeps) string { - for _, includedFile := range includedPamAuthFiles { - if !PamTextIncludesFile(pamText, includedFile) { - continue - } - path := filepath.Join(deps.pamDir, includedFile) - data, err := deps.readFile(path) - if err != nil { - continue - } - if pamContentHasModule(string(data), module) { - return includedFile - } - } - return "" -} - -func pamContentHasModule(content, module string) bool { - lines := strings.Split(content, "\n") - for _, line := range lines { - trimmed := strings.TrimSpace(line) - if trimmed == "" || strings.HasPrefix(trimmed, "#") { - continue - } - if strings.Contains(trimmed, module) { - return true - } - } - return false -} - -func hasManagedLockscreenPamFile(content string) bool { - return strings.Contains(content, LockscreenPamManagedBlockStart) && - strings.Contains(content, LockscreenPamManagedBlockEnd) -} - -func hasManagedLockscreenU2FPamFile(content string) bool { - return strings.Contains(content, LockscreenU2FPamManagedBlockStart) && - strings.Contains(content, LockscreenU2FPamManagedBlockEnd) -} - -func pamDirectiveType(line string) string { - fields := strings.Fields(line) - if len(fields) == 0 { - return "" - } - - directiveType := strings.TrimPrefix(fields[0], "-") - switch directiveType { - case "auth", "account", "password", "session": - return directiveType - default: - return "" - } -} - -func isExcludedLockscreenPamLine(line string) bool { - for _, field := range strings.Fields(line) { - if strings.HasPrefix(field, "#") { - break - } - if strings.Contains(field, "pam_u2f") || strings.Contains(field, "pam_fprintd") { - return true - } - } - return false -} - -func parseLockscreenPamIncludeDirective(trimmed string, inheritedFilter string) (lockscreenPamIncludeDirective, bool) { - fields := strings.Fields(trimmed) - if len(fields) >= 2 && fields[0] == "@include" { - return lockscreenPamIncludeDirective{ - target: fields[1], - filterType: inheritedFilter, - }, true - } - - if len(fields) >= 3 && (fields[1] == "include" || fields[1] == "substack") { - lineType := pamDirectiveType(trimmed) - if lineType == "" { - return lockscreenPamIncludeDirective{}, false - } - return lockscreenPamIncludeDirective{ - target: fields[2], - filterType: lineType, - }, true - } - - if len(fields) >= 3 && fields[1] == "@include" { - lineType := pamDirectiveType(trimmed) - if lineType == "" { - return lockscreenPamIncludeDirective{}, false - } - return lockscreenPamIncludeDirective{ - target: fields[2], - filterType: lineType, - }, true - } - - return lockscreenPamIncludeDirective{}, false -} - -func resolveLockscreenPamIncludePath(pamDir, target string) (string, error) { - if strings.TrimSpace(target) == "" { - return "", fmt.Errorf("empty PAM include target") - } - - cleanPamDir := filepath.Clean(pamDir) - if filepath.IsAbs(target) { - cleanTarget := filepath.Clean(target) - if filepath.Dir(cleanTarget) != cleanPamDir { - return "", fmt.Errorf("unsupported PAM include outside %s: %s", cleanPamDir, target) - } - return cleanTarget, nil - } - - cleanTarget := filepath.Clean(target) - if cleanTarget == "." || cleanTarget == ".." || strings.HasPrefix(cleanTarget, ".."+string(os.PathSeparator)) { - return "", fmt.Errorf("invalid PAM include target: %s", target) - } - - return filepath.Join(cleanPamDir, cleanTarget), nil -} - -func (r lockscreenPamResolver) resolveService(serviceName string, filterType string, stack []string) ([]string, error) { - path, err := resolveLockscreenPamIncludePath(r.pamDir, serviceName) - if err != nil { - return nil, err - } - - for _, seen := range stack { - if seen == path { - chain := append(append([]string{}, stack...), path) - display := make([]string, 0, len(chain)) - for _, item := range chain { - display = append(display, filepath.Base(item)) - } - return nil, fmt.Errorf("cyclic PAM include detected: %s", strings.Join(display, " -> ")) - } - } - - data, err := r.readFile(path) - if err != nil { - return nil, fmt.Errorf("failed to read PAM file %s: %w", path, err) - } - - var resolved []string - for _, rawLine := range strings.Split(strings.ReplaceAll(string(data), "\r\n", "\n"), "\n") { - rawLine = strings.TrimRight(rawLine, "\r") - trimmed := strings.TrimSpace(rawLine) - if trimmed == "" || strings.HasPrefix(trimmed, "#") || trimmed == "#%PAM-1.0" { - continue - } - - if include, ok := parseLockscreenPamIncludeDirective(trimmed, filterType); ok { - lineType := pamDirectiveType(trimmed) - if filterType != "" && lineType != "" && lineType != filterType { - continue - } - - nested, err := r.resolveService(include.target, include.filterType, append(stack, path)) - if err != nil { - return nil, err - } - resolved = append(resolved, nested...) - continue - } - - lineType := pamDirectiveType(trimmed) - if lineType == "" { - return nil, fmt.Errorf("unsupported PAM directive in %s: %s", filepath.Base(path), trimmed) - } - if filterType != "" && lineType != filterType { - continue - } - if isExcludedLockscreenPamLine(trimmed) { - continue - } - - resolved = append(resolved, rawLine) - } - - return resolved, nil -} - -func buildManagedLockscreenPamContent(pamDir string, readFile func(string) ([]byte, error)) (string, error) { - resolver := lockscreenPamResolver{ - pamDir: pamDir, - readFile: readFile, - } - - resolvedLines, err := resolver.resolveService("login", "", nil) - if err != nil { - return "", err - } - if len(resolvedLines) == 0 { - return "", fmt.Errorf("no auth directives remained after filtering %s", filepath.Join(pamDir, "login")) - } - - hasAuth := false - for _, line := range resolvedLines { - if pamDirectiveType(strings.TrimSpace(line)) == "auth" { - hasAuth = true - break - } - } - if !hasAuth { - return "", fmt.Errorf("no auth directives remained after filtering %s", filepath.Join(pamDir, "login")) - } - - var b strings.Builder - b.WriteString("#%PAM-1.0\n") - b.WriteString(LockscreenPamManagedBlockStart + "\n") - for _, line := range resolvedLines { - b.WriteString(line) - b.WriteByte('\n') - } - b.WriteString(LockscreenPamManagedBlockEnd + "\n") - return b.String(), nil -} - -func buildManagedLockscreenU2FPamContent() string { - var b strings.Builder - b.WriteString("#%PAM-1.0\n") - b.WriteString(LockscreenU2FPamManagedBlockStart + "\n") - b.WriteString("auth required pam_u2f.so cue nouserok timeout=10\n") - b.WriteString(LockscreenU2FPamManagedBlockEnd + "\n") - return b.String() -} - -func syncLockscreenPamConfigWithDeps(logFunc func(string), sudoPassword string, deps syncDeps) error { - if deps.isNixOS() { - logFunc("ℹ NixOS detected. DMS continues to use /etc/pam.d/login for lock screen password auth on NixOS unless you declare security.pam.services.dankshell yourself. U2F and fingerprint are handled separately and should not be included in dankshell.") - return nil - } - - existingData, err := deps.readFile(deps.dankshellPath) - if err == nil { - if !hasManagedLockscreenPamFile(string(existingData)) { - logFunc("ℹ Custom /etc/pam.d/dankshell found (no DMS block). Skipping.") - return nil - } - } else if !os.IsNotExist(err) { - return fmt.Errorf("failed to read %s: %w", deps.dankshellPath, err) - } - - content, err := buildManagedLockscreenPamContent(deps.pamDir, deps.readFile) - if err != nil { - return fmt.Errorf("failed to build %s from %s: %w", deps.dankshellPath, filepath.Join(deps.pamDir, "login"), err) - } - - if err := writeManagedPamFile(content, deps.dankshellPath, sudoPassword, deps); err != nil { - return fmt.Errorf("failed to write %s: %w", deps.dankshellPath, err) - } - - logFunc("✓ Created or updated /etc/pam.d/dankshell for lock screen authentication") - return nil -} - -func syncLockscreenU2FPamConfigWithDeps(logFunc func(string), sudoPassword string, enabled bool, deps syncDeps) error { - if deps.isNixOS() { - logFunc("ℹ NixOS detected. DMS does not manage /etc/pam.d/dankshell-u2f on NixOS. Keep using the bundled U2F helper or configure a custom PAM service yourself.") - return nil - } - - existingData, err := deps.readFile(deps.dankshellU2fPath) - if err != nil && !os.IsNotExist(err) { - return fmt.Errorf("failed to read %s: %w", deps.dankshellU2fPath, err) - } - - if enabled { - if err == nil && !hasManagedLockscreenU2FPamFile(string(existingData)) { - logFunc("ℹ Custom /etc/pam.d/dankshell-u2f found (no DMS block). Skipping.") - return nil - } - if err := writeManagedPamFile(buildManagedLockscreenU2FPamContent(), deps.dankshellU2fPath, sudoPassword, deps); err != nil { - return fmt.Errorf("failed to write %s: %w", deps.dankshellU2fPath, err) - } - logFunc("✓ Created or updated /etc/pam.d/dankshell-u2f for lock screen security-key authentication") - return nil - } - - if os.IsNotExist(err) { - return nil - } - if err == nil && !hasManagedLockscreenU2FPamFile(string(existingData)) { - logFunc("ℹ Custom /etc/pam.d/dankshell-u2f found (no DMS block). Leaving it untouched.") - return nil - } - - if err := deps.runSudoCmd(sudoPassword, "rm", "-f", deps.dankshellU2fPath); err != nil { - return fmt.Errorf("failed to remove %s: %w", deps.dankshellU2fPath, err) - } - logFunc("✓ Removed DMS-managed /etc/pam.d/dankshell-u2f") - return nil -} - -func stripManagedGreeterPamBlock(content string) (string, bool) { - lines := strings.Split(content, "\n") - filtered := make([]string, 0, len(lines)) - inManagedBlock := false - removed := false - - for _, line := range lines { - trimmed := strings.TrimSpace(line) - if trimmed == GreeterPamManagedBlockStart { - inManagedBlock = true - removed = true - continue - } - if trimmed == GreeterPamManagedBlockEnd { - inManagedBlock = false - removed = true - continue - } - if inManagedBlock { - removed = true - continue - } - filtered = append(filtered, line) - } - - return strings.Join(filtered, "\n"), removed -} - -func stripLegacyGreeterPamLines(content string) (string, bool) { - lines := strings.Split(content, "\n") - filtered := make([]string, 0, len(lines)) - removed := false - - for i := 0; i < len(lines); i++ { - trimmed := strings.TrimSpace(lines[i]) - if strings.HasPrefix(trimmed, legacyGreeterPamFprintComment) || strings.HasPrefix(trimmed, legacyGreeterPamU2FComment) { - removed = true - if i+1 < len(lines) { - nextLine := strings.TrimSpace(lines[i+1]) - if strings.HasPrefix(nextLine, "auth") && - (strings.Contains(nextLine, "pam_fprintd") || strings.Contains(nextLine, "pam_u2f")) { - i++ - } - } - continue - } - filtered = append(filtered, lines[i]) - } - - return strings.Join(filtered, "\n"), removed -} - -func insertManagedGreeterPamBlock(content string, blockLines []string, greetdPamPath string) (string, error) { - lines := strings.Split(content, "\n") - for i, line := range lines { - trimmed := strings.TrimSpace(line) - if trimmed != "" && !strings.HasPrefix(trimmed, "#") && strings.HasPrefix(trimmed, "auth") { - block := strings.Join(blockLines, "\n") - prefix := strings.Join(lines[:i], "\n") - suffix := strings.Join(lines[i:], "\n") - switch { - case prefix == "": - return block + "\n" + suffix, nil - case suffix == "": - return prefix + "\n" + block, nil - default: - return prefix + "\n" + block + "\n" + suffix, nil - } - } - } - return "", fmt.Errorf("no auth directive found in %s", greetdPamPath) -} - -func syncGreeterPamConfigWithDeps(logFunc func(string), sudoPassword string, settings AuthSettings, forceAuth bool, deps syncDeps) error { - var wantFprint, wantU2f bool - fprintToggleEnabled := forceAuth - u2fToggleEnabled := forceAuth - if forceAuth { - wantFprint = deps.pamModuleExists("pam_fprintd.so") - wantU2f = deps.pamModuleExists("pam_u2f.so") - } else { - fprintToggleEnabled = settings.GreeterEnableFprint - u2fToggleEnabled = settings.GreeterEnableU2f - fprintModule := deps.pamModuleExists("pam_fprintd.so") - u2fModule := deps.pamModuleExists("pam_u2f.so") - wantFprint = settings.GreeterEnableFprint && fprintModule - wantU2f = settings.GreeterEnableU2f && u2fModule - if settings.GreeterEnableFprint && !fprintModule { - logFunc("⚠ Warning: greeter fingerprint toggle is enabled, but pam_fprintd.so was not found.") - } - if settings.GreeterEnableU2f && !u2fModule { - logFunc("⚠ Warning: greeter security key toggle is enabled, but pam_u2f.so was not found.") - } - } - - if deps.isNixOS() { - logFunc("ℹ NixOS detected: PAM config is managed by NixOS modules. Skipping DMS PAM block write.") - logFunc(" Configure fingerprint/U2F auth via your greetd NixOS module options (e.g. security.pam.services.greetd).") - return nil - } - - pamData, err := deps.readFile(deps.greetdPath) - if err != nil { - return fmt.Errorf("failed to read %s: %w", deps.greetdPath, err) - } - originalContent := string(pamData) - content, _ := stripManagedGreeterPamBlock(originalContent) - content, _ = stripLegacyGreeterPamLines(content) - - includedFprintFile := detectIncludedPamModule(content, "pam_fprintd.so", deps) - includedU2fFile := detectIncludedPamModule(content, "pam_u2f.so", deps) - fprintAvailableForCurrentUser := deps.fingerprintAvailableForCurrentUser() - if wantFprint && includedFprintFile != "" { - 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 wantU2f && includedU2fFile != "" { - logFunc("⚠ pam_u2f already present in included " + includedU2fFile + " (managed by authselect/pam-auth-update). Skipping DMS U2F block to avoid double security-key auth.") - wantU2f = false - } - if !wantFprint && includedFprintFile != "" { - if fprintToggleEnabled { - logFunc("ℹ Fingerprint auth is still enabled via included " + includedFprintFile + ".") - if fprintAvailableForCurrentUser { - logFunc(" DMS toggle is enabled, and effective auth is provided by the included PAM stack.") - } else { - logFunc(" No enrolled fingerprints detected for the current user; password auth remains the effective path.") - } - } else { - if fprintAvailableForCurrentUser { - logFunc("ℹ Fingerprint auth is active via included " + includedFprintFile + " while DMS fingerprint toggle is off.") - logFunc(" Password login will work but may be delayed while the fingerprint module runs first.") - logFunc(" To eliminate the delay, " + pamManagerHintForCurrentDistro()) - } else { - logFunc("ℹ pam_fprintd is present via included " + includedFprintFile + ", but no enrolled fingerprints were detected for the current user.") - logFunc(" Password auth remains the effective login path.") - } - } - } - if !wantU2f && includedU2fFile != "" { - if u2fToggleEnabled { - logFunc("ℹ Security-key auth is still enabled via included " + includedU2fFile + ".") - logFunc(" DMS toggle is enabled, but effective auth is provided by the included PAM stack.") - } else { - logFunc("⚠ Security-key auth is active via included " + includedU2fFile + " while DMS security-key toggle is off.") - logFunc(" " + pamManagerHintForCurrentDistro()) - } - } - - if wantFprint || wantU2f { - blockLines := []string{GreeterPamManagedBlockStart} - if wantFprint { - blockLines = append(blockLines, "auth sufficient pam_fprintd.so max-tries=1 timeout=5") - } - if wantU2f { - blockLines = append(blockLines, "auth sufficient pam_u2f.so cue nouserok timeout=10") - } - blockLines = append(blockLines, GreeterPamManagedBlockEnd) - - content, err = insertManagedGreeterPamBlock(content, blockLines, deps.greetdPath) - if err != nil { - return err - } - } - - if content == originalContent { - return nil - } - - if err := writeManagedPamFile(content, deps.greetdPath, sudoPassword, deps); err != nil { - return fmt.Errorf("failed to install updated PAM config at %s: %w", deps.greetdPath, err) - } - if wantFprint || wantU2f { - logFunc("✓ Configured greetd PAM for fingerprint/U2F") - } else { - logFunc("✓ Cleared DMS-managed greeter PAM auth block") - } - return nil -} - -func writeManagedPamFile(content string, destPath string, sudoPassword string, deps syncDeps) error { - tmpFile, err := deps.createTemp("", "dms-pam-*.conf") - if err != nil { - return err - } - tmpPath := tmpFile.Name() - defer func() { - _ = deps.removeFile(tmpPath) - }() - - if _, err := tmpFile.WriteString(content); err != nil { - tmpFile.Close() - return err - } - if err := tmpFile.Close(); err != nil { - return err - } - if err := deps.runSudoCmd(sudoPassword, "cp", tmpPath, destPath); err != nil { - return err - } - if err := deps.runSudoCmd(sudoPassword, "chmod", "644", destPath); err != nil { - return fmt.Errorf("failed to set permissions on %s: %w", destPath, err) - } - return nil -} - -func pamManagerHintForCurrentDistro() string { - osInfo, err := distros.GetOSInfo() - if err != nil { - return "Disable it in your PAM manager (authselect/pam-auth-update) or in the included PAM stack to force password-only greeter login." - } - config, exists := distros.Registry[osInfo.Distribution.ID] - if !exists { - return "Disable it in your PAM manager (authselect/pam-auth-update) or in the included PAM stack to force password-only greeter login." - } - - switch config.Family { - case distros.FamilyFedora: - return "Disable it in authselect to force password-only greeter login." - case distros.FamilyDebian, distros.FamilyUbuntu: - return "Disable it in pam-auth-update to force password-only greeter login." - default: - return "Disable it in your distro PAM manager (authselect/pam-auth-update) or in the included PAM stack to force password-only greeter login." - } -} - -func pamModuleExists(module string) bool { - for _, libDir := range []string{ - "/usr/lib64/security", - "/usr/lib/security", - "/lib64/security", - "/lib/security", - "/lib/x86_64-linux-gnu/security", - "/usr/lib/x86_64-linux-gnu/security", - "/lib/aarch64-linux-gnu/security", - "/usr/lib/aarch64-linux-gnu/security", - "/run/current-system/sw/lib64/security", - "/run/current-system/sw/lib/security", - } { - if _, err := os.Stat(filepath.Join(libDir, module)); err == nil { - return true - } - } - return false -} - -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 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 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 runSudoCmd(sudoPassword string, command string, args ...string) error { - var cmd *exec.Cmd - - if sudoPassword != "" { - fullArgs := append([]string{command}, args...) - quotedArgs := make([]string, len(fullArgs)) - for i, arg := range fullArgs { - quotedArgs[i] = "'" + strings.ReplaceAll(arg, "'", "'\\''") + "'" - } - cmdStr := strings.Join(quotedArgs, " ") - - cmd = distros.ExecSudoCommand(context.Background(), sudoPassword, cmdStr) - } else { - cmd = exec.Command("sudo", append([]string{command}, args...)...) - } - - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - return cmd.Run() -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/pam/pam_test.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/pam/pam_test.go deleted file mode 100644 index 113bf10..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/pam/pam_test.go +++ /dev/null @@ -1,671 +0,0 @@ -package pam - -import ( - "fmt" - "os" - "path/filepath" - "strings" - "testing" -) - -func writeTestFile(t *testing.T, path string, content string) { - t.Helper() - if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { - t.Fatalf("failed to create parent dir for %s: %v", path, err) - } - if err := os.WriteFile(path, []byte(content), 0o644); err != nil { - t.Fatalf("failed to write %s: %v", path, err) - } -} - -type pamTestEnv struct { - pamDir string - greetdPath string - dankshellPath string - dankshellU2fPath string - tmpDir string - homeDir string - availableModules map[string]bool - fingerprintAvailable bool -} - -func newPamTestEnv(t *testing.T) *pamTestEnv { - t.Helper() - - root := t.TempDir() - pamDir := filepath.Join(root, "pam.d") - tmpDir := filepath.Join(root, "tmp") - homeDir := filepath.Join(root, "home") - - for _, dir := range []string{pamDir, tmpDir, homeDir} { - if err := os.MkdirAll(dir, 0o755); err != nil { - t.Fatalf("failed to create %s: %v", dir, err) - } - } - - return &pamTestEnv{ - pamDir: pamDir, - greetdPath: filepath.Join(pamDir, "greetd"), - dankshellPath: filepath.Join(pamDir, "dankshell"), - dankshellU2fPath: filepath.Join(pamDir, "dankshell-u2f"), - tmpDir: tmpDir, - homeDir: homeDir, - availableModules: map[string]bool{}, - } -} - -func (e *pamTestEnv) writePamFile(t *testing.T, name string, content string) { - t.Helper() - writeTestFile(t, filepath.Join(e.pamDir, name), content) -} - -func (e *pamTestEnv) writeSettings(t *testing.T, content string) { - t.Helper() - writeTestFile(t, filepath.Join(e.homeDir, ".config", "DankMaterialShell", "settings.json"), content) -} - -func (e *pamTestEnv) deps(isNixOS bool) syncDeps { - return syncDeps{ - pamDir: e.pamDir, - greetdPath: e.greetdPath, - dankshellPath: e.dankshellPath, - dankshellU2fPath: e.dankshellU2fPath, - isNixOS: func() bool { return isNixOS }, - readFile: os.ReadFile, - stat: os.Stat, - createTemp: func(_ string, pattern string) (*os.File, error) { - return os.CreateTemp(e.tmpDir, pattern) - }, - removeFile: os.Remove, - runSudoCmd: func(_ string, command string, args ...string) error { - switch command { - case "cp": - if len(args) != 2 { - return fmt.Errorf("unexpected cp args: %v", args) - } - data, err := os.ReadFile(args[0]) - if err != nil { - return err - } - if err := os.MkdirAll(filepath.Dir(args[1]), 0o755); err != nil { - return err - } - return os.WriteFile(args[1], data, 0o644) - case "chmod": - if len(args) != 2 { - return fmt.Errorf("unexpected chmod args: %v", args) - } - return nil - case "rm": - if len(args) != 2 || args[0] != "-f" { - return fmt.Errorf("unexpected rm args: %v", args) - } - if err := os.Remove(args[1]); err != nil && !os.IsNotExist(err) { - return err - } - return nil - default: - return fmt.Errorf("unexpected sudo command: %s %v", command, args) - } - }, - pamModuleExists: func(module string) bool { - return e.availableModules[module] - }, - fingerprintAvailableForCurrentUser: func() bool { - return e.fingerprintAvailable - }, - } -} - -func readFileString(t *testing.T, path string) string { - t.Helper() - data, err := os.ReadFile(path) - if err != nil { - t.Fatalf("failed to read %s: %v", path, err) - } - return string(data) -} - -func TestHasManagedLockscreenPamFile(t *testing.T) { - t.Parallel() - - tests := []struct { - name string - content string - want bool - }{ - { - name: "both markers present", - content: "#%PAM-1.0\n" + - LockscreenPamManagedBlockStart + "\n" + - "auth sufficient pam_unix.so\n" + - LockscreenPamManagedBlockEnd + "\n", - want: true, - }, - { - name: "missing end marker is not managed", - content: "#%PAM-1.0\n" + - LockscreenPamManagedBlockStart + "\n" + - "auth sufficient pam_unix.so\n", - want: false, - }, - { - name: "custom file is not managed", - content: "#%PAM-1.0\nauth sufficient pam_unix.so\n", - want: false, - }, - } - - for _, tt := range tests { - tt := tt - t.Run(tt.name, func(t *testing.T) { - t.Parallel() - if got := hasManagedLockscreenPamFile(tt.content); got != tt.want { - t.Fatalf("hasManagedLockscreenPamFile() = %v, want %v", got, tt.want) - } - }) - } -} - -func TestBuildManagedLockscreenPamContent(t *testing.T) { - t.Parallel() - - tests := []struct { - name string - files map[string]string - wantContains []string - wantNotContains []string - wantCounts map[string]int - wantErr string - }{ - { - name: "preserves custom modules and strips direct u2f and fprint directives", - files: map[string]string{ - "login": "#%PAM-1.0\n" + - "auth include system-auth\n" + - "account include system-auth\n" + - "session include system-auth\n", - "system-auth": "auth requisite pam_nologin.so\n" + - "auth sufficient pam_unix.so try_first_pass nullok\n" + - "auth sufficient pam_u2f.so cue\n" + - "auth sufficient pam_fprintd.so max-tries=1\n" + - "auth required pam_radius_auth.so conf=/etc/raddb/server\n" + - "account required pam_access.so\n" + - "session optional pam_lastlog.so silent\n", - }, - wantContains: []string{ - "#%PAM-1.0", - LockscreenPamManagedBlockStart, - LockscreenPamManagedBlockEnd, - "auth requisite pam_nologin.so", - "auth sufficient pam_unix.so try_first_pass nullok", - "auth required pam_radius_auth.so conf=/etc/raddb/server", - "account required pam_access.so", - "session optional pam_lastlog.so silent", - }, - wantNotContains: []string{ - "pam_u2f", - "pam_fprintd", - }, - wantCounts: map[string]int{ - "auth required pam_radius_auth.so conf=/etc/raddb/server": 1, - "account required pam_access.so": 1, - }, - }, - { - name: "resolves nested include substack and @include transitively", - files: map[string]string{ - "login": "#%PAM-1.0\n" + - "auth include system-auth\n" + - "account include system-auth\n" + - "password include system-auth\n" + - "session include system-auth\n", - "system-auth": "auth substack custom-auth\n" + - "account include custom-auth\n" + - "password include custom-auth\n" + - "session @include common-session\n", - "custom-auth": "auth required pam_custom.so one=two\n" + - "account required pam_custom_account.so\n" + - "password required pam_custom_password.so\n", - "common-session": "session optional pam_fprintd.so max-tries=1\n" + - "session optional pam_lastlog.so silent\n", - }, - wantContains: []string{ - "auth required pam_custom.so one=two", - "account required pam_custom_account.so", - "password required pam_custom_password.so", - "session optional pam_lastlog.so silent", - }, - wantNotContains: []string{ - "pam_fprintd", - }, - wantCounts: map[string]int{ - "auth required pam_custom.so one=two": 1, - "account required pam_custom_account.so": 1, - "password required pam_custom_password.so": 1, - "session optional pam_lastlog.so silent": 1, - }, - }, - { - name: "missing include fails", - files: map[string]string{ - "login": "#%PAM-1.0\nauth include missing-auth\n", - }, - wantErr: "failed to read PAM file", - }, - { - name: "cyclic include fails", - files: map[string]string{ - "login": "#%PAM-1.0\nauth include system-auth\n", - "system-auth": "auth include login\n", - }, - wantErr: "cyclic PAM include detected", - }, - { - name: "no auth directives remain after filtering fails", - files: map[string]string{ - "login": "#%PAM-1.0\nauth include system-auth\n", - "system-auth": "auth sufficient pam_u2f.so cue\n", - }, - wantErr: "no auth directives remained after filtering", - }, - } - - for _, tt := range tests { - tt := tt - t.Run(tt.name, func(t *testing.T) { - t.Parallel() - - env := newPamTestEnv(t) - for name, content := range tt.files { - env.writePamFile(t, name, content) - } - - content, err := buildManagedLockscreenPamContent(env.pamDir, os.ReadFile) - if tt.wantErr != "" { - if err == nil { - t.Fatalf("expected error containing %q, got nil", tt.wantErr) - } - if !strings.Contains(err.Error(), tt.wantErr) { - t.Fatalf("error = %q, want substring %q", err.Error(), tt.wantErr) - } - return - } - if err != nil { - t.Fatalf("buildManagedLockscreenPamContent returned error: %v", err) - } - - for _, want := range tt.wantContains { - if !strings.Contains(content, want) { - t.Errorf("missing expected string %q in output:\n%s", want, content) - } - } - for _, notWant := range tt.wantNotContains { - if strings.Contains(content, notWant) { - t.Errorf("unexpected string %q found in output:\n%s", notWant, content) - } - } - for want, wantCount := range tt.wantCounts { - if gotCount := strings.Count(content, want); gotCount != wantCount { - t.Errorf("count for %q = %d, want %d\noutput:\n%s", want, gotCount, wantCount, content) - } - } - }) - } -} - -func TestSyncLockscreenPamConfigWithDeps(t *testing.T) { - t.Parallel() - - t.Run("custom dankshell file is skipped untouched", func(t *testing.T) { - t.Parallel() - - env := newPamTestEnv(t) - customContent := "#%PAM-1.0\nauth required pam_unix.so\n" - env.writePamFile(t, "dankshell", customContent) - - var logs []string - err := syncLockscreenPamConfigWithDeps(func(msg string) { - logs = append(logs, msg) - }, "", env.deps(false)) - if err != nil { - t.Fatalf("syncLockscreenPamConfigWithDeps returned error: %v", err) - } - - if got := readFileString(t, env.dankshellPath); got != customContent { - t.Fatalf("custom dankshell content changed\ngot:\n%s\nwant:\n%s", got, customContent) - } - if len(logs) == 0 || !strings.Contains(logs[0], "Custom /etc/pam.d/dankshell found") { - t.Fatalf("expected custom-file skip log, got %v", logs) - } - }) - - t.Run("managed dankshell file is rewritten from resolved login stack", func(t *testing.T) { - t.Parallel() - - env := newPamTestEnv(t) - env.writePamFile(t, "login", "#%PAM-1.0\nauth include system-auth\naccount include system-auth\n") - env.writePamFile(t, "system-auth", "auth sufficient pam_unix.so try_first_pass nullok\nauth sufficient pam_u2f.so cue\naccount required pam_access.so\n") - env.writePamFile(t, "dankshell", "#%PAM-1.0\n"+LockscreenPamManagedBlockStart+"\nauth required pam_env.so\n"+LockscreenPamManagedBlockEnd+"\n") - - var logs []string - err := syncLockscreenPamConfigWithDeps(func(msg string) { - logs = append(logs, msg) - }, "", env.deps(false)) - if err != nil { - t.Fatalf("syncLockscreenPamConfigWithDeps returned error: %v", err) - } - - output := readFileString(t, env.dankshellPath) - for _, want := range []string{ - LockscreenPamManagedBlockStart, - "auth sufficient pam_unix.so try_first_pass nullok", - "account required pam_access.so", - LockscreenPamManagedBlockEnd, - } { - if !strings.Contains(output, want) { - t.Errorf("missing expected string %q in rewritten dankshell:\n%s", want, output) - } - } - if strings.Contains(output, "pam_u2f") { - t.Errorf("rewritten dankshell still contains pam_u2f:\n%s", output) - } - if len(logs) == 0 || !strings.Contains(logs[len(logs)-1], "Created or updated /etc/pam.d/dankshell") { - t.Fatalf("expected success log, got %v", logs) - } - }) - - t.Run("mutable systems fail when login stack cannot be converted safely", func(t *testing.T) { - t.Parallel() - - env := newPamTestEnv(t) - err := syncLockscreenPamConfigWithDeps(func(string) {}, "", env.deps(false)) - if err == nil { - t.Fatal("expected error when login PAM file is missing, got nil") - } - if !strings.Contains(err.Error(), "failed to build") { - t.Fatalf("error = %q, want substring %q", err.Error(), "failed to build") - } - }) - - t.Run("NixOS remains informational and does not write dankshell", func(t *testing.T) { - t.Parallel() - - env := newPamTestEnv(t) - var logs []string - - err := syncLockscreenPamConfigWithDeps(func(msg string) { - logs = append(logs, msg) - }, "", env.deps(true)) - if err != nil { - t.Fatalf("syncLockscreenPamConfigWithDeps returned error on NixOS path: %v", err) - } - if len(logs) == 0 || !strings.Contains(logs[0], "NixOS detected") || !strings.Contains(logs[0], "/etc/pam.d/login") { - t.Fatalf("expected NixOS informational log mentioning /etc/pam.d/login, got %v", logs) - } - if _, err := os.Stat(env.dankshellPath); !os.IsNotExist(err) { - t.Fatalf("expected no dankshell file to be written on NixOS path, stat err = %v", err) - } - }) -} - -func TestSyncLockscreenU2FPamConfigWithDeps(t *testing.T) { - t.Parallel() - - t.Run("enabled creates managed file", func(t *testing.T) { - t.Parallel() - - env := newPamTestEnv(t) - var logs []string - - err := syncLockscreenU2FPamConfigWithDeps(func(msg string) { - logs = append(logs, msg) - }, "", true, env.deps(false)) - if err != nil { - t.Fatalf("syncLockscreenU2FPamConfigWithDeps returned error: %v", err) - } - - got := readFileString(t, env.dankshellU2fPath) - if got != buildManagedLockscreenU2FPamContent() { - t.Fatalf("unexpected managed dankshell-u2f content:\n%s", got) - } - if len(logs) == 0 || !strings.Contains(logs[len(logs)-1], "Created or updated /etc/pam.d/dankshell-u2f") { - t.Fatalf("expected create log, got %v", logs) - } - }) - - t.Run("enabled rewrites existing managed file", func(t *testing.T) { - t.Parallel() - - env := newPamTestEnv(t) - env.writePamFile(t, "dankshell-u2f", "#%PAM-1.0\n"+LockscreenU2FPamManagedBlockStart+"\nauth required pam_u2f.so old\n"+LockscreenU2FPamManagedBlockEnd+"\n") - - if err := syncLockscreenU2FPamConfigWithDeps(func(string) {}, "", true, env.deps(false)); err != nil { - t.Fatalf("syncLockscreenU2FPamConfigWithDeps returned error: %v", err) - } - if got := readFileString(t, env.dankshellU2fPath); got != buildManagedLockscreenU2FPamContent() { - t.Fatalf("managed dankshell-u2f was not rewritten:\n%s", got) - } - }) - - t.Run("disabled removes DMS-managed file", func(t *testing.T) { - t.Parallel() - - env := newPamTestEnv(t) - env.writePamFile(t, "dankshell-u2f", buildManagedLockscreenU2FPamContent()) - - var logs []string - err := syncLockscreenU2FPamConfigWithDeps(func(msg string) { - logs = append(logs, msg) - }, "", false, env.deps(false)) - if err != nil { - t.Fatalf("syncLockscreenU2FPamConfigWithDeps returned error: %v", err) - } - if _, err := os.Stat(env.dankshellU2fPath); !os.IsNotExist(err) { - t.Fatalf("expected managed dankshell-u2f to be removed, stat err = %v", err) - } - if len(logs) == 0 || !strings.Contains(logs[len(logs)-1], "Removed DMS-managed /etc/pam.d/dankshell-u2f") { - t.Fatalf("expected removal log, got %v", logs) - } - }) - - t.Run("disabled preserves custom file", func(t *testing.T) { - t.Parallel() - - env := newPamTestEnv(t) - customContent := "#%PAM-1.0\nauth required pam_u2f.so cue\n" - env.writePamFile(t, "dankshell-u2f", customContent) - - var logs []string - err := syncLockscreenU2FPamConfigWithDeps(func(msg string) { - logs = append(logs, msg) - }, "", false, env.deps(false)) - if err != nil { - t.Fatalf("syncLockscreenU2FPamConfigWithDeps returned error: %v", err) - } - if got := readFileString(t, env.dankshellU2fPath); got != customContent { - t.Fatalf("custom dankshell-u2f content changed\ngot:\n%s\nwant:\n%s", got, customContent) - } - if len(logs) == 0 || !strings.Contains(logs[0], "Custom /etc/pam.d/dankshell-u2f found") { - t.Fatalf("expected custom-file log, got %v", logs) - } - }) -} - -func TestSyncGreeterPamConfigWithDeps(t *testing.T) { - t.Parallel() - - t.Run("adds managed block for enabled auth modules", func(t *testing.T) { - t.Parallel() - - env := newPamTestEnv(t) - env.availableModules["pam_fprintd.so"] = true - env.availableModules["pam_u2f.so"] = true - env.writePamFile(t, "greetd", "#%PAM-1.0\nauth include system-auth\naccount include system-auth\n") - env.writePamFile(t, "system-auth", "auth sufficient pam_unix.so\naccount required pam_unix.so\n") - - settings := AuthSettings{GreeterEnableFprint: true, GreeterEnableU2f: true} - if err := syncGreeterPamConfigWithDeps(func(string) {}, "", settings, false, env.deps(false)); err != nil { - t.Fatalf("syncGreeterPamConfigWithDeps returned error: %v", err) - } - - got := readFileString(t, env.greetdPath) - for _, want := range []string{ - GreeterPamManagedBlockStart, - "auth sufficient pam_fprintd.so max-tries=1 timeout=5", - "auth sufficient pam_u2f.so cue nouserok timeout=10", - GreeterPamManagedBlockEnd, - } { - if !strings.Contains(got, want) { - t.Errorf("missing expected string %q in greetd PAM:\n%s", want, got) - } - } - if strings.Index(got, GreeterPamManagedBlockStart) > strings.Index(got, "auth include system-auth") { - t.Fatalf("managed block was not inserted before first auth line:\n%s", got) - } - }) - - t.Run("avoids duplicate fingerprint when included stack already provides it", func(t *testing.T) { - t.Parallel() - - env := newPamTestEnv(t) - env.availableModules["pam_fprintd.so"] = true - env.fingerprintAvailable = true - original := "#%PAM-1.0\nauth include system-auth\naccount include system-auth\n" - env.writePamFile(t, "greetd", original) - env.writePamFile(t, "system-auth", "auth sufficient pam_fprintd.so max-tries=1\nauth sufficient pam_unix.so\n") - - settings := AuthSettings{GreeterEnableFprint: true} - if err := syncGreeterPamConfigWithDeps(func(string) {}, "", settings, false, env.deps(false)); err != nil { - t.Fatalf("syncGreeterPamConfigWithDeps returned error: %v", err) - } - - got := readFileString(t, env.greetdPath) - if got != original { - t.Fatalf("greetd PAM changed despite included pam_fprintd stack\ngot:\n%s\nwant:\n%s", got, original) - } - if strings.Contains(got, GreeterPamManagedBlockStart) { - t.Fatalf("managed block should not be inserted when included stack already has pam_fprintd:\n%s", got) - } - }) -} - -func TestRemoveManagedGreeterPamBlockWithDeps(t *testing.T) { - t.Parallel() - - env := newPamTestEnv(t) - env.writePamFile(t, "greetd", "#%PAM-1.0\n"+ - legacyGreeterPamFprintComment+"\n"+ - "auth sufficient pam_fprintd.so max-tries=1\n"+ - GreeterPamManagedBlockStart+"\n"+ - "auth sufficient pam_u2f.so cue nouserok timeout=10\n"+ - GreeterPamManagedBlockEnd+"\n"+ - "auth include system-auth\n") - - if err := removeManagedGreeterPamBlockWithDeps(func(string) {}, "", env.deps(false)); err != nil { - t.Fatalf("removeManagedGreeterPamBlockWithDeps returned error: %v", err) - } - - got := readFileString(t, env.greetdPath) - if strings.Contains(got, GreeterPamManagedBlockStart) || strings.Contains(got, legacyGreeterPamFprintComment) { - t.Fatalf("managed or legacy DMS auth lines remained in greetd PAM:\n%s", got) - } - if !strings.Contains(got, "auth include system-auth") { - t.Fatalf("expected non-DMS greetd auth lines to remain:\n%s", got) - } -} - -func TestSyncAuthConfigWithDeps(t *testing.T) { - t.Parallel() - - t.Run("creates lockscreen targets and skips greetd when greeter is not installed", func(t *testing.T) { - t.Parallel() - - env := newPamTestEnv(t) - env.writeSettings(t, `{"enableU2f":true}`) - env.writePamFile(t, "login", "#%PAM-1.0\nauth include system-auth\naccount include system-auth\n") - env.writePamFile(t, "system-auth", "auth sufficient pam_unix.so try_first_pass nullok\naccount required pam_access.so\n") - - var logs []string - err := syncAuthConfigWithDeps(func(msg string) { - logs = append(logs, msg) - }, "", SyncAuthOptions{HomeDir: env.homeDir}, env.deps(false)) - if err != nil { - t.Fatalf("syncAuthConfigWithDeps returned error: %v", err) - } - - if _, err := os.Stat(env.dankshellPath); err != nil { - t.Fatalf("expected dankshell to be created: %v", err) - } - if got := readFileString(t, env.dankshellU2fPath); got != buildManagedLockscreenU2FPamContent() { - t.Fatalf("unexpected dankshell-u2f content:\n%s", got) - } - if len(logs) == 0 || !strings.Contains(logs[len(logs)-1], "greetd not found") { - t.Fatalf("expected greetd skip log, got %v", logs) - } - }) - - t.Run("separate greeter and lockscreen toggles are respected", func(t *testing.T) { - t.Parallel() - - env := newPamTestEnv(t) - env.availableModules["pam_fprintd.so"] = true - env.writeSettings(t, `{"enableU2f":false,"greeterEnableFprint":true,"greeterEnableU2f":false}`) - env.writePamFile(t, "login", "#%PAM-1.0\nauth include system-auth\naccount include system-auth\n") - env.writePamFile(t, "system-auth", "auth sufficient pam_unix.so try_first_pass nullok\naccount required pam_access.so\n") - env.writePamFile(t, "greetd", "#%PAM-1.0\nauth include system-auth\naccount include system-auth\n") - - err := syncAuthConfigWithDeps(func(string) {}, "", SyncAuthOptions{HomeDir: env.homeDir}, env.deps(false)) - if err != nil { - t.Fatalf("syncAuthConfigWithDeps returned error: %v", err) - } - - dankshell := readFileString(t, env.dankshellPath) - if strings.Contains(dankshell, "pam_fprintd") || strings.Contains(dankshell, "pam_u2f") { - t.Fatalf("lockscreen PAM should strip fingerprint and U2F modules:\n%s", dankshell) - } - if _, err := os.Stat(env.dankshellU2fPath); !os.IsNotExist(err) { - t.Fatalf("expected dankshell-u2f to remain absent when enableU2f is false, stat err = %v", err) - } - - greetd := readFileString(t, env.greetdPath) - if !strings.Contains(greetd, "auth sufficient pam_fprintd.so max-tries=1 timeout=5") { - t.Fatalf("expected greetd PAM to receive fingerprint auth block:\n%s", greetd) - } - if strings.Contains(greetd, "auth sufficient pam_u2f.so cue nouserok timeout=10") { - t.Fatalf("did not expect greetd PAM to receive U2F auth block:\n%s", greetd) - } - }) - - t.Run("NixOS remains informational and non-mutating", func(t *testing.T) { - t.Parallel() - - env := newPamTestEnv(t) - env.availableModules["pam_fprintd.so"] = true - env.availableModules["pam_u2f.so"] = true - env.writeSettings(t, `{"enableU2f":true,"greeterEnableFprint":true,"greeterEnableU2f":true}`) - originalGreetd := "#%PAM-1.0\nauth include system-auth\naccount include system-auth\n" - env.writePamFile(t, "greetd", originalGreetd) - - var logs []string - err := syncAuthConfigWithDeps(func(msg string) { - logs = append(logs, msg) - }, "", SyncAuthOptions{HomeDir: env.homeDir}, env.deps(true)) - if err != nil { - t.Fatalf("syncAuthConfigWithDeps returned error: %v", err) - } - - if _, err := os.Stat(env.dankshellPath); !os.IsNotExist(err) { - t.Fatalf("expected dankshell to remain absent on NixOS path, stat err = %v", err) - } - if _, err := os.Stat(env.dankshellU2fPath); !os.IsNotExist(err) { - t.Fatalf("expected dankshell-u2f to remain absent on NixOS path, stat err = %v", err) - } - if got := readFileString(t, env.greetdPath); got != originalGreetd { - t.Fatalf("expected greetd PAM to remain unchanged on NixOS path\ngot:\n%s\nwant:\n%s", got, originalGreetd) - } - if len(logs) < 2 || !strings.Contains(strings.Join(logs, "\n"), "NixOS detected") { - t.Fatalf("expected informational NixOS logs, got %v", logs) - } - }) -} |