feat(doorman): break-glass local unlock — disaster path without HA [#345]
Founder ruling 2026-09-03: cover HA-down / network-down / power recovery. DOORMAN_BREAKGLASS_IDS badges fire the relay LOCALLY (no HA dependency), then best-effort POST flagged local_unlock:true so HA logs without re-dispatching. --check-breakglass decision mode; 7/7 breakglass tests, full suites green. https://projects.knownelement.com/issues/345
This commit is contained in:
+31
-1
@@ -45,6 +45,17 @@ log() {
|
||||
fi
|
||||
}
|
||||
|
||||
# Break-glass: disaster-path badges that unlock LOCALLY (relay fires on
|
||||
# the reader host, no HA, no network). Covers HA crash / power recovery
|
||||
# / network loss. Founder-designated IDs only; list lives in the 0600
|
||||
# env file, mirrored in the ticket trail (#345).
|
||||
is_breakglass() {
|
||||
case " ${DOORMAN_BREAKGLASS_IDS:-} " in
|
||||
*" $1 "*) return 0 ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
usage() {
|
||||
sed -n '2,40p' "$0" | sed 's/^# \{0,1\}//'
|
||||
}
|
||||
@@ -119,13 +130,24 @@ process_scan() {
|
||||
ts="$(date +%Y-%m-%dT%H:%M:%S%z)"
|
||||
reader="${DOORMAN_READER_NAME:-$(hostname)}"
|
||||
log "ID $id scanned."
|
||||
# Break-glass first: disaster badges unlock LOCALLY regardless of
|
||||
# HA/network state (founder ruling 2026-09-03, #345). HA is still
|
||||
# notified best-effort, flagged so it logs without re-dispatching.
|
||||
if is_breakglass "$id"; then
|
||||
log "BREAKGLASS badge $id — local unlock, no HA dependency."
|
||||
fire_relay
|
||||
fi
|
||||
if [ -z "$DOORMAN_WEBHOOK_URL" ]; then
|
||||
log "no webhook configured (DOORMAN_WEBHOOK_URL) — scan logged only"
|
||||
return 0
|
||||
fi
|
||||
# badge IDs are digits by construction; reader/ts are env/hostname —
|
||||
# the JSON below cannot carry user-controlled quotes or backslashes
|
||||
body="$(printf '{"badge_id":"%s","reader":"%s","ts":"%s"}' "$id" "$reader" "$ts")"
|
||||
if is_breakglass "$id"; then
|
||||
body="$(printf '{"badge_id":"%s","reader":"%s","ts":"%s","local_unlock":true}' "$id" "$reader" "$ts")"
|
||||
else
|
||||
body="$(printf '{"badge_id":"%s","reader":"%s","ts":"%s"}' "$id" "$reader" "$ts")"
|
||||
fi
|
||||
code="$(curl -sS --max-time "$DOORMAN_HTTP_TIMEOUT" \
|
||||
-H 'Content-Type: application/json' -d "$body" \
|
||||
-w '%{http_code}' "$DOORMAN_WEBHOOK_URL" 2>/dev/null)"
|
||||
@@ -186,6 +208,11 @@ while [ $# -gt 0 ]; do
|
||||
SELFTEST_FILE="${2:?--selftest requires a fixture path}"
|
||||
shift 2
|
||||
;;
|
||||
--check-breakglass)
|
||||
MODE="checkbreak"
|
||||
BREAKGLASS_ID="${2:?--check-breakglass requires a badge id}"
|
||||
shift 2
|
||||
;;
|
||||
--width)
|
||||
WIDTH="${2:?--width requires 24 or 16}"
|
||||
shift 2
|
||||
@@ -216,5 +243,8 @@ case "$MODE" in
|
||||
fi
|
||||
stdbuf -oL od -v -A n -t u1 -w"$WIDTH" "$SELFTEST_FILE" | decode_stream "$WIDTH"
|
||||
;;
|
||||
checkbreak)
|
||||
if is_breakglass "$BREAKGLASS_ID"; then exit 0; else exit 1; fi
|
||||
;;
|
||||
live) run_live ;;
|
||||
esac
|
||||
|
||||
Reference in New Issue
Block a user