Appearance
Cloud Credentials HTTP API
This is the reference for the operator-facing Cloud Credentials read + revoke HTTP surface. It maps each operation to its OpenAPI schema, ReBAC gate, audit emission, outbox event, and the closed Problem.code taxonomy. The wire-contract origin is api/openapi/plexsphere-v1.yaml; this doc is a map, not a duplicate contract.
The Cloud Credentials Custodian is otherwise an in-process bounded context — issuance and rotation have no HTTP surface and remain in-process only. This surface adds exactly three operations: two reads and one operator revoke trigger. The revoke handler delegates to the Custodian, which stays the only mutation entry point into the context. The ReBAC chain the gates below resolve against is the cloud definition in schema/authz.zed — manage is owner-only, operate adds the operator relation, and observe adds the auditor relation. There is no cloud#read; the read paths gate on observe, the lowest-privilege read-equivalent. For the cursor-paginated list idiom this surface inherits, see ../api/clouds.md — Clouds and Cloud Credentials share the same HMAC-signed caller-bound cursor mechanism.
Operations
| Method | Path | Operation ID | ReBAC gate | Audit relation | Outbox event | Body cap |
|---|---|---|---|---|---|---|
| GET | /v1/clouds/{id}/cloud-credentials | ListCloudCredentials | top-level cloud#observe on the parent Cloud (BEFORE the persistence read) + per-row cloud#observe filter | cloud_credential.list (granted, with post-filter item_count) | (none) | n/a |
| GET | /v1/cloud-credentials/{id} | GetCloudCredential | cloud#observe on the owning Cloud (AFTER an unavoidable pre-authz row read) | cloud_credential.read | (none) | n/a |
| POST | /v1/cloud-credentials/{id}/revoke | RevokeCloudCredential | cloud#manage on the owning Cloud | cloud_credential.revoke | CloudCredentialRevoked (emitted by the Custodian) | 8 KiB |
body_cap = 8 KiB(MaxCloudCredentialRequestBodyBytesininternal/transport/http/v1/cloudcredentials/wiring.go) is enforced before the JSON decoder runs on the revoke body; an over-cap body surfaces as413 request_body_too_large.ListCloudCredentials.limitis clamped at the handler to[1, 200]with default50.ListCloudCredentials.cursoris opaque, HMAC-signed by the server through theCursorCodecport and bound to the per-(caller, pepper) pseudonym; a tampered cursor surfaces as400 invalid_cursorand a cursor minted by one caller and replayed by another surfaces as403 cursor_binding_mismatch. The keyset is(created_at, cloud_credential_id)so the page renders in the order the credentials were created with a deterministic tie-break.ListCloudCredentialsruns a top-levelcloud#observecheck on the parent Cloud before the persistence read so an unauthorised caller never observes the existence side-channel of the Cloud's credential set, then layers a per-rowcloud#observefilter for defence-in-depth. Thenext_cursoris set whenever the persistence layer returned a full page regardless of how many rows the per-row filter dropped, so a thin authorised cohort still pages forward.GetCloudCredentialmust read the persistence row before the ReBAC check because the credential id on the path does not encode its owning Cloud — the gate target is unknowable until the row is read. The accepted narrow existence side-channel (a404versus a403for a guessed UUID) is documented in the handler's DECISION block; the403is emitted via the audit-first permission-denied path so the probe is recorded.RevokeCloudCredentialreads the row to resolve the parent Cloud, runs thecloud#managegate, then delegates to the Custodian. Revocation is idempotent: revoking an already-revoked credential returns200with the unchanged metadata rather than a conflict, mirroring the domainCustodian.Revoke/Repository.RevokealreadyRevokedcontract.
Path & query parameters
| Operation | Parameter | Type | Required | Notes |
|---|---|---|---|---|
| ListCloudCredentials | id (path) | string (uuid) | yes | Parent Cloud UUIDv7. Non-zero. Malformed → 400 invalid_cloud_id. The shared CloudID parameter component. |
| ListCloudCredentials | cursor (query) | string | no | Opaque HMAC-signed continuation. Tampered → 400 invalid_cursor; cross-caller replay → 403 cursor_binding_mismatch. |
| ListCloudCredentials | limit (query) | integer | no | [1, 200], default 50. Out-of-range → 400 invalid_limit. |
| GetCloudCredential / RevokeCloudCredential | id (path) | string (uuid) | yes | Cloud Credential UUIDv7. Non-zero. Malformed → 400 invalid_cloud_credential_id. The shared CloudCredentialID parameter component. |
Schemas
The OpenAPI spec is the authoritative source for field shapes. The schemas this surface uses are:
- Request:
CloudCredentialRevokeRequest(a single non-emptyreasonstring). - Response:
CloudCredentialResponse(single),CloudCredentialList(paged). - Embedded:
CloudCredentialStatus(closed enum:active,expired,revoked).
The response carries the resolved id, cloud_id, display_name, version, derived status, expires_at, nullable revoked_at, nullable expired_at, and the created_at / updated_at lifecycle timestamps. The shape is shared by GetCloudCredential, ListCloudCredentials, and RevokeCloudCredential so clients only need one binding.
Derived status
status is computed by the read surface from the lifecycle timestamps; it is not a stored column. The precedence is fixed:
revokedwhenrevoked_atis set — this wins overexpiredeven when both timestamps are populated, because the deliberate operator action is the more salient lifecycle fact than passive expiry.- otherwise
expiredwhenexpired_atis set ORexpires_atis in the past. - otherwise
active.
Omitted KV fields
The projection is deliberately metadata-only. The KV mount, KV path, KV version, and every byte of secret material are never exposed on this surface — the storage location is a storage-internal detail an operator has no reason to see, and leaking it would widen the credential's blast radius. The omission is structural: the transport-tier view type carries no kv_* field, so the projection cannot leak the storage location even by accident.
Error taxonomy
All error responses use the shared Problem envelope (application/problem+json). The 403 path uses the richer PermissionDenied shape carrying the ReBAC denial reason and request correlation_id.
| Code | Status | Where | Meaning |
|---|---|---|---|
invalid_cloud_id | 400 | List | Parent Cloud {id} was not a non-zero UUID. |
invalid_cloud_credential_id | 400 | Get / Revoke | Credential {id} was not a non-zero UUID. |
invalid_limit | 400 | List | Out of [1, 200]. |
invalid_cursor | 400 | List | HMAC verification or structural decode failed. |
invalid_body | 400 | Revoke | Body could not be read or did not parse as CloudCredentialRevokeRequest. |
invalid_revoke_reason | 400 | Revoke | reason was empty or whitespace-only. |
unauthenticated | 401 | every operation | Request carries no authenticated principal. |
cursor_binding_mismatch | 403 | List | Cursor was minted for a different caller (per-(caller, pepper) HMAC binding rejected the replay). |
cloud_credential_not_found | 404 | Get / Revoke | No Cloud Credential with the given {id}. |
request_body_too_large | 413 | Revoke | Body exceeded the 8 KiB Cloud Credentials ceiling. |
cloud_credentials_not_provisioned | 501 | every operation | The composition root has not wired the read + revoke dependency bundle yet. |
internal | 500 | every operation | Server-side failure path. |
A 403 permission-denied response carries the extended PermissionDenied fields documented in ../api/authz.md.
Cross-references
../../../api/openapi/plexsphere-v1.yaml— OpenAPI 3.1 spec; the*CloudCredential*operations and theCloudCredentialResponse/CloudCredentialList/CloudCredentialStatus/CloudCredentialRevokeRequestschemas.../../../internal/transport/http/v1/cloudcredentials/— the transport-tier implementation: the three handlers, the closedProblem.codetaxonomy, the metadata-only projection, the body-cap and limit-clamp constants, and the per-row visibility filter on the list path.../../../schema/authz.zed— ReBAC schema; theclouddefinition declares themanage/operate/observepermissions this surface gates on.../api/clouds.md— sibling Cloud Inventory CRUD surface, with the same HMAC-signed caller-bound cursor pagination idiom and the sameobserve/manageReBAC gates.../api/authz.md— thePermissionDeniedshape returned on 403.