Fix OpenAPI spec parsing errors in Swagger UI
- Replace type: [string, "null"] array syntax with
type: string + nullable: true on AuditEvent.actor_id,
AuditEvent.target_id, PolicyRule.not_before, and
PolicyRule.expires_at; Swagger UI 5 cannot parse the
JSON Schema array form
- Add missing username field to /v1/token/validate response
schema (added to handler in d6cc827 but never synced)
- Add missing GET /v1/pgcreds endpoint to spec
- Sync web/static/openapi.yaml (served file) with root;
the static copy was many commits out of date, missing
all policy/tags schemas and endpoints
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
82
openapi.yaml
82
openapi.yaml
@@ -100,13 +100,15 @@ components:
|
|||||||
format: date-time
|
format: date-time
|
||||||
example: "2026-03-11T09:01:23Z"
|
example: "2026-03-11T09:01:23Z"
|
||||||
actor_id:
|
actor_id:
|
||||||
type: [string, "null"]
|
type: string
|
||||||
format: uuid
|
format: uuid
|
||||||
|
nullable: true
|
||||||
description: UUID of the account that performed the action. Null for bootstrap events.
|
description: UUID of the account that performed the action. Null for bootstrap events.
|
||||||
example: 550e8400-e29b-41d4-a716-446655440000
|
example: 550e8400-e29b-41d4-a716-446655440000
|
||||||
target_id:
|
target_id:
|
||||||
type: [string, "null"]
|
type: string
|
||||||
format: uuid
|
format: uuid
|
||||||
|
nullable: true
|
||||||
description: UUID of the affected account, if applicable.
|
description: UUID of the affected account, if applicable.
|
||||||
ip_address:
|
ip_address:
|
||||||
type: string
|
type: string
|
||||||
@@ -205,16 +207,18 @@ components:
|
|||||||
type: boolean
|
type: boolean
|
||||||
example: true
|
example: true
|
||||||
not_before:
|
not_before:
|
||||||
type: [string, "null"]
|
type: string
|
||||||
format: date-time
|
format: date-time
|
||||||
|
nullable: true
|
||||||
description: |
|
description: |
|
||||||
Earliest time the rule becomes active. NULL means no constraint
|
Earliest time the rule becomes active. NULL means no constraint
|
||||||
(always active). Rules where `not_before > now()` are skipped
|
(always active). Rules where `not_before > now()` are skipped
|
||||||
during evaluation.
|
during evaluation.
|
||||||
example: "2026-04-01T00:00:00Z"
|
example: "2026-04-01T00:00:00Z"
|
||||||
expires_at:
|
expires_at:
|
||||||
type: [string, "null"]
|
type: string
|
||||||
format: date-time
|
format: date-time
|
||||||
|
nullable: true
|
||||||
description: |
|
description: |
|
||||||
Time after which the rule is no longer active. NULL means no
|
Time after which the rule is no longer active. NULL means no
|
||||||
constraint (never expires). Rules where expires_at is in the past
|
constraint (never expires). Rules where expires_at is in the past
|
||||||
@@ -602,6 +606,10 @@ paths:
|
|||||||
format: uuid
|
format: uuid
|
||||||
description: Subject (account UUID). Present when valid=true.
|
description: Subject (account UUID). Present when valid=true.
|
||||||
example: 550e8400-e29b-41d4-a716-446655440000
|
example: 550e8400-e29b-41d4-a716-446655440000
|
||||||
|
username:
|
||||||
|
type: string
|
||||||
|
description: Account username. Present when valid=true and the account exists.
|
||||||
|
example: alice
|
||||||
roles:
|
roles:
|
||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
@@ -615,7 +623,7 @@ paths:
|
|||||||
example: "2026-04-10T12:34:56Z"
|
example: "2026-04-10T12:34:56Z"
|
||||||
examples:
|
examples:
|
||||||
valid:
|
valid:
|
||||||
value: {valid: true, sub: "550e8400-...", roles: [editor], expires_at: "2026-04-10T12:34:56Z"}
|
value: {valid: true, sub: "550e8400-...", username: alice, roles: [editor], expires_at: "2026-04-10T12:34:56Z"}
|
||||||
invalid:
|
invalid:
|
||||||
value: {valid: false}
|
value: {valid: false}
|
||||||
"429":
|
"429":
|
||||||
@@ -1263,6 +1271,70 @@ paths:
|
|||||||
"404":
|
"404":
|
||||||
$ref: "#/components/responses/NotFound"
|
$ref: "#/components/responses/NotFound"
|
||||||
|
|
||||||
|
/v1/pgcreds:
|
||||||
|
get:
|
||||||
|
summary: List accessible Postgres credentials
|
||||||
|
description: |
|
||||||
|
Return all Postgres credentials accessible to the authenticated account:
|
||||||
|
credentials owned by the account plus any explicitly granted by an admin.
|
||||||
|
|
||||||
|
The `id` field is the credential record ID; use it together with the
|
||||||
|
`service_account_id` to fetch full details via
|
||||||
|
`GET /v1/accounts/{id}/pgcreds`. Passwords are **not** returned by this
|
||||||
|
endpoint.
|
||||||
|
operationId: listAccessiblePGCreds
|
||||||
|
tags: [Admin — Credentials]
|
||||||
|
security:
|
||||||
|
- bearerAuth: []
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Array of accessible Postgres credential summaries.
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: object
|
||||||
|
required: [id, service_account_id, host, port, database, username, created_at, updated_at]
|
||||||
|
properties:
|
||||||
|
id:
|
||||||
|
type: integer
|
||||||
|
description: Credential record ID.
|
||||||
|
example: 7
|
||||||
|
service_account_id:
|
||||||
|
type: string
|
||||||
|
format: uuid
|
||||||
|
description: UUID of the system account that owns these credentials.
|
||||||
|
example: 550e8400-e29b-41d4-a716-446655440000
|
||||||
|
service_account_name:
|
||||||
|
type: string
|
||||||
|
description: Username of the owning system account (omitted if unavailable).
|
||||||
|
example: payments-api
|
||||||
|
host:
|
||||||
|
type: string
|
||||||
|
example: db.example.com
|
||||||
|
port:
|
||||||
|
type: integer
|
||||||
|
example: 5432
|
||||||
|
database:
|
||||||
|
type: string
|
||||||
|
example: mydb
|
||||||
|
username:
|
||||||
|
type: string
|
||||||
|
example: myuser
|
||||||
|
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"
|
||||||
|
"401":
|
||||||
|
$ref: "#/components/responses/Unauthorized"
|
||||||
|
"503":
|
||||||
|
$ref: "#/components/responses/VaultSealed"
|
||||||
|
|
||||||
/v1/audit:
|
/v1/audit:
|
||||||
get:
|
get:
|
||||||
summary: Query audit log (admin)
|
summary: Query audit log (admin)
|
||||||
|
|||||||
@@ -118,6 +118,121 @@ components:
|
|||||||
description: JSON blob with event-specific metadata. Never contains credentials.
|
description: JSON blob with event-specific metadata. Never contains credentials.
|
||||||
example: '{"jti":"f47ac10b-..."}'
|
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
|
||||||
|
not_before:
|
||||||
|
type: string
|
||||||
|
format: date-time
|
||||||
|
nullable: true
|
||||||
|
description: |
|
||||||
|
Earliest time the rule becomes active. NULL means no constraint
|
||||||
|
(always active). Rules where `not_before > now()` are skipped
|
||||||
|
during evaluation.
|
||||||
|
example: "2026-04-01T00:00:00Z"
|
||||||
|
expires_at:
|
||||||
|
type: string
|
||||||
|
format: date-time
|
||||||
|
nullable: true
|
||||||
|
description: |
|
||||||
|
Time after which the rule is no longer active. NULL means no
|
||||||
|
constraint (never expires). Rules where expires_at is in the past
|
||||||
|
are skipped during evaluation.
|
||||||
|
example: "2026-06-01T00:00:00Z"
|
||||||
|
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:
|
PGCreds:
|
||||||
type: object
|
type: object
|
||||||
required: [host, port, database, username, password]
|
required: [host, port, database, username, password]
|
||||||
@@ -192,6 +307,18 @@ components:
|
|||||||
error: rate limit exceeded
|
error: rate limit exceeded
|
||||||
code: rate_limited
|
code: rate_limited
|
||||||
|
|
||||||
|
VaultSealed:
|
||||||
|
description: |
|
||||||
|
The vault is sealed. The server is running but has no key material.
|
||||||
|
Unseal via `POST /v1/vault/unseal` before retrying.
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/Error"
|
||||||
|
example:
|
||||||
|
error: vault is sealed
|
||||||
|
code: vault_sealed
|
||||||
|
|
||||||
paths:
|
paths:
|
||||||
|
|
||||||
# ── Public ────────────────────────────────────────────────────────────────
|
# ── Public ────────────────────────────────────────────────────────────────
|
||||||
@@ -200,14 +327,16 @@ paths:
|
|||||||
get:
|
get:
|
||||||
summary: Health check
|
summary: Health check
|
||||||
description: |
|
description: |
|
||||||
Returns `{"status":"ok"}` if the server is running and the vault is
|
Returns server health status. Always returns HTTP 200, even when the
|
||||||
unsealed, or `{"status":"sealed"}` if the vault is sealed.
|
vault is sealed. No auth required.
|
||||||
No auth required.
|
|
||||||
|
When the vault is sealed, `status` is `"sealed"` and most other
|
||||||
|
endpoints return 503. When healthy, `status` is `"ok"`.
|
||||||
operationId: getHealth
|
operationId: getHealth
|
||||||
tags: [Public]
|
tags: [Public]
|
||||||
responses:
|
responses:
|
||||||
"200":
|
"200":
|
||||||
description: Server is healthy (may be sealed).
|
description: Server is running (check `status` for sealed state).
|
||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
@@ -218,84 +347,6 @@ paths:
|
|||||||
enum: [ok, sealed]
|
enum: [ok, sealed]
|
||||||
example: ok
|
example: ok
|
||||||
|
|
||||||
/v1/vault/status:
|
|
||||||
get:
|
|
||||||
summary: Vault seal status
|
|
||||||
description: Returns `{"sealed": true}` or `{"sealed": false}`. No auth required.
|
|
||||||
operationId: getVaultStatus
|
|
||||||
tags: [Vault]
|
|
||||||
responses:
|
|
||||||
"200":
|
|
||||||
description: Current seal state.
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
sealed:
|
|
||||||
type: boolean
|
|
||||||
|
|
||||||
/v1/vault/unseal:
|
|
||||||
post:
|
|
||||||
summary: Unseal the vault
|
|
||||||
description: |
|
|
||||||
Accepts a passphrase, derives the master key, and unseals the vault.
|
|
||||||
Rate-limited to 3 requests per second, burst of 5.
|
|
||||||
No auth required (the vault is sealed, so no tokens can be validated).
|
|
||||||
operationId: unsealVault
|
|
||||||
tags: [Vault]
|
|
||||||
requestBody:
|
|
||||||
required: true
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
type: object
|
|
||||||
required: [passphrase]
|
|
||||||
properties:
|
|
||||||
passphrase:
|
|
||||||
type: string
|
|
||||||
description: Master passphrase for key derivation.
|
|
||||||
responses:
|
|
||||||
"200":
|
|
||||||
description: Vault unsealed successfully.
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
status:
|
|
||||||
type: string
|
|
||||||
example: unsealed
|
|
||||||
"401":
|
|
||||||
description: Unseal failed (wrong passphrase).
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
$ref: "#/components/schemas/Error"
|
|
||||||
|
|
||||||
/v1/vault/seal:
|
|
||||||
post:
|
|
||||||
summary: Seal the vault
|
|
||||||
description: |
|
|
||||||
Seals the vault, zeroing all key material in memory.
|
|
||||||
Requires admin authentication. The caller's token becomes invalid
|
|
||||||
after sealing.
|
|
||||||
operationId: sealVault
|
|
||||||
tags: [Vault]
|
|
||||||
security:
|
|
||||||
- bearerAuth: []
|
|
||||||
responses:
|
|
||||||
"200":
|
|
||||||
description: Vault sealed successfully.
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
status:
|
|
||||||
type: string
|
|
||||||
example: sealed
|
|
||||||
|
|
||||||
/v1/keys/public:
|
/v1/keys/public:
|
||||||
get:
|
get:
|
||||||
summary: Ed25519 public key (JWK)
|
summary: Ed25519 public key (JWK)
|
||||||
@@ -336,6 +387,121 @@ paths:
|
|||||||
description: Base64url-encoded public key bytes.
|
description: Base64url-encoded public key bytes.
|
||||||
example: 11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo
|
example: 11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo
|
||||||
|
|
||||||
|
/v1/vault/status:
|
||||||
|
get:
|
||||||
|
summary: Vault seal status
|
||||||
|
description: |
|
||||||
|
Returns whether the vault is currently sealed. Always accessible,
|
||||||
|
even when sealed. No auth required.
|
||||||
|
|
||||||
|
Clients should poll this after startup or after a 503 `vault_sealed`
|
||||||
|
response to determine when to attempt an unseal.
|
||||||
|
operationId: getVaultStatus
|
||||||
|
tags: [Public]
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Current vault seal state.
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
required: [sealed]
|
||||||
|
properties:
|
||||||
|
sealed:
|
||||||
|
type: boolean
|
||||||
|
example: false
|
||||||
|
|
||||||
|
/v1/vault/unseal:
|
||||||
|
post:
|
||||||
|
summary: Unseal the vault
|
||||||
|
description: |
|
||||||
|
Provide the master passphrase to derive the encryption key, decrypt
|
||||||
|
the Ed25519 signing key, and unseal the vault. Once unsealed, all
|
||||||
|
other endpoints become available.
|
||||||
|
|
||||||
|
Rate limited to 3 requests per second per IP (burst 5) to limit
|
||||||
|
brute-force attempts against the passphrase.
|
||||||
|
|
||||||
|
The passphrase is never logged. A generic `"unseal failed"` error
|
||||||
|
is returned for any failure (wrong passphrase, vault already unsealed
|
||||||
|
mid-flight, etc.) to avoid leaking information.
|
||||||
|
operationId: unsealVault
|
||||||
|
tags: [Public]
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
required: [passphrase]
|
||||||
|
properties:
|
||||||
|
passphrase:
|
||||||
|
type: string
|
||||||
|
description: Master passphrase used to derive the encryption key.
|
||||||
|
example: correct-horse-battery-staple
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Vault unsealed (or was already unsealed).
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
status:
|
||||||
|
type: string
|
||||||
|
enum: [unsealed, already unsealed]
|
||||||
|
example: unsealed
|
||||||
|
"400":
|
||||||
|
$ref: "#/components/responses/BadRequest"
|
||||||
|
"401":
|
||||||
|
description: Wrong passphrase or key decryption failure.
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/Error"
|
||||||
|
example:
|
||||||
|
error: unseal failed
|
||||||
|
code: unauthorized
|
||||||
|
"429":
|
||||||
|
$ref: "#/components/responses/RateLimited"
|
||||||
|
|
||||||
|
/v1/vault/seal:
|
||||||
|
post:
|
||||||
|
summary: Seal the vault (admin)
|
||||||
|
description: |
|
||||||
|
Zero all key material in memory and transition the server to the
|
||||||
|
sealed state. After this call:
|
||||||
|
|
||||||
|
- All subsequent requests (except health, vault status, and unseal)
|
||||||
|
return 503 `vault_sealed`.
|
||||||
|
- The caller's own JWT is immediately invalidated because the public
|
||||||
|
key needed to verify it is no longer held in memory.
|
||||||
|
- The server can be unsealed again via `POST /v1/vault/unseal`.
|
||||||
|
|
||||||
|
This is an emergency operation. Use it to protect key material if a
|
||||||
|
compromise is suspected. It does **not** restart the server or wipe
|
||||||
|
the database.
|
||||||
|
operationId: sealVault
|
||||||
|
tags: [Admin — Vault]
|
||||||
|
security:
|
||||||
|
- bearerAuth: []
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Vault sealed (or was already sealed).
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
status:
|
||||||
|
type: string
|
||||||
|
enum: [sealed, already sealed]
|
||||||
|
example: sealed
|
||||||
|
"401":
|
||||||
|
$ref: "#/components/responses/Unauthorized"
|
||||||
|
"403":
|
||||||
|
$ref: "#/components/responses/Forbidden"
|
||||||
|
|
||||||
/v1/auth/login:
|
/v1/auth/login:
|
||||||
post:
|
post:
|
||||||
summary: Login
|
summary: Login
|
||||||
@@ -440,6 +606,10 @@ paths:
|
|||||||
format: uuid
|
format: uuid
|
||||||
description: Subject (account UUID). Present when valid=true.
|
description: Subject (account UUID). Present when valid=true.
|
||||||
example: 550e8400-e29b-41d4-a716-446655440000
|
example: 550e8400-e29b-41d4-a716-446655440000
|
||||||
|
username:
|
||||||
|
type: string
|
||||||
|
description: Account username. Present when valid=true and the account exists.
|
||||||
|
example: alice
|
||||||
roles:
|
roles:
|
||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
@@ -453,7 +623,7 @@ paths:
|
|||||||
example: "2026-04-10T12:34:56Z"
|
example: "2026-04-10T12:34:56Z"
|
||||||
examples:
|
examples:
|
||||||
valid:
|
valid:
|
||||||
value: {valid: true, sub: "550e8400-...", roles: [editor], expires_at: "2026-04-10T12:34:56Z"}
|
value: {valid: true, sub: "550e8400-...", username: alice, roles: [editor], expires_at: "2026-04-10T12:34:56Z"}
|
||||||
invalid:
|
invalid:
|
||||||
value: {valid: false}
|
value: {valid: false}
|
||||||
"429":
|
"429":
|
||||||
@@ -578,6 +748,68 @@ paths:
|
|||||||
"401":
|
"401":
|
||||||
$ref: "#/components/responses/Unauthorized"
|
$ref: "#/components/responses/Unauthorized"
|
||||||
|
|
||||||
|
/v1/auth/password:
|
||||||
|
put:
|
||||||
|
summary: Change own password (self-service)
|
||||||
|
description: |
|
||||||
|
Change the password of the currently authenticated human account.
|
||||||
|
The caller must supply the correct `current_password` to prevent
|
||||||
|
token-theft attacks: possession of a valid JWT alone is not sufficient.
|
||||||
|
|
||||||
|
On success:
|
||||||
|
- The stored Argon2id hash is replaced with the new password hash.
|
||||||
|
- All active sessions *except* the caller's current token are revoked.
|
||||||
|
- The lockout failure counter is cleared.
|
||||||
|
|
||||||
|
On failure (wrong current password):
|
||||||
|
- A login failure is recorded against the account, subject to the
|
||||||
|
same lockout rules as `POST /v1/auth/login`.
|
||||||
|
|
||||||
|
Only applies to human accounts. System accounts have no password.
|
||||||
|
operationId: changePassword
|
||||||
|
tags: [Auth]
|
||||||
|
security:
|
||||||
|
- bearerAuth: []
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
required: [current_password, new_password]
|
||||||
|
properties:
|
||||||
|
current_password:
|
||||||
|
type: string
|
||||||
|
description: The account's current password (required for verification).
|
||||||
|
example: old-s3cr3t
|
||||||
|
new_password:
|
||||||
|
type: string
|
||||||
|
description: The new password. Minimum 12 characters.
|
||||||
|
example: new-s3cr3t-long
|
||||||
|
responses:
|
||||||
|
"204":
|
||||||
|
description: Password changed. Other active sessions revoked.
|
||||||
|
"400":
|
||||||
|
$ref: "#/components/responses/BadRequest"
|
||||||
|
"401":
|
||||||
|
description: Current password is incorrect.
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/Error"
|
||||||
|
example:
|
||||||
|
error: current password is incorrect
|
||||||
|
code: unauthorized
|
||||||
|
"429":
|
||||||
|
description: Account temporarily locked due to too many failed attempts.
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/Error"
|
||||||
|
example:
|
||||||
|
error: account temporarily locked
|
||||||
|
code: account_locked
|
||||||
|
|
||||||
# ── Admin ──────────────────────────────────────────────────────────────────
|
# ── Admin ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/v1/auth/totp:
|
/v1/auth/totp:
|
||||||
@@ -911,6 +1143,76 @@ paths:
|
|||||||
"404":
|
"404":
|
||||||
$ref: "#/components/responses/NotFound"
|
$ref: "#/components/responses/NotFound"
|
||||||
|
|
||||||
|
post:
|
||||||
|
summary: Grant a role to an account (admin)
|
||||||
|
description: |
|
||||||
|
Add a single role to an account's role set. If the role already exists,
|
||||||
|
this is a no-op. Roles take effect in the **next** token issued or
|
||||||
|
renewed; existing tokens continue to carry the roles embedded at
|
||||||
|
issuance time.
|
||||||
|
operationId: grantRole
|
||||||
|
tags: [Admin — Accounts]
|
||||||
|
security:
|
||||||
|
- bearerAuth: []
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
required: [role]
|
||||||
|
properties:
|
||||||
|
role:
|
||||||
|
type: string
|
||||||
|
example: editor
|
||||||
|
responses:
|
||||||
|
"204":
|
||||||
|
description: Role granted.
|
||||||
|
"400":
|
||||||
|
$ref: "#/components/responses/BadRequest"
|
||||||
|
"401":
|
||||||
|
$ref: "#/components/responses/Unauthorized"
|
||||||
|
"403":
|
||||||
|
$ref: "#/components/responses/Forbidden"
|
||||||
|
"404":
|
||||||
|
$ref: "#/components/responses/NotFound"
|
||||||
|
|
||||||
|
/v1/accounts/{id}/roles/{role}:
|
||||||
|
parameters:
|
||||||
|
- name: id
|
||||||
|
in: path
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
format: uuid
|
||||||
|
example: 550e8400-e29b-41d4-a716-446655440000
|
||||||
|
- name: role
|
||||||
|
in: path
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
example: editor
|
||||||
|
|
||||||
|
delete:
|
||||||
|
summary: Revoke a role from an account (admin)
|
||||||
|
description: |
|
||||||
|
Remove a single role from an account's role set. Roles take effect in
|
||||||
|
the **next** token issued or renewed; existing tokens continue to carry
|
||||||
|
the roles embedded at issuance time.
|
||||||
|
operationId: revokeRole
|
||||||
|
tags: [Admin — Accounts]
|
||||||
|
security:
|
||||||
|
- bearerAuth: []
|
||||||
|
responses:
|
||||||
|
"204":
|
||||||
|
description: Role revoked.
|
||||||
|
"401":
|
||||||
|
$ref: "#/components/responses/Unauthorized"
|
||||||
|
"403":
|
||||||
|
$ref: "#/components/responses/Forbidden"
|
||||||
|
"404":
|
||||||
|
$ref: "#/components/responses/NotFound"
|
||||||
|
|
||||||
/v1/accounts/{id}/pgcreds:
|
/v1/accounts/{id}/pgcreds:
|
||||||
parameters:
|
parameters:
|
||||||
- name: id
|
- name: id
|
||||||
@@ -969,6 +1271,70 @@ paths:
|
|||||||
"404":
|
"404":
|
||||||
$ref: "#/components/responses/NotFound"
|
$ref: "#/components/responses/NotFound"
|
||||||
|
|
||||||
|
/v1/pgcreds:
|
||||||
|
get:
|
||||||
|
summary: List accessible Postgres credentials
|
||||||
|
description: |
|
||||||
|
Return all Postgres credentials accessible to the authenticated account:
|
||||||
|
credentials owned by the account plus any explicitly granted by an admin.
|
||||||
|
|
||||||
|
The `id` field is the credential record ID; use it together with the
|
||||||
|
`service_account_id` to fetch full details via
|
||||||
|
`GET /v1/accounts/{id}/pgcreds`. Passwords are **not** returned by this
|
||||||
|
endpoint.
|
||||||
|
operationId: listAccessiblePGCreds
|
||||||
|
tags: [Admin — Credentials]
|
||||||
|
security:
|
||||||
|
- bearerAuth: []
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Array of accessible Postgres credential summaries.
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: object
|
||||||
|
required: [id, service_account_id, host, port, database, username, created_at, updated_at]
|
||||||
|
properties:
|
||||||
|
id:
|
||||||
|
type: integer
|
||||||
|
description: Credential record ID.
|
||||||
|
example: 7
|
||||||
|
service_account_id:
|
||||||
|
type: string
|
||||||
|
format: uuid
|
||||||
|
description: UUID of the system account that owns these credentials.
|
||||||
|
example: 550e8400-e29b-41d4-a716-446655440000
|
||||||
|
service_account_name:
|
||||||
|
type: string
|
||||||
|
description: Username of the owning system account (omitted if unavailable).
|
||||||
|
example: payments-api
|
||||||
|
host:
|
||||||
|
type: string
|
||||||
|
example: db.example.com
|
||||||
|
port:
|
||||||
|
type: integer
|
||||||
|
example: 5432
|
||||||
|
database:
|
||||||
|
type: string
|
||||||
|
example: mydb
|
||||||
|
username:
|
||||||
|
type: string
|
||||||
|
example: myuser
|
||||||
|
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"
|
||||||
|
"401":
|
||||||
|
$ref: "#/components/responses/Unauthorized"
|
||||||
|
"503":
|
||||||
|
$ref: "#/components/responses/VaultSealed"
|
||||||
|
|
||||||
/v1/audit:
|
/v1/audit:
|
||||||
get:
|
get:
|
||||||
summary: Query audit log (admin)
|
summary: Query audit log (admin)
|
||||||
@@ -980,7 +1346,10 @@ paths:
|
|||||||
`token_issued`, `token_renewed`, `token_revoked`, `token_expired`,
|
`token_issued`, `token_renewed`, `token_revoked`, `token_expired`,
|
||||||
`account_created`, `account_updated`, `account_deleted`,
|
`account_created`, `account_updated`, `account_deleted`,
|
||||||
`role_granted`, `role_revoked`, `totp_enrolled`, `totp_removed`,
|
`role_granted`, `role_revoked`, `totp_enrolled`, `totp_removed`,
|
||||||
`pgcred_accessed`, `pgcred_updated`.
|
`pgcred_accessed`, `pgcred_updated`, `pgcred_access_granted`,
|
||||||
|
`pgcred_access_revoked`, `tag_added`, `tag_removed`,
|
||||||
|
`policy_rule_created`, `policy_rule_updated`, `policy_rule_deleted`,
|
||||||
|
`policy_deny`, `vault_sealed`, `vault_unsealed`.
|
||||||
operationId: listAudit
|
operationId: listAudit
|
||||||
tags: [Admin — Audit]
|
tags: [Admin — Audit]
|
||||||
security:
|
security:
|
||||||
@@ -1041,6 +1410,310 @@ paths:
|
|||||||
"403":
|
"403":
|
||||||
$ref: "#/components/responses/Forbidden"
|
$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/accounts/{id}/password:
|
||||||
|
parameters:
|
||||||
|
- name: id
|
||||||
|
in: path
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
format: uuid
|
||||||
|
example: 550e8400-e29b-41d4-a716-446655440000
|
||||||
|
|
||||||
|
put:
|
||||||
|
summary: Admin password reset (admin)
|
||||||
|
description: |
|
||||||
|
Reset the password for a human account without requiring the current
|
||||||
|
password. This is intended for account recovery (e.g. a user forgot
|
||||||
|
their password).
|
||||||
|
|
||||||
|
On success:
|
||||||
|
- The stored Argon2id hash is replaced with the new password hash.
|
||||||
|
- All active sessions for the target account are revoked.
|
||||||
|
|
||||||
|
Only applies to human accounts. The new password must be at least
|
||||||
|
12 characters.
|
||||||
|
operationId: adminSetPassword
|
||||||
|
tags: [Admin — Accounts]
|
||||||
|
security:
|
||||||
|
- bearerAuth: []
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
required: [new_password]
|
||||||
|
properties:
|
||||||
|
new_password:
|
||||||
|
type: string
|
||||||
|
description: The new password. Minimum 12 characters.
|
||||||
|
example: new-s3cr3t-long
|
||||||
|
responses:
|
||||||
|
"204":
|
||||||
|
description: Password reset. All active sessions for the account revoked.
|
||||||
|
"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 -7) 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"
|
||||||
|
not_before:
|
||||||
|
type: string
|
||||||
|
format: date-time
|
||||||
|
description: Earliest activation time (RFC3339, optional).
|
||||||
|
example: "2026-04-01T00:00:00Z"
|
||||||
|
expires_at:
|
||||||
|
type: string
|
||||||
|
format: date-time
|
||||||
|
description: Expiry time (RFC3339, optional).
|
||||||
|
example: "2026-06-01T00:00:00Z"
|
||||||
|
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"
|
||||||
|
not_before:
|
||||||
|
type: string
|
||||||
|
format: date-time
|
||||||
|
description: Set earliest activation time (RFC3339).
|
||||||
|
example: "2026-04-01T00:00:00Z"
|
||||||
|
expires_at:
|
||||||
|
type: string
|
||||||
|
format: date-time
|
||||||
|
description: Set expiry time (RFC3339).
|
||||||
|
example: "2026-06-01T00:00:00Z"
|
||||||
|
clear_not_before:
|
||||||
|
type: boolean
|
||||||
|
description: Set to true to remove not_before constraint.
|
||||||
|
clear_expires_at:
|
||||||
|
type: boolean
|
||||||
|
description: Set to true to remove expires_at constraint.
|
||||||
|
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:
|
tags:
|
||||||
- name: Public
|
- name: Public
|
||||||
description: No authentication required.
|
description: No authentication required.
|
||||||
@@ -1056,3 +1729,7 @@ tags:
|
|||||||
description: Requires admin role.
|
description: Requires admin role.
|
||||||
- name: Admin — Audit
|
- name: Admin — Audit
|
||||||
description: Requires admin role.
|
description: Requires admin role.
|
||||||
|
- name: Admin — Policy
|
||||||
|
description: Requires admin role. Manage policy rules and account tags.
|
||||||
|
- name: Admin — Vault
|
||||||
|
description: Requires admin role. Emergency vault seal operation.
|
||||||
|
|||||||
Reference in New Issue
Block a user