502 lines
22 KiB
Bash
Executable File
502 lines
22 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
|
|
# Fingerprint login, which is two systems that must be kept honest with each
|
|
# other: fprintd holds the enrolled prints, authselect decides whether PAM
|
|
# asks the reader. A print enrolled while with-fingerprint is off does
|
|
# nothing, and that silence -- "I enrolled a finger and nothing happened" --
|
|
# is the failure this card exists to name.
|
|
#
|
|
# Two things changed, and each inverts a pin this file used to hold.
|
|
#
|
|
# Enrollment is Panama's now. It used to open GNOME's Users panel, which was
|
|
# honest while fprintd's guided capture was the only thing there; the helper
|
|
# drives EnrollStart itself and streams a touch counter, so the handoff is the
|
|
# thing that would now be wrong. The needle is inverted rather than deleted:
|
|
# the page must NOT open that panel.
|
|
#
|
|
# And the card is no longer hidden by the absence of a reader. `pamEnabled`
|
|
# used to be reported as false whenever fprintd had nothing to say, so the one
|
|
# state a person genuinely has to act on -- the PAM feature switched on, no
|
|
# reader attached, every unlock now waiting on a device that is not there --
|
|
# rendered as no card at all. The two facts are read independently, and the
|
|
# card appears when either is true.
|
|
#
|
|
# SAFETY. Enrollment claims a real device and authselect rewrites PAM, so
|
|
# nothing here may reach either. `env -i` with a stub directory first on PATH,
|
|
# both bus addresses pointed at sockets that do not exist, PANAMA_PATH pointed
|
|
# at a recording panama-sudo that runs nothing, and the enrollment stream
|
|
# replayed from PANAMA_FINGERPRINT_FIXTURE -- a canned fprintd whose calls land
|
|
# in a log file instead of on the bus. The PATH claim is asserted before the
|
|
# helper is run at all.
|
|
|
|
set -uo pipefail
|
|
|
|
repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
|
helper="$repo_dir/config/dot/quickshell/scripts/panama-fingerprint"
|
|
service="$repo_dir/config/dot/quickshell/services/Fingerprint.qml"
|
|
settings_dir="$repo_dir/config/dot/quickshell/modules/settings"
|
|
page="$settings_dir/UsersPage.qml"
|
|
|
|
fail() {
|
|
printf 'fingerprint contract: %s\n' "$1" >&2
|
|
exit 1
|
|
}
|
|
|
|
for path in "$helper" "$service" "$page"; do
|
|
[[ -r "$path" ]] || fail "missing $path"
|
|
done
|
|
[[ -x "$helper" ]] || fail 'panama-fingerprint is not executable'
|
|
|
|
file_calling() {
|
|
grep -rl --include='*.qml' -F "$1" "$settings_dir" | head -1
|
|
}
|
|
|
|
# ── Enrollment stays here ────────────────────────────────────────────────────
|
|
#
|
|
# The inverted needle. gnome-handoff-contract lost its UsersPage exception at
|
|
# the same time; this is the other half, said from the page's side.
|
|
grep -rn --include='*.qml' -E 'openGnomePanel\("(system", *"users|users")' "$settings_dir" \
|
|
&& fail 'a settings page still sends fingerprint enrollment to GNOME Users, which Panama owns'
|
|
|
|
enroll_page="$(file_calling 'Fingerprint.startEnroll(')"
|
|
[[ -n "$enroll_page" ]] || fail 'nothing starts an enrollment'
|
|
grep -q 'Fingerprint.cancelEnroll()' "$enroll_page" \
|
|
|| fail 'an enrollment in progress cannot be cancelled, so the reader stays claimed'
|
|
grep -qE 'Fingerprint\.(removeFinger|removeAll)\(' "$enroll_page" \
|
|
|| fail 'an enrolled finger cannot be removed'
|
|
|
|
# ── The card is visible in the state that needs acting on ────────────────────
|
|
#
|
|
# `readerPresent || unlockFeatureEnabled`. Either alone hides the stuck state:
|
|
# the feature on with no reader is invisible under the first, and a machine
|
|
# with a reader and nothing configured is invisible under the second.
|
|
python3 - "$page" "$service" <<'PY' || fail 'the fingerprint card is not visible in the state that needs acting on'
|
|
import re
|
|
import sys
|
|
|
|
page = open(sys.argv[1], encoding="utf-8").read()
|
|
service = open(sys.argv[2], encoding="utf-8").read()
|
|
|
|
DISJUNCTION = re.compile(
|
|
r"readerPresent\s*\|\|\s*(root\.)?unlockFeatureEnabled"
|
|
r"|unlockFeatureEnabled\s*\|\|\s*(root\.)?readerPresent")
|
|
|
|
visible = [line for line in page.splitlines()
|
|
if "visible:" in line and "Fingerprint." in line]
|
|
if not visible:
|
|
print("nothing decides whether the fingerprint card is shown", file=sys.stderr)
|
|
raise SystemExit(1)
|
|
card = visible[0]
|
|
|
|
# Either the page spells it out, or it binds to a service property that does.
|
|
# The second is better -- one expression, so the card and the state it
|
|
# describes cannot disagree -- so the indirection is followed rather than
|
|
# forbidden.
|
|
if DISJUNCTION.search(card):
|
|
raise SystemExit(0)
|
|
named = re.search(r"visible:\s*Fingerprint\.(\w+)", card)
|
|
if named:
|
|
definition = re.search(rf"property bool {named.group(1)}:([^\n]*)", service)
|
|
if definition and DISJUNCTION.search(definition.group(1)):
|
|
raise SystemExit(0)
|
|
print(f"visible: Fingerprint.{named.group(1)}, which is "
|
|
f"{definition.group(1).strip() if definition else 'not defined'}", file=sys.stderr)
|
|
raise SystemExit(1)
|
|
print(card.strip(), file=sys.stderr)
|
|
raise SystemExit(1)
|
|
PY
|
|
|
|
# And it says why it is there. A card that appears on a machine with no reader
|
|
# and offers nothing but a switch reads as a bug in the card.
|
|
stuck_page="$(file_calling 'Fingerprint.unlockFeatureEnabled')"
|
|
[[ -n "$stuck_page" ]] || fail 'no page reads the unlock feature independently of the reader'
|
|
grep -qiE 'no reader' "$stuck_page" \
|
|
|| fail 'the stuck state does not say there is no reader'
|
|
grep -qE 'Turn off' "$stuck_page" \
|
|
|| fail 'the stuck state offers no way out of it'
|
|
|
|
grep -Fq 'onToggled: value => Fingerprint.setUnlockEnabled(value)' "$page" \
|
|
|| fail 'the unlock switch does not drive authselect'
|
|
grep -Fq 'Fingerprint.refresh()' "$page" \
|
|
|| fail 'the page never reads the fingerprint state'
|
|
|
|
# ── A finger is named in exactly one place ───────────────────────────────────
|
|
#
|
|
# fprintd's vocabulary is "right-index-finger". What that READS as is a
|
|
# presentation decision, and two copies of it disagree the first time one is
|
|
# edited -- so the service presents, the helper only validates, and no page
|
|
# carries a finger name of its own.
|
|
grep -q 'function fingerLabel' "$service" \
|
|
|| fail 'finger names have no single place to be presented from'
|
|
python3 - "$service" "$settings_dir" "$repo_dir/config/dot/quickshell/services" <<'PY' \
|
|
|| fail 'fprintd vocabulary is spelled out in more than one place'
|
|
import pathlib
|
|
import re
|
|
import sys
|
|
|
|
FINGER = re.compile(r'"(?:left|right)-(?:thumb|(?:index|middle|ring|little)-finger)"')
|
|
owner = pathlib.Path(sys.argv[1]).resolve()
|
|
|
|
# A single id elsewhere is a default choice -- "start on the right index
|
|
# finger" -- and that is allowed. A second one is a copy of the list, and the
|
|
# copy is what falls behind: the one in Fingerprint.qml grows a finger, the
|
|
# other does not, and half the interface offers nine.
|
|
for directory in sys.argv[2:]:
|
|
for path in sorted(pathlib.Path(directory).rglob("*.qml")):
|
|
if path.resolve() == owner:
|
|
continue
|
|
found = set(FINGER.findall(path.read_text(encoding="utf-8")))
|
|
if len(found) > 1:
|
|
print(f"{path}: {sorted(found)}", file=sys.stderr)
|
|
raise SystemExit(1)
|
|
raise SystemExit(0)
|
|
PY
|
|
|
|
# The service carries the enrollment state the panel counts with.
|
|
for property in unlockFeatureEnabled enrolling enrollStage enrollTotal; do
|
|
grep -qE "property (bool|int|var|string) $property" "$service" \
|
|
|| fail "the service does not expose $property, so the page cannot show the enrollment"
|
|
done
|
|
|
|
# ── The privileged change still says why ─────────────────────────────────────
|
|
grep -Fq 'authselect' "$helper" && grep -Fq 'with-fingerprint' "$helper" \
|
|
|| fail 'the helper does not manage the authselect feature'
|
|
grep -Fq -- '--reason' "$helper" \
|
|
|| fail 'the privileged change carries no stated reason'
|
|
|
|
# ── The helper cannot walk past the stubs ────────────────────────────────────
|
|
absolute="$(grep -nE '"/(usr/)?s?bin/[a-z-]+"' "$helper")"
|
|
[[ -z "$absolute" ]] \
|
|
|| fail "the helper names a binary by absolute path, so PATH stubs cannot contain it: $absolute"
|
|
|
|
command -v jq >/dev/null 2>&1 || { printf 'fingerprint contract: SKIP (no jq)\n'; exit 0; }
|
|
command -v python3 >/dev/null 2>&1 || { printf 'fingerprint contract: SKIP (no python3)\n'; exit 0; }
|
|
|
|
# ── The fake machine ─────────────────────────────────────────────────────────
|
|
|
|
work="$(mktemp -d /tmp/panama-fingerprint-contract.XXXXXX)"
|
|
stub_dir="$work/bin"
|
|
state_dir="$work/state"
|
|
home_dir="$work/home"
|
|
run_dir="$work/run"
|
|
panama_dir="$work/panama/bin"
|
|
mkdir -p "$stub_dir" "$state_dir" "$home_dir" "$run_dir" "$panama_dir"
|
|
trap 'rm -rf "$work"' EXIT
|
|
|
|
cat >"$stub_dir/fprintd-list" <<STUB
|
|
#!/usr/bin/env bash
|
|
printf 'fprintd-list %s\n' "\$*" >>"$state_dir/argv"
|
|
if [[ -e "$state_dir/no-reader" ]]; then
|
|
echo 'Impossible to enumerate devices: No devices available' >&2
|
|
exit 1
|
|
fi
|
|
cat <<'OUT'
|
|
found 1 devices
|
|
Device at /net/reactivated/Fprint/Device/0
|
|
Using device /net/reactivated/Fprint/Device/0
|
|
Fingerprints for user gib on Goodix MOC Fingerprint Sensor (press):
|
|
- #0: right-index-finger
|
|
- #1: left-thumb
|
|
OUT
|
|
STUB
|
|
|
|
cat >"$stub_dir/authselect" <<STUB
|
|
#!/usr/bin/env bash
|
|
printf 'authselect %s\n' "\$*" >>"$state_dir/argv"
|
|
if [[ "\$1" == "current" ]]; then
|
|
echo 'Profile ID: local'
|
|
[[ -e "$state_dir/feature-on" ]] && echo '- with-fingerprint'
|
|
exit 0
|
|
fi
|
|
printf 'fingerprint contract: authselect was asked to change PAM\n' >&2
|
|
exit 1
|
|
STUB
|
|
|
|
# Records the escalation and runs NOTHING. The reason is the point: this page's
|
|
# prompt has to say what it is about to do, and the only way to see that is to
|
|
# catch the arguments before they become an authselect invocation.
|
|
cat >"$panama_dir/panama-sudo" <<STUB
|
|
#!/usr/bin/env bash
|
|
printf 'panama-sudo %s\n' "\$*" >>"$state_dir/argv"
|
|
exit 0
|
|
STUB
|
|
|
|
# Anything else that could reach the machine is closed rather than left open.
|
|
for blocked in sudo pkexec gdbus busctl dbus-send fprintd-enroll fprintd-delete; do
|
|
cat >"$stub_dir/$blocked" <<STUB
|
|
#!/usr/bin/env bash
|
|
printf '$blocked %s\n' "\$*" >>"$state_dir/argv"
|
|
printf 'fingerprint contract: the helper reached for $blocked\n' >&2
|
|
exit 1
|
|
STUB
|
|
done
|
|
chmod +x "$stub_dir"/* "$panama_dir/panama-sudo"
|
|
|
|
runh() {
|
|
env -i \
|
|
PATH="$stub_dir:/usr/bin:/bin" \
|
|
HOME="$home_dir" \
|
|
USER=gib \
|
|
XDG_RUNTIME_DIR="$run_dir" \
|
|
PANAMA_PATH="$work/panama" \
|
|
DBUS_SESSION_BUS_ADDRESS="unix:path=$run_dir/absent-session-bus" \
|
|
DBUS_SYSTEM_BUS_ADDRESS="unix:path=$run_dir/absent-system-bus" \
|
|
LANG=C LC_ALL=C \
|
|
"$@"
|
|
}
|
|
|
|
run() { runh "$helper" "$@"; }
|
|
|
|
# Enrollment and removal replay a canned fprintd; nothing they do reaches a bus.
|
|
fixture="$work/fprintd.json"
|
|
log="$work/fprintd.log"
|
|
runf() {
|
|
runh env PANAMA_FINGERPRINT_FIXTURE="$fixture" PANAMA_FINGERPRINT_LOG="$log" \
|
|
"$helper" "$@"
|
|
}
|
|
|
|
# The safety claim, verified rather than assumed.
|
|
for binary in fprintd-list authselect sudo pkexec; do
|
|
resolved="$(env -i PATH="$stub_dir:/usr/bin:/bin" bash -c "command -v $binary" || true)"
|
|
[[ "$resolved" == "$stub_dir/$binary" ]] \
|
|
|| fail "$binary resolves to '$resolved', not the stub; refusing to run against the real one"
|
|
done
|
|
|
|
# ── status: two facts, read independently ────────────────────────────────────
|
|
|
|
status="$(run status)"
|
|
jq -e '.reader and .readerName == "Goodix MOC Fingerprint Sensor"' <<<"$status" >/dev/null \
|
|
|| fail "the reader name did not parse: $status"
|
|
jq -e '.enrolled == ["right-index-finger", "left-thumb"]' <<<"$status" >/dev/null \
|
|
|| fail "enrolled fingers did not parse: $status"
|
|
jq -e '.unlockFeatureEnabled == false and .error == ""' <<<"$status" >/dev/null \
|
|
|| fail "authselect state misread as enabled: $status"
|
|
# The old name is still answered, because the service and this file both read
|
|
# it and a silent rename would report "off" forever.
|
|
jq -e '.pamEnabled == .unlockFeatureEnabled' <<<"$status" >/dev/null \
|
|
|| fail "the two names for the PAM feature disagree: $status"
|
|
|
|
touch "$state_dir/feature-on"
|
|
jq -e '.unlockFeatureEnabled == true' <<<"$(run status)" >/dev/null \
|
|
|| fail 'with-fingerprint enabled was not detected'
|
|
|
|
# THE regression. No reader, feature on: the state that used to render as no
|
|
# card at all, because the feature was only read when fprintd answered.
|
|
touch "$state_dir/no-reader"
|
|
stuck="$(run status)"
|
|
jq -e '.reader == false and .error == ""' <<<"$stuck" >/dev/null \
|
|
|| fail "a readerless machine was reported as a problem: $stuck"
|
|
jq -e '.unlockFeatureEnabled == true' <<<"$stuck" >/dev/null \
|
|
|| fail "the PAM feature was not read on a machine with no reader, which is the one state somebody has to be able to turn off: $stuck"
|
|
jq -e '.enrolled == []' <<<"$stuck" >/dev/null \
|
|
|| fail "a machine with no reader reported enrolled fingers: $stuck"
|
|
rm -f "$state_dir/no-reader" "$state_dir/feature-on"
|
|
|
|
# ── The privileged change goes through, with the right feature and a reason ──
|
|
|
|
: >"$state_dir/argv"
|
|
run set-unlock on >/dev/null
|
|
grep -Fq 'authselect enable-feature with-fingerprint' "$state_dir/argv" \
|
|
|| fail "set-unlock on did not enable the authselect feature: $(cat "$state_dir/argv")"
|
|
grep -qE 'panama-sudo --reason .*fingerprint' "$state_dir/argv" \
|
|
|| fail 'the prompt does not say what it is about to do'
|
|
grep -Eq '^(sudo|pkexec) ' "$state_dir/argv" \
|
|
&& fail 'the privileged change escalated without a stated reason'
|
|
|
|
: >"$state_dir/argv"
|
|
run set-unlock off >/dev/null
|
|
grep -Fq 'authselect disable-feature with-fingerprint' "$state_dir/argv" \
|
|
|| fail 'set-unlock off did not disable the authselect feature'
|
|
run set-unlock sideways >/dev/null 2>&1 \
|
|
&& fail 'set-unlock accepted something that is neither on nor off'
|
|
|
|
# ── enroll: a stream, one line per touch ─────────────────────────────────────
|
|
#
|
|
# The whole point of doing this natively: the page counts touches while they
|
|
# happen. So the stream is read as a stream -- every line valid JSON on its own,
|
|
# the counter monotonic and bounded, and a terminal line that says which way it
|
|
# ended.
|
|
|
|
printf '{"enrollStages":5,"results":["enroll-stage-passed","enroll-stage-passed","enroll-retry-scan-too-short","enroll-stage-passed","enroll-stage-passed","enroll-stage-passed","enroll-completed"],"error":""}\n' \
|
|
>"$fixture"
|
|
: >"$log"
|
|
|
|
runf enroll right-index-finger >"$work/stream.jsonl" \
|
|
|| fail "a completed enrollment exited nonzero: $(cat "$work/stream.jsonl")"
|
|
python3 - "$work/stream.jsonl" <<'PY' || fail 'the enrollment stream is not a usable progress stream'
|
|
import json
|
|
import sys
|
|
|
|
lines = [line for line in open(sys.argv[1], encoding="utf-8").read().splitlines() if line.strip()]
|
|
if len(lines) < 3:
|
|
print(f"only {len(lines)} line(s) of progress", file=sys.stderr)
|
|
raise SystemExit(1)
|
|
|
|
events = []
|
|
for line in lines:
|
|
try:
|
|
events.append(json.loads(line))
|
|
except ValueError:
|
|
print(f"not a JSON object on its own line: {line}", file=sys.stderr)
|
|
raise SystemExit(1)
|
|
|
|
for event in events:
|
|
for field in ("ok", "stage", "done", "total", "result"):
|
|
if field not in event:
|
|
print(f"a progress line is missing {field}: {event}", file=sys.stderr)
|
|
raise SystemExit(1)
|
|
if event["total"] != 5:
|
|
print(f"the touch count changed mid-enrollment: {event}", file=sys.stderr)
|
|
raise SystemExit(1)
|
|
if not 0 <= event["done"] <= event["total"]:
|
|
print(f"the counter left its own range: {event}", file=sys.stderr)
|
|
raise SystemExit(1)
|
|
|
|
counts = [event["done"] for event in events]
|
|
if counts != sorted(counts):
|
|
print(f"the counter went backwards: {counts}", file=sys.stderr)
|
|
raise SystemExit(1)
|
|
|
|
# A retry is a touch that did NOT count. If it advanced the counter, the panel
|
|
# would promise a finish that never arrives.
|
|
retries = [event for event in events if "retry" in str(event["result"])]
|
|
if not retries:
|
|
print("the failed touch produced no line at all, so the panel says nothing",
|
|
file=sys.stderr)
|
|
raise SystemExit(1)
|
|
if retries[0]["done"] != 2:
|
|
print(f"a retried touch was counted as progress: {retries[0]}", file=sys.stderr)
|
|
raise SystemExit(1)
|
|
|
|
last = events[-1]
|
|
if not last["ok"] or last["done"] != last["total"]:
|
|
print(f"a completed enrollment did not finish full: {last}", file=sys.stderr)
|
|
raise SystemExit(1)
|
|
raise SystemExit(0)
|
|
PY
|
|
|
|
# The device is claimed and released around it, in that order. A reader left
|
|
# claimed by a crashed enrollment refuses the next one, and the message a
|
|
# person gets is "busy with something else" forever.
|
|
python3 - "$log" <<'PY' || fail 'the reader is not claimed and released around an enrollment'
|
|
import json
|
|
import sys
|
|
|
|
calls = [json.loads(line)["method"] for line in open(sys.argv[1], encoding="utf-8") if line.strip()]
|
|
if not calls:
|
|
print("nothing was called at all", file=sys.stderr)
|
|
raise SystemExit(1)
|
|
if calls[0] != "Claim":
|
|
print(f"the device was used before it was claimed: {calls}", file=sys.stderr)
|
|
raise SystemExit(1)
|
|
if calls[-1] != "Release":
|
|
print(f"the device was left claimed: {calls}", file=sys.stderr)
|
|
raise SystemExit(1)
|
|
if "EnrollStart" not in calls:
|
|
print(f"nothing started an enrollment: {calls}", file=sys.stderr)
|
|
raise SystemExit(1)
|
|
if calls.index("EnrollStart") < calls.index("Claim"):
|
|
print(f"enrollment started before the claim: {calls}", file=sys.stderr)
|
|
raise SystemExit(1)
|
|
raise SystemExit(0)
|
|
PY
|
|
|
|
# A finger already on the reader ends the stream badly, and says so in words
|
|
# rather than in fprintd's vocabulary.
|
|
printf '{"enrollStages":5,"results":["enroll-stage-passed","enroll-duplicate"],"error":""}\n' \
|
|
>"$fixture"
|
|
: >"$log"
|
|
duplicate="$(runf enroll left-thumb)" && fail 'a failed enrollment exited zero'
|
|
last="$(tail -1 <<<"$duplicate")"
|
|
jq -e '.ok == false and .stage == "failed"' <<<"$last" >/dev/null \
|
|
|| fail "a failed enrollment did not end in a failure line: $last"
|
|
jq -e '(.error | length) > 0 and (.error | test("[a-z] [a-z]"))' <<<"$last" >/dev/null \
|
|
|| fail "the failure is reported in fprintd's vocabulary rather than in words: $last"
|
|
grep -Fq '"Release"' "$log" \
|
|
|| fail 'a failed enrollment left the reader claimed'
|
|
|
|
# A refused claim -- the reader busy with the lock screen, most often -- is one
|
|
# line, not a hang.
|
|
printf '{"enrollStages":5,"results":[],"error":"The fingerprint reader is busy with something else."}\n' \
|
|
>"$fixture"
|
|
: >"$log"
|
|
refused="$(runf enroll right-thumb)" && fail 'a refused claim exited zero'
|
|
jq -e '.ok == false and (.error | length) > 0' <<<"$(tail -1 <<<"$refused")" >/dev/null \
|
|
|| fail "a refused claim did not report a failure: $refused"
|
|
|
|
# ── Nonsense is refused before the reader is touched ─────────────────────────
|
|
printf '{"enrollStages":5,"results":["enroll-completed"],"error":""}\n' >"$fixture"
|
|
for bad in 'third-eye' 'right-index-finger; reboot' '' 'left-thumb-finger'; do
|
|
: >"$log"
|
|
refusal="$(runf enroll "$bad" 2>/dev/null)" \
|
|
&& fail "enroll accepted ${bad@Q} as a finger"
|
|
jq -e '.ok == false and (.error | length) > 0' <<<"$(tail -1 <<<"$refusal")" >/dev/null \
|
|
|| fail "a bad finger name did not come back as a stream failure: $refusal"
|
|
[[ ! -s "$log" ]] \
|
|
|| fail "a bad finger name claimed the reader before being refused: ${bad@Q}"
|
|
: >"$log"
|
|
[[ -n "$(runf remove "$bad" 2>/dev/null | jq -r '.error // ""')" ]] \
|
|
|| fail "remove accepted ${bad@Q} as a finger"
|
|
[[ ! -s "$log" ]] \
|
|
|| fail "a bad finger name reached the reader through remove: ${bad@Q}"
|
|
done
|
|
|
|
# ── remove: one finger, or all of them, and never somebody else's ────────────
|
|
|
|
: >"$log"
|
|
removed="$(runf remove right-index-finger)" || fail 'remove failed against the canned fprintd'
|
|
jq -e 'has("reader") and has("enrolled") and has("unlockFeatureEnabled")' <<<"$removed" >/dev/null \
|
|
|| fail "remove does not answer with the fresh state: $removed"
|
|
python3 - "$log" right-index-finger <<'PY' || fail 'removing one finger did not delete that one finger'
|
|
import json
|
|
import sys
|
|
|
|
calls = [json.loads(line) for line in open(sys.argv[1], encoding="utf-8") if line.strip()]
|
|
methods = [call["method"] for call in calls]
|
|
if methods[0] != "Claim" or methods[-1] != "Release":
|
|
print(f"the device was not claimed and released around it: {methods}", file=sys.stderr)
|
|
raise SystemExit(1)
|
|
delete = next((call for call in calls if call["method"].startswith("DeleteEnrolledFinger")), None)
|
|
if delete is None:
|
|
print(f"nothing was deleted: {methods}", file=sys.stderr)
|
|
raise SystemExit(1)
|
|
if delete["method"] == "DeleteEnrolledFingers2":
|
|
print("removing one finger deleted all of them", file=sys.stderr)
|
|
raise SystemExit(1)
|
|
if sys.argv[2] not in delete["arguments"]:
|
|
print(f"a different finger was deleted: {delete}", file=sys.stderr)
|
|
raise SystemExit(1)
|
|
raise SystemExit(0)
|
|
PY
|
|
|
|
: >"$log"
|
|
runf remove-all >/dev/null || fail 'remove-all failed against the canned fprintd'
|
|
# DeleteEnrolledFingers2 works on the claimed user. Its predecessor took a user
|
|
# name, which is how a typo deletes somebody else's prints.
|
|
grep -Fq '"DeleteEnrolledFingers2"' "$log" \
|
|
|| fail 'remove-all does not use the method that works on the claimed user'
|
|
python3 - "$log" <<'PY' || fail 'remove-all names a user, which is how a typo deletes somebody else'
|
|
import json
|
|
import sys
|
|
|
|
for line in open(sys.argv[1], encoding="utf-8"):
|
|
if not line.strip():
|
|
continue
|
|
call = json.loads(line)
|
|
if call["method"] == "DeleteEnrolledFingers2" and call["arguments"]:
|
|
print(call, file=sys.stderr)
|
|
raise SystemExit(1)
|
|
raise SystemExit(0)
|
|
PY
|
|
|
|
# ── With no fixture and no bus, it still answers in its own shape ────────────
|
|
#
|
|
# The page reads this stream line by line. A traceback on stdout, or nothing at
|
|
# all, leaves an enrollment panel open forever with no way to know it failed.
|
|
absent="$(run enroll right-index-finger 2>/dev/null)"
|
|
[[ -n "$absent" ]] || fail 'enrollment against no fprintd printed nothing at all'
|
|
jq -e '.ok == false and (.error | length) > 0' <<<"$(tail -1 <<<"$absent")" >/dev/null \
|
|
|| fail "enrollment against no fprintd did not answer in the stream's own shape: $absent"
|
|
|
|
printf 'fingerprint contract: PASS (status, stuck state, enroll stream, remove, reason)\n'
|