feat(doors): role policy and audit for door commands #72

Open
opened 2026-09-24 12:56:31 +00:00 by gabogg · 0 comments
Owner

Split out of #64 during its triage on 2026-09-24 (grilling session). The scope below is settled.

Problem

POST /api/doors/{door_index_code}/control (app/controllers/door_controller.py:77) sends a door command to HikCentral through DoorStateManager.control_door_async (app/services/door_service.py:1418). Commands are unlock, open, close, remain_open, remain_closed and restore (DoorControlRequest, app/schemas/models.py:287). The route only requires require_auth, so any logged-in operator can hold any door open indefinitely (remain_open). Nothing records who issued a command, and a command that changes nothing, or that HikCentral rejects, leaves no trace: door_hardware_state_transitions only records state changes.

Door control is expected to grow (follow-up capabilities for opening doors through the system), so the role policy and audit trail should exist before it does.

Settled scope

  1. Role policy, split by command kind:
    • One-shot commands (open, unlock, close, restore): operators and admins.
    • Persistent commands (remain_open, remain_closed), which leave a door in that state until someone reverses it: admins only. Operators get 403 with the existing FORBIDDEN_ADMIN error code.
  2. Audit log: a new door_commands table recording, for every attempt: timestamp, the user, the door, the command, the outcome (ACCEPTED, REJECTED by policy, FAILED upstream) and the error message. Written through the door repository; pruned at 365 days by the existing retention job. Refused and failed attempts are recorded too.
  3. Dashboard: persistent-command controls stay visible but disabled for operators, with a tooltip saying admin is required. The backend enforces the policy regardless.
  4. Glossary: add Door Command to CONTEXT.md (one-shot vs persistent), in this issue's PR.

Acceptance criteria

  • Operators can send one-shot commands; persistent commands return 403 FORBIDDEN_ADMIN for operators and succeed for admins (tests for both roles).
  • Every attempt (accepted, rejected by policy, failed upstream) writes one door_commands row with the user and outcome (tests for each outcome, with Artemis mocked).
  • door_commands created via CREATE TABLE IF NOT EXISTS, included in retention pruning.
  • Persistent-command controls rendered disabled with a tooltip for operators; enabled for admins.
  • CONTEXT.md defines Door Command (one-shot vs persistent).
  • Full suite green (pytest + node --test).

Related: #64.

> Split out of #64 during its triage on 2026-09-24 (grilling session). The scope below is settled. ## Problem `POST /api/doors/{door_index_code}/control` (`app/controllers/door_controller.py:77`) sends a door command to HikCentral through `DoorStateManager.control_door_async` (`app/services/door_service.py:1418`). Commands are `unlock`, `open`, `close`, `remain_open`, `remain_closed` and `restore` (`DoorControlRequest`, `app/schemas/models.py:287`). The route only requires `require_auth`, so **any logged-in operator can hold any door open indefinitely** (`remain_open`). **Nothing records who issued a command**, and a command that changes nothing, or that HikCentral rejects, leaves no trace: `door_hardware_state_transitions` only records state changes. Door control is expected to grow (follow-up capabilities for opening doors through the system), so the role policy and audit trail should exist before it does. ## Settled scope 1. **Role policy, split by command kind:** - **One-shot** commands (`open`, `unlock`, `close`, `restore`): operators and admins. - **Persistent** commands (`remain_open`, `remain_closed`), which leave a door in that state until someone reverses it: **admins only**. Operators get **403 with the existing `FORBIDDEN_ADMIN` error code**. 2. **Audit log:** a new `door_commands` table recording, for **every** attempt: timestamp, the user, the door, the command, the outcome (`ACCEPTED`, `REJECTED` by policy, `FAILED` upstream) and the error message. Written through the door repository; pruned at 365 days by the existing retention job. Refused and failed attempts are recorded too. 3. **Dashboard:** persistent-command controls stay **visible but disabled** for operators, with a tooltip saying admin is required. The backend enforces the policy regardless. 4. **Glossary:** add **Door Command** to `CONTEXT.md` (one-shot vs persistent), in this issue's PR. ## Acceptance criteria - [ ] Operators can send one-shot commands; persistent commands return 403 `FORBIDDEN_ADMIN` for operators and succeed for admins (tests for both roles). - [ ] Every attempt (accepted, rejected by policy, failed upstream) writes one `door_commands` row with the user and outcome (tests for each outcome, with Artemis mocked). - [ ] `door_commands` created via `CREATE TABLE IF NOT EXISTS`, included in retention pruning. - [ ] Persistent-command controls rendered disabled with a tooltip for operators; enabled for admins. - [ ] `CONTEXT.md` defines **Door Command** (one-shot vs persistent). - [ ] Full suite green (pytest + `node --test`). Related: #64.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
gabogg/hikcentral#72
No description provided.