Just-In-Time Admin Elevation

Sensitive permissions — key rotation, audit export, role mutation, legal-hold release — should never sit on a session indefinitely. JIT elevation models the request → approval → time-boxed grant → auto-expiry lifecycle layered on top of standard RBAC, so an operator runs as their normal role 99% of the time and only borrows admin rights for the minutes they need.

State machine

graph LR REQ["request created"] --> PEND["pending"] PEND -->|approve quorum met| APR["approved"] PEND -->|deny / self-approve violation| DEN["denied"] PEND -->|expires_at reached, no decision| EXP["expired"] APR --> GRT["grant active (granted_perms ⊆ requested)"] GRT -->|grant.expires_at reached| OUT["expired (auto)"] GRT -->|operator revokes| REV["revoked"]

Policy: enterprise vs government

ElevationPolicy is a four-knob struct. The default constructor returns enterprise defaults; ElevationPolicy::government() returns the FedRAMP-shaped preset.

Knob Enterprise default Government preset
min_approvers12
max_window60 minutes8 hours
forbid_self_approvetruetrue
requires_reasontruetrue

Per-request grant windows are silently clamped down to max_window; the granted permission set can never widen beyond what the requester asked for. Both invariants are enforced inside ElevationService, not at the HTTP layer, so the SDK gets the same guarantees.

Tables

SQL Schema
CREATE TABLE elevation_requests (
    id                    UUID PRIMARY KEY,
    org_id                UUID NOT NULL REFERENCES orgs(id) ON DELETE CASCADE,
    requester_user_id     UUID NOT NULL REFERENCES users(id),
    requested_perms       JSONB NOT NULL,
    reason                TEXT NOT NULL,
    created_at            TIMESTAMPTZ NOT NULL DEFAULT now(),
    expires_at            TIMESTAMPTZ NOT NULL,
    decided_by_user_id    UUID REFERENCES users(id),
    decision              TEXT CHECK (decision IN ('approved','denied','expired')),
    decided_at            TIMESTAMPTZ
);

CREATE TABLE elevation_grants (
    id              UUID PRIMARY KEY,
    request_id      UUID NOT NULL REFERENCES elevation_requests(id) ON DELETE CASCADE,
    granted_perms   JSONB NOT NULL,
    expires_at      TIMESTAMPTZ NOT NULL,
    revoked_at      TIMESTAMPTZ
);

REST routes

All routes are mounted under /api/v1/admin/elevation.

Method + path Caller Effect
POST /admin/elevation/requestRequesterCreate request, returns id
GET /admin/elevation/pendingApproverList requests awaiting decision
GET /admin/elevation/activeAnyoneList grants in their valid window
POST /admin/elevation/{id}/approveApproverApproves; emits grant when quorum reached
POST /admin/elevation/{id}/denyApproverCloses the request, no grant

CLI

Bash
tetrapus-admin elevation request \
  --perms "audit.export,users.delete" \
  --reason "incident IR-2026-44 — exporting hold for counsel"
# 8c4a-f12d-…

tetrapus-admin elevation approve --id 8c4a-f12d-…
# approved

GUI

Workspace → Profile → Org admin → Elevation tab. Two panels: my pending requests and requests awaiting my decision. Approving in the GUI calls the same /approve endpoint as the CLI.

Layering note for developers

The elevation core is intentionally pure types — no I/O, no DB, no async orchestration. It models requests, grants, policies, and a Clock trait you can mock in tests (SystemClock / MockClock). Persistence and quorum counting live in the server; the orchestration layer counts approvals and only calls ElevationService::approve once the policy quorum is satisfied.

Related

Questions?

Reach out for help with integration, deployment, or custom domain codecs.