mirror of
https://github.com/Dvorinka/Containr.git
synced 2026-10-28 07:41:27 +00:00
feat: personal access tokens with scoped bearer auth
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]>
This commit is contained in:
1 parent
f9ae5b962c
commit
e881747388
17 files changed
+1141
-30
No files matched your search
@@ -152,6 +152,97 @@ paths:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Error'
|
||||
|
||||
/user/tokens:
|
||||
get:
|
||||
tags:
|
||||
- User
|
||||
summary: List personal access tokens
|
||||
description: Returns the caller's non-revoked personal access token metadata. Token values and hashes are never returned.
|
||||
security:
|
||||
- bearerAuth: []
|
||||
responses:
|
||||
'200':
|
||||
description: Token list
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
tokens:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/UserToken'
|
||||
'401':
|
||||
description: Unauthorized
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Error'
|
||||
post:
|
||||
tags:
|
||||
- User
|
||||
summary: Create personal access token
|
||||
description: Issues a `cnp_` bearer token for CLI/MCP/agent authentication. The raw token is returned exactly once and never stored — only its SHA-256 hash persists. Admin-scope tokens require a platform admin account.
|
||||
security:
|
||||
- bearerAuth: []
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/CreateUserTokenRequest'
|
||||
responses:
|
||||
'201':
|
||||
description: Token created; raw value returned once
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/UserTokenCreated'
|
||||
'400':
|
||||
description: Invalid input
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Error'
|
||||
'403':
|
||||
description: Admin scope requires platform admin
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Error'
|
||||
|
||||
/user/tokens/{id}:
|
||||
delete:
|
||||
tags:
|
||||
- User
|
||||
summary: Revoke personal access token
|
||||
description: Marks one of the caller's tokens as revoked. Clients presenting it are rejected from then on.
|
||||
security:
|
||||
- bearerAuth: []
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
responses:
|
||||
'200':
|
||||
description: Token revoked
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
message:
|
||||
type: string
|
||||
'404':
|
||||
description: Token not found
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Error'
|
||||
|
||||
# Projects
|
||||
/projects:
|
||||
get:
|
||||
@@ -6395,6 +6486,66 @@ components:
|
||||
type: string
|
||||
description: Human-readable label for the token (e.g. node name)
|
||||
|
||||
UserToken:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
format: uuid
|
||||
name:
|
||||
type: string
|
||||
key_prefix:
|
||||
type: string
|
||||
description: First 12 chars of the token, for identification in lists
|
||||
scope:
|
||||
type: string
|
||||
enum: [read, write, admin]
|
||||
expires_at:
|
||||
type: string
|
||||
format: date-time
|
||||
last_used_at:
|
||||
type: string
|
||||
format: date-time
|
||||
created_at:
|
||||
type: string
|
||||
format: date-time
|
||||
required:
|
||||
- id
|
||||
- name
|
||||
- key_prefix
|
||||
- scope
|
||||
- created_at
|
||||
|
||||
UserTokenCreated:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/UserToken'
|
||||
- type: object
|
||||
properties:
|
||||
token:
|
||||
type: string
|
||||
description: Raw cnp_ token value, returned exactly once at creation
|
||||
required:
|
||||
- token
|
||||
|
||||
CreateUserTokenRequest:
|
||||
type: object
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
description: Human-readable label (e.g. "laptop cli", "ci deploy")
|
||||
scope:
|
||||
type: string
|
||||
enum: [read, write, admin]
|
||||
default: write
|
||||
description: read = GET/HEAD only; write = all non-admin routes; admin = everything (requires admin account)
|
||||
expires_in_days:
|
||||
type: integer
|
||||
minimum: 0
|
||||
maximum: 3650
|
||||
description: 0 or omitted = never expires
|
||||
required:
|
||||
- name
|
||||
|
||||
AgentCapabilities:
|
||||
type: object
|
||||
properties:
|
||||
|
||||
@@ -90,3 +90,30 @@ backend restart.
|
||||
once), copy the prefilled install command, revoke tokens when done. Agents
|
||||
register over `POST /api/agents/register` and heartbeat via
|
||||
`POST /api/agents/heartbeat`.
|
||||
|
||||
## Personal access tokens
|
||||
|
||||
**Settings → Personal Access Tokens**: issue `cnp_…` bearer tokens for the
|
||||
CLI, MCP server, and scripts — the same privileges as your account, bounded
|
||||
by scope. The raw token is shown once at creation; only its SHA-256 hash is
|
||||
stored.
|
||||
|
||||
Scopes:
|
||||
|
||||
- `read` — `GET`/`HEAD`/`OPTIONS` only; mutations return `403 SCOPE_INSUFFICIENT`.
|
||||
- `write` — everything except admin routes.
|
||||
- `admin` — full access; requires a platform admin account, and the token is
|
||||
still capped at the owner's privileges.
|
||||
|
||||
A token can never outrank its owner: non-admin scopes report
|
||||
`is_admin = false` even on admin accounts.
|
||||
|
||||
```
|
||||
GET /api/v1/user/tokens list (metadata only, never hashes)
|
||||
POST /api/v1/user/tokens {name, scope?, expires_in_days?} → raw token once
|
||||
DELETE /api/v1/user/tokens/:id revoke (owner-scoped)
|
||||
```
|
||||
|
||||
Use as `Authorization: Bearer cnp_…`, or `containr auth login cnp_…`.
|
||||
Revocation takes effect on the next request; `last_used_at` updates on each
|
||||
authenticated call.
|
||||
Reference in new issue
Block a user