Skip to content

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.zedmanage 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

MethodPathOperation IDReBAC gateAudit relationOutbox eventBody cap
GET/v1/clouds/{id}/cloud-credentialsListCloudCredentialstop-level cloud#observe on the parent Cloud (BEFORE the persistence read) + per-row cloud#observe filtercloud_credential.list (granted, with post-filter item_count)(none)n/a
GET/v1/cloud-credentials/{id}GetCloudCredentialcloud#observe on the owning Cloud (AFTER an unavoidable pre-authz row read)cloud_credential.read(none)n/a
POST/v1/cloud-credentials/{id}/revokeRevokeCloudCredentialcloud#manage on the owning Cloudcloud_credential.revokeCloudCredentialRevoked (emitted by the Custodian)8 KiB
  • body_cap = 8 KiB (MaxCloudCredentialRequestBodyBytes in internal/transport/http/v1/cloudcredentials/wiring.go) is enforced before the JSON decoder runs on the revoke body; an over-cap body surfaces as 413 request_body_too_large.
  • ListCloudCredentials.limit is clamped at the handler to [1, 200] with default 50.
  • ListCloudCredentials.cursor is opaque, HMAC-signed by the server through the CursorCodec port and bound to the per-(caller, pepper) pseudonym; a tampered cursor surfaces as 400 invalid_cursor and a cursor minted by one caller and replayed by another surfaces as 403 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.
  • ListCloudCredentials runs a top-level cloud#observe check 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-row cloud#observe filter for defence-in-depth. The next_cursor is 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.
  • GetCloudCredential must 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 (a 404 versus a 403 for a guessed UUID) is documented in the handler's DECISION block; the 403 is emitted via the audit-first permission-denied path so the probe is recorded.
  • RevokeCloudCredential reads the row to resolve the parent Cloud, runs the cloud#manage gate, then delegates to the Custodian. Revocation is idempotent: revoking an already-revoked credential returns 200 with the unchanged metadata rather than a conflict, mirroring the domain Custodian.Revoke / Repository.RevokealreadyRevoked contract.

Path & query parameters

OperationParameterTypeRequiredNotes
ListCloudCredentialsid (path)string (uuid)yesParent Cloud UUIDv7. Non-zero. Malformed → 400 invalid_cloud_id. The shared CloudID parameter component.
ListCloudCredentialscursor (query)stringnoOpaque HMAC-signed continuation. Tampered → 400 invalid_cursor; cross-caller replay → 403 cursor_binding_mismatch.
ListCloudCredentialslimit (query)integerno[1, 200], default 50. Out-of-range → 400 invalid_limit.
GetCloudCredential / RevokeCloudCredentialid (path)string (uuid)yesCloud 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-empty reason string).
  • 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:

  • revoked when revoked_at is set — this wins over expired even when both timestamps are populated, because the deliberate operator action is the more salient lifecycle fact than passive expiry.
  • otherwise expired when expired_at is set OR expires_at is 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.

CodeStatusWhereMeaning
invalid_cloud_id400ListParent Cloud {id} was not a non-zero UUID.
invalid_cloud_credential_id400Get / RevokeCredential {id} was not a non-zero UUID.
invalid_limit400ListOut of [1, 200].
invalid_cursor400ListHMAC verification or structural decode failed.
invalid_body400RevokeBody could not be read or did not parse as CloudCredentialRevokeRequest.
invalid_revoke_reason400Revokereason was empty or whitespace-only.
unauthenticated401every operationRequest carries no authenticated principal.
cursor_binding_mismatch403ListCursor was minted for a different caller (per-(caller, pepper) HMAC binding rejected the replay).
cloud_credential_not_found404Get / RevokeNo Cloud Credential with the given {id}.
request_body_too_large413RevokeBody exceeded the 8 KiB Cloud Credentials ceiling.
cloud_credentials_not_provisioned501every operationThe composition root has not wired the read + revoke dependency bundle yet.
internal500every operationServer-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 the CloudCredentialResponse / CloudCredentialList / CloudCredentialStatus / CloudCredentialRevokeRequest schemas.
  • ../../../internal/transport/http/v1/cloudcredentials/ — the transport-tier implementation: the three handlers, the closed Problem.code taxonomy, 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; the cloud definition declares the manage / operate / observe permissions this surface gates on.
  • ../api/clouds.md — sibling Cloud Inventory CRUD surface, with the same HMAC-signed caller-bound cursor pagination idiom and the same observe / manage ReBAC gates.
  • ../api/authz.md — the PermissionDenied shape returned on 403.