Merge branch 'fingerprint-lid-aware' into quattro

This commit is contained in:
David Heinemeier Hansson
2026-07-23 13:52:31 -07:00
12 changed files with 360 additions and 50 deletions
+2 -11
View File
@@ -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
+11
View File
@@ -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
+3 -3
View File
@@ -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
}
+24 -6
View File
@@ -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 <<EOF
$polkit_gate
auth sufficient pam_fprintd.so
auth required pam_unix.so
+21
View File
@@ -0,0 +1,21 @@
echo "Gate polkit fingerprint auth behind the lid state (password when the lid is shut)"
# Existing fingerprint setups have pam_fprintd first in /etc/pam.d/polkit-1 but
# no lid gate, so a closed-lid pkexec would block on the unreachable reader for
# the full pam_fprintd timeout before offering the password. Insert a pam_exec
# gate before pam_fprintd that skips fingerprint while the lid is closed. New
# setups already get this from omarchy-setup-security-fingerprint.
#
# The gate points at the fixed /usr/bin path the omarchy package always
# provides, so it keeps working across package installs and dev-link (which
# overlays $OMARCHY_PATH but leaves /usr/bin untouched). pam_exec needs a
# literal absolute path — it does not expand env vars.
polkit_pam="/etc/pam.d/polkit-1"
gate="auth [success=1 default=ignore] pam_exec.so quiet /usr/bin/omarchy-hw-laptop-closed"
if [[ -f $polkit_pam ]] &&
grep -q 'pam_fprintd\.so' "$polkit_pam" &&
! grep -q 'omarchy-hw-laptop-closed' "$polkit_pam"; then
sudo sed -i "/pam_fprintd\.so/i $gate" "$polkit_pam"
fi
+25 -2
View File
@@ -24,6 +24,9 @@ Item {
readonly property int fieldFontSize: Math.round(Style.font.heading * 1.125)
readonly property int passwordDotFontSize: Math.round(Style.font.heading * 1.33)
readonly property int passwordDotLetterSpacing: Math.round(Style.font.heading * 0.19)
// Space to keep clear on each side of the field for the fingerprint icon
// (icon width plus a gap) so the centered dots never run under it.
readonly property real fingerprintReserve: fingerprintConfigured ? Math.round(fingerprintIcon.implicitWidth + 12) : 0
// Shrink the dots to fit once the password outgrows the field, so every
// keystroke stays visible — otherwise long passwords clip with no feedback.
readonly property real passwordDotScale: dotMetrics.advanceWidth > 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
}
}
}
}
+30 -7
View File
@@ -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
+46 -13
View File
@@ -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 {
+6 -3
View File
@@ -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
}
}
@@ -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
}
}
+71
View File
@@ -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"
+13 -5
View File
@@ -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