1
0
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:
bbedward
2026-07-09 15:06:49 -04:00
committed by dms-ci[bot]
parent fa26dbb544
commit c86676df9b
7 changed files with 474 additions and 56 deletions
+22
View File
@@ -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 {
+1 -1
View File
@@ -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)
+1 -1
View File
@@ -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
View File
@@ -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)
}
+212 -1
View File
@@ -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);
}
}
}
}
+50 -2
View File
@@ -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();
}