feat(email-intake): admin CRUD for tenant intake tokens (PMS-450 phase 2) #342
No reviewers
Labels
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
psa-systems/mokosh-server!342
Loading…
Reference in a new issue
No description provided.
Delete branch "feat/PMS-450-phase2-token-admin"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Adds the operator-facing surface that turns Phase 1 from "the operator SQL-inserts a token hash and crosses their fingers" into "the operator clicks New Token, copies the plaintext into their mail gateway, and revokes it later from the same UI". Three endpoints under
/api/v1/intake-tokens, all admin-gated:revoked_at)Security posture matches what an MSP operator expects from a webhook credential surface:
CreatedIntakeTokenResponse), with the metadata-onlyIntakeTokenResponseshape used by every other read. The list endpoint deliberately does not carry the plaintext;idx_tenant_intake_tokens_hashUNIQUE keeps double-mint a constraint violation, not a silent shadow);revoked_at = COALESCE(revoked_at, NOW())) so the audit trail of "who used this token when" stays intact, and re-revoke is idempotent (the COALESCE pins the first timestamp);RequireAdminpaired withRequireAuthso non-admin attempts are 403/401, never 200.Token rotation == revoke + mint a fresh one; there is no update endpoint. That is the standard webhook-credential pattern: once a bearer is in a mail gateway's config, the only safe rotation is "issue new, swap, revoke old".
The integration test under
tests/intake_token_admin.rspins the full round trip: tech POST fails, admin POST succeeds, the DB carries the SHA-256 hash and no plaintext, the minted token immediately works against/email-intake, list returns metadata only (notokenfield), tech cannot list, revoke flipsrevoked_at, the revoked token immediately fails against/email-intakewith 401, re-revoke is idempotent (timestamp unchanged), and the revoked row stays visible to the list endpoint.Phase 2 follow-ups still parked under PMS-450:
tenant_settings.email_intake_default_company_idso a missing-sender intake auto-creates a contact instead of 422'ing;referencesarray) into a ticket comment on the existing ticket rather than just returning its id;email_intake_logaudit table that captures every raw payload (headers + body) so a malformed sender can be debugged post-hoc.#PMS-450
Adds the operator-facing surface that turns Phase 1 from "the operator SQL-inserts a token hash and crosses their fingers" into "the operator clicks New Token, copies the plaintext into their mail gateway, and revokes it later from the same UI". Three endpoints under `/api/v1/intake-tokens`, all admin-gated: - GET /api/v1/intake-tokens -> list (active + revoked) so an operator can audit - POST /api/v1/intake-tokens -> mint a fresh bearer - DELETE /api/v1/intake-tokens/{id} -> revoke (soft-delete via `revoked_at`) Security posture matches what an MSP operator expects from a webhook credential surface: - the plaintext bearer is on the create response EXACTLY ONCE (the new `CreatedIntakeTokenResponse`), with the metadata-only `IntakeTokenResponse` shape used by every other read. The list endpoint deliberately does not carry the plaintext; - only the SHA-256 hash hits the DB (matches the Phase 1 lookup path; the existing `idx_tenant_intake_tokens_hash` UNIQUE keeps double-mint a constraint violation, not a silent shadow); - 32 random bytes URL-safe base64 = ~43 chars of bearer with 256 bits of entropy. Same minting pattern mokosh-bootstrap.rs uses for OIDC confidential clients (PMS-122); - revoke is a soft-delete (`revoked_at = COALESCE(revoked_at, NOW())`) so the audit trail of "who used this token when" stays intact, and re-revoke is idempotent (the COALESCE pins the first timestamp); - admin-only via `RequireAdmin` paired with `RequireAuth` so non-admin attempts are 403/401, never 200. Token rotation == revoke + mint a fresh one; there is no update endpoint. That is the standard webhook-credential pattern: once a bearer is in a mail gateway's config, the only safe rotation is "issue new, swap, revoke old". The integration test under `tests/intake_token_admin.rs` pins the full round trip: tech POST fails, admin POST succeeds, the DB carries the SHA-256 hash and no plaintext, the minted token immediately works against `/email-intake`, list returns metadata only (no `token` field), tech cannot list, revoke flips `revoked_at`, the revoked token immediately fails against `/email-intake` with 401, re-revoke is idempotent (timestamp unchanged), and the revoked row stays visible to the list endpoint. Phase 2 follow-ups still parked under PMS-450: - `tenant_settings.email_intake_default_company_id` so a missing-sender intake auto-creates a contact instead of 422'ing; - turn a recognised reply (matched via the `references` array) into a ticket comment on the existing ticket rather than just returning its id; - an `email_intake_log` audit table that captures every raw payload (headers + body) so a malformed sender can be debugged post-hoc. #PMS-450