Reject revokes the node's MQTT login and API key (#195) #206

Merged
logan merged 1 commits from fix/195-reject-revokes-credentials into main 2026-10-04 18:28:10 -04:00
Owner

Fixes #195.

What changed

  • reject_node now revokes: status -> rejected first (fail-closed), then dynsec.delete_node_client, then deletes node_keys/{id}. A dynsec failure returns 502 with the key kept; retrying reject is idempotent. Re-approving after a reject mints a fresh key — the revoked one is never handed back.
  • /upload now also requires approval_status == "approved" (403 otherwise), using an uncached read so revocation takes effect on the next upload. The same doc feeds the existing org_id stamp, replacing the cached read.
  • dynsec.reconcile_all() deletes rejected nodes' broker logins on c2-core startup, so nodes rejected before this change (or whose revoke failed) get cleaned up on the next deploy.
  • Reject confirm dialog says it revokes.

Tests

  • New tests/test_node_reject_revoke.py (10 tests: revoke order, dynsec-failure 502, 404, admin required, re-approve gets a new key, upload gate for rejected/pending/missing/approved/wrong key, reconcile revoke). 6 of them fail on main.
  • c2-core: 535 passed, 3 warnings.
  • Frontend: string-only change; no node/npm on this box, so tsc not run.

Not changed

  • require_service_or_firebase_token (/systems node read) still keys off node_keys alone. After a successful reject that key is gone; only in the 502 partial-failure window does it still match.
  • The node page only shows Reject for pending nodes; an approved node can only be revoked via Delete or the API.

🤖 Generated with Claude Code

Fixes #195. **What changed** - `reject_node` now revokes: status -> `rejected` first (fail-closed), then `dynsec.delete_node_client`, then deletes `node_keys/{id}`. A dynsec failure returns 502 with the key kept; retrying reject is idempotent. Re-approving after a reject mints a fresh key — the revoked one is never handed back. - `/upload` now also requires `approval_status == "approved"` (403 otherwise), using an uncached read so revocation takes effect on the next upload. The same doc feeds the existing org_id stamp, replacing the cached read. - `dynsec.reconcile_all()` deletes rejected nodes' broker logins on c2-core startup, so nodes rejected before this change (or whose revoke failed) get cleaned up on the next deploy. - Reject confirm dialog says it revokes. **Tests** - New `tests/test_node_reject_revoke.py` (10 tests: revoke order, dynsec-failure 502, 404, admin required, re-approve gets a new key, upload gate for rejected/pending/missing/approved/wrong key, reconcile revoke). 6 of them fail on main. - c2-core: `535 passed, 3 warnings`. - Frontend: string-only change; no node/npm on this box, so tsc **not run**. **Not changed** - `require_service_or_firebase_token` (`/systems` node read) still keys off `node_keys` alone. After a successful reject that key is gone; only in the 502 partial-failure window does it still match. - The node page only shows Reject for *pending* nodes; an approved node can only be revoked via Delete or the API. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
logan added 1 commit 2026-10-04 17:59:17 -04:00
reject_node only set approval_status, so an approved-then-rejected node kept
its dynsec login, its /upload key and /systems read access.

- reject_node: status -> rejected first (fail-closed), then
  dynsec.delete_node_client, then delete node_keys. dynsec failure -> 502,
  key kept, retry is idempotent. Re-approve mints a fresh key.
- upload.py: valid key alone is not enough; node must be approved (403),
  uncached read so revocation bites immediately.
- dynsec.reconcile_all: deletes rejected nodes' logins on startup, healing
  nodes rejected before this change or whose revoke failed.
- Reject confirm text says it revokes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Author
Owner

Review note: before merging, check prod Firestore for any node that has a node_keys doc but approval_status != "approved" (e.g. reissue-key was run on a pending node; reissue never sets approval_status). Pre-PR such a node could upload; after this PR every upload from it 403s and the recording is lost. Query: node_keys ids minus nodes where approval_status==approved should be empty.

Review note: before merging, check prod Firestore for any node that has a node_keys doc but approval_status != "approved" (e.g. reissue-key was run on a pending node; reissue never sets approval_status). Pre-PR such a node could upload; after this PR every upload from it 403s and the recording is lost. Query: node_keys ids minus nodes where approval_status==approved should be empty.
logan merged commit a03ec63846 into main 2026-10-04 18:28:10 -04:00
Sign in to join this conversation.
No Reviewers
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: logan/server-26#206