mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-08-07 22:18:30 -04:00
feat: new PAM auth management & settings in greeter/lockscreen
- Introduce external management of greetd PAM
- New functionality to validate and apply custom PAM service paths in lockscreen
Port 1.5
(cherry picked from commit b169fe0d77)
This commit is contained in:
@@ -1,7 +1,10 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||||
@@ -56,10 +59,108 @@ var authResolveLockCmd = &cobra.Command{
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var authListServicesCmd = &cobra.Command{
|
||||||
|
Use: "list-services",
|
||||||
|
Short: "List candidate lock-screen PAM services available on this system",
|
||||||
|
Long: "Enumerate the lock-screen PAM services that exist on this system and report their resolved auth stack (whether it has an auth directive and whether fingerprint/U2F modules appear inline).",
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
asJSON, _ := cmd.Flags().GetBool("json")
|
||||||
|
services := sharedpam.ListLockscreenPamServices()
|
||||||
|
|
||||||
|
if asJSON {
|
||||||
|
payload := struct {
|
||||||
|
Services []sharedpam.LockscreenPamServiceInfo `json:"services"`
|
||||||
|
}{Services: services}
|
||||||
|
data, err := json.MarshalIndent(payload, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Error encoding services: %v", err)
|
||||||
|
}
|
||||||
|
fmt.Println(string(data))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(services) == 0 {
|
||||||
|
fmt.Println("No candidate lock-screen PAM services found.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, s := range services {
|
||||||
|
fmt.Printf("%-20s %-30s auth=%-5t fingerprint=%-5t u2f=%t\n", s.Name, s.Path, s.HasAuth, s.InlineFingerprint, s.InlineU2f)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var authValidateCmd = &cobra.Command{
|
||||||
|
Use: "validate",
|
||||||
|
Short: "Validate a PAM service file for use as the DMS lock-screen password stack",
|
||||||
|
Long: "Validate one PAM service (by --service NAME or --path /abs/file) for use as the DMS lock-screen password stack. Exits 1 when the file is not usable.",
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
path, _ := cmd.Flags().GetString("path")
|
||||||
|
service, _ := cmd.Flags().GetString("service")
|
||||||
|
asJSON, _ := cmd.Flags().GetBool("json")
|
||||||
|
|
||||||
|
if (path == "") == (service == "") {
|
||||||
|
log.Fatalf("Error: exactly one of --path or --service is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
var result sharedpam.LockscreenPamValidation
|
||||||
|
if path != "" {
|
||||||
|
if !filepath.IsAbs(path) {
|
||||||
|
result = sharedpam.LockscreenPamValidation{
|
||||||
|
Path: path,
|
||||||
|
MissingModules: []string{},
|
||||||
|
Warnings: []string{},
|
||||||
|
Errors: []string{"--path must be an absolute file path"},
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result = sharedpam.ValidateLockscreenPamPath(path)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result = sharedpam.ValidateLockscreenPamService(service)
|
||||||
|
}
|
||||||
|
|
||||||
|
if asJSON {
|
||||||
|
data, err := json.MarshalIndent(result, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Error encoding validation: %v", err)
|
||||||
|
}
|
||||||
|
fmt.Println(string(data))
|
||||||
|
} else {
|
||||||
|
printLockscreenPamValidation(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !result.Valid {
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func printLockscreenPamValidation(result sharedpam.LockscreenPamValidation) {
|
||||||
|
fmt.Printf("Path: %s\n", result.Path)
|
||||||
|
fmt.Printf("Valid: %t\n", result.Valid)
|
||||||
|
fmt.Printf("Has auth: %t\n", result.HasAuth)
|
||||||
|
fmt.Printf("Inline fingerprint: %t\n", result.InlineFingerprint)
|
||||||
|
fmt.Printf("Inline U2F: %t\n", result.InlineU2f)
|
||||||
|
if len(result.MissingModules) > 0 {
|
||||||
|
fmt.Printf("Missing modules: %s\n", strings.Join(result.MissingModules, ", "))
|
||||||
|
}
|
||||||
|
for _, w := range result.Warnings {
|
||||||
|
fmt.Println("⚠ " + w)
|
||||||
|
}
|
||||||
|
for _, e := range result.Errors {
|
||||||
|
fmt.Println("✗ " + e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
authSyncCmd.Flags().BoolP("yes", "y", false, "Non-interactive mode: skip prompts")
|
authSyncCmd.Flags().BoolP("yes", "y", false, "Non-interactive mode: skip prompts")
|
||||||
authSyncCmd.Flags().BoolP("terminal", "t", false, "Run auth sync in a new terminal (for entering sudo password)")
|
authSyncCmd.Flags().BoolP("terminal", "t", false, "Run auth sync in a new terminal (for entering sudo password)")
|
||||||
authResolveLockCmd.Flags().BoolP("quiet", "q", false, "Only print the resulting file path")
|
authResolveLockCmd.Flags().BoolP("quiet", "q", false, "Only print the resulting file path")
|
||||||
|
|
||||||
|
authListServicesCmd.Flags().Bool("json", false, "Output as JSON")
|
||||||
|
|
||||||
|
authValidateCmd.Flags().String("path", "", "Absolute path to a PAM service file to validate")
|
||||||
|
authValidateCmd.Flags().String("service", "", "Name of a PAM service to resolve across the system PAM dirs")
|
||||||
|
authValidateCmd.Flags().Bool("json", false, "Output as JSON")
|
||||||
}
|
}
|
||||||
|
|
||||||
func syncAuth(nonInteractive bool) error {
|
func syncAuth(nonInteractive bool) error {
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ func init() {
|
|||||||
runCmd.Flags().MarkHidden("daemon-child")
|
runCmd.Flags().MarkHidden("daemon-child")
|
||||||
|
|
||||||
greeterCmd.AddCommand(greeterInstallCmd, greeterSyncCmd, greeterEnableCmd, greeterStatusCmd, greeterUninstallCmd, greeterLaunchSessionCmd)
|
greeterCmd.AddCommand(greeterInstallCmd, greeterSyncCmd, greeterEnableCmd, greeterStatusCmd, greeterUninstallCmd, greeterLaunchSessionCmd)
|
||||||
authCmd.AddCommand(authSyncCmd, authResolveLockCmd)
|
authCmd.AddCommand(authSyncCmd, authResolveLockCmd, authListServicesCmd, authValidateCmd)
|
||||||
setupCmd.AddCommand(setupBindsCmd, setupLayoutCmd, setupColorsCmd, setupAlttabCmd, setupOutputsCmd, setupCursorCmd, setupWindowrulesCmd)
|
setupCmd.AddCommand(setupBindsCmd, setupLayoutCmd, setupColorsCmd, setupAlttabCmd, setupOutputsCmd, setupCursorCmd, setupWindowrulesCmd)
|
||||||
updateCmd.AddCommand(updateCheckCmd)
|
updateCmd.AddCommand(updateCheckCmd)
|
||||||
pluginsCmd.AddCommand(pluginsBrowseCmd, pluginsListCmd, pluginsInstallCmd, pluginsUninstallCmd, pluginsUpdateCmd)
|
pluginsCmd.AddCommand(pluginsBrowseCmd, pluginsListCmd, pluginsInstallCmd, pluginsUninstallCmd, pluginsUpdateCmd)
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ func init() {
|
|||||||
runCmd.Flags().MarkHidden("daemon-child")
|
runCmd.Flags().MarkHidden("daemon-child")
|
||||||
|
|
||||||
greeterCmd.AddCommand(greeterInstallCmd, greeterSyncCmd, greeterEnableCmd, greeterStatusCmd, greeterUninstallCmd, greeterLaunchSessionCmd)
|
greeterCmd.AddCommand(greeterInstallCmd, greeterSyncCmd, greeterEnableCmd, greeterStatusCmd, greeterUninstallCmd, greeterLaunchSessionCmd)
|
||||||
authCmd.AddCommand(authSyncCmd, authResolveLockCmd)
|
authCmd.AddCommand(authSyncCmd, authResolveLockCmd, authListServicesCmd, authValidateCmd)
|
||||||
setupCmd.AddCommand(setupBindsCmd, setupLayoutCmd, setupColorsCmd, setupAlttabCmd, setupOutputsCmd, setupCursorCmd, setupWindowrulesCmd)
|
setupCmd.AddCommand(setupBindsCmd, setupLayoutCmd, setupColorsCmd, setupAlttabCmd, setupOutputsCmd, setupCursorCmd, setupWindowrulesCmd)
|
||||||
pluginsCmd.AddCommand(pluginsBrowseCmd, pluginsListCmd, pluginsInstallCmd, pluginsUninstallCmd, pluginsUpdateCmd)
|
pluginsCmd.AddCommand(pluginsBrowseCmd, pluginsListCmd, pluginsInstallCmd, pluginsUninstallCmd, pluginsUpdateCmd)
|
||||||
rootCmd.AddCommand(getCommonCommands()...)
|
rootCmd.AddCommand(getCommonCommands()...)
|
||||||
|
|||||||
+277
-4
@@ -70,10 +70,11 @@ var includedPamAuthFiles = []string{
|
|||||||
}
|
}
|
||||||
|
|
||||||
type AuthSettings struct {
|
type AuthSettings struct {
|
||||||
EnableFprint bool `json:"enableFprint"`
|
EnableFprint bool `json:"enableFprint"`
|
||||||
EnableU2f bool `json:"enableU2f"`
|
EnableU2f bool `json:"enableU2f"`
|
||||||
GreeterEnableFprint bool `json:"greeterEnableFprint"`
|
GreeterEnableFprint bool `json:"greeterEnableFprint"`
|
||||||
GreeterEnableU2f bool `json:"greeterEnableU2f"`
|
GreeterEnableU2f bool `json:"greeterEnableU2f"`
|
||||||
|
GreeterPamExternallyManaged bool `json:"greeterPamExternallyManaged"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type SyncAuthOptions struct {
|
type SyncAuthOptions struct {
|
||||||
@@ -236,6 +237,14 @@ func syncAuthConfigWithDeps(logFunc func(string), sudoPassword string, options S
|
|||||||
return fmt.Errorf("failed to inspect %s: %w", deps.greetdPath, err)
|
return fmt.Errorf("failed to inspect %s: %w", deps.greetdPath, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if settings.GreeterPamExternallyManaged {
|
||||||
|
if err := removeManagedGreeterPamBlockWithDeps(logFunc, sudoPassword, deps); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
logFunc("ℹ /etc/pam.d/greetd is externally managed. Skipping DMS greeter PAM sync.")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
if err := syncGreeterPamConfigWithDeps(logFunc, sudoPassword, settings, options.ForceGreeterAuth, deps); err != nil {
|
if err := syncGreeterPamConfigWithDeps(logFunc, sudoPassword, settings, options.ForceGreeterAuth, deps); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -583,6 +592,270 @@ func buildManagedLockscreenPamContent(baseDirs []string, readFile func(string) (
|
|||||||
return b.String(), nil
|
return b.String(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// lockscreenPamCandidateServices are the services surfaced by list-services:
|
||||||
|
// the standalone entry points plus the shared auth blocks users may pin.
|
||||||
|
var lockscreenPamCandidateServices = []string{
|
||||||
|
"login",
|
||||||
|
"system-auth",
|
||||||
|
"system-login",
|
||||||
|
"system-local-login",
|
||||||
|
"common-auth",
|
||||||
|
"base-auth",
|
||||||
|
}
|
||||||
|
|
||||||
|
// LockscreenPamServiceInfo describes one candidate lock-screen PAM service.
|
||||||
|
type LockscreenPamServiceInfo struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Dir string `json:"dir"`
|
||||||
|
Path string `json:"path"`
|
||||||
|
HasAuth bool `json:"hasAuth"`
|
||||||
|
InlineFingerprint bool `json:"inlineFingerprint"`
|
||||||
|
InlineU2f bool `json:"inlineU2f"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// LockscreenPamValidation is the result of validating a PAM service file for
|
||||||
|
// use as the DMS lock-screen password stack.
|
||||||
|
type LockscreenPamValidation struct {
|
||||||
|
Valid bool `json:"valid"`
|
||||||
|
Path string `json:"path"`
|
||||||
|
HasAuth bool `json:"hasAuth"`
|
||||||
|
InlineFingerprint bool `json:"inlineFingerprint"`
|
||||||
|
InlineU2f bool `json:"inlineU2f"`
|
||||||
|
MissingModules []string `json:"missingModules"`
|
||||||
|
Warnings []string `json:"warnings"`
|
||||||
|
Errors []string `json:"errors"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type lockscreenPamValidateDeps struct {
|
||||||
|
baseDirs []string
|
||||||
|
readFile func(string) ([]byte, error)
|
||||||
|
stat func(string) (os.FileInfo, error)
|
||||||
|
pamModuleExists func(string) bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func defaultValidateDeps() lockscreenPamValidateDeps {
|
||||||
|
return lockscreenPamValidateDeps{
|
||||||
|
baseDirs: lockscreenPamBaseDirs,
|
||||||
|
readFile: os.ReadFile,
|
||||||
|
stat: os.Stat,
|
||||||
|
pamModuleExists: pamModuleExists,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// lockscreenPamAnalysis is a non-destructive walk of a PAM service. Unlike
|
||||||
|
// resolveService it detects (rather than strips) pam_fprintd/pam_u2f and
|
||||||
|
// records unknown directives instead of hard-failing on them.
|
||||||
|
type lockscreenPamAnalysis struct {
|
||||||
|
lines []string
|
||||||
|
hasAuth bool
|
||||||
|
inlineFingerprint bool
|
||||||
|
inlineU2f bool
|
||||||
|
modules []string
|
||||||
|
unknownDirectives []string
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r lockscreenPamResolver) analyzePath(path string) lockscreenPamAnalysis {
|
||||||
|
var acc lockscreenPamAnalysis
|
||||||
|
if err := r.analyzeInto(filepath.Clean(path), "", nil, &acc); err != nil {
|
||||||
|
acc.err = err
|
||||||
|
}
|
||||||
|
return acc
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r lockscreenPamResolver) analyzeInto(path string, filterType string, stack []string, acc *lockscreenPamAnalysis) error {
|
||||||
|
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 fmt.Errorf("cyclic PAM include detected: %s", strings.Join(display, " -> "))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := r.readFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to read PAM file %s: %w", path, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
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, "#") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if include, ok := parseLockscreenPamIncludeDirective(trimmed, filterType); ok {
|
||||||
|
lineType := pamDirectiveType(trimmed)
|
||||||
|
if filterType != "" && lineType != "" && lineType != filterType {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
nestedPath := include.target
|
||||||
|
if filepath.IsAbs(nestedPath) {
|
||||||
|
nestedPath = filepath.Clean(nestedPath)
|
||||||
|
} else {
|
||||||
|
located, err := r.locate(include.target)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to read PAM file %s: %w", include.target, err)
|
||||||
|
}
|
||||||
|
nestedPath = located
|
||||||
|
}
|
||||||
|
if err := r.analyzeInto(nestedPath, include.filterType, append(stack, path), acc); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
lineType := pamDirectiveType(trimmed)
|
||||||
|
if lineType == "" {
|
||||||
|
acc.unknownDirectives = append(acc.unknownDirectives, trimmed)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if filterType != "" && lineType != filterType {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
acc.lines = append(acc.lines, rawLine)
|
||||||
|
if lineType == "auth" {
|
||||||
|
acc.hasAuth = true
|
||||||
|
}
|
||||||
|
|
||||||
|
foundModule := false
|
||||||
|
for _, field := range strings.Fields(trimmed) {
|
||||||
|
if strings.HasPrefix(field, "#") {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if strings.Contains(field, "pam_fprintd") {
|
||||||
|
acc.inlineFingerprint = true
|
||||||
|
}
|
||||||
|
if strings.Contains(field, "pam_u2f") {
|
||||||
|
acc.inlineU2f = true
|
||||||
|
}
|
||||||
|
if !foundModule && strings.HasSuffix(field, ".so") {
|
||||||
|
acc.modules = append(acc.modules, field)
|
||||||
|
foundModule = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListLockscreenPamServices enumerates the candidate lock-screen services that
|
||||||
|
// exist on this system, earlier base dir winning per name (libpam precedence).
|
||||||
|
func ListLockscreenPamServices() []LockscreenPamServiceInfo {
|
||||||
|
return listLockscreenPamServices(lockscreenPamBaseDirs, os.ReadFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
func listLockscreenPamServices(baseDirs []string, readFile func(string) ([]byte, error)) []LockscreenPamServiceInfo {
|
||||||
|
resolver := lockscreenPamResolver{baseDirs: baseDirs, readFile: readFile}
|
||||||
|
out := make([]LockscreenPamServiceInfo, 0, len(lockscreenPamCandidateServices))
|
||||||
|
for _, name := range lockscreenPamCandidateServices {
|
||||||
|
path, err := resolver.locate(name)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
info := LockscreenPamServiceInfo{
|
||||||
|
Name: name,
|
||||||
|
Dir: filepath.Dir(path),
|
||||||
|
Path: path,
|
||||||
|
}
|
||||||
|
if analysis := resolver.analyzePath(path); analysis.err == nil {
|
||||||
|
info.HasAuth = analysis.hasAuth
|
||||||
|
info.InlineFingerprint = analysis.inlineFingerprint
|
||||||
|
info.InlineU2f = analysis.inlineU2f
|
||||||
|
}
|
||||||
|
out = append(out, info)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateLockscreenPamService validates a named service resolved across the
|
||||||
|
// system PAM base dirs.
|
||||||
|
func ValidateLockscreenPamService(name string) LockscreenPamValidation {
|
||||||
|
return validateLockscreenPam(name, "", defaultValidateDeps())
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateLockscreenPamPath validates an arbitrary absolute PAM file, which may
|
||||||
|
// live outside the standard base dirs.
|
||||||
|
func ValidateLockscreenPamPath(path string) LockscreenPamValidation {
|
||||||
|
return validateLockscreenPam("", path, defaultValidateDeps())
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateLockscreenPam(serviceName string, path string, deps lockscreenPamValidateDeps) LockscreenPamValidation {
|
||||||
|
result := LockscreenPamValidation{
|
||||||
|
MissingModules: []string{},
|
||||||
|
Warnings: []string{},
|
||||||
|
Errors: []string{},
|
||||||
|
}
|
||||||
|
resolver := lockscreenPamResolver{baseDirs: deps.baseDirs, readFile: deps.readFile}
|
||||||
|
|
||||||
|
var analysis lockscreenPamAnalysis
|
||||||
|
if path != "" {
|
||||||
|
result.Path = path
|
||||||
|
analysis = resolver.analyzePath(path)
|
||||||
|
} else {
|
||||||
|
located, err := resolver.locate(serviceName)
|
||||||
|
if err != nil {
|
||||||
|
result.Errors = append(result.Errors, fmt.Sprintf("PAM service %q not found: %v", serviceName, err))
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
result.Path = located
|
||||||
|
analysis = resolver.analyzePath(located)
|
||||||
|
}
|
||||||
|
|
||||||
|
if analysis.err != nil {
|
||||||
|
result.Errors = append(result.Errors, analysis.err.Error())
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
result.HasAuth = analysis.hasAuth
|
||||||
|
result.InlineFingerprint = analysis.inlineFingerprint
|
||||||
|
result.InlineU2f = analysis.inlineU2f
|
||||||
|
|
||||||
|
if !analysis.hasAuth {
|
||||||
|
result.Errors = append(result.Errors, "no auth directives found after include resolution")
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, directive := range analysis.unknownDirectives {
|
||||||
|
result.Warnings = append(result.Warnings, "unsupported PAM directive (libpam may still handle it at runtime): "+directive)
|
||||||
|
}
|
||||||
|
|
||||||
|
seen := map[string]bool{}
|
||||||
|
for _, ref := range analysis.modules {
|
||||||
|
name := filepath.Base(ref)
|
||||||
|
if seen[name] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[name] = true
|
||||||
|
if moduleReferenceExists(ref, deps) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
result.MissingModules = append(result.MissingModules, name)
|
||||||
|
result.Warnings = append(result.Warnings, "referenced PAM module not found: "+name)
|
||||||
|
}
|
||||||
|
|
||||||
|
if analysis.inlineFingerprint {
|
||||||
|
result.Warnings = append(result.Warnings, "pam_fprintd is present in the resolved stack; may double-prompt with DMS's separate fingerprint context")
|
||||||
|
}
|
||||||
|
if analysis.inlineU2f {
|
||||||
|
result.Warnings = append(result.Warnings, "pam_u2f is present in the resolved stack; may double-prompt with DMS's separate U2F context")
|
||||||
|
}
|
||||||
|
|
||||||
|
result.Valid = len(result.Errors) == 0
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func moduleReferenceExists(ref string, deps lockscreenPamValidateDeps) bool {
|
||||||
|
if filepath.IsAbs(ref) {
|
||||||
|
_, err := deps.stat(ref)
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
return deps.pamModuleExists(ref)
|
||||||
|
}
|
||||||
|
|
||||||
const UserLockscreenPamService = "dankshell"
|
const UserLockscreenPamService = "dankshell"
|
||||||
|
|
||||||
func UserLockscreenPamDir() string {
|
func UserLockscreenPamDir() string {
|
||||||
|
|||||||
@@ -786,6 +786,223 @@ func TestRemoveManagedGreeterPamBlockWithDeps(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (e *pamTestEnv) validateDeps() lockscreenPamValidateDeps {
|
||||||
|
return lockscreenPamValidateDeps{
|
||||||
|
baseDirs: []string{e.pamDir},
|
||||||
|
readFile: os.ReadFile,
|
||||||
|
stat: os.Stat,
|
||||||
|
pamModuleExists: func(module string) bool { return e.availableModules[module] },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestListLockscreenPamServices(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
t.Run("dedupes by name with earlier base dir winning", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
etcDir := t.TempDir()
|
||||||
|
vendorDir := t.TempDir()
|
||||||
|
// login exists in both dirs; system-auth only in the vendor dir.
|
||||||
|
writeTestFile(t, filepath.Join(etcDir, "login"), "#%PAM-1.0\nauth required pam_unix.so\naccount required pam_unix.so\n")
|
||||||
|
writeTestFile(t, filepath.Join(vendorDir, "login"), "#%PAM-1.0\nauth required pam_deny.so\n")
|
||||||
|
writeTestFile(t, filepath.Join(vendorDir, "system-auth"), "#%PAM-1.0\nauth sufficient pam_unix.so\naccount required pam_unix.so\n")
|
||||||
|
|
||||||
|
services := listLockscreenPamServices([]string{etcDir, vendorDir}, os.ReadFile)
|
||||||
|
if len(services) != 2 {
|
||||||
|
t.Fatalf("expected 2 services (login, system-auth), got %d: %+v", len(services), services)
|
||||||
|
}
|
||||||
|
byName := map[string]LockscreenPamServiceInfo{}
|
||||||
|
for _, s := range services {
|
||||||
|
byName[s.Name] = s
|
||||||
|
}
|
||||||
|
login, ok := byName["login"]
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("expected login service, got %+v", services)
|
||||||
|
}
|
||||||
|
if login.Dir != etcDir || login.Path != filepath.Join(etcDir, "login") {
|
||||||
|
t.Fatalf("expected login to resolve in earlier dir %s, got dir=%s path=%s", etcDir, login.Dir, login.Path)
|
||||||
|
}
|
||||||
|
if !login.HasAuth {
|
||||||
|
t.Fatalf("expected login to report hasAuth")
|
||||||
|
}
|
||||||
|
if byName["system-auth"].Dir != vendorDir {
|
||||||
|
t.Fatalf("expected system-auth to resolve in vendor dir, got %s", byName["system-auth"].Dir)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("include resolution sets hasAuth and detects inline fprintd/u2f", 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\nauth sufficient pam_fprintd.so\nauth sufficient pam_u2f.so cue\naccount required pam_unix.so\n")
|
||||||
|
|
||||||
|
services := listLockscreenPamServices([]string{env.pamDir}, os.ReadFile)
|
||||||
|
var login LockscreenPamServiceInfo
|
||||||
|
for _, s := range services {
|
||||||
|
if s.Name == "login" {
|
||||||
|
login = s
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !login.HasAuth {
|
||||||
|
t.Fatalf("expected hasAuth via resolved include")
|
||||||
|
}
|
||||||
|
if !login.InlineFingerprint || !login.InlineU2f {
|
||||||
|
t.Fatalf("expected inline fingerprint and u2f detection, got %+v", login)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidateLockscreenPam(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
t.Run("valid service with resolved auth", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
env := newPamTestEnv(t)
|
||||||
|
env.availableModules["pam_unix.so"] = 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\naccount required pam_unix.so\n")
|
||||||
|
|
||||||
|
result := validateLockscreenPam("login", "", env.validateDeps())
|
||||||
|
if !result.Valid {
|
||||||
|
t.Fatalf("expected valid result, got %+v", result)
|
||||||
|
}
|
||||||
|
if !result.HasAuth {
|
||||||
|
t.Fatalf("expected hasAuth true")
|
||||||
|
}
|
||||||
|
if len(result.Errors) != 0 {
|
||||||
|
t.Fatalf("expected no errors, got %v", result.Errors)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("path outside base dirs is read directly", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
env := newPamTestEnv(t)
|
||||||
|
env.availableModules["pam_unix.so"] = true
|
||||||
|
outside := filepath.Join(t.TempDir(), "custom-pam")
|
||||||
|
writeTestFile(t, outside, "#%PAM-1.0\nauth sufficient pam_unix.so\naccount required pam_unix.so\n")
|
||||||
|
|
||||||
|
result := validateLockscreenPam("", outside, env.validateDeps())
|
||||||
|
if !result.Valid || result.Path != outside {
|
||||||
|
t.Fatalf("expected valid result for outside path, got %+v", result)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("missing module produces warning and missingModules but stays valid", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
env := newPamTestEnv(t)
|
||||||
|
env.availableModules["pam_unix.so"] = true
|
||||||
|
env.writePamFile(t, "system-auth", "#%PAM-1.0\nauth sufficient pam_unix.so\nauth required pam_absent.so\naccount required pam_unix.so\n")
|
||||||
|
|
||||||
|
result := validateLockscreenPam("system-auth", "", env.validateDeps())
|
||||||
|
if !result.Valid {
|
||||||
|
t.Fatalf("expected valid despite missing module, got %+v", result)
|
||||||
|
}
|
||||||
|
if len(result.MissingModules) != 1 || result.MissingModules[0] != "pam_absent.so" {
|
||||||
|
t.Fatalf("expected missing pam_absent.so, got %v", result.MissingModules)
|
||||||
|
}
|
||||||
|
if !containsSubstr(result.Warnings, "pam_absent.so") {
|
||||||
|
t.Fatalf("expected warning about missing module, got %v", result.Warnings)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("unknown directive is a warning not an error", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
env := newPamTestEnv(t)
|
||||||
|
env.availableModules["pam_unix.so"] = true
|
||||||
|
env.availableModules["pam_foo.so"] = true
|
||||||
|
env.writePamFile(t, "system-auth", "#%PAM-1.0\nauth sufficient pam_unix.so\nbadtype required pam_foo.so\naccount required pam_unix.so\n")
|
||||||
|
|
||||||
|
result := validateLockscreenPam("system-auth", "", env.validateDeps())
|
||||||
|
if !result.Valid {
|
||||||
|
t.Fatalf("expected valid with unknown directive, got %+v", result)
|
||||||
|
}
|
||||||
|
if len(result.Errors) != 0 {
|
||||||
|
t.Fatalf("expected no errors, got %v", result.Errors)
|
||||||
|
}
|
||||||
|
if !containsSubstr(result.Warnings, "unsupported PAM directive") {
|
||||||
|
t.Fatalf("expected unsupported directive warning, got %v", result.Warnings)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("cyclic include is an error", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
env := newPamTestEnv(t)
|
||||||
|
env.writePamFile(t, "login", "#%PAM-1.0\nauth include system-auth\n")
|
||||||
|
env.writePamFile(t, "system-auth", "auth include login\n")
|
||||||
|
|
||||||
|
result := validateLockscreenPam("login", "", env.validateDeps())
|
||||||
|
if result.Valid {
|
||||||
|
t.Fatalf("expected invalid on cyclic include, got %+v", result)
|
||||||
|
}
|
||||||
|
if !containsSubstr(result.Errors, "cyclic PAM include detected") {
|
||||||
|
t.Fatalf("expected cyclic include error, got %v", result.Errors)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("no auth directives is an error", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
env := newPamTestEnv(t)
|
||||||
|
env.availableModules["pam_unix.so"] = true
|
||||||
|
env.writePamFile(t, "system-auth", "#%PAM-1.0\naccount required pam_unix.so\n")
|
||||||
|
|
||||||
|
result := validateLockscreenPam("system-auth", "", env.validateDeps())
|
||||||
|
if result.Valid {
|
||||||
|
t.Fatalf("expected invalid when no auth directives, got %+v", result)
|
||||||
|
}
|
||||||
|
if !containsSubstr(result.Errors, "no auth directives") {
|
||||||
|
t.Fatalf("expected no-auth error, got %v", result.Errors)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("missing file is an error", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
env := newPamTestEnv(t)
|
||||||
|
result := validateLockscreenPam("", filepath.Join(env.pamDir, "does-not-exist"), env.validateDeps())
|
||||||
|
if result.Valid || len(result.Errors) == 0 {
|
||||||
|
t.Fatalf("expected invalid for missing file, got %+v", result)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("inline fingerprint and u2f produce warnings", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
env := newPamTestEnv(t)
|
||||||
|
env.availableModules["pam_unix.so"] = true
|
||||||
|
env.availableModules["pam_fprintd.so"] = true
|
||||||
|
env.availableModules["pam_u2f.so"] = true
|
||||||
|
env.writePamFile(t, "system-auth", "#%PAM-1.0\nauth sufficient pam_unix.so\nauth sufficient pam_fprintd.so\nauth sufficient pam_u2f.so cue\naccount required pam_unix.so\n")
|
||||||
|
|
||||||
|
result := validateLockscreenPam("system-auth", "", env.validateDeps())
|
||||||
|
if !result.Valid {
|
||||||
|
t.Fatalf("expected valid, got %+v", result)
|
||||||
|
}
|
||||||
|
if !result.InlineFingerprint || !result.InlineU2f {
|
||||||
|
t.Fatalf("expected inline flags set, got %+v", result)
|
||||||
|
}
|
||||||
|
if !containsSubstr(result.Warnings, "pam_fprintd") || !containsSubstr(result.Warnings, "pam_u2f") {
|
||||||
|
t.Fatalf("expected double-prompt warnings, got %v", result.Warnings)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func containsSubstr(items []string, substr string) bool {
|
||||||
|
for _, item := range items {
|
||||||
|
if strings.Contains(item, substr) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
func TestSyncAuthConfigWithDeps(t *testing.T) {
|
func TestSyncAuthConfigWithDeps(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
@@ -848,6 +1065,39 @@ func TestSyncAuthConfigWithDeps(t *testing.T) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("externally managed greetd is stripped and greeter sync skipped", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
env := newPamTestEnv(t)
|
||||||
|
env.availableModules["pam_fprintd.so"] = true
|
||||||
|
env.writeSettings(t, `{"greeterPamExternallyManaged":true,"greeterEnableFprint":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\naccount required pam_unix.so\n")
|
||||||
|
env.writePamFile(t, "greetd", "#%PAM-1.0\nauth include system-auth\n"+
|
||||||
|
GreeterPamManagedBlockStart+"\n"+
|
||||||
|
"auth sufficient pam_fprintd.so max-tries=2 timeout=10\n"+
|
||||||
|
GreeterPamManagedBlockEnd+"\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)
|
||||||
|
}
|
||||||
|
|
||||||
|
greetd := readFileString(t, env.greetdPath)
|
||||||
|
if strings.Contains(greetd, GreeterPamManagedBlockStart) || strings.Contains(greetd, "pam_fprintd") {
|
||||||
|
t.Fatalf("expected DMS-managed block stripped from externally managed greetd:\n%s", greetd)
|
||||||
|
}
|
||||||
|
if !strings.Contains(greetd, "auth include system-auth") {
|
||||||
|
t.Fatalf("expected non-DMS greetd lines to remain:\n%s", greetd)
|
||||||
|
}
|
||||||
|
if !containsSubstr(logs, "externally managed") {
|
||||||
|
t.Fatalf("expected externally-managed skip log, got %v", logs)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
t.Run("NixOS remains informational and non-mutating", func(t *testing.T) {
|
t.Run("NixOS remains informational and non-mutating", func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
|
|||||||
@@ -895,6 +895,10 @@ Singleton {
|
|||||||
readonly property bool greeterU2fReady: Processes.greeterU2fReady
|
readonly property bool greeterU2fReady: Processes.greeterU2fReady
|
||||||
readonly property string greeterU2fReason: Processes.greeterU2fReason
|
readonly property string greeterU2fReason: Processes.greeterU2fReason
|
||||||
readonly property string greeterU2fSource: Processes.greeterU2fSource
|
readonly property string greeterU2fSource: Processes.greeterU2fSource
|
||||||
|
property string lockPamPath: ""
|
||||||
|
property bool lockPamInlineFprint: false
|
||||||
|
property bool lockPamInlineU2f: false
|
||||||
|
property bool greeterPamExternallyManaged: false
|
||||||
property string lockScreenInactiveColor: "#000000"
|
property string lockScreenInactiveColor: "#000000"
|
||||||
property int lockScreenNotificationMode: 0
|
property int lockScreenNotificationMode: 0
|
||||||
property bool lockScreenVideoEnabled: false
|
property bool lockScreenVideoEnabled: false
|
||||||
|
|||||||
@@ -233,6 +233,7 @@ var SPEC = {
|
|||||||
greeterLockDateFormat: { def: "", onChange: "markGreeterSyncPending" },
|
greeterLockDateFormat: { def: "", onChange: "markGreeterSyncPending" },
|
||||||
greeterFontFamily: { def: "", onChange: "markGreeterSyncPending" },
|
greeterFontFamily: { def: "", onChange: "markGreeterSyncPending" },
|
||||||
greeterWallpaperFillMode: { def: "", onChange: "markGreeterSyncPending" },
|
greeterWallpaperFillMode: { def: "", onChange: "markGreeterSyncPending" },
|
||||||
|
greeterPamExternallyManaged: { def: false, onChange: "markGreeterSyncPending" },
|
||||||
greeterSyncPending: { def: false },
|
greeterSyncPending: { def: false },
|
||||||
greeterSyncBaseline: { def: {} },
|
greeterSyncBaseline: { def: {} },
|
||||||
mediaSize: { def: 1 },
|
mediaSize: { def: 1 },
|
||||||
@@ -455,6 +456,9 @@ var SPEC = {
|
|||||||
maxFprintTries: { def: 15 },
|
maxFprintTries: { def: 15 },
|
||||||
enableU2f: { def: false, onChange: "scheduleAuthApply" },
|
enableU2f: { def: false, onChange: "scheduleAuthApply" },
|
||||||
u2fMode: { def: "or" },
|
u2fMode: { def: "or" },
|
||||||
|
lockPamPath: { def: "" },
|
||||||
|
lockPamInlineFprint: { def: false },
|
||||||
|
lockPamInlineU2f: { def: false },
|
||||||
lockScreenInactiveColor: { def: "#000000" },
|
lockScreenInactiveColor: { def: "#000000" },
|
||||||
lockScreenNotificationMode: { def: 0 },
|
lockScreenNotificationMode: { def: 0 },
|
||||||
lockScreenVideoEnabled: { def: false },
|
lockScreenVideoEnabled: { def: false },
|
||||||
|
|||||||
@@ -90,6 +90,17 @@ Scope {
|
|||||||
fprint.checkAvail();
|
fprint.checkAvail();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
readonly property bool customPamActive: SettingsData.lockPamPath !== "" && customPamWatcher.loaded
|
||||||
|
readonly property bool fprintSuppressedByCustomPam: customPamActive && SettingsData.lockPamInlineFprint
|
||||||
|
readonly property bool u2fSuppressedByCustomPam: customPamActive && SettingsData.lockPamInlineU2f
|
||||||
|
|
||||||
|
FileView {
|
||||||
|
id: customPamWatcher
|
||||||
|
|
||||||
|
path: SettingsData.lockPamPath !== "" ? SettingsData.lockPamPath : ""
|
||||||
|
printErrors: false
|
||||||
|
}
|
||||||
|
|
||||||
FileView {
|
FileView {
|
||||||
id: dankshellConfigWatcher
|
id: dankshellConfigWatcher
|
||||||
|
|
||||||
@@ -148,6 +159,8 @@ Scope {
|
|||||||
id: passwd
|
id: passwd
|
||||||
|
|
||||||
config: {
|
config: {
|
||||||
|
if (root.customPamActive)
|
||||||
|
return SettingsData.lockPamPath.slice(SettingsData.lockPamPath.lastIndexOf("/") + 1);
|
||||||
if (dankshellConfigWatcher.loaded)
|
if (dankshellConfigWatcher.loaded)
|
||||||
return "dankshell";
|
return "dankshell";
|
||||||
if (nixosMarker.loaded || root.runningFromNixStore)
|
if (nixosMarker.loaded || root.runningFromNixStore)
|
||||||
@@ -157,6 +170,10 @@ Scope {
|
|||||||
return "login";
|
return "login";
|
||||||
}
|
}
|
||||||
configDirectory: {
|
configDirectory: {
|
||||||
|
if (root.customPamActive) {
|
||||||
|
const idx = SettingsData.lockPamPath.lastIndexOf("/");
|
||||||
|
return idx > 0 ? SettingsData.lockPamPath.slice(0, idx) : "/";
|
||||||
|
}
|
||||||
if (dankshellConfigWatcher.loaded)
|
if (dankshellConfigWatcher.loaded)
|
||||||
return "/etc/pam.d";
|
return "/etc/pam.d";
|
||||||
if (nixosMarker.loaded || root.runningFromNixStore)
|
if (nixosMarker.loaded || root.runningFromNixStore)
|
||||||
@@ -248,7 +265,7 @@ Scope {
|
|||||||
property int errorTries
|
property int errorTries
|
||||||
|
|
||||||
function checkAvail(): void {
|
function checkAvail(): void {
|
||||||
if (!available || !SettingsData.enableFprint || !root.lockSecured) {
|
if (!available || !SettingsData.enableFprint || !root.lockSecured || root.fprintSuppressedByCustomPam) {
|
||||||
abort();
|
abort();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -308,7 +325,7 @@ Scope {
|
|||||||
property bool available: SettingsData.lockU2fReady
|
property bool available: SettingsData.lockU2fReady
|
||||||
|
|
||||||
function checkAvail(): void {
|
function checkAvail(): void {
|
||||||
if (!available || !SettingsData.enableU2f || !root.lockSecured) {
|
if (!available || !SettingsData.enableU2f || !root.lockSecured || root.u2fSuppressedByCustomPam) {
|
||||||
abort();
|
abort();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -318,7 +335,7 @@ Scope {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function startForSecondFactor(): void {
|
function startForSecondFactor(): void {
|
||||||
if (!available || !SettingsData.enableU2f) {
|
if (!available || !SettingsData.enableU2f || root.u2fSuppressedByCustomPam) {
|
||||||
root.completeUnlock();
|
root.completeUnlock();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -331,7 +348,7 @@ Scope {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function startForAlternativeAuth(): void {
|
function startForAlternativeAuth(): void {
|
||||||
if (!available || !SettingsData.enableU2f || SettingsData.u2fMode !== "or" || root.unlockInProgress || passwd.active || active)
|
if (!available || !SettingsData.enableU2f || root.u2fSuppressedByCustomPam || SettingsData.u2fMode !== "or" || root.unlockInProgress || passwd.active || active)
|
||||||
return;
|
return;
|
||||||
abort();
|
abort();
|
||||||
root.u2fPending = true;
|
root.u2fPending = true;
|
||||||
@@ -486,6 +503,19 @@ Scope {
|
|||||||
u2f.checkAvail();
|
u2f.checkAvail();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onLockPamPathChanged(): void {
|
||||||
|
fprint.checkAvail();
|
||||||
|
u2f.checkAvail();
|
||||||
|
}
|
||||||
|
|
||||||
|
function onLockPamInlineFprintChanged(): void {
|
||||||
|
fprint.checkAvail();
|
||||||
|
}
|
||||||
|
|
||||||
|
function onLockPamInlineU2fChanged(): void {
|
||||||
|
u2f.checkAvail();
|
||||||
|
}
|
||||||
|
|
||||||
function onU2fModeChanged(): void {
|
function onU2fModeChanged(): void {
|
||||||
if (root.lockSecured) {
|
if (root.lockSecured) {
|
||||||
u2f.abort();
|
u2f.abort();
|
||||||
|
|||||||
@@ -18,6 +18,9 @@ Item {
|
|||||||
readonly property bool greeterU2fToggleAvailable: SettingsData.greeterU2fCanEnable || SettingsData.greeterEnableU2f
|
readonly property bool greeterU2fToggleAvailable: SettingsData.greeterU2fCanEnable || SettingsData.greeterEnableU2f
|
||||||
|
|
||||||
function greeterFingerprintDescription() {
|
function greeterFingerprintDescription() {
|
||||||
|
if (SettingsData.greeterPamExternallyManaged)
|
||||||
|
return I18n.tr("greetd PAM is externally managed. DMS will not change fingerprint auth; configure it yourself.");
|
||||||
|
|
||||||
const source = SettingsData.greeterFingerprintSource;
|
const source = SettingsData.greeterFingerprintSource;
|
||||||
const reason = SettingsData.greeterFingerprintReason;
|
const reason = SettingsData.greeterFingerprintReason;
|
||||||
|
|
||||||
@@ -51,6 +54,9 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function greeterU2fDescription() {
|
function greeterU2fDescription() {
|
||||||
|
if (SettingsData.greeterPamExternallyManaged)
|
||||||
|
return I18n.tr("greetd PAM is externally managed. DMS will not change security-key auth; configure it yourself.");
|
||||||
|
|
||||||
const source = SettingsData.greeterU2fSource;
|
const source = SettingsData.greeterU2fSource;
|
||||||
const reason = SettingsData.greeterU2fReason;
|
const reason = SettingsData.greeterU2fReason;
|
||||||
|
|
||||||
@@ -499,6 +505,15 @@ Item {
|
|||||||
horizontalAlignment: Text.AlignLeft
|
horizontalAlignment: Text.AlignLeft
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SettingsToggleRow {
|
||||||
|
settingKey: "greeterPamExternallyManaged"
|
||||||
|
tags: ["greeter", "pam", "managed", "external", "greetd", "auth"]
|
||||||
|
text: I18n.tr("Manage greetd PAM automatically")
|
||||||
|
description: SettingsData.greeterPamExternallyManaged ? I18n.tr("DMS never touches /etc/pam.d/greetd; any existing DMS block is removed on next sync. Configure fingerprint and security-key auth yourself.") : I18n.tr("DMS writes its managed block into /etc/pam.d/greetd during sync.")
|
||||||
|
checked: !SettingsData.greeterPamExternallyManaged
|
||||||
|
onToggled: checked => SettingsData.set("greeterPamExternallyManaged", !checked)
|
||||||
|
}
|
||||||
|
|
||||||
SettingsToggleRow {
|
SettingsToggleRow {
|
||||||
settingKey: "greeterEnableFprint"
|
settingKey: "greeterEnableFprint"
|
||||||
tags: ["greeter", "fingerprint", "fprintd", "login", "auth"]
|
tags: ["greeter", "fingerprint", "fprintd", "login", "auth"]
|
||||||
@@ -506,7 +521,7 @@ Item {
|
|||||||
description: root.greeterFingerprintDescription()
|
description: root.greeterFingerprintDescription()
|
||||||
descriptionColor: (SettingsData.greeterFingerprintReason === "ready" || SettingsData.greeterFingerprintReason === "configured_externally") ? Theme.surfaceVariantText : Theme.warning
|
descriptionColor: (SettingsData.greeterFingerprintReason === "ready" || SettingsData.greeterFingerprintReason === "configured_externally") ? Theme.surfaceVariantText : Theme.warning
|
||||||
checked: SettingsData.greeterEnableFprint
|
checked: SettingsData.greeterEnableFprint
|
||||||
enabled: root.greeterFprintToggleAvailable
|
enabled: root.greeterFprintToggleAvailable && !SettingsData.greeterPamExternallyManaged
|
||||||
onToggled: checked => SettingsData.set("greeterEnableFprint", checked)
|
onToggled: checked => SettingsData.set("greeterEnableFprint", checked)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -517,7 +532,7 @@ Item {
|
|||||||
description: root.greeterU2fDescription()
|
description: root.greeterU2fDescription()
|
||||||
descriptionColor: (SettingsData.greeterU2fReason === "ready" || SettingsData.greeterU2fReason === "configured_externally") ? Theme.surfaceVariantText : Theme.warning
|
descriptionColor: (SettingsData.greeterU2fReason === "ready" || SettingsData.greeterU2fReason === "configured_externally") ? Theme.surfaceVariantText : Theme.warning
|
||||||
checked: SettingsData.greeterEnableU2f
|
checked: SettingsData.greeterEnableU2f
|
||||||
enabled: root.greeterU2fToggleAvailable
|
enabled: root.greeterU2fToggleAvailable && !SettingsData.greeterPamExternallyManaged
|
||||||
onToggled: checked => SettingsData.set("greeterEnableU2f", checked)
|
onToggled: checked => SettingsData.set("greeterEnableU2f", checked)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -718,9 +733,13 @@ Item {
|
|||||||
|
|
||||||
MouseArea {
|
MouseArea {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
cursorShape: Qt.PointingHandCursor
|
hoverEnabled: true
|
||||||
enabled: !root.greeterSyncRunning && !root.greeterInstallActionRunning
|
acceptedButtons: Qt.AllButtons
|
||||||
onClicked: root.runGreeterSync()
|
cursorShape: !root.greeterSyncRunning && !root.greeterInstallActionRunning ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||||
|
onClicked: mouse => {
|
||||||
|
if (mouse.button === Qt.LeftButton && !root.greeterSyncRunning && !root.greeterInstallActionRunning)
|
||||||
|
root.runGreeterSync();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Row {
|
Row {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import QtQuick
|
import QtQuick
|
||||||
import Quickshell
|
import Quickshell
|
||||||
|
import Quickshell.Io
|
||||||
import qs.Common
|
import qs.Common
|
||||||
import qs.Modals.FileBrowser
|
import qs.Modals.FileBrowser
|
||||||
import qs.Services
|
import qs.Services
|
||||||
@@ -12,6 +13,71 @@ Item {
|
|||||||
readonly property bool lockFprintToggleAvailable: SettingsData.lockFingerprintCanEnable || SettingsData.enableFprint
|
readonly property bool lockFprintToggleAvailable: SettingsData.lockFingerprintCanEnable || SettingsData.enableFprint
|
||||||
readonly property bool lockU2fToggleAvailable: SettingsData.lockU2fCanEnable || SettingsData.enableU2f
|
readonly property bool lockU2fToggleAvailable: SettingsData.lockU2fCanEnable || SettingsData.enableU2f
|
||||||
|
|
||||||
|
property var authServices: []
|
||||||
|
property bool authValidateRunning: false
|
||||||
|
property bool authValidateOk: false
|
||||||
|
property bool authValidateWarn: false
|
||||||
|
property string authValidateMessage: ""
|
||||||
|
property string authPendingApplyPath: ""
|
||||||
|
property bool authShowCustom: false
|
||||||
|
|
||||||
|
readonly property string authAutoLabel: I18n.tr("Auto (recommended)")
|
||||||
|
readonly property string authCustomLabel: I18n.tr("Custom…")
|
||||||
|
|
||||||
|
function authServiceLabel(service) {
|
||||||
|
const label = service.name.split("-").map(w => w.charAt(0).toUpperCase() + w.slice(1)).join("-");
|
||||||
|
return service.dir === "/etc/pam.d" ? label : label + " (" + service.dir + ")";
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly property var authOptions: {
|
||||||
|
var opts = [authAutoLabel];
|
||||||
|
for (var i = 0; i < authServices.length; i++)
|
||||||
|
opts.push(authServiceLabel(authServices[i]));
|
||||||
|
opts.push(authCustomLabel);
|
||||||
|
return opts;
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly property string authCurrentValue: {
|
||||||
|
if (SettingsData.lockPamPath === "")
|
||||||
|
return authAutoLabel;
|
||||||
|
for (var i = 0; i < authServices.length; i++) {
|
||||||
|
if (authServices[i].path === SettingsData.lockPamPath)
|
||||||
|
return authServiceLabel(authServices[i]);
|
||||||
|
}
|
||||||
|
return authCustomLabel;
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshAuthServices() {
|
||||||
|
authListServicesProcess.running = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyAutoAuthSource() {
|
||||||
|
SettingsData.set("lockPamPath", "");
|
||||||
|
SettingsData.set("lockPamInlineFprint", false);
|
||||||
|
SettingsData.set("lockPamInlineU2f", false);
|
||||||
|
root.authValidateOk = false;
|
||||||
|
root.authValidateWarn = false;
|
||||||
|
root.authValidateMessage = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateAndApplyAuthSource(path) {
|
||||||
|
if (!path || path === "")
|
||||||
|
return;
|
||||||
|
if (!path.startsWith("/")) {
|
||||||
|
root.authValidateOk = false;
|
||||||
|
root.authValidateWarn = false;
|
||||||
|
root.authValidateMessage = I18n.tr("Not a valid path");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
root.authPendingApplyPath = path;
|
||||||
|
root.authValidateMessage = "";
|
||||||
|
root.authValidateOk = false;
|
||||||
|
root.authValidateWarn = false;
|
||||||
|
root.authValidateRunning = true;
|
||||||
|
authValidateProcess.command = ["dms", "auth", "validate", "--path", path, "--json"];
|
||||||
|
authValidateProcess.running = true;
|
||||||
|
}
|
||||||
|
|
||||||
function lockFingerprintDescription() {
|
function lockFingerprintDescription() {
|
||||||
switch (SettingsData.lockFingerprintReason) {
|
switch (SettingsData.lockFingerprintReason) {
|
||||||
case "ready":
|
case "ready":
|
||||||
@@ -48,10 +114,15 @@ Item {
|
|||||||
SettingsData.refreshAuthAvailability();
|
SettingsData.refreshAuthAvailability();
|
||||||
}
|
}
|
||||||
|
|
||||||
Component.onCompleted: refreshAuthDetection()
|
Component.onCompleted: {
|
||||||
|
refreshAuthDetection();
|
||||||
|
refreshAuthServices();
|
||||||
|
}
|
||||||
onVisibleChanged: {
|
onVisibleChanged: {
|
||||||
if (visible)
|
if (visible) {
|
||||||
refreshAuthDetection();
|
refreshAuthDetection();
|
||||||
|
refreshAuthServices();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
FileBrowserModal {
|
FileBrowserModal {
|
||||||
@@ -64,6 +135,78 @@ Item {
|
|||||||
onFileSelected: path => SettingsData.set("lockScreenVideoPath", path)
|
onFileSelected: path => SettingsData.set("lockScreenVideoPath", path)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Process {
|
||||||
|
id: authListServicesProcess
|
||||||
|
command: ["dms", "auth", "list-services", "--json"]
|
||||||
|
running: false
|
||||||
|
|
||||||
|
property string collected: ""
|
||||||
|
|
||||||
|
stdout: StdioCollector {
|
||||||
|
onStreamFinished: authListServicesProcess.collected = text || ""
|
||||||
|
}
|
||||||
|
|
||||||
|
onExited: exitCode => {
|
||||||
|
if (exitCode !== 0) {
|
||||||
|
root.authServices = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(authListServicesProcess.collected);
|
||||||
|
root.authServices = (data && Array.isArray(data.services)) ? data.services.filter(s => s.hasAuth) : [];
|
||||||
|
} catch (e) {
|
||||||
|
root.authServices = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Process {
|
||||||
|
id: authValidateProcess
|
||||||
|
running: false
|
||||||
|
|
||||||
|
property string collected: ""
|
||||||
|
|
||||||
|
stdout: StdioCollector {
|
||||||
|
onStreamFinished: authValidateProcess.collected = text || ""
|
||||||
|
}
|
||||||
|
|
||||||
|
onExited: exitCode => {
|
||||||
|
root.authValidateRunning = false;
|
||||||
|
var data = null;
|
||||||
|
try {
|
||||||
|
data = JSON.parse(authValidateProcess.collected);
|
||||||
|
} catch (e) {
|
||||||
|
data = null;
|
||||||
|
}
|
||||||
|
if (!data) {
|
||||||
|
root.authValidateOk = false;
|
||||||
|
root.authValidateWarn = false;
|
||||||
|
root.authValidateMessage = I18n.tr("Could not run validation. Ensure DMS is installed and dms is in PATH.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (data.valid) {
|
||||||
|
SettingsData.set("lockPamPath", root.authPendingApplyPath);
|
||||||
|
SettingsData.set("lockPamInlineFprint", data.inlineFingerprint === true);
|
||||||
|
SettingsData.set("lockPamInlineU2f", data.inlineU2f === true);
|
||||||
|
const warns = Array.isArray(data.warnings) ? data.warnings : [];
|
||||||
|
root.authValidateOk = true;
|
||||||
|
root.authValidateWarn = warns.length > 0;
|
||||||
|
root.authValidateMessage = warns.length > 0 ? (I18n.tr("Applied with warnings:") + "\n" + warns.join("\n")) : I18n.tr("Applied successfully.");
|
||||||
|
} else {
|
||||||
|
root.authValidateOk = false;
|
||||||
|
root.authValidateWarn = false;
|
||||||
|
const errs = Array.isArray(data.errors) ? data.errors : [];
|
||||||
|
const missing = Array.isArray(data.missingModules) ? data.missingModules : [];
|
||||||
|
var msg = I18n.tr("Invalid — not applied.");
|
||||||
|
if (errs.length > 0)
|
||||||
|
msg = msg + "\n" + errs.join("\n");
|
||||||
|
if (missing.length > 0)
|
||||||
|
msg = msg + "\n" + I18n.tr("Missing modules: ") + missing.join(", ");
|
||||||
|
root.authValidateMessage = msg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
DankFlickable {
|
DankFlickable {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
clip: true
|
clip: true
|
||||||
@@ -207,6 +350,109 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SettingsCard {
|
||||||
|
width: parent.width
|
||||||
|
iconName: "key"
|
||||||
|
title: I18n.tr("Authentication Source")
|
||||||
|
settingKey: "lockAuthSource"
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: I18n.tr("Auto resolves the system auth stack automatically. Picking a source pins the lock screen to that PAM file.")
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
width: parent.width
|
||||||
|
wrapMode: Text.Wrap
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsDropdownRow {
|
||||||
|
settingKey: "lockPamPath"
|
||||||
|
tags: ["lock", "screen", "pam", "authentication", "source", "service"]
|
||||||
|
text: I18n.tr("Authentication Source")
|
||||||
|
description: SettingsData.lockPamPath !== "" ? I18n.tr("Pinned to ") + SettingsData.lockPamPath : I18n.tr("Which PAM service the lock screen uses to authenticate")
|
||||||
|
options: root.authOptions
|
||||||
|
currentValue: root.authCurrentValue
|
||||||
|
onValueChanged: value => {
|
||||||
|
if (value === root.authAutoLabel) {
|
||||||
|
root.authShowCustom = false;
|
||||||
|
root.applyAutoAuthSource();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (value === root.authCustomLabel) {
|
||||||
|
root.authShowCustom = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
root.authShowCustom = false;
|
||||||
|
var svc = root.authServices.find(s => root.authServiceLabel(s) === value);
|
||||||
|
if (svc)
|
||||||
|
root.validateAndApplyAuthSource(svc.path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
width: parent.width
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
visible: root.authShowCustom || root.authCurrentValue === root.authCustomLabel
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: I18n.tr("Enter the absolute path to a PAM service file, then validate and apply.")
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
width: parent.width
|
||||||
|
wrapMode: Text.Wrap
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
width: parent.width
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
DankTextField {
|
||||||
|
id: customPamField
|
||||||
|
width: parent.width - validatePamButton.width - Theme.spacingS
|
||||||
|
placeholderText: "/etc/pam.d/my-service"
|
||||||
|
text: SettingsData.lockPamPath
|
||||||
|
backgroundColor: Theme.surfaceContainerHighest
|
||||||
|
}
|
||||||
|
|
||||||
|
DankButton {
|
||||||
|
id: validatePamButton
|
||||||
|
text: I18n.tr("Validate & Apply")
|
||||||
|
enabled: !root.authValidateRunning && customPamField.text.trim() !== ""
|
||||||
|
onClicked: root.validateAndApplyAuthSource(customPamField.text.trim())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: parent.width
|
||||||
|
height: Math.min(160, authStatusText.implicitHeight + Theme.spacingM * 2)
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: Theme.surfaceContainerHighest
|
||||||
|
visible: root.authValidateMessage !== ""
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
id: authStatusText
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: Theme.spacingM
|
||||||
|
text: root.authValidateMessage
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
font.family: "monospace"
|
||||||
|
color: !root.authValidateOk ? Theme.error : (root.authValidateWarn ? Theme.warning : Theme.surfaceVariantText)
|
||||||
|
wrapMode: Text.Wrap
|
||||||
|
verticalAlignment: Text.AlignTop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
visible: (SettingsData.lockPamInlineFprint && SettingsData.enableFprint) || (SettingsData.lockPamInlineU2f && SettingsData.enableU2f)
|
||||||
|
text: I18n.tr("The chosen PAM stack already prompts for fingerprint or security key, so DMS's separate prompts are suppressed to avoid a double prompt.")
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.warning
|
||||||
|
width: parent.width
|
||||||
|
wrapMode: Text.Wrap
|
||||||
|
topPadding: Theme.spacingS
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
SettingsCard {
|
SettingsCard {
|
||||||
width: parent.width
|
width: parent.width
|
||||||
iconName: "lock"
|
iconName: "lock"
|
||||||
|
|||||||
@@ -4634,6 +4634,45 @@
|
|||||||
"zenbrowser"
|
"zenbrowser"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"section": "lockAuthSource",
|
||||||
|
"label": "Authentication Source",
|
||||||
|
"tabIndex": 11,
|
||||||
|
"category": "Lock Screen",
|
||||||
|
"keywords": [
|
||||||
|
"authentication",
|
||||||
|
"lock",
|
||||||
|
"login",
|
||||||
|
"pam",
|
||||||
|
"password",
|
||||||
|
"pinned",
|
||||||
|
"screen",
|
||||||
|
"security",
|
||||||
|
"service",
|
||||||
|
"source"
|
||||||
|
],
|
||||||
|
"icon": "key",
|
||||||
|
"description": "Pinned to "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"section": "lockPamPath",
|
||||||
|
"label": "Authentication Source",
|
||||||
|
"tabIndex": 11,
|
||||||
|
"category": "Lock Screen",
|
||||||
|
"keywords": [
|
||||||
|
"authentication",
|
||||||
|
"lock",
|
||||||
|
"login",
|
||||||
|
"pam",
|
||||||
|
"password",
|
||||||
|
"pinned",
|
||||||
|
"screen",
|
||||||
|
"security",
|
||||||
|
"service",
|
||||||
|
"source"
|
||||||
|
],
|
||||||
|
"description": "Pinned to "
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"section": "lockScreenVideoCycling",
|
"section": "lockScreenVideoCycling",
|
||||||
"label": "Automatic Cycling",
|
"label": "Automatic Cycling",
|
||||||
@@ -7954,14 +7993,57 @@
|
|||||||
"keywords": [
|
"keywords": [
|
||||||
"auth",
|
"auth",
|
||||||
"authentication",
|
"authentication",
|
||||||
|
"block",
|
||||||
|
"configure",
|
||||||
"display manager",
|
"display manager",
|
||||||
|
"existing",
|
||||||
|
"external",
|
||||||
"fingerprint",
|
"fingerprint",
|
||||||
"fprintd",
|
|
||||||
"greetd",
|
"greetd",
|
||||||
"greeter",
|
"greeter",
|
||||||
"login"
|
"login",
|
||||||
|
"managed",
|
||||||
|
"never",
|
||||||
|
"next",
|
||||||
|
"pam",
|
||||||
|
"removed",
|
||||||
|
"security",
|
||||||
|
"sync",
|
||||||
|
"touches",
|
||||||
|
"yourself"
|
||||||
],
|
],
|
||||||
"icon": "fingerprint"
|
"icon": "fingerprint",
|
||||||
|
"description": "DMS never touches /etc/pam.d/greetd; any existing DMS block is removed on next sync. Configure fingerprint and security-key auth yourself."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"section": "greeterPamExternallyManaged",
|
||||||
|
"label": "Manage greetd PAM automatically",
|
||||||
|
"tabIndex": 31,
|
||||||
|
"category": "Greeter",
|
||||||
|
"keywords": [
|
||||||
|
"auth",
|
||||||
|
"automatically",
|
||||||
|
"block",
|
||||||
|
"configure",
|
||||||
|
"display manager",
|
||||||
|
"existing",
|
||||||
|
"external",
|
||||||
|
"fingerprint",
|
||||||
|
"greetd",
|
||||||
|
"greeter",
|
||||||
|
"login",
|
||||||
|
"manage",
|
||||||
|
"managed",
|
||||||
|
"never",
|
||||||
|
"next",
|
||||||
|
"pam",
|
||||||
|
"removed",
|
||||||
|
"security",
|
||||||
|
"sync",
|
||||||
|
"touches",
|
||||||
|
"yourself"
|
||||||
|
],
|
||||||
|
"description": "DMS never touches /etc/pam.d/greetd; any existing DMS block is removed on next sync. Configure fingerprint and security-key auth yourself."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"section": "greeterRememberLastSession",
|
"section": "greeterRememberLastSession",
|
||||||
|
|||||||
Reference in New Issue
Block a user