diff --git a/README.md b/README.md new file mode 100644 index 0000000..87155a9 --- /dev/null +++ b/README.md @@ -0,0 +1,99 @@ +# doorman — badge access, modernized + +Physical access control for the server-room door: badge-reader listener +for the reader host, dispatching scans to Home Assistant, which owns the +whitelist, logging, alerts, and unlock decision. + +**Dedicated docs topic: [Discourse t/318](https://community.turnsys.com/t/318)** +· Tracking: [Redmine #345](https://projects.knownelement.com/issues/345) +(umbrella) → [#355](https://projects.knownelement.com/issues/355) +(code & testing) → [#356](https://projects.knownelement.com/issues/356) +(deployment) · Ops status: [Discourse t/297](https://community.turnsys.com/t/297) +· Fleet context: [KNEL/PFVCluster](https://git.knownelement.com/KNEL/PFVCluster) + +## Lineage + +Successor of the 2018 Perl doorman +([KNEL/LegacyTechops `doorman/`](https://git.knownelement.com/KNEL/LegacyTechops/src/branch/main/doorman/doorman.pl)), +carried verbatim under [`legacy/`](legacy/doorman.pl). Same reader class +(keyboard-emulating USB RFID), same decode semantics — but the defunct +plain-HTTP auth portal (`doors.pfv.turnsys.net`) is NOT replicated: HA is +the single authentication brain. + +## Architecture + +``` +USB RFID reader (HID keyboard) + └─> /dev/input/by-id/*-event-kbd (reader host: pfvsvrpi prod, + | ultix-field dev) + v + bin/doorman.sh od(1) + awk(1) decode — bash and + | coreutils ONLY, no package installs + v + POST {badge_id, reader, ts} -----> Home Assistant webhook + |– whitelist decision + |– logbook/recorder history + |– unknown badge -> instant alert + '- unlock (e-lock or relay) +``` + +Fail-closed by construction: if HA is unreachable, the scan is logged and +dropped — the door does not open. The optional local relay bridge +(`usbrelay`, 2018 lineage) exists behind `DOORMAN_UNLOCK_ON_2XX` (default +**off**) for the transition period; native HA webhooks always answer 200, +so that flag must stay off until HA returns real accept/deny codes. + +### Webhook contract (for the HA side) + +`POST ` with JSON: + +```json +{"badge_id": "0000000001", "reader": "pfvsvrpi", "ts": "2026-09-02T19:12:44-0500"} +``` + +- Values are digits + env-provided strings; the JSON cannot carry + user-controlled quotes/backslashes. +- Any 2xx satisfies the listener; HA automations do the real work. +- Configure the HA webhook trigger to this URL; the token lives in the + URL path — treat it as a secret, keep it in `/etc/default/doorman` + (0600) on the reader host, never in git. + +## Deploy (reader host) + +```bash +sudo mkdir -p /opt/doorman +sudo cp bin/doorman.sh /opt/doorman/bin/ # + this repo, or rsync +sudo cp deploy/doorman.service /etc/systemd/system/ +sudo cp .env.example /etc/default/doorman # fill in, chown root:root +sudo chmod 600 /etc/default/doorman +sudo systemctl daemon-reload && sudo systemctl enable --now doorman +``` + +Record width auto-detects (24 B on 64-bit kernels, 16 B on 32-bit Pis). +Logs go to syslog/journald (`-t doorman`); `DOORMAN_DEBUG=true` mirrors +to stderr. Deployment discipline: **ultix-field (dev) first, verify +end-to-end, then pfvsvrpi (prod)** — never both at once (#356). + +## Testing (no hardware needed) + +```bash +bash scripts/test.sh # decoder suite: synthetic event fixtures +bash tests/shellcheck.sh # lint gate (docker, zero-warning) +bash scripts/check-rules.sh # full rule audit +``` + +`--selftest` mode decodes a fixture file and prints one badge ID per +line — that is the entire test surface of the decoder, including the +2018 quirks we intentionally preserve (usage passthrough 0-9, bad-usage +parity-anchor behavior, empty-scan suppression). + +## Repo map + +| Path | What | +|------|------| +| `bin/doorman.sh` | The listener daemon | +| `deploy/doorman.service` | systemd unit (`/etc/default/doorman` env file) | +| `tests/run-tests.sh` | Offline decoder suite (9 tests) | +| `legacy/` | Verbatim 2018 Perl snapshot (not maintained) | +| `scripts/` | Rule engine + hooks (adopted from TSYSGroupAIOS) | +| `questions-v1.md` | Points to the active round (PFVCluster questions-v4.md) |