mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-08-01 19:18:28 -04:00
auth: add some more intelligent pam config resolution for lock screen
and greeter
related #2789
port: 1.5
(cherry picked from commit 9cf2ca7196)
This commit is contained in:
@@ -35,9 +35,31 @@ var authSyncCmd = &cobra.Command{
|
||||
},
|
||||
}
|
||||
|
||||
var authResolveLockCmd = &cobra.Command{
|
||||
Use: "resolve-lock",
|
||||
Short: "Generate the lock-screen PAM config from the system auth stack",
|
||||
Long: "Resolve the distribution's PAM auth stack into a self-contained lock-screen config under the user state directory.\n" +
|
||||
"Runs unprivileged (reads /etc/pam.d, writes to the user's state dir) and is used by the shell as a fallback when /etc/pam.d/dankshell is not managed.\n" +
|
||||
"Prints the path of the generated file.",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
quiet, _ := cmd.Flags().GetBool("quiet")
|
||||
logFunc := func(msg string) {
|
||||
if !quiet {
|
||||
fmt.Println(msg)
|
||||
}
|
||||
}
|
||||
path, err := sharedpam.WriteUserLockscreenPamConfig(logFunc)
|
||||
if err != nil {
|
||||
log.Fatalf("Error resolving lock-screen PAM config: %v", err)
|
||||
}
|
||||
fmt.Println(path)
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
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)")
|
||||
authResolveLockCmd.Flags().BoolP("quiet", "q", false, "Only print the resulting file path")
|
||||
}
|
||||
|
||||
func syncAuth(nonInteractive bool) error {
|
||||
|
||||
@@ -20,7 +20,7 @@ func init() {
|
||||
runCmd.Flags().MarkHidden("daemon-child")
|
||||
|
||||
greeterCmd.AddCommand(greeterInstallCmd, greeterSyncCmd, greeterEnableCmd, greeterStatusCmd, greeterUninstallCmd, greeterLaunchSessionCmd)
|
||||
authCmd.AddCommand(authSyncCmd)
|
||||
authCmd.AddCommand(authSyncCmd, authResolveLockCmd)
|
||||
setupCmd.AddCommand(setupBindsCmd, setupLayoutCmd, setupColorsCmd, setupAlttabCmd, setupOutputsCmd, setupCursorCmd, setupWindowrulesCmd)
|
||||
updateCmd.AddCommand(updateCheckCmd)
|
||||
pluginsCmd.AddCommand(pluginsBrowseCmd, pluginsListCmd, pluginsInstallCmd, pluginsUninstallCmd, pluginsUpdateCmd)
|
||||
|
||||
@@ -20,7 +20,7 @@ func init() {
|
||||
runCmd.Flags().MarkHidden("daemon-child")
|
||||
|
||||
greeterCmd.AddCommand(greeterInstallCmd, greeterSyncCmd, greeterEnableCmd, greeterStatusCmd, greeterUninstallCmd, greeterLaunchSessionCmd)
|
||||
authCmd.AddCommand(authSyncCmd)
|
||||
authCmd.AddCommand(authSyncCmd, authResolveLockCmd)
|
||||
setupCmd.AddCommand(setupBindsCmd, setupLayoutCmd, setupColorsCmd, setupAlttabCmd, setupOutputsCmd, setupCursorCmd, setupWindowrulesCmd)
|
||||
pluginsCmd.AddCommand(pluginsBrowseCmd, pluginsListCmd, pluginsInstallCmd, pluginsUninstallCmd, pluginsUpdateCmd)
|
||||
rootCmd.AddCommand(getCommonCommands()...)
|
||||
|
||||
+170
-46
@@ -12,6 +12,7 @@ import (
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -32,6 +33,32 @@ const (
|
||||
DankshellU2FPamPath = "/etc/pam.d/dankshell-u2f"
|
||||
)
|
||||
|
||||
// lockscreenPamEntryCandidates are the /etc/pam.d entry-point services tried in
|
||||
// order. "login" is first so systems that ship it behave exactly as before; the
|
||||
// rest cover distros (or minimal installs) with no /etc/pam.d/login.
|
||||
// lockscreenPamBaseDirs mirrors libpam's search order: /etc overrides, then the
|
||||
// vendor dir (/usr/lib) and the stateless-distro default (/usr/share).
|
||||
var lockscreenPamBaseDirs = []string{"/etc/pam.d", "/usr/lib/pam.d", "/usr/share/pam.d"}
|
||||
|
||||
// Standalone auth+account services, most universal first. login exists almost
|
||||
// everywhere (util-linux); system-* cover Fedora/Arch/Gentoo/SUSE-Leap.
|
||||
var lockscreenPamEntryCandidates = []string{
|
||||
"login",
|
||||
"system-auth",
|
||||
"system-login",
|
||||
"system-local-login",
|
||||
}
|
||||
|
||||
// Fallback for distros with no standalone login service, only shared building
|
||||
// blocks: openSUSE/Debian (common-*), Alpine/postmarketOS (base-*).
|
||||
var lockscreenPamSharedIncludePairs = []struct {
|
||||
auth string
|
||||
account string
|
||||
}{
|
||||
{auth: "common-auth", account: "common-account"},
|
||||
{auth: "base-auth", account: "base-account"},
|
||||
}
|
||||
|
||||
var includedPamAuthFiles = []string{
|
||||
"system-auth",
|
||||
"common-auth",
|
||||
@@ -75,10 +102,50 @@ type lockscreenPamIncludeDirective struct {
|
||||
}
|
||||
|
||||
type lockscreenPamResolver struct {
|
||||
pamDir string
|
||||
baseDirs []string
|
||||
readFile func(string) ([]byte, error)
|
||||
}
|
||||
|
||||
// locate resolves a service/include name across baseDirs (libpam vendor-dir
|
||||
// fallback). Targets may not escape the base dirs.
|
||||
func (r lockscreenPamResolver) locate(target string) (string, error) {
|
||||
target = strings.TrimSpace(target)
|
||||
if target == "" {
|
||||
return "", fmt.Errorf("empty PAM include target")
|
||||
}
|
||||
|
||||
if filepath.IsAbs(target) {
|
||||
clean := filepath.Clean(target)
|
||||
for _, dir := range r.baseDirs {
|
||||
if filepath.Dir(clean) == filepath.Clean(dir) {
|
||||
return clean, nil
|
||||
}
|
||||
}
|
||||
return "", fmt.Errorf("unsupported PAM include outside PAM dirs: %s", target)
|
||||
}
|
||||
|
||||
clean := filepath.Clean(target)
|
||||
if clean == "." || clean == ".." || strings.HasPrefix(clean, ".."+string(os.PathSeparator)) {
|
||||
return "", fmt.Errorf("invalid PAM include target: %s", target)
|
||||
}
|
||||
|
||||
var firstErr error
|
||||
for _, dir := range r.baseDirs {
|
||||
path := filepath.Join(filepath.Clean(dir), clean)
|
||||
if _, err := r.readFile(path); err != nil {
|
||||
if firstErr == nil {
|
||||
firstErr = err
|
||||
}
|
||||
continue
|
||||
}
|
||||
return path, nil
|
||||
}
|
||||
if firstErr == nil {
|
||||
firstErr = os.ErrNotExist
|
||||
}
|
||||
return "", firstErr
|
||||
}
|
||||
|
||||
func defaultSyncDeps() syncDeps {
|
||||
return syncDeps{
|
||||
pamDir: "/etc/pam.d",
|
||||
@@ -375,32 +442,10 @@ func parseLockscreenPamIncludeDirective(trimmed string, inheritedFilter string)
|
||||
return lockscreenPamIncludeDirective{}, false
|
||||
}
|
||||
|
||||
func resolveLockscreenPamIncludePath(pamDir, target string) (string, error) {
|
||||
if strings.TrimSpace(target) == "" {
|
||||
return "", fmt.Errorf("empty PAM include target")
|
||||
}
|
||||
|
||||
cleanPamDir := filepath.Clean(pamDir)
|
||||
if filepath.IsAbs(target) {
|
||||
cleanTarget := filepath.Clean(target)
|
||||
if filepath.Dir(cleanTarget) != cleanPamDir {
|
||||
return "", fmt.Errorf("unsupported PAM include outside %s: %s", cleanPamDir, target)
|
||||
}
|
||||
return cleanTarget, nil
|
||||
}
|
||||
|
||||
cleanTarget := filepath.Clean(target)
|
||||
if cleanTarget == "." || cleanTarget == ".." || strings.HasPrefix(cleanTarget, ".."+string(os.PathSeparator)) {
|
||||
return "", fmt.Errorf("invalid PAM include target: %s", target)
|
||||
}
|
||||
|
||||
return filepath.Join(cleanPamDir, cleanTarget), nil
|
||||
}
|
||||
|
||||
func (r lockscreenPamResolver) resolveService(serviceName string, filterType string, stack []string) ([]string, error) {
|
||||
path, err := resolveLockscreenPamIncludePath(r.pamDir, serviceName)
|
||||
path, err := r.locate(serviceName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("failed to read PAM file %s: %w", serviceName, err)
|
||||
}
|
||||
|
||||
for _, seen := range stack {
|
||||
@@ -458,29 +503,73 @@ func (r lockscreenPamResolver) resolveService(serviceName string, filterType str
|
||||
return resolved, nil
|
||||
}
|
||||
|
||||
func buildManagedLockscreenPamContent(pamDir string, readFile func(string) ([]byte, error)) (string, error) {
|
||||
resolver := lockscreenPamResolver{
|
||||
pamDir: pamDir,
|
||||
readFile: readFile,
|
||||
}
|
||||
|
||||
resolvedLines, err := resolver.resolveService("login", "", nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if len(resolvedLines) == 0 {
|
||||
return "", fmt.Errorf("no auth directives remained after filtering %s", filepath.Join(pamDir, "login"))
|
||||
}
|
||||
|
||||
hasAuth := false
|
||||
for _, line := range resolvedLines {
|
||||
func resolvedLinesHaveAuth(lines []string) bool {
|
||||
for _, line := range lines {
|
||||
if pamDirectiveType(strings.TrimSpace(line)) == "auth" {
|
||||
hasAuth = true
|
||||
break
|
||||
return true
|
||||
}
|
||||
}
|
||||
if !hasAuth {
|
||||
return "", fmt.Errorf("no auth directives remained after filtering %s", filepath.Join(pamDir, "login"))
|
||||
return false
|
||||
}
|
||||
|
||||
func (r lockscreenPamResolver) resolveLines() ([]string, error) {
|
||||
var lastErr error
|
||||
|
||||
// Standalone login-like services: an existing one is authoritative.
|
||||
for _, service := range lockscreenPamEntryCandidates {
|
||||
if _, err := r.locate(service); err != nil {
|
||||
lastErr = err
|
||||
continue
|
||||
}
|
||||
lines, err := r.resolveService(service, "", nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !resolvedLinesHaveAuth(lines) {
|
||||
return nil, fmt.Errorf("no auth directives remained after filtering %s", service)
|
||||
}
|
||||
return lines, nil
|
||||
}
|
||||
|
||||
// Shared building blocks for distros without a login service (openSUSE,
|
||||
// Alpine): stitch the auth stanza to the account stanza when present.
|
||||
for _, pair := range lockscreenPamSharedIncludePairs {
|
||||
if _, err := r.locate(pair.auth); err != nil {
|
||||
lastErr = err
|
||||
continue
|
||||
}
|
||||
authLines, err := r.resolveService(pair.auth, "auth", nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !resolvedLinesHaveAuth(authLines) {
|
||||
lastErr = fmt.Errorf("no auth directives remained after filtering %s", pair.auth)
|
||||
continue
|
||||
}
|
||||
|
||||
resolved := append([]string{}, authLines...)
|
||||
if _, err := r.locate(pair.account); err == nil {
|
||||
acctLines, err := r.resolveService(pair.account, "account", nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resolved = append(resolved, acctLines...)
|
||||
}
|
||||
return resolved, nil
|
||||
}
|
||||
|
||||
if lastErr != nil {
|
||||
return nil, fmt.Errorf("no usable PAM auth service found: %w", lastErr)
|
||||
}
|
||||
return nil, fmt.Errorf("no usable PAM auth service found")
|
||||
}
|
||||
|
||||
func buildManagedLockscreenPamContent(baseDirs []string, readFile func(string) ([]byte, error)) (string, error) {
|
||||
resolver := lockscreenPamResolver{baseDirs: baseDirs, readFile: readFile}
|
||||
|
||||
resolvedLines, err := resolver.resolveLines()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var b strings.Builder
|
||||
@@ -494,6 +583,41 @@ func buildManagedLockscreenPamContent(pamDir string, readFile func(string) ([]by
|
||||
return b.String(), nil
|
||||
}
|
||||
|
||||
const UserLockscreenPamService = "dankshell"
|
||||
|
||||
func UserLockscreenPamDir() string {
|
||||
return filepath.Join(utils.XDGStateHome(), "DankMaterialShell", "pam")
|
||||
}
|
||||
|
||||
// WriteUserLockscreenPamConfig resolves the distro's real auth stack into a
|
||||
// self-contained lock-screen service under the user state dir, unprivileged
|
||||
// (reads world-readable PAM dirs, writes the user's own state dir). Rewrites
|
||||
// only on change to avoid inotify churn. Returns the written path.
|
||||
func WriteUserLockscreenPamConfig(logFunc func(string)) (string, error) {
|
||||
content, err := buildManagedLockscreenPamContent(lockscreenPamBaseDirs, os.ReadFile)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to resolve system PAM auth stack: %w", err)
|
||||
}
|
||||
|
||||
dir := UserLockscreenPamDir()
|
||||
if err := os.MkdirAll(dir, 0o700); err != nil {
|
||||
return "", fmt.Errorf("failed to create %s: %w", dir, err)
|
||||
}
|
||||
|
||||
path := filepath.Join(dir, UserLockscreenPamService)
|
||||
if existing, err := os.ReadFile(path); err == nil && string(existing) == content {
|
||||
return path, nil
|
||||
}
|
||||
if err := os.WriteFile(path, []byte(content), 0o600); err != nil {
|
||||
return "", fmt.Errorf("failed to write %s: %w", path, err)
|
||||
}
|
||||
|
||||
if logFunc != nil {
|
||||
logFunc("✓ Wrote lock-screen PAM config " + path)
|
||||
}
|
||||
return path, nil
|
||||
}
|
||||
|
||||
func buildManagedLockscreenU2FPamContent() string {
|
||||
var b strings.Builder
|
||||
b.WriteString("#%PAM-1.0\n")
|
||||
@@ -522,7 +646,7 @@ func syncLockscreenPamConfigWithDeps(logFunc func(string), sudoPassword string,
|
||||
return fmt.Errorf("failed to read %s: %w", deps.dankshellPath, err)
|
||||
}
|
||||
|
||||
content, err := buildManagedLockscreenPamContent(deps.pamDir, deps.readFile)
|
||||
content, err := buildManagedLockscreenPamContent([]string{deps.pamDir}, deps.readFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to build %s from %s: %w", deps.dankshellPath, filepath.Join(deps.pamDir, "login"), err)
|
||||
}
|
||||
|
||||
@@ -246,6 +246,31 @@ func TestBuildManagedLockscreenPamContent(t *testing.T) {
|
||||
"session optional pam_lastlog.so silent": 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "falls back to system-auth when login is absent",
|
||||
files: map[string]string{
|
||||
"system-auth": "#%PAM-1.0\nauth sufficient pam_unix.so try_first_pass nullok\naccount required pam_unix.so\n",
|
||||
},
|
||||
wantContains: []string{
|
||||
"auth sufficient pam_unix.so try_first_pass nullok",
|
||||
"account required pam_unix.so",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "no usable service when none of the candidates exist",
|
||||
files: map[string]string{
|
||||
"other": "#%PAM-1.0\nauth required pam_deny.so\n",
|
||||
},
|
||||
wantErr: "no usable PAM auth service found",
|
||||
},
|
||||
{
|
||||
name: "existing login with bad include is authoritative and does not fall back",
|
||||
files: map[string]string{
|
||||
"login": "#%PAM-1.0\nauth include missing-auth\n",
|
||||
"system-auth": "#%PAM-1.0\nauth sufficient pam_unix.so\naccount required pam_unix.so\n",
|
||||
},
|
||||
wantErr: "failed to read PAM file",
|
||||
},
|
||||
{
|
||||
name: "missing include fails",
|
||||
files: map[string]string{
|
||||
@@ -281,7 +306,7 @@ func TestBuildManagedLockscreenPamContent(t *testing.T) {
|
||||
env.writePamFile(t, name, content)
|
||||
}
|
||||
|
||||
content, err := buildManagedLockscreenPamContent(env.pamDir, os.ReadFile)
|
||||
content, err := buildManagedLockscreenPamContent([]string{env.pamDir}, os.ReadFile)
|
||||
if tt.wantErr != "" {
|
||||
if err == nil {
|
||||
t.Fatalf("expected error containing %q, got nil", tt.wantErr)
|
||||
@@ -314,6 +339,192 @@ func TestBuildManagedLockscreenPamContent(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// Real /etc/pam.d layouts of the non-Arch-shaped distros (#2789).
|
||||
func TestBuildManagedLockscreenPamContent_DistroShapes(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
files map[string]string
|
||||
wantContains []string
|
||||
wantNotContains []string
|
||||
}{
|
||||
{
|
||||
// openSUSE: `include` (not @include), common-auth symlinked to
|
||||
// common-auth-pc (here just a plain file), bracketed securetty
|
||||
// control, keyring modules, pam_sss.
|
||||
name: "openSUSE include + common-auth + bracket control",
|
||||
files: map[string]string{
|
||||
"login": "#%PAM-1.0\n" +
|
||||
"auth requisite pam_nologin.so\n" +
|
||||
"auth [user_unknown=ignore success=ok ignore=ignore auth_err=die default=bad] pam_securetty.so\n" +
|
||||
"auth include common-auth\n" +
|
||||
"account include common-account\n" +
|
||||
"session required pam_loginuid.so\n" +
|
||||
"session include common-session\n",
|
||||
"common-auth": "auth required pam_env.so\n" +
|
||||
"auth optional pam_gnome_keyring.so\n" +
|
||||
"auth sufficient pam_unix.so try_first_pass\n" +
|
||||
"auth required pam_sss.so use_first_pass\n",
|
||||
"common-account": "account required pam_unix.so try_first_pass\naccount sufficient pam_localuser.so\n",
|
||||
"common-session": "session optional pam_gnome_keyring.so auto_start\n",
|
||||
},
|
||||
wantContains: []string{
|
||||
"pam_securetty.so",
|
||||
"auth sufficient pam_unix.so try_first_pass",
|
||||
"auth required pam_sss.so use_first_pass",
|
||||
"account required pam_unix.so try_first_pass",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "openSUSE without login stitches common-auth and common-account",
|
||||
files: map[string]string{
|
||||
"common-auth": "auth required pam_env.so\n" +
|
||||
"auth optional pam_gnome_keyring.so\n" +
|
||||
"auth sufficient pam_unix.so try_first_pass\n" +
|
||||
"auth required pam_sss.so use_first_pass\n",
|
||||
"common-account": "account required pam_unix.so try_first_pass\n" +
|
||||
"account sufficient pam_localuser.so\n" +
|
||||
"account required pam_sss.so use_first_pass\n",
|
||||
},
|
||||
wantContains: []string{
|
||||
"auth sufficient pam_unix.so try_first_pass",
|
||||
"auth required pam_sss.so use_first_pass",
|
||||
"account required pam_unix.so try_first_pass",
|
||||
"account required pam_sss.so use_first_pass",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "openSUSE with only common-auth resolves auth-only",
|
||||
files: map[string]string{
|
||||
"common-auth": "auth sufficient pam_unix.so try_first_pass\nauth required pam_deny.so\n",
|
||||
},
|
||||
wantContains: []string{"auth sufficient pam_unix.so try_first_pass"},
|
||||
wantNotContains: []string{
|
||||
"account",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Debian @include common-auth and common-account",
|
||||
files: map[string]string{
|
||||
"login": "#%PAM-1.0\n" +
|
||||
"auth requisite pam_nologin.so\n" +
|
||||
"@include common-auth\n" +
|
||||
"@include common-account\n" +
|
||||
"session required pam_loginuid.so\n" +
|
||||
"@include common-session\n",
|
||||
"common-auth": "auth\t[success=1 default=ignore]\tpam_unix.so nullok\n" +
|
||||
"auth\trequisite\t\t\tpam_deny.so\n" +
|
||||
"auth\trequired\t\t\tpam_permit.so\n",
|
||||
"common-account": "account\t[success=1 new_authtok_reqd=done default=ignore]\tpam_unix.so\naccount\trequisite\t\t\tpam_deny.so\n",
|
||||
"common-session": "session\t[default=1]\t\t\tpam_permit.so\n",
|
||||
},
|
||||
wantContains: []string{
|
||||
"auth\t[success=1 default=ignore]\tpam_unix.so nullok",
|
||||
"account\t[success=1 new_authtok_reqd=done default=ignore]\tpam_unix.so",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "NixOS flat login with absolute paths and dash directives",
|
||||
files: map[string]string{
|
||||
"login": "#%PAM-1.0\n" +
|
||||
"auth required /nix/store/abc-pam/lib/security/pam_unix.so likeauth nullok try_first_pass\n" +
|
||||
"auth sufficient /nix/store/abc-pam-u2f/lib/security/pam_u2f.so\n" +
|
||||
"-auth optional /nix/store/abc-kbd/lib/security/pam_gnome_keyring.so\n" +
|
||||
"account required /nix/store/abc-pam/lib/security/pam_unix.so\n" +
|
||||
"-session optional /nix/store/abc-sd/lib/security/pam_systemd.so\n",
|
||||
},
|
||||
wantContains: []string{
|
||||
"auth required /nix/store/abc-pam/lib/security/pam_unix.so likeauth nullok try_first_pass",
|
||||
"-auth optional /nix/store/abc-kbd/lib/security/pam_gnome_keyring.so",
|
||||
"account required /nix/store/abc-pam/lib/security/pam_unix.so",
|
||||
},
|
||||
wantNotContains: []string{"pam_u2f"},
|
||||
},
|
||||
{
|
||||
name: "Gentoo deep include chain login->system-local-login->system-login->system-auth",
|
||||
files: map[string]string{
|
||||
"login": "#%PAM-1.0\nauth\tinclude\t\tsystem-local-login\naccount\tinclude\t\tsystem-local-login\n",
|
||||
"system-local-login": "auth\trequired\tpam_group.so\nauth\tinclude\t\tsystem-login\naccount\tinclude\t\tsystem-login\n",
|
||||
"system-login": "auth\tinclude\t\tsystem-auth\naccount\tinclude\t\tsystem-auth\n",
|
||||
"system-auth": "auth\trequired\tpam_env.so\nauth\tsufficient\tpam_unix.so try_first_pass likeauth nullok\nauth\trequired\tpam_deny.so\naccount\trequired\tpam_unix.so\n",
|
||||
},
|
||||
wantContains: []string{
|
||||
"auth\trequired\tpam_group.so",
|
||||
"auth\tsufficient\tpam_unix.so try_first_pass likeauth nullok",
|
||||
"account\trequired\tpam_unix.so",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "no login, entry falls through to system-auth",
|
||||
files: map[string]string{
|
||||
"system-auth": "#%PAM-1.0\n" +
|
||||
"auth required pam_env.so\n" +
|
||||
"auth sufficient pam_unix.so nullok\n" +
|
||||
"auth sufficient pam_sss.so forward_pass\n" +
|
||||
"auth required pam_deny.so\n" +
|
||||
"account required pam_unix.so\n" +
|
||||
"account [default=bad success=ok user_unknown=ignore] pam_sss.so\n",
|
||||
},
|
||||
wantContains: []string{
|
||||
"auth sufficient pam_unix.so nullok",
|
||||
"auth sufficient pam_sss.so forward_pass",
|
||||
"account required pam_unix.so",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
env := newPamTestEnv(t)
|
||||
for name, content := range tt.files {
|
||||
env.writePamFile(t, name, content)
|
||||
}
|
||||
|
||||
content, err := buildManagedLockscreenPamContent([]string{env.pamDir}, os.ReadFile)
|
||||
if err != nil {
|
||||
t.Fatalf("buildManagedLockscreenPamContent returned error: %v", err)
|
||||
}
|
||||
if !strings.Contains(content, "auth") {
|
||||
t.Fatalf("resolved content has no auth line:\n%s", content)
|
||||
}
|
||||
for _, want := range tt.wantContains {
|
||||
if !strings.Contains(content, want) {
|
||||
t.Errorf("missing expected string %q in output:\n%s", want, content)
|
||||
}
|
||||
}
|
||||
for _, notWant := range tt.wantNotContains {
|
||||
if strings.Contains(content, notWant) {
|
||||
t.Errorf("unexpected string %q found in output:\n%s", notWant, content)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildManagedLockscreenPamContent_VendorDirFallback(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Stateless/vendored-PAM systems (Clear Linux) ship the stack in
|
||||
// /usr/lib|share/pam.d with /etc/pam.d empty; includes resolve in that dir.
|
||||
etcDir := t.TempDir()
|
||||
vendorDir := t.TempDir()
|
||||
writeTestFile(t, filepath.Join(vendorDir, "login"), "#%PAM-1.0\nauth include system-auth\naccount include system-auth\n")
|
||||
writeTestFile(t, filepath.Join(vendorDir, "system-auth"), "auth sufficient pam_unix.so nullok\naccount required pam_unix.so\n")
|
||||
|
||||
content, err := buildManagedLockscreenPamContent([]string{etcDir, vendorDir}, os.ReadFile)
|
||||
if err != nil {
|
||||
t.Fatalf("buildManagedLockscreenPamContent returned error: %v", err)
|
||||
}
|
||||
for _, want := range []string{"auth sufficient pam_unix.so nullok", "account required pam_unix.so"} {
|
||||
if !strings.Contains(content, want) {
|
||||
t.Errorf("missing %q in output:\n%s", want, content)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSyncLockscreenPamConfigWithDeps(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
||||
@@ -18,10 +18,9 @@ Item {
|
||||
|
||||
readonly property bool _descriptionIsHtml: /<[a-z][^>]*>/i.test((eventData && eventData.description) || "")
|
||||
|
||||
// _locationUrl makes the location row clickable: the location itself when
|
||||
// it is a URL, the meeting link when the event has one (conference
|
||||
// events carry placeholder locations like "Microsoft Teams Meeting"),
|
||||
// otherwise a geo: search (RFC 5870) so addresses open in the maps app.
|
||||
// _locationUrl makes the location row clickable: a URL location opens
|
||||
// directly, conference placeholders open the meeting link, and anything
|
||||
// else opens as a geo: search in the maps app.
|
||||
function _locationUrl() {
|
||||
const loc = ((eventData && eventData.location) || "").trim();
|
||||
if (loc === "")
|
||||
@@ -249,7 +248,21 @@ Item {
|
||||
anchors.fill: parent
|
||||
enabled: root._locationUrl() !== ""
|
||||
cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||
onClicked: Qt.openUrlExternally(root._locationUrl())
|
||||
// Qt.openUrlExternally can't handle geo: URIs, so
|
||||
// route those through the dankcal daemon's opener.
|
||||
onClicked: {
|
||||
const url = root._locationUrl();
|
||||
if (url.startsWith("geo:") && CalendarDankBackend.connected) {
|
||||
CalendarDankBackend.sendRequest("system.openUri", {
|
||||
"uri": url
|
||||
}, response => {
|
||||
if (response && response.error)
|
||||
Qt.openUrlExternally(url);
|
||||
});
|
||||
return;
|
||||
}
|
||||
Qt.openUrlExternally(url);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,14 +111,60 @@ Scope {
|
||||
printErrors: false
|
||||
}
|
||||
|
||||
// Fallback stack written by `dms auth resolve-lock` when no managed
|
||||
// /etc/pam.d/dankshell exists. See #2789.
|
||||
readonly property string userPamDir: Paths.strip(Paths.state) + "/pam"
|
||||
|
||||
FileView {
|
||||
id: userPamWatcher
|
||||
|
||||
path: root.userPamDir + "/dankshell"
|
||||
printErrors: false
|
||||
}
|
||||
|
||||
Process {
|
||||
id: resolveUserPam
|
||||
|
||||
command: ["dms", "auth", "resolve-lock", "--quiet"]
|
||||
running: false
|
||||
onExited: exitCode => {
|
||||
if (exitCode === 0)
|
||||
userPamWatcher.reload();
|
||||
}
|
||||
}
|
||||
|
||||
function ensureUserPamConfig(): void {
|
||||
if (root.runningFromNixStore || resolveUserPam.running)
|
||||
return;
|
||||
resolveUserPam.running = true;
|
||||
}
|
||||
|
||||
Component.onCompleted: ensureUserPamConfig()
|
||||
|
||||
// Detects Nix-installed DMS on non-NixOS systems
|
||||
readonly property bool runningFromNixStore: Quickshell.shellDir.startsWith("/nix/store/")
|
||||
|
||||
PamContext {
|
||||
id: passwd
|
||||
|
||||
config: dankshellConfigWatcher.loaded ? "dankshell" : "login"
|
||||
configDirectory: (dankshellConfigWatcher.loaded || nixosMarker.loaded || root.runningFromNixStore) ? "/etc/pam.d" : Quickshell.shellDir + "/assets/pam"
|
||||
config: {
|
||||
if (dankshellConfigWatcher.loaded)
|
||||
return "dankshell";
|
||||
if (nixosMarker.loaded || root.runningFromNixStore)
|
||||
return "login";
|
||||
if (userPamWatcher.loaded)
|
||||
return "dankshell";
|
||||
return "login";
|
||||
}
|
||||
configDirectory: {
|
||||
if (dankshellConfigWatcher.loaded)
|
||||
return "/etc/pam.d";
|
||||
if (nixosMarker.loaded || root.runningFromNixStore)
|
||||
return "/etc/pam.d";
|
||||
if (userPamWatcher.loaded)
|
||||
return root.userPamDir;
|
||||
return Quickshell.shellDir + "/assets/pam";
|
||||
}
|
||||
|
||||
onMessageChanged: {
|
||||
// collected by position, not text, so it works in any locale
|
||||
@@ -415,6 +461,8 @@ Scope {
|
||||
root.attemptInfoMessages = [];
|
||||
root.lockoutAnnouncedThisAttempt = false;
|
||||
root.resetAuthFlows();
|
||||
if (!dankshellConfigWatcher.loaded && !nixosMarker.loaded && !userPamWatcher.loaded)
|
||||
ensureUserPamConfig();
|
||||
fprint.checkAvail();
|
||||
u2f.checkAvail();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user