- internal/ui/ui.go: add PGCred, Tags to AccountDetailData; register
PUT /accounts/{id}/pgcreds and PUT /accounts/{id}/tags routes; add
pgcreds_form.html and tags_editor.html to shared template set; remove
unused AccountTagsData; fix fieldalignment on PolicyRuleView, PoliciesData
- internal/ui/handlers_accounts.go: add handleSetPGCreds — encrypts
password via crypto.SealAESGCM, writes audit EventPGCredUpdated, renders
pgcreds_form fragment; password never echoed; load PG creds and tags in
handleAccountDetail
- internal/ui/handlers_policy.go: fix handleSetAccountTags to render with
AccountDetailData instead of removed AccountTagsData
- internal/ui/ui_test.go: add 5 PG credential UI tests
- web/templates/fragments/pgcreds_form.html: new fragment — metadata display
+ set/replace form; system accounts only; password write-only
- web/templates/fragments/tags_editor.html: new fragment — textarea editor
with HTMX PUT for atomic tag replacement
- web/templates/fragments/policy_form.html: rewrite to use structured fields
matching handleCreatePolicyRule (roles/account_types/actions multi-select,
resource_type, subject_uuid, service_names, required_tags, checkbox)
- web/templates/policies.html: new policies management page
- web/templates/fragments/policy_row.html: new HTMX table row with toggle
and delete
- web/templates/account_detail.html: add Tags card and PG Credentials card
- web/templates/base.html: add Policies nav link
- internal/server/server.go: remove ~220 lines of duplicate tag/policy
handler code (real implementations are in handlers_policy.go)
- internal/policy/engine_wrapper.go: fix corrupted source; use errors.New
- internal/db/policy_test.go: use model.AccountTypeHuman constant
- cmd/mciasctl/main.go: add nolint:gosec to int(os.Stdin.Fd()) calls
- gofmt/goimports: db/policy_test.go, policy/defaults.go,
policy/engine_test.go, ui/ui.go, cmd/mciasctl/main.go
- fieldalignment: model.PolicyRuleRecord, policy.Engine, policy.Rule,
policy.RuleBody, ui.PolicyRuleView
Security: PG password encrypted AES-256-GCM with fresh random nonce before
storage; plaintext never logged or returned in any response; audit event
written on every credential write.
1292 lines
39 KiB
YAML
1292 lines
39 KiB
YAML
openapi: "3.1.0"
|
|
|
|
info:
|
|
title: MCIAS Authentication API
|
|
version: "1.0"
|
|
description: |
|
|
MCIAS (Metacircular Identity and Access System) provides JWT-based
|
|
authentication, account management, TOTP, and Postgres credential storage.
|
|
|
|
All tokens are Ed25519-signed JWTs (algorithm `EdDSA`). Bearer tokens must
|
|
be sent in the `Authorization` header as `Bearer <token>`.
|
|
|
|
Rate limiting applies to `/v1/auth/login` and `/v1/token/validate`:
|
|
10 requests per second per IP, burst of 10.
|
|
|
|
servers:
|
|
- url: https://auth.example.com:8443
|
|
description: Production
|
|
|
|
components:
|
|
securitySchemes:
|
|
bearerAuth:
|
|
type: http
|
|
scheme: bearer
|
|
bearerFormat: JWT
|
|
|
|
schemas:
|
|
Error:
|
|
type: object
|
|
required: [error, code]
|
|
properties:
|
|
error:
|
|
type: string
|
|
description: Human-readable error message.
|
|
example: invalid credentials
|
|
code:
|
|
type: string
|
|
description: Machine-readable error code.
|
|
example: unauthorized
|
|
|
|
TokenResponse:
|
|
type: object
|
|
required: [token, expires_at]
|
|
properties:
|
|
token:
|
|
type: string
|
|
description: Ed25519-signed JWT (EdDSA).
|
|
example: eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9...
|
|
expires_at:
|
|
type: string
|
|
format: date-time
|
|
description: Token expiry in RFC 3339 format.
|
|
example: "2026-04-10T12:34:56Z"
|
|
|
|
Account:
|
|
type: object
|
|
required: [id, username, account_type, status, created_at, updated_at, totp_enabled]
|
|
properties:
|
|
id:
|
|
type: string
|
|
format: uuid
|
|
description: Account UUID (use this in all API calls).
|
|
example: 550e8400-e29b-41d4-a716-446655440000
|
|
username:
|
|
type: string
|
|
example: alice
|
|
account_type:
|
|
type: string
|
|
enum: [human, system]
|
|
example: human
|
|
status:
|
|
type: string
|
|
enum: [active, inactive, deleted]
|
|
example: active
|
|
created_at:
|
|
type: string
|
|
format: date-time
|
|
example: "2026-03-11T09:00:00Z"
|
|
updated_at:
|
|
type: string
|
|
format: date-time
|
|
example: "2026-03-11T09:00:00Z"
|
|
totp_enabled:
|
|
type: boolean
|
|
description: Whether TOTP is enrolled and required for this account.
|
|
example: false
|
|
|
|
AuditEvent:
|
|
type: object
|
|
required: [id, event_type, event_time, ip_address]
|
|
properties:
|
|
id:
|
|
type: integer
|
|
example: 42
|
|
event_type:
|
|
type: string
|
|
example: login_ok
|
|
event_time:
|
|
type: string
|
|
format: date-time
|
|
example: "2026-03-11T09:01:23Z"
|
|
actor_id:
|
|
type: string
|
|
format: uuid
|
|
nullable: true
|
|
description: UUID of the account that performed the action. Null for bootstrap events.
|
|
example: 550e8400-e29b-41d4-a716-446655440000
|
|
target_id:
|
|
type: string
|
|
format: uuid
|
|
nullable: true
|
|
description: UUID of the affected account, if applicable.
|
|
ip_address:
|
|
type: string
|
|
example: "192.0.2.1"
|
|
details:
|
|
type: string
|
|
description: JSON blob with event-specific metadata. Never contains credentials.
|
|
example: '{"jti":"f47ac10b-..."}'
|
|
|
|
TagsResponse:
|
|
type: object
|
|
required: [tags]
|
|
properties:
|
|
tags:
|
|
type: array
|
|
items:
|
|
type: string
|
|
description: Current tag list for the account.
|
|
example: ["env:production", "svc:payments-api"]
|
|
|
|
RuleBody:
|
|
type: object
|
|
required: [effect]
|
|
description: |
|
|
The match conditions and effect of a policy rule. All fields except
|
|
`effect` are optional; an omitted field acts as a wildcard.
|
|
properties:
|
|
effect:
|
|
type: string
|
|
enum: [allow, deny]
|
|
example: allow
|
|
roles:
|
|
type: array
|
|
items:
|
|
type: string
|
|
description: Subject must have at least one of these roles.
|
|
example: ["svc:payments-api"]
|
|
account_types:
|
|
type: array
|
|
items:
|
|
type: string
|
|
enum: [human, system]
|
|
description: Subject account type must be one of these.
|
|
example: ["system"]
|
|
subject_uuid:
|
|
type: string
|
|
format: uuid
|
|
description: Match only this specific subject UUID.
|
|
example: 550e8400-e29b-41d4-a716-446655440000
|
|
actions:
|
|
type: array
|
|
items:
|
|
type: string
|
|
description: |
|
|
One of the defined action constants, e.g. `pgcreds:read`,
|
|
`accounts:list`. Subject action must be in this list.
|
|
example: ["pgcreds:read"]
|
|
resource_type:
|
|
type: string
|
|
description: Resource type the rule applies to.
|
|
example: pgcreds
|
|
owner_matches_subject:
|
|
type: boolean
|
|
description: Resource owner UUID must equal the subject UUID.
|
|
example: true
|
|
service_names:
|
|
type: array
|
|
items:
|
|
type: string
|
|
description: Resource service name must be one of these.
|
|
example: ["payments-api"]
|
|
required_tags:
|
|
type: array
|
|
items:
|
|
type: string
|
|
description: Resource must have ALL of these tags.
|
|
example: ["env:staging"]
|
|
|
|
PolicyRule:
|
|
type: object
|
|
required: [id, priority, description, rule, enabled, created_at, updated_at]
|
|
properties:
|
|
id:
|
|
type: integer
|
|
example: 1
|
|
priority:
|
|
type: integer
|
|
description: Lower number = evaluated first.
|
|
example: 100
|
|
description:
|
|
type: string
|
|
example: Allow payments-api to read its own pgcreds
|
|
rule:
|
|
$ref: "#/components/schemas/RuleBody"
|
|
enabled:
|
|
type: boolean
|
|
example: true
|
|
created_at:
|
|
type: string
|
|
format: date-time
|
|
example: "2026-03-11T09:00:00Z"
|
|
updated_at:
|
|
type: string
|
|
format: date-time
|
|
example: "2026-03-11T09:00:00Z"
|
|
|
|
PGCreds:
|
|
type: object
|
|
required: [host, port, database, username, password]
|
|
properties:
|
|
host:
|
|
type: string
|
|
example: db.example.com
|
|
port:
|
|
type: integer
|
|
example: 5432
|
|
database:
|
|
type: string
|
|
example: mydb
|
|
username:
|
|
type: string
|
|
example: myuser
|
|
password:
|
|
type: string
|
|
description: >
|
|
Plaintext password (sent over TLS, stored encrypted at rest with
|
|
AES-256-GCM). Only returned to admin callers.
|
|
example: hunter2
|
|
|
|
responses:
|
|
Unauthorized:
|
|
description: Token missing, invalid, expired, or credentials incorrect.
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/Error"
|
|
example:
|
|
error: invalid credentials
|
|
code: unauthorized
|
|
|
|
Forbidden:
|
|
description: Token valid but lacks the required role.
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/Error"
|
|
example:
|
|
error: forbidden
|
|
code: forbidden
|
|
|
|
NotFound:
|
|
description: Requested resource does not exist.
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/Error"
|
|
example:
|
|
error: account not found
|
|
code: not_found
|
|
|
|
BadRequest:
|
|
description: Malformed request or missing required fields.
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/Error"
|
|
example:
|
|
error: username and password are required
|
|
code: bad_request
|
|
|
|
RateLimited:
|
|
description: Rate limit exceeded.
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/Error"
|
|
example:
|
|
error: rate limit exceeded
|
|
code: rate_limited
|
|
|
|
paths:
|
|
|
|
# ── Public ────────────────────────────────────────────────────────────────
|
|
|
|
/v1/health:
|
|
get:
|
|
summary: Health check
|
|
description: Returns `{"status":"ok"}` if the server is running. No auth required.
|
|
operationId: getHealth
|
|
tags: [Public]
|
|
responses:
|
|
"200":
|
|
description: Server is healthy.
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
status:
|
|
type: string
|
|
example: ok
|
|
|
|
/v1/keys/public:
|
|
get:
|
|
summary: Ed25519 public key (JWK)
|
|
description: |
|
|
Returns the server's Ed25519 public key in JWK format (RFC 8037).
|
|
Relying parties use this to verify JWT signatures offline.
|
|
|
|
Cache this key at startup. Refresh it if signature verification begins
|
|
failing (indicates key rotation).
|
|
|
|
**Important:** Always validate the `alg` header of the JWT (`EdDSA`)
|
|
before calling the signature verification routine. Never accept `none`.
|
|
operationId: getPublicKey
|
|
tags: [Public]
|
|
responses:
|
|
"200":
|
|
description: Ed25519 public key in JWK format.
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
required: [kty, crv, use, alg, x]
|
|
properties:
|
|
kty:
|
|
type: string
|
|
example: OKP
|
|
crv:
|
|
type: string
|
|
example: Ed25519
|
|
use:
|
|
type: string
|
|
example: sig
|
|
alg:
|
|
type: string
|
|
example: EdDSA
|
|
x:
|
|
type: string
|
|
description: Base64url-encoded public key bytes.
|
|
example: 11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo
|
|
|
|
/v1/auth/login:
|
|
post:
|
|
summary: Login
|
|
description: |
|
|
Authenticate with username + password and optionally a TOTP code.
|
|
Returns an Ed25519-signed JWT.
|
|
|
|
Rate limited to 10 requests per second per IP (burst 10).
|
|
|
|
Error responses always use the generic message `"invalid credentials"`
|
|
regardless of whether the user exists, the password is wrong, or the
|
|
account is inactive. This prevents user enumeration.
|
|
|
|
If the account has TOTP enrolled, `totp_code` is required.
|
|
Omitting it returns HTTP 401 with code `totp_required`.
|
|
operationId: login
|
|
tags: [Public]
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
required: [username, password]
|
|
properties:
|
|
username:
|
|
type: string
|
|
example: alice
|
|
password:
|
|
type: string
|
|
example: s3cr3t
|
|
totp_code:
|
|
type: string
|
|
description: Current 6-digit TOTP code. Required if TOTP is enrolled.
|
|
example: "123456"
|
|
responses:
|
|
"200":
|
|
description: Login successful. Returns JWT and expiry.
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/TokenResponse"
|
|
"400":
|
|
$ref: "#/components/responses/BadRequest"
|
|
"401":
|
|
description: Invalid credentials, inactive account, or missing TOTP code.
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/Error"
|
|
examples:
|
|
invalid_credentials:
|
|
value: {error: invalid credentials, code: unauthorized}
|
|
totp_required:
|
|
value: {error: TOTP code required, code: totp_required}
|
|
"429":
|
|
$ref: "#/components/responses/RateLimited"
|
|
|
|
/v1/token/validate:
|
|
post:
|
|
summary: Validate a JWT
|
|
description: |
|
|
Validate a JWT and return its claims. Reflects revocations immediately
|
|
(online validation). Use this for high-security paths where offline
|
|
verification is insufficient.
|
|
|
|
The token may be supplied either as a Bearer header or in the JSON body.
|
|
|
|
**Always inspect the `valid` field.** The response is always HTTP 200;
|
|
do not branch on the status code.
|
|
|
|
Rate limited to 10 requests per second per IP (burst 10).
|
|
operationId: validateToken
|
|
tags: [Public]
|
|
security:
|
|
- bearerAuth: []
|
|
- {}
|
|
requestBody:
|
|
description: Optionally supply the token in the body instead of the header.
|
|
required: false
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
token:
|
|
type: string
|
|
example: eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9...
|
|
responses:
|
|
"200":
|
|
description: Validation result. Always HTTP 200; check `valid`.
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
required: [valid]
|
|
properties:
|
|
valid:
|
|
type: boolean
|
|
sub:
|
|
type: string
|
|
format: uuid
|
|
description: Subject (account UUID). Present when valid=true.
|
|
example: 550e8400-e29b-41d4-a716-446655440000
|
|
roles:
|
|
type: array
|
|
items:
|
|
type: string
|
|
description: Role list. Present when valid=true.
|
|
example: [editor]
|
|
expires_at:
|
|
type: string
|
|
format: date-time
|
|
description: Expiry. Present when valid=true.
|
|
example: "2026-04-10T12:34:56Z"
|
|
examples:
|
|
valid:
|
|
value: {valid: true, sub: "550e8400-...", roles: [editor], expires_at: "2026-04-10T12:34:56Z"}
|
|
invalid:
|
|
value: {valid: false}
|
|
"429":
|
|
$ref: "#/components/responses/RateLimited"
|
|
|
|
# ── Authenticated ──────────────────────────────────────────────────────────
|
|
|
|
/v1/auth/logout:
|
|
post:
|
|
summary: Logout
|
|
description: |
|
|
Revoke the current bearer token immediately. The JTI is recorded in the
|
|
revocation table; subsequent validation calls will return `valid=false`.
|
|
operationId: logout
|
|
tags: [Auth]
|
|
security:
|
|
- bearerAuth: []
|
|
responses:
|
|
"204":
|
|
description: Token revoked.
|
|
"401":
|
|
$ref: "#/components/responses/Unauthorized"
|
|
|
|
/v1/auth/renew:
|
|
post:
|
|
summary: Renew token
|
|
description: |
|
|
Exchange the current token for a fresh one. The old token is revoked.
|
|
The new token reflects any role changes made since the original login.
|
|
|
|
Token expiry is recalculated: 30 days for regular users, 8 hours for
|
|
admins.
|
|
operationId: renewToken
|
|
tags: [Auth]
|
|
security:
|
|
- bearerAuth: []
|
|
responses:
|
|
"200":
|
|
description: New token issued. Old token revoked.
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/TokenResponse"
|
|
"401":
|
|
$ref: "#/components/responses/Unauthorized"
|
|
|
|
/v1/auth/totp/enroll:
|
|
post:
|
|
summary: Begin TOTP enrollment
|
|
description: |
|
|
Generate a TOTP secret for the authenticated account and return it as a
|
|
bare secret and as an `otpauth://` URI (scan with any authenticator app).
|
|
|
|
The secret is shown **once**. It is stored encrypted at rest and is not
|
|
retrievable after this call.
|
|
|
|
TOTP is not required until the enrollment is confirmed via
|
|
`POST /v1/auth/totp/confirm`. Abandoning after this call does not lock
|
|
the account.
|
|
operationId: enrollTOTP
|
|
tags: [Auth]
|
|
security:
|
|
- bearerAuth: []
|
|
responses:
|
|
"200":
|
|
description: TOTP secret generated.
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
required: [secret, otpauth_uri]
|
|
properties:
|
|
secret:
|
|
type: string
|
|
description: Base32-encoded TOTP secret. Store in an authenticator app.
|
|
example: JBSWY3DPEHPK3PXP
|
|
otpauth_uri:
|
|
type: string
|
|
description: Standard otpauth URI for QR-code generation.
|
|
example: "otpauth://totp/MCIAS:alice?secret=JBSWY3DPEHPK3PXP&issuer=MCIAS"
|
|
"401":
|
|
$ref: "#/components/responses/Unauthorized"
|
|
|
|
/v1/auth/totp/confirm:
|
|
post:
|
|
summary: Confirm TOTP enrollment
|
|
description: |
|
|
Verify the provided TOTP code against the pending secret. On success,
|
|
TOTP becomes required for all future logins for this account.
|
|
operationId: confirmTOTP
|
|
tags: [Auth]
|
|
security:
|
|
- bearerAuth: []
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
required: [code]
|
|
properties:
|
|
code:
|
|
type: string
|
|
description: Current 6-digit TOTP code.
|
|
example: "123456"
|
|
responses:
|
|
"204":
|
|
description: TOTP confirmed. Required for future logins.
|
|
"400":
|
|
$ref: "#/components/responses/BadRequest"
|
|
"401":
|
|
$ref: "#/components/responses/Unauthorized"
|
|
|
|
# ── Admin ──────────────────────────────────────────────────────────────────
|
|
|
|
/v1/auth/totp:
|
|
delete:
|
|
summary: Remove TOTP from account (admin)
|
|
description: |
|
|
Clear TOTP enrollment for an account. Use for account recovery when a
|
|
user loses their TOTP device. The account can log in with password only
|
|
after this call.
|
|
operationId: removeTOTP
|
|
tags: [Admin — Auth]
|
|
security:
|
|
- bearerAuth: []
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
required: [account_id]
|
|
properties:
|
|
account_id:
|
|
type: string
|
|
format: uuid
|
|
example: 550e8400-e29b-41d4-a716-446655440000
|
|
responses:
|
|
"204":
|
|
description: TOTP removed.
|
|
"401":
|
|
$ref: "#/components/responses/Unauthorized"
|
|
"403":
|
|
$ref: "#/components/responses/Forbidden"
|
|
"404":
|
|
$ref: "#/components/responses/NotFound"
|
|
|
|
/v1/token/issue:
|
|
post:
|
|
summary: Issue service account token (admin)
|
|
description: |
|
|
Issue a long-lived bearer token for a system account. If the account
|
|
already has an active token, it is revoked and replaced.
|
|
|
|
Only one active token exists per system account at a time.
|
|
|
|
Issued tokens expire after 1 year (configurable via
|
|
`tokens.service_expiry`).
|
|
operationId: issueServiceToken
|
|
tags: [Admin — Tokens]
|
|
security:
|
|
- bearerAuth: []
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
required: [account_id]
|
|
properties:
|
|
account_id:
|
|
type: string
|
|
format: uuid
|
|
example: 550e8400-e29b-41d4-a716-446655440000
|
|
responses:
|
|
"200":
|
|
description: Token issued.
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/TokenResponse"
|
|
"400":
|
|
$ref: "#/components/responses/BadRequest"
|
|
"401":
|
|
$ref: "#/components/responses/Unauthorized"
|
|
"403":
|
|
$ref: "#/components/responses/Forbidden"
|
|
"404":
|
|
$ref: "#/components/responses/NotFound"
|
|
|
|
/v1/token/{jti}:
|
|
delete:
|
|
summary: Revoke token by JTI (admin)
|
|
description: |
|
|
Revoke any token by its JWT ID (`jti` claim). The token is immediately
|
|
invalid for all future validation calls.
|
|
operationId: revokeToken
|
|
tags: [Admin — Tokens]
|
|
security:
|
|
- bearerAuth: []
|
|
parameters:
|
|
- name: jti
|
|
in: path
|
|
required: true
|
|
schema:
|
|
type: string
|
|
format: uuid
|
|
example: f47ac10b-58cc-4372-a567-0e02b2c3d479
|
|
responses:
|
|
"204":
|
|
description: Token revoked.
|
|
"401":
|
|
$ref: "#/components/responses/Unauthorized"
|
|
"403":
|
|
$ref: "#/components/responses/Forbidden"
|
|
"404":
|
|
$ref: "#/components/responses/NotFound"
|
|
|
|
/v1/accounts:
|
|
get:
|
|
summary: List accounts (admin)
|
|
operationId: listAccounts
|
|
tags: [Admin — Accounts]
|
|
security:
|
|
- bearerAuth: []
|
|
responses:
|
|
"200":
|
|
description: Array of accounts.
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: array
|
|
items:
|
|
$ref: "#/components/schemas/Account"
|
|
"401":
|
|
$ref: "#/components/responses/Unauthorized"
|
|
"403":
|
|
$ref: "#/components/responses/Forbidden"
|
|
|
|
post:
|
|
summary: Create account (admin)
|
|
description: |
|
|
Create a human or system account.
|
|
|
|
- `human` accounts require a `password`.
|
|
- `system` accounts must not include a `password`; authenticate via
|
|
tokens issued by `POST /v1/token/issue`.
|
|
operationId: createAccount
|
|
tags: [Admin — Accounts]
|
|
security:
|
|
- bearerAuth: []
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
required: [username, account_type]
|
|
properties:
|
|
username:
|
|
type: string
|
|
example: alice
|
|
account_type:
|
|
type: string
|
|
enum: [human, system]
|
|
example: human
|
|
password:
|
|
type: string
|
|
description: Required for human accounts. Hashed with Argon2id.
|
|
example: s3cr3t
|
|
responses:
|
|
"201":
|
|
description: Account created.
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/Account"
|
|
"400":
|
|
$ref: "#/components/responses/BadRequest"
|
|
"401":
|
|
$ref: "#/components/responses/Unauthorized"
|
|
"403":
|
|
$ref: "#/components/responses/Forbidden"
|
|
"409":
|
|
description: Username already taken.
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/Error"
|
|
example:
|
|
error: username already exists
|
|
code: conflict
|
|
|
|
/v1/accounts/{id}:
|
|
parameters:
|
|
- name: id
|
|
in: path
|
|
required: true
|
|
description: Account UUID.
|
|
schema:
|
|
type: string
|
|
format: uuid
|
|
example: 550e8400-e29b-41d4-a716-446655440000
|
|
|
|
get:
|
|
summary: Get account (admin)
|
|
operationId: getAccount
|
|
tags: [Admin — Accounts]
|
|
security:
|
|
- bearerAuth: []
|
|
responses:
|
|
"200":
|
|
description: Account details.
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/Account"
|
|
"401":
|
|
$ref: "#/components/responses/Unauthorized"
|
|
"403":
|
|
$ref: "#/components/responses/Forbidden"
|
|
"404":
|
|
$ref: "#/components/responses/NotFound"
|
|
|
|
patch:
|
|
summary: Update account (admin)
|
|
description: Update mutable account fields. Currently only `status` is patchable.
|
|
operationId: updateAccount
|
|
tags: [Admin — Accounts]
|
|
security:
|
|
- bearerAuth: []
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
status:
|
|
type: string
|
|
enum: [active, inactive]
|
|
example: inactive
|
|
responses:
|
|
"204":
|
|
description: Account updated.
|
|
"400":
|
|
$ref: "#/components/responses/BadRequest"
|
|
"401":
|
|
$ref: "#/components/responses/Unauthorized"
|
|
"403":
|
|
$ref: "#/components/responses/Forbidden"
|
|
"404":
|
|
$ref: "#/components/responses/NotFound"
|
|
|
|
delete:
|
|
summary: Delete account (admin)
|
|
description: |
|
|
Soft-delete an account. Sets status to `deleted` and revokes all active
|
|
tokens. The account record is retained for audit purposes.
|
|
operationId: deleteAccount
|
|
tags: [Admin — Accounts]
|
|
security:
|
|
- bearerAuth: []
|
|
responses:
|
|
"204":
|
|
description: Account deleted.
|
|
"401":
|
|
$ref: "#/components/responses/Unauthorized"
|
|
"403":
|
|
$ref: "#/components/responses/Forbidden"
|
|
"404":
|
|
$ref: "#/components/responses/NotFound"
|
|
|
|
/v1/accounts/{id}/roles:
|
|
parameters:
|
|
- name: id
|
|
in: path
|
|
required: true
|
|
schema:
|
|
type: string
|
|
format: uuid
|
|
example: 550e8400-e29b-41d4-a716-446655440000
|
|
|
|
get:
|
|
summary: Get account roles (admin)
|
|
operationId: getRoles
|
|
tags: [Admin — Accounts]
|
|
security:
|
|
- bearerAuth: []
|
|
responses:
|
|
"200":
|
|
description: Current role list.
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
required: [roles]
|
|
properties:
|
|
roles:
|
|
type: array
|
|
items:
|
|
type: string
|
|
example: [editor, readonly]
|
|
"401":
|
|
$ref: "#/components/responses/Unauthorized"
|
|
"403":
|
|
$ref: "#/components/responses/Forbidden"
|
|
"404":
|
|
$ref: "#/components/responses/NotFound"
|
|
|
|
put:
|
|
summary: Set account roles (admin)
|
|
description: |
|
|
Replace the account's full role list. Roles take effect in the **next**
|
|
token issued or renewed; existing tokens continue to carry the roles
|
|
embedded at issuance time.
|
|
operationId: setRoles
|
|
tags: [Admin — Accounts]
|
|
security:
|
|
- bearerAuth: []
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
required: [roles]
|
|
properties:
|
|
roles:
|
|
type: array
|
|
items:
|
|
type: string
|
|
example: [editor, readonly]
|
|
responses:
|
|
"204":
|
|
description: Roles updated.
|
|
"400":
|
|
$ref: "#/components/responses/BadRequest"
|
|
"401":
|
|
$ref: "#/components/responses/Unauthorized"
|
|
"403":
|
|
$ref: "#/components/responses/Forbidden"
|
|
"404":
|
|
$ref: "#/components/responses/NotFound"
|
|
|
|
/v1/accounts/{id}/pgcreds:
|
|
parameters:
|
|
- name: id
|
|
in: path
|
|
required: true
|
|
schema:
|
|
type: string
|
|
format: uuid
|
|
example: 550e8400-e29b-41d4-a716-446655440000
|
|
|
|
get:
|
|
summary: Get Postgres credentials (admin)
|
|
description: |
|
|
Retrieve stored Postgres connection credentials. Password is returned
|
|
in plaintext over TLS. Stored encrypted at rest with AES-256-GCM.
|
|
operationId: getPGCreds
|
|
tags: [Admin — Credentials]
|
|
security:
|
|
- bearerAuth: []
|
|
responses:
|
|
"200":
|
|
description: Postgres credentials.
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/PGCreds"
|
|
"401":
|
|
$ref: "#/components/responses/Unauthorized"
|
|
"403":
|
|
$ref: "#/components/responses/Forbidden"
|
|
"404":
|
|
$ref: "#/components/responses/NotFound"
|
|
|
|
put:
|
|
summary: Set Postgres credentials (admin)
|
|
description: Store or replace Postgres credentials for an account.
|
|
operationId: setPGCreds
|
|
tags: [Admin — Credentials]
|
|
security:
|
|
- bearerAuth: []
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/PGCreds"
|
|
responses:
|
|
"204":
|
|
description: Credentials stored.
|
|
"400":
|
|
$ref: "#/components/responses/BadRequest"
|
|
"401":
|
|
$ref: "#/components/responses/Unauthorized"
|
|
"403":
|
|
$ref: "#/components/responses/Forbidden"
|
|
"404":
|
|
$ref: "#/components/responses/NotFound"
|
|
|
|
/v1/audit:
|
|
get:
|
|
summary: Query audit log (admin)
|
|
description: |
|
|
Retrieve audit log entries, newest first. Supports pagination and
|
|
filtering. The log is append-only and never contains credentials.
|
|
|
|
Event types include: `login_ok`, `login_fail`, `login_totp_fail`,
|
|
`token_issued`, `token_renewed`, `token_revoked`, `token_expired`,
|
|
`account_created`, `account_updated`, `account_deleted`,
|
|
`role_granted`, `role_revoked`, `totp_enrolled`, `totp_removed`,
|
|
`pgcred_accessed`, `pgcred_updated`.
|
|
operationId: listAudit
|
|
tags: [Admin — Audit]
|
|
security:
|
|
- bearerAuth: []
|
|
parameters:
|
|
- name: limit
|
|
in: query
|
|
schema:
|
|
type: integer
|
|
default: 50
|
|
minimum: 1
|
|
maximum: 1000
|
|
example: 50
|
|
- name: offset
|
|
in: query
|
|
schema:
|
|
type: integer
|
|
default: 0
|
|
example: 0
|
|
- name: event_type
|
|
in: query
|
|
schema:
|
|
type: string
|
|
description: Filter by event type.
|
|
example: login_fail
|
|
- name: actor_id
|
|
in: query
|
|
schema:
|
|
type: string
|
|
format: uuid
|
|
description: Filter by actor account UUID.
|
|
example: 550e8400-e29b-41d4-a716-446655440000
|
|
responses:
|
|
"200":
|
|
description: Paginated audit log.
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
required: [events, total, limit, offset]
|
|
properties:
|
|
events:
|
|
type: array
|
|
items:
|
|
$ref: "#/components/schemas/AuditEvent"
|
|
total:
|
|
type: integer
|
|
description: Total number of matching events (for pagination).
|
|
example: 142
|
|
limit:
|
|
type: integer
|
|
example: 50
|
|
offset:
|
|
type: integer
|
|
example: 0
|
|
"401":
|
|
$ref: "#/components/responses/Unauthorized"
|
|
"403":
|
|
$ref: "#/components/responses/Forbidden"
|
|
|
|
/v1/accounts/{id}/tags:
|
|
parameters:
|
|
- name: id
|
|
in: path
|
|
required: true
|
|
schema:
|
|
type: string
|
|
format: uuid
|
|
example: 550e8400-e29b-41d4-a716-446655440000
|
|
|
|
get:
|
|
summary: Get account tags (admin)
|
|
description: |
|
|
Return the current tag set for an account. Tags are used by the policy
|
|
engine for machine/service gating (e.g. `env:production`,
|
|
`svc:payments-api`).
|
|
operationId: getAccountTags
|
|
tags: [Admin — Policy]
|
|
security:
|
|
- bearerAuth: []
|
|
responses:
|
|
"200":
|
|
description: Tag list.
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/TagsResponse"
|
|
"401":
|
|
$ref: "#/components/responses/Unauthorized"
|
|
"403":
|
|
$ref: "#/components/responses/Forbidden"
|
|
"404":
|
|
$ref: "#/components/responses/NotFound"
|
|
|
|
put:
|
|
summary: Set account tags (admin)
|
|
description: |
|
|
Replace the account's full tag set atomically. Pass an empty array to
|
|
clear all tags. Changes take effect immediately for new policy
|
|
evaluations; no token renewal is required.
|
|
operationId: setAccountTags
|
|
tags: [Admin — Policy]
|
|
security:
|
|
- bearerAuth: []
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
required: [tags]
|
|
properties:
|
|
tags:
|
|
type: array
|
|
items:
|
|
type: string
|
|
example: ["env:production", "svc:payments-api"]
|
|
responses:
|
|
"200":
|
|
description: Updated tag list.
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/TagsResponse"
|
|
"400":
|
|
$ref: "#/components/responses/BadRequest"
|
|
"401":
|
|
$ref: "#/components/responses/Unauthorized"
|
|
"403":
|
|
$ref: "#/components/responses/Forbidden"
|
|
"404":
|
|
$ref: "#/components/responses/NotFound"
|
|
|
|
/v1/policy/rules:
|
|
get:
|
|
summary: List policy rules (admin)
|
|
description: |
|
|
Return all operator-defined policy rules ordered by priority (ascending).
|
|
Built-in default rules (IDs -1 to -6) are not included.
|
|
operationId: listPolicyRules
|
|
tags: [Admin — Policy]
|
|
security:
|
|
- bearerAuth: []
|
|
responses:
|
|
"200":
|
|
description: Array of policy rules.
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: array
|
|
items:
|
|
$ref: "#/components/schemas/PolicyRule"
|
|
"401":
|
|
$ref: "#/components/responses/Unauthorized"
|
|
"403":
|
|
$ref: "#/components/responses/Forbidden"
|
|
|
|
post:
|
|
summary: Create policy rule (admin)
|
|
description: |
|
|
Create a new operator policy rule. Rules are evaluated in priority order
|
|
(lower number = evaluated first, default 100). Deny-wins: if any matching
|
|
rule has effect `deny`, access is denied regardless of allow rules.
|
|
operationId: createPolicyRule
|
|
tags: [Admin — Policy]
|
|
security:
|
|
- bearerAuth: []
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
required: [description, rule]
|
|
properties:
|
|
description:
|
|
type: string
|
|
example: Allow payments-api to read its own pgcreds
|
|
priority:
|
|
type: integer
|
|
description: Evaluation priority. Lower = first. Default 100.
|
|
example: 50
|
|
rule:
|
|
$ref: "#/components/schemas/RuleBody"
|
|
responses:
|
|
"201":
|
|
description: Rule created.
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/PolicyRule"
|
|
"400":
|
|
$ref: "#/components/responses/BadRequest"
|
|
"401":
|
|
$ref: "#/components/responses/Unauthorized"
|
|
"403":
|
|
$ref: "#/components/responses/Forbidden"
|
|
|
|
/v1/policy/rules/{id}:
|
|
parameters:
|
|
- name: id
|
|
in: path
|
|
required: true
|
|
schema:
|
|
type: integer
|
|
example: 1
|
|
|
|
get:
|
|
summary: Get policy rule (admin)
|
|
operationId: getPolicyRule
|
|
tags: [Admin — Policy]
|
|
security:
|
|
- bearerAuth: []
|
|
responses:
|
|
"200":
|
|
description: Policy rule.
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/PolicyRule"
|
|
"401":
|
|
$ref: "#/components/responses/Unauthorized"
|
|
"403":
|
|
$ref: "#/components/responses/Forbidden"
|
|
"404":
|
|
$ref: "#/components/responses/NotFound"
|
|
|
|
patch:
|
|
summary: Update policy rule (admin)
|
|
description: |
|
|
Update one or more fields of an existing policy rule. All fields are
|
|
optional; omitted fields are left unchanged.
|
|
operationId: updatePolicyRule
|
|
tags: [Admin — Policy]
|
|
security:
|
|
- bearerAuth: []
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
description:
|
|
type: string
|
|
example: Updated description
|
|
priority:
|
|
type: integer
|
|
example: 75
|
|
enabled:
|
|
type: boolean
|
|
example: false
|
|
rule:
|
|
$ref: "#/components/schemas/RuleBody"
|
|
responses:
|
|
"200":
|
|
description: Updated rule.
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/PolicyRule"
|
|
"400":
|
|
$ref: "#/components/responses/BadRequest"
|
|
"401":
|
|
$ref: "#/components/responses/Unauthorized"
|
|
"403":
|
|
$ref: "#/components/responses/Forbidden"
|
|
"404":
|
|
$ref: "#/components/responses/NotFound"
|
|
|
|
delete:
|
|
summary: Delete policy rule (admin)
|
|
description: Permanently delete a policy rule. This action cannot be undone.
|
|
operationId: deletePolicyRule
|
|
tags: [Admin — Policy]
|
|
security:
|
|
- bearerAuth: []
|
|
responses:
|
|
"204":
|
|
description: Rule deleted.
|
|
"401":
|
|
$ref: "#/components/responses/Unauthorized"
|
|
"403":
|
|
$ref: "#/components/responses/Forbidden"
|
|
"404":
|
|
$ref: "#/components/responses/NotFound"
|
|
|
|
tags:
|
|
- name: Public
|
|
description: No authentication required.
|
|
- name: Auth
|
|
description: Requires a valid bearer token.
|
|
- name: Admin — Auth
|
|
description: Requires admin role.
|
|
- name: Admin — Tokens
|
|
description: Requires admin role.
|
|
- name: Admin — Accounts
|
|
description: Requires admin role.
|
|
- name: Admin — Credentials
|
|
description: Requires admin role.
|
|
- name: Admin — Audit
|
|
description: Requires admin role.
|
|
- name: Admin — Policy
|
|
description: Requires admin role. Manage policy rules and account tags.
|