1
0
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:
purian23
2026-03-08 22:28:32 -04:00
parent baa956c3a1
commit acf63c57e8
6 changed files with 476 additions and 122 deletions

View File

@@ -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 {

View File

@@ -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

View File

@@ -11,7 +11,7 @@ Singleton {
readonly property string greetCfgDir: Quickshell.env("DMS_GREET_CFG_DIR") || "/var/cache/dms-greeter"
readonly property string sessionConfigPath: greetCfgDir + "/session.json"
readonly property string memoryFile: greetCfgDir + "/memory.json"
readonly property string memoryFile: greetCfgDir + "/.local/state/memory.json"
readonly property bool rememberLastSession: GreetdEnv.readBoolOverride(Quickshell.env, ["DMS_GREET_REMEMBER_LAST_SESSION", "DMS_SAVE_SESSION"], true)
readonly property bool rememberLastUser: GreetdEnv.readBoolOverride(Quickshell.env, ["DMS_GREET_REMEMBER_LAST_USER", "DMS_SAVE_USERNAME"], true)

View File

@@ -37,6 +37,9 @@ Item {
property bool cancelingExternalAuthForPassword: false
property int defaultAuthTimeoutMs: 12000
property int externalAuthTimeoutMs: 45000
property int memoryFlushDelayMs: 120
property string pendingLaunchCommand: ""
property var pendingLaunchEnv: []
property int passwordFailureCount: 0
property int passwordAttemptLimitHint: 0
property string authFeedbackMessage: ""
@@ -49,7 +52,7 @@ Item {
property string externalAuthAutoStartedForUser: ""
readonly property bool greeterPamHasFprint: pamModuleEnabled(greetdPamText, "pam_fprintd") || (greetdPamText.includes("system-auth") && pamModuleEnabled(systemAuthPamText, "pam_fprintd")) || (greetdPamText.includes("common-auth") && pamModuleEnabled(commonAuthPamText, "pam_fprintd")) || (greetdPamText.includes("password-auth") && pamModuleEnabled(passwordAuthPamText, "pam_fprintd"))
readonly property bool greeterPamHasU2f: pamModuleEnabled(greetdPamText, "pam_u2f") || (greetdPamText.includes("system-auth") && pamModuleEnabled(systemAuthPamText, "pam_u2f")) || (greetdPamText.includes("common-auth") && pamModuleEnabled(commonAuthPamText, "pam_u2f")) || (greetdPamText.includes("password-auth") && pamModuleEnabled(passwordAuthPamText, "pam_u2f"))
readonly property bool greeterExternalAuthAvailable: greeterPamHasFprint || greeterPamHasU2f
readonly property bool greeterExternalAuthAvailable: (greeterPamHasFprint && GreetdSettings.greeterEnableFprint) || (greeterPamHasU2f && GreetdSettings.greeterEnableU2f)
function initWeatherService() {
if (weatherInitialized)
@@ -1618,7 +1621,9 @@ Item {
} else if (GreetdMemory.lastSuccessfulUser) {
GreetdMemory.setLastSuccessfulUser("");
}
Greetd.launch(sessionCmd.split(" "), ["XDG_SESSION_TYPE=wayland"]);
pendingLaunchCommand = sessionCmd;
pendingLaunchEnv = ["XDG_SESSION_TYPE=wayland"];
memoryFlushTimer.restart();
}
function onAuthFailure(message) {
@@ -1661,6 +1666,20 @@ Item {
}
}
Timer {
id: memoryFlushTimer
interval: memoryFlushDelayMs
onTriggered: {
if (!pendingLaunchCommand)
return;
const sessionCommand = pendingLaunchCommand;
const launchEnv = pendingLaunchEnv;
pendingLaunchCommand = "";
pendingLaunchEnv = [];
Greetd.launch(sessionCommand.split(" "), launchEnv);
}
}
Timer {
id: authTimeout
interval: defaultAuthTimeoutMs

View File

@@ -179,6 +179,22 @@ export QT_QPA_PLATFORM=wayland
export QT_WAYLAND_DISABLE_WINDOWDECORATION=1
export EGL_PLATFORM=gbm
export DMS_RUN_GREETER=1
ensure_cache_tree() {
local base="$1"
mkdir -p "$base/.local/state" "$base/.local/share" "$base/.cache"
}
if ! ensure_cache_tree "$CACHE_DIR" 2>/dev/null; then
FALLBACK_CACHE_DIR="/tmp/dms-greeter-${UID:-$(id -u)}"
echo "Warning: cache directory '$CACHE_DIR' is not writable; falling back to '$FALLBACK_CACHE_DIR'" >&2
CACHE_DIR="$FALLBACK_CACHE_DIR"
if ! ensure_cache_tree "$CACHE_DIR"; then
echo "Error: failed to initialize fallback cache directory '$CACHE_DIR'" >&2
exit 1
fi
fi
export DMS_GREET_CFG_DIR="$CACHE_DIR"
if [[ -n "$REMEMBER_LAST_SESSION" ]]; then
@@ -203,11 +219,6 @@ if [[ -n "$REMEMBER_LAST_USER" ]]; then
export DMS_SAVE_USERNAME
fi
mkdir -p "$CACHE_DIR"
mkdir -p "$CACHE_DIR/.local/state"
mkdir -p "$CACHE_DIR/.local/share"
mkdir -p "$CACHE_DIR/.cache"
export HOME="$CACHE_DIR"
export XDG_STATE_HOME="$CACHE_DIR/.local/state"
export XDG_DATA_HOME="$CACHE_DIR/.local/share"

View File

@@ -49,18 +49,24 @@ Item {
readonly property bool greeterInstalled: greeterBinaryExists || greeterEnabled
readonly property string greeterActionLabel: {
if (!root.greeterInstalled) return I18n.tr("Install");
if (!root.greeterEnabled) return I18n.tr("Activate");
if (!root.greeterInstalled)
return I18n.tr("Install");
if (!root.greeterEnabled)
return I18n.tr("Activate");
return I18n.tr("Uninstall");
}
readonly property string greeterActionIcon: {
if (!root.greeterInstalled) return "download";
if (!root.greeterEnabled) return "login";
if (!root.greeterInstalled)
return "download";
if (!root.greeterEnabled)
return "login";
return "delete";
}
readonly property var greeterActionCommand: {
if (!root.greeterInstalled) return ["dms", "greeter", "install", "--terminal"];
if (!root.greeterEnabled) return ["dms", "greeter", "enable", "--terminal"];
if (!root.greeterInstalled)
return ["dms", "greeter", "install", "--terminal"];
if (!root.greeterEnabled)
return ["dms", "greeter", "enable", "--terminal"];
return ["dms", "greeter", "uninstall", "--terminal", "--yes"];
}
property string greeterPendingAction: ""
@@ -79,9 +85,7 @@ Item {
}
function runGreeterInstallAction() {
root.greeterPendingAction = !root.greeterInstalled ? "install"
: !root.greeterEnabled ? "activate"
: "uninstall";
root.greeterPendingAction = !root.greeterInstalled ? "install" : !root.greeterEnabled ? "activate" : "uninstall";
greeterStatusText = I18n.tr("Opening terminal: ") + root.greeterActionLabel + "…";
greeterInstallActionRunning = true;
greeterInstallActionProcess.running = true;
@@ -241,6 +245,7 @@ Item {
root.greeterStatusText = failure;
root.launchGreeterSyncTerminalFallback(false, "");
}
root.checkGreeterInstallState();
}
}
@@ -406,7 +411,10 @@ Item {
}
}
Item { width: 1; height: Theme.spacingM }
Item {
width: 1
height: Theme.spacingM
}
RowLayout {
width: parent.width
@@ -420,7 +428,9 @@ Item {
enabled: !root.greeterInstallActionRunning && !root.greeterSyncRunning
}
Item { Layout.fillWidth: true }
Item {
Layout.fillWidth: true
}
DankButton {
text: I18n.tr("Refresh")