mirror of
https://github.com/Dvorinka/Containr.git
synced 2026-10-28 15:51:26 +00:00
Add cnp_ personal access tokens so the CLI, MCP server, and external agents authenticate without a browser session — the agent-first spine from ROADMAP2 phase A. Modeled on agent_auth_tokens: raw token shown once, only SHA-256 hash stored. - user_tokens table (goose migration + sqlc), name/prefix/scope/expiry, owner-scoped revoke, last_used_at stamping - middleware: cnp_ bearer resolution, read scope gates mutations with 403 SCOPE_INSUFFICIENT, non-admin scopes cap is_admin even for admin owners — a token never outranks its scope or its owner - GET/POST/DELETE /api/v1/user/tokens + OpenAPI + generated types - Settings UI: create with scope/expiry pickers, one-time token display with copy, list with last-used/expiry, confirm-revoke - docs: OPERATIONS.md section, README feature line Verified end-to-end against live Postgres: create/list/revoke, read scope blocks PUT (403), write PAT on admin owner blocked from admin routes (403), admin scope works, revoked token returns 401. Authored By: TDvorak <[email protected]>
26 lines
1.2 KiB
SQL
26 lines
1.2 KiB
SQL
-- +goose Up
|
|
-- Personal access tokens for CLI/MCP/agent authentication.
|
|
-- Only sha256 hashes are stored — raw tokens (cnp_…) are shown once at
|
|
-- creation and never persisted. Scope gates what the token may do:
|
|
-- read = GET/HEAD only, write = all non-admin routes, admin = everything
|
|
-- (admin still requires the owning user to be a platform admin).
|
|
CREATE TABLE IF NOT EXISTS public.user_tokens (
|
|
id uuid DEFAULT gen_random_uuid() NOT NULL PRIMARY KEY,
|
|
user_id uuid NOT NULL REFERENCES public.users(id) ON DELETE CASCADE,
|
|
name character varying(255) NOT NULL,
|
|
key_prefix character varying(16) NOT NULL,
|
|
token_hash text NOT NULL UNIQUE,
|
|
scope character varying(20) NOT NULL DEFAULT 'write'
|
|
CHECK (scope IN ('read', 'write', 'admin')),
|
|
expires_at timestamp with time zone,
|
|
last_used_at timestamp with time zone,
|
|
revoked_at timestamp with time zone,
|
|
created_at timestamp with time zone DEFAULT now() NOT NULL
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_user_tokens_user_id ON public.user_tokens(user_id);
|
|
CREATE INDEX IF NOT EXISTS idx_user_tokens_token_hash ON public.user_tokens(token_hash);
|
|
|
|
-- +goose Down
|
|
DROP TABLE IF EXISTS public.user_tokens;
|