diff --git a/bin/omarchy-hw-clamshell b/bin/omarchy-hw-clamshell index 0792e6ec..238f6627 100755 --- a/bin/omarchy-hw-clamshell +++ b/bin/omarchy-hw-clamshell @@ -3,14 +3,5 @@ # omarchy:summary=Returns true when clamshell mode is active # omarchy:hidden=true -lid_closed=false - -for state in /proc/acpi/button/lid/*/state; do - [[ -r $state ]] || continue - if [[ $(< "$state") == *"closed"* ]]; then - lid_closed=true - break - fi -done - -[[ $lid_closed == "true" ]] && omarchy-hw-external-monitors +# Clamshell = lid closed while driving one or more external monitors. +omarchy-hw-laptop-closed && omarchy-hw-external-monitors diff --git a/bin/omarchy-hw-laptop-closed b/bin/omarchy-hw-laptop-closed new file mode 100755 index 00000000..8e03f965 --- /dev/null +++ b/bin/omarchy-hw-laptop-closed @@ -0,0 +1,11 @@ +#!/bin/bash + +# omarchy:summary=Returns true when the laptop lid is closed +# omarchy:hidden=true + +for state in /proc/acpi/button/lid/*/state; do + [[ -r $state ]] || continue + [[ $(< "$state") == *"closed"* ]] && exit 0 +done + +exit 1 diff --git a/bin/omarchy-remove-security-fingerprint b/bin/omarchy-remove-security-fingerprint index cdb54387..9bff3782 100755 --- a/bin/omarchy-remove-security-fingerprint +++ b/bin/omarchy-remove-security-fingerprint @@ -13,10 +13,10 @@ remove_pam_config() { sudo sed -i '/pam_fprintd\.so/d' /etc/pam.d/sudo fi - # Remove from polkit - if [[ -f /etc/pam.d/polkit-1 ]] && grep -Fq 'pam_fprintd.so' /etc/pam.d/polkit-1; then + # Remove from polkit (both the fingerprint module and its clamshell gate) + if [[ -f /etc/pam.d/polkit-1 ]] && grep -Eq 'pam_fprintd\.so|omarchy-hw-laptop-closed' /etc/pam.d/polkit-1; then echo "Removing fingerprint authentication from polkit..." - sudo sed -i '/pam_fprintd\.so/d' /etc/pam.d/polkit-1 + sudo sed -i -e '/pam_fprintd\.so/d' -e '/omarchy-hw-laptop-closed/d' /etc/pam.d/polkit-1 fi } diff --git a/bin/omarchy-setup-security-fingerprint b/bin/omarchy-setup-security-fingerprint index b4977c59..f56bf805 100755 --- a/bin/omarchy-setup-security-fingerprint +++ b/bin/omarchy-setup-security-fingerprint @@ -25,13 +25,31 @@ setup_pam_config() { sudo sed -i '1i auth sufficient pam_fprintd.so' /etc/pam.d/sudo fi - # Configure polkit - if [[ -f /etc/pam.d/polkit-1 ]] && ! grep -q 'pam_fprintd.so' /etc/pam.d/polkit-1; then - echo "Configuring polkit for fingerprint authentication..." - sudo sed -i '1i auth sufficient pam_fprintd.so' /etc/pam.d/polkit-1 - elif [[ ! -f /etc/pam.d/polkit-1 ]]; then + # Configure polkit. A clamshell gate runs before pam_fprintd: when the lid + # is shut the reader is unreachable, so it skips fingerprint (success=1) and + # PAM drops straight to the password prompt. Lid open → fingerprint, then + # password as the fallback. + # + # pam_exec needs a literal absolute path (no env expansion). Point at the + # fixed /usr/bin path the omarchy package always provides, so the gate keeps + # working across package installs and dev-link — the latter overlays + # $OMARCHY_PATH trees but leaves /usr/bin untouched. + local polkit_gate="auth [success=1 default=ignore] pam_exec.so quiet /usr/bin/omarchy-hw-laptop-closed" + + if [[ -f /etc/pam.d/polkit-1 ]]; then + if ! grep -q 'pam_fprintd.so' /etc/pam.d/polkit-1; then + echo "Configuring polkit for fingerprint authentication..." + sudo sed -i '1i auth sufficient pam_fprintd.so' /etc/pam.d/polkit-1 + fi + if ! grep -q 'omarchy-hw-laptop-closed' /etc/pam.d/polkit-1; then + echo "Adding clamshell gate to polkit..." + # Insert immediately before pam_fprintd so success=1 skips exactly it. + sudo sed -i "/pam_fprintd\.so/i $polkit_gate" /etc/pam.d/polkit-1 + fi + else echo "Creating polkit configuration with fingerprint authentication..." - sudo tee /etc/pam.d/polkit-1 >/dev/null <<'EOF' + sudo tee /etc/pam.d/polkit-1 >/dev/null < 0 @@ -130,9 +133,11 @@ Item { id: passwordInput anchors.fill: parent anchors.topMargin: inputField.borderTop - anchors.rightMargin: inputField.borderRight + 18 + // Reserve the fingerprint icon's width on both sides so the centered + // dots stay symmetric and never slide under the icon as they grow. + anchors.rightMargin: inputField.borderRight + 18 + root.fingerprintReserve anchors.bottomMargin: inputField.borderBottom - anchors.leftMargin: inputField.borderLeft + 18 + anchors.leftMargin: inputField.borderLeft + 18 + root.fingerprintReserve verticalAlignment: TextInput.AlignVCenter horizontalAlignment: TextInput.AlignHCenter activeFocusOnPress: true @@ -190,6 +195,24 @@ Item { verticalAlignment: Text.AlignVCenter elide: Text.ElideRight } + + // Fingerprint hint pinned inside the field's right edge when a sensor is + // enrolled, so the user knows they can touch to unlock instead of typing. + // Matches hyprlock, which draws its fingerprint icon in the same spot. + Text { + id: fingerprintIcon + objectName: "fingerprintIndicator" + anchors.right: parent.right + anchors.rightMargin: inputField.borderRight + 18 + anchors.verticalCenter: parent.verticalCenter + visible: root.fingerprintConfigured + text: "󰈷" + color: Color.lock.placeholder + font.family: Style.font.family + font.pixelSize: Math.round(root.fieldFontSize * 1.1) + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + } } } } diff --git a/shell/plugins/lock/Service.qml b/shell/plugins/lock/Service.qml index b15a0821..e2c9f20e 100644 --- a/shell/plugins/lock/Service.qml +++ b/shell/plugins/lock/Service.qml @@ -22,6 +22,9 @@ Item { property bool fingerprintAuthenticating: false property bool passwordPamConfigured: false property bool fingerprintConfigured: false + // Lid shut → the reader is unreachable, so hide the affordance and don't + // bother scanning; fall back to the password like the polkit dialog does. + property bool laptopClosed: false property bool previewVisible: false property string enteredPassword: "" property string pendingPassword: "" @@ -34,6 +37,8 @@ Item { readonly property bool locked: lockRequested || sessionLock.locked || sessionLock.secure readonly property bool authenticating: authenticatingPassword || fingerprintAuthenticating + // Fingerprint is only offered when a sensor is enrolled and the lid is open. + readonly property bool fingerprintAvailable: fingerprintConfigured && !laptopClosed function realScreenCount() { var screens = Quickshell.screens || [] @@ -82,6 +87,10 @@ Item { if (!fingerprintCheckProc.running) fingerprintCheckProc.running = true } + function refreshLaptopClosed() { + if (!laptopClosedProc.running) laptopClosedProc.running = true + } + function logEvent(event) { lastEvent = event lastEventAt = new Date().toISOString() @@ -115,6 +124,7 @@ Item { Qt.callLater(function() { root.refreshBackground() root.refreshFingerprintStatus() + root.refreshLaptopClosed() }) return true @@ -182,7 +192,7 @@ Item { } function startFingerprint() { - if (!lockRequested || !sessionLock.secure || !fingerprintConfigured) return + if (!lockRequested || !sessionLock.secure || !fingerprintAvailable) return if (fingerprintPam.active || fingerprintAuthenticating) return fingerprintAuthenticating = true @@ -197,7 +207,7 @@ Item { if (!lockRequested) return if (result === PamResult.Success) { finishUnlock() - } else if (fingerprintConfigured) { + } else if (fingerprintAvailable) { fingerprintRetryTimer.restart() } } @@ -245,7 +255,7 @@ Item { anchors.fill: parent backgroundPath: root.backgroundPath backgroundVersion: root.backgroundVersion - fingerprintConfigured: root.fingerprintConfigured + fingerprintConfigured: root.fingerprintAvailable authenticatingPassword: root.authenticatingPassword failureMessage: root.failureMessage failedAttempts: root.failedAttempts @@ -275,7 +285,7 @@ Item { anchors.fill: parent backgroundPath: root.backgroundPath backgroundVersion: root.backgroundVersion - fingerprintConfigured: root.fingerprintConfigured + fingerprintConfigured: root.fingerprintAvailable authenticatingPassword: false failureMessage: "" failedAttempts: 0 @@ -324,7 +334,7 @@ Item { onError: function(error) { root.fingerprintAuthenticating = false - if (root.lockRequested && root.fingerprintConfigured) fingerprintRetryTimer.restart() + if (root.lockRequested && root.fingerprintAvailable) fingerprintRetryTimer.restart() } } @@ -356,8 +366,19 @@ Item { stdout: StdioCollector { id: fingerprintCheckStdout; waitForEnd: true } onExited: { root.fingerprintConfigured = String(fingerprintCheckStdout.text || "").trim() === "yes" - if (root.lockRequested && root.fingerprintConfigured) root.startFingerprint() - else if (!root.fingerprintConfigured && fingerprintPam.active) fingerprintPam.abort() + if (root.lockRequested && root.fingerprintAvailable) root.startFingerprint() + else if (!root.fingerprintAvailable && fingerprintPam.active) fingerprintPam.abort() + } + } + + Process { + id: laptopClosedProc + command: ["bash", "-c", "omarchy-hw-laptop-closed && echo closed || echo open"] + stdout: StdioCollector { id: laptopClosedStdout; waitForEnd: true } + onExited: { + root.laptopClosed = String(laptopClosedStdout.text || "").trim() === "closed" + if (root.lockRequested && root.fingerprintAvailable) root.startFingerprint() + else if (!root.fingerprintAvailable && fingerprintPam.active) fingerprintPam.abort() } } @@ -425,6 +446,7 @@ Item { Component.onCompleted: { refreshBackground() refreshFingerprintStatus() + refreshLaptopClosed() } IpcHandler { @@ -450,6 +472,7 @@ Item { realScreens: root.realScreenCount(), passwordPam: root.passwordPamConfigured, fingerprint: root.fingerprintConfigured, + laptopClosed: root.laptopClosed, authenticating: root.authenticating, lastEvent: root.lastEvent, lastEventAt: root.lastEventAt diff --git a/shell/plugins/polkit/PolkitAgent.qml b/shell/plugins/polkit/PolkitAgent.qml index a27ab019..8ce95973 100644 --- a/shell/plugins/polkit/PolkitAgent.qml +++ b/shell/plugins/polkit/PolkitAgent.qml @@ -32,24 +32,34 @@ Item { property bool responseVisible: false property bool failed: false property bool errorFlash: false - property bool fingerprintFirst: false + // pam_fprintd appears in the polkit PAM stack (a sensor is enrolled). + property bool fingerprintConfigured: false + // Lid shut right now — the reader is physically unreachable, so we fall back + // to the password even when a sensor is enrolled. Refreshed per request. + property bool laptopClosed: false property int shakeOffset: 0 readonly property bool dialogVisible: polkitAgent.isActive || closing - readonly property bool fingerprintWaiting: dialogVisible && !responseRequired && !submitted && (fingerprintFirst || promptLooksFingerprint(currentPrompt + " " + currentSupplementary)) - readonly property int cardWidth: Math.min(Style.space(312), Math.max(Style.space(260), panel.width - Style.gapsOut * 2)) + // We show one method at a time. Fingerprint owns the dialog while PAM is + // waiting on the reader (lid open, sensor enrolled); the moment PAM asks for + // a password — including immediately when the lid is shut and the clamshell + // gate skips pam_fprintd — we switch to the password field instead. + readonly property bool fingerprintMode: fingerprintConfigured && !laptopClosed && dialogVisible && !responseRequired && !submitted && !errorFlash readonly property int cardHeight: panel.height > 0 ? Math.min(fieldHeight + contentMargin * 2, panel.height - Style.gapsOut * 2) : fieldHeight + contentMargin * 2 - - function promptLooksFingerprint(text) { - return PolkitModel.promptLooksFingerprint(text) - } + // Password mode is a wide field; fingerprint mode collapses to a square that + // just frames the centered sensor icon. + readonly property int cardWidth: fingerprintMode ? cardHeight : Math.min(Style.space(312), Math.max(Style.space(260), panel.width - Style.gapsOut * 2)) function authorizationLabel(message) { return PolkitModel.authorizationLabel(message) } function loadPamConfig(raw) { - fingerprintFirst = PolkitModel.fingerprintFirstFromPamConfig(raw) + fingerprintConfigured = PolkitModel.fingerprintConfiguredFromPamConfig(raw) + } + + function refreshLidState() { + if (!laptopClosedProc.running) laptopClosedProc.running = true } function resetSnapshot() { @@ -83,13 +93,16 @@ Item { closing = false submitted = false passwordInput.text = "" + refreshLidState() syncFromFlow() Qt.callLater(refocus) } function refocus() { if (!dialogVisible) return - if (fingerprintWaiting) keyCatcher.forceActiveFocus() + // In fingerprint mode there is no field to type into — park focus on the + // key catcher so Escape still cancels; otherwise focus the password field. + if (fingerprintMode) keyCatcher.forceActiveFocus() else passwordInput.forceActiveFocus() } @@ -149,10 +162,17 @@ Item { watchChanges: true printErrors: false onLoaded: root.loadPamConfig(text()) - onLoadFailed: root.fingerprintFirst = false + onLoadFailed: root.fingerprintConfigured = false onFileChanged: reload() } + Process { + id: laptopClosedProc + command: ["bash", "-c", "omarchy-hw-laptop-closed && echo closed || echo open"] + stdout: StdioCollector { id: laptopClosedOut; waitForEnd: true } + onExited: root.laptopClosed = String(laptopClosedOut.text || "").trim() === "closed" + } + PolkitAgent { id: polkitAgent path: "/org/omarchy/PolkitAgent" @@ -248,8 +268,22 @@ Item { } } + // Fingerprint mode shows just the sensor icon, centered and alone \u2014 no + // padlock, no field, no prompt text. + OpticalGlyph { + anchors.centerIn: parent + width: Math.round(root.fieldHeight * 0.7) + height: width + visible: root.fingerprintMode + text: "\udb80\ude37" + fontFamily: root.fontFamily + fontSize: Math.round(root.fieldHeight * 0.7) + color: root.errorFlash ? Color.polkit.textError : root.accent + } + Row { id: cardRow + visible: !root.fingerprintMode anchors.fill: parent anchors.topMargin: card.contentTopInset anchors.rightMargin: card.contentRightInset @@ -287,8 +321,7 @@ Item { color: root.errorFlash ? Color.polkit.textError : root.foreground cursorVisible: activeFocus && !root.submitted && !root.errorFlash readOnly: root.submitted || root.errorFlash - enabled: root.dialogVisible && !root.fingerprintWaiting - visible: !root.fingerprintWaiting + enabled: root.dialogVisible onAccepted: root.submitResponse() Keys.onPressed: function(event) { if (event.key === Qt.Key_Escape) { @@ -308,7 +341,7 @@ Item { font.family: root.fontFamily font.pixelSize: Style.font.iconLarge elide: Text.ElideRight - visible: passwordInput.visible && passwordInput.text.length === 0 + visible: passwordInput.text.length === 0 } Rectangle { diff --git a/shell/plugins/polkit/PolkitModel.js b/shell/plugins/polkit/PolkitModel.js index ca2ea142..7ce71c78 100644 --- a/shell/plugins/polkit/PolkitModel.js +++ b/shell/plugins/polkit/PolkitModel.js @@ -3,13 +3,16 @@ function promptLooksFingerprint(text) { return s.indexOf("finger") !== -1 || s.indexOf("fprint") !== -1 || s.indexOf("swipe") !== -1 } -function fingerprintFirstFromPamConfig(raw) { +function fingerprintConfiguredFromPamConfig(raw) { + // Fingerprint is available whenever pam_fprintd appears anywhere in the auth + // stack — it need not be the first module. A clamshell gate (pam_exec) may + // legitimately precede it to skip fingerprint while the lid is closed. var lines = String(raw || "").split("\n") for (var i = 0; i < lines.length; i++) { var line = lines[i].replace(/^\s+|\s+$/g, "") if (!line || line.charAt(0) === "#") continue if (!line.match(/^auth\s+/)) continue - return line.indexOf("pam_fprintd.so") !== -1 + if (line.indexOf("pam_fprintd.so") !== -1) return true } return false } @@ -23,7 +26,7 @@ function authorizationLabel(message) { if (typeof module !== "undefined") { module.exports = { promptLooksFingerprint: promptLooksFingerprint, - fingerprintFirstFromPamConfig: fingerprintFirstFromPamConfig, + fingerprintConfiguredFromPamConfig: fingerprintConfiguredFromPamConfig, authorizationLabel: authorizationLabel } } diff --git a/test/shell.d/fixtures/lock-fingerprint-indicator/shell.qml b/test/shell.d/fixtures/lock-fingerprint-indicator/shell.qml new file mode 100644 index 00000000..1a5f92fe --- /dev/null +++ b/test/shell.d/fixtures/lock-fingerprint-indicator/shell.qml @@ -0,0 +1,108 @@ +import QtQuick +import Quickshell +import qs.Commons + +ShellRoot { + id: root + + readonly property string resultPath: Quickshell.env("OMARCHY_QML_TEST_RESULT") + readonly property string rootPath: Quickshell.env("OMARCHY_PATH") + property var failures: [] + + function fail(message) { + failures.push(String(message)) + } + + function assertTrue(condition, message) { + if (!condition) fail(message) + } + + function shellQuote(value) { + return "'" + String(value).replace(/'/g, "'\\''") + "'" + } + + function writeResult() { + var payload = JSON.stringify({ + ok: failures.length === 0, + failures: failures + }) + + if (resultPath) { + Quickshell.execDetached(["bash", "-lc", "printf '%s' " + shellQuote(payload) + " > " + shellQuote(resultPath)]) + } + } + + Item { id: host; width: 800; height: 600 } + + TextMetrics { + id: probe + font.family: Style.font.family + } + + Timer { + interval: 1 + running: true + repeat: false + onTriggered: { + try { + var component = Qt.createComponent("file://" + root.rootPath + "/shell/plugins/lock/LockView.qml", Component.PreferSynchronous) + if (component.status !== Component.Ready) { + root.fail("LockView failed to load: " + component.errorString()) + return + } + + var view = component.createObject(host, { width: 800, height: 600, loadBackground: false }) + if (!view) { + root.fail("LockView failed to instantiate: " + component.errorString()) + return + } + + var indicator = view.children ? findByObjectName(view, "fingerprintIndicator") : null + root.assertTrue(indicator !== null, "fingerprint indicator exists in the lock view") + + if (indicator) { + view.fingerprintConfigured = false + root.assertTrue(!indicator.visible, "fingerprint indicator is hidden when no sensor is configured") + + view.fingerprintConfigured = true + root.assertTrue(indicator.visible, "fingerprint indicator is shown when a sensor is configured") + + // The field reserves space for the icon so a long password can never + // slide underneath it. The reserve must exceed the icon's own width + // (leaving a gap), and the shrunk dots must fit the reserved-clear + // area even at extreme lengths. + root.assertTrue(view.fingerprintReserve > indicator.width, + "reserved space exceeds the icon width, got reserve " + view.fingerprintReserve + " vs icon " + indicator.width) + + view.passwordText = "x".repeat(80) + var clearWidth = view.fieldWidth - 2 * view.fingerprintReserve + probe.font.pixelSize = Math.max(1, Math.floor(view.passwordDotFontSize * view.passwordDotScale)) + probe.font.letterSpacing = view.passwordDotLetterSpacing * view.passwordDotScale + probe.text = "●".repeat(80) + root.assertTrue(probe.advanceWidth <= clearWidth, + "80 dots stay clear of the fingerprint icon, need " + probe.advanceWidth + "px of " + clearWidth) + + view.fingerprintConfigured = false + root.assertTrue(view.fingerprintReserve === 0, "no space is reserved when no sensor is configured") + } + + view.destroy() + } catch (error) { + root.fail("lock fingerprint indicator fixture threw: " + error) + } finally { + root.writeResult() + } + } + } + + function findByObjectName(node, name) { + if (!node) return null + if (node.objectName === name) return node + var kids = node.children || [] + for (var i = 0; i < kids.length; i++) { + var found = findByObjectName(kids[i], name) + if (found) return found + } + return null + } +} diff --git a/test/shell.d/lock-fingerprint-indicator-test.sh b/test/shell.d/lock-fingerprint-indicator-test.sh new file mode 100755 index 00000000..70ad7d54 --- /dev/null +++ b/test/shell.d/lock-fingerprint-indicator-test.sh @@ -0,0 +1,71 @@ +#!/bin/bash + +set -euo pipefail + +source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh" + +TMPDIR="" +QS_PID="" + +cleanup() { + if [[ -n $QS_PID ]] && kill -0 "$QS_PID" 2>/dev/null; then + kill "$QS_PID" 2>/dev/null || true + wait "$QS_PID" 2>/dev/null || true + fi + [[ -n $TMPDIR && -d $TMPDIR ]] && rm -rf "$TMPDIR" +} +trap cleanup EXIT + +if [[ -z ${WAYLAND_DISPLAY:-} ]]; then + pass "no Wayland compositor; skipping lock fingerprint indicator test" + exit 0 +fi + +if ! command -v quickshell >/dev/null 2>&1; then + pass "quickshell not installed; skipping lock fingerprint indicator test" + exit 0 +fi + +require_command jq + +TMPDIR=$(mktemp -d) +result="$TMPDIR/result.json" +log="$TMPDIR/quickshell.log" +config_dir="$TMPDIR/lock-fingerprint-indicator" +mkdir -p "$config_dir" "$TMPDIR/home" +cp "$SHELL_TEST_DIR/fixtures/lock-fingerprint-indicator/shell.qml" "$config_dir/shell.qml" +ln -s "$ROOT/shell/Ui" "$config_dir/Ui" +ln -s "$ROOT/shell/Commons" "$config_dir/Commons" + +OMARCHY_PATH="$ROOT" \ +OMARCHY_QML_TEST_RESULT="$result" \ +HOME="$TMPDIR/home" \ +QML2_IMPORT_PATH="$ROOT/shell${QML2_IMPORT_PATH:+:$QML2_IMPORT_PATH}" \ +QML_IMPORT_PATH="$ROOT/shell${QML_IMPORT_PATH:+:$QML_IMPORT_PATH}" \ +PATH="$ROOT/bin:$PATH" \ + quickshell -p "$config_dir" --no-color >"$log" 2>&1 & +QS_PID=$! + +for _ in {1..80}; do + [[ -s $result ]] && break + if ! kill -0 "$QS_PID" 2>/dev/null; then + sed -n '1,220p' "$log" >&2 + fail "lock fingerprint indicator quickshell exited before writing result" + fi + sleep 0.1 +done + +[[ -s $result ]] || { + sed -n '1,220p' "$log" >&2 + fail "lock fingerprint indicator test timed out" +} + +if ! jq -e '.ok == true' "$result" >/dev/null; then + printf 'Lock fingerprint indicator result:\n' >&2 + jq . "$result" >&2 + printf 'Lock fingerprint indicator log:\n' >&2 + sed -n '1,220p' "$log" >&2 + fail "fingerprint indicator tracks the configured sensor" +fi + +pass "fingerprint indicator tracks the configured sensor" diff --git a/test/shell.d/polkit-test.sh b/test/shell.d/polkit-test.sh index 2683e2b5..5283589d 100644 --- a/test/shell.d/polkit-test.sh +++ b/test/shell.d/polkit-test.sh @@ -23,19 +23,27 @@ assertEqual( ) assert( - polkit.fingerprintFirstFromPamConfig(` + polkit.fingerprintConfiguredFromPamConfig(` # comment auth sufficient pam_fprintd.so auth include system-auth `), - 'polkit detects fingerprint-first PAM config' + 'polkit detects fingerprint in a PAM config' ) assert( - !polkit.fingerprintFirstFromPamConfig(` + polkit.fingerprintConfiguredFromPamConfig(` +auth [success=1 default=ignore] pam_exec.so quiet /usr/bin/omarchy-hw-laptop-closed +auth sufficient pam_fprintd.so +auth required pam_unix.so +`), + 'polkit detects fingerprint even behind a clamshell gate' +) +assert( + !polkit.fingerprintConfiguredFromPamConfig(` account include system-auth auth include system-auth -auth sufficient pam_fprintd.so +auth required pam_unix.so `), - 'polkit detects password-first PAM config' + 'polkit reports no fingerprint when pam_fprintd is absent' ) JS