Appearance
Cloud Credential Assignments HTTP API
This is the reference for the Credential Assignment request + decision HTTP surface. It maps each operation to its OpenAPI schema, ReBAC gate, audit emission, lifecycle 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 transport-tier implementation lives in internal/transport/http/v1/credentialassignments/, and the lifecycle rules every transition obeys are owned by the application service in internal/provisioning/credentialassignment/.
A Credential Assignment binds one Cloud Credential to one consuming Project. The binding is request-and-approve: a request opens in the requested state and is materialised — wired into the ReBAC graph as a cloudcredential#uses tuple — only when a second principal moves it to approved. For the ReBAC model behind the gates below — the cloudcredential#uses relation, the dual-write tuple sync, and the state machine — see the explanation page ../../contexts/provisioning/rebac.md.
Operations
| Method | Path | Operation ID | ReBAC gate | Audit relation | Outbox event | Body cap |
|---|---|---|---|---|---|---|
| POST | /v1/projects/{id}/credential-assignments | RequestCredentialAssignment | Project admin OR maintainer | credential_assignment.request | CredentialAssignmentRequested | 8 KiB |
| GET | /v1/projects/{id}/credential-assignments | ListCredentialAssignments | Project read (top-level + per-row filter) | credential_assignment.list | (none) | n/a |
| POST | /v1/credential-assignments/{id}/approve | ApproveCredentialAssignment | Cloud Credential assign (+ self-approval denial) | credential_assignment.approve | CredentialAssignmentMaterialised | n/a |
| POST | /v1/credential-assignments/{id}/reject | RejectCredentialAssignment | Cloud Credential assign | credential_assignment.reject | CredentialAssignmentRejected | 8 KiB |
| POST | /v1/credential-assignments/{id}/revoke | RevokeCredentialAssignment | Cloud Credential assign | credential_assignment.revoke | CredentialAssignmentRevoked | 8 KiB |
body_cap = 8 KiB(MaxCredentialAssignmentRequestBodyBytesininternal/transport/http/v1/credentialassignments/wiring.go) is enforced before the JSON decoder runs; an over-cap body surfaces as413 request_body_too_large.ApproveCredentialAssignmentandListCredentialAssignmentscarry no request body, so the cap does not apply.RequestCredentialAssignmentis gated by a dual ReBAC check on the parent Project:adminis checked first,maintaineronly ifadminis denied, so a granted admin costs one round-trip. Either relation is sufficient — a Project maintainer may open a request.ApproveCredentialAssignment,RejectCredentialAssignment, andRevokeCredentialAssignmentresolve the parent assignment row to reach the bound Cloud Credential, then gate on that credential'sassignpermission (owner + assigneron thecloudcredentialdefinition). Owning the parent Cloud is not sufficient —assigndoes not derive fromparent.ApproveCredentialAssignmentcarries one extra gate: the approving principal must not be the principal that requested the assignment. A self-approval is rejected with403 self_approval_deniedso every assignment is decided by a second party.ListCredentialAssignmentsruns a single top-levelreadcheck on the owning Project, then layers a per-rowreadvisibility filter on the creation-ordered persistence window — the response page is the subset the caller is authorised to see.next_cursoris set whenever the persistence layer returned a full page regardless of how many rows the per-row filter dropped.ListCredentialAssignments.cursoris opaque and HMAC-signed, bound to the per-(caller, pepper) pseudonym. A tampered or unknown-version envelope surfaces as400 invalid_cursor; a cursor minted by a different caller surfaces as403 cursor_binding_mismatch.ListCredentialAssignments.limitis clamped at the handler to[1, 200]with default50.- Each transition is
authz-checked before the persistence write or read, so an unauthorised caller receives403without the existence side-channel a "load-then-check" flow would leak. - The audit relations above are the verb strings the transport handler stamps on its
application/problem-side audit rows; the application service additionally records a domain-layer audit row per granted transition — see../../contexts/provisioning/rebac.md.
Path & query parameters
| Operation | Parameter | Type | Required | Notes |
|---|---|---|---|---|
| RequestCredentialAssignment / ListCredentialAssignments | id (path) | string (uuid) | yes | Project identifier. UUIDv7, non-zero. Malformed → 400 invalid_project_id. The ProjectID parameter component. |
| ApproveCredentialAssignment / RejectCredentialAssignment / RevokeCredentialAssignment | id (path) | string (uuid) | yes | Credential Assignment identifier. UUIDv7, non-zero. Malformed → 400 invalid_credential_assignment_id. The CredentialAssignmentID parameter component. |
| ListCredentialAssignments | cursor (query) | string | no | Opaque HMAC-signed continuation token from a prior next_cursor. Tampered → 400 invalid_cursor; replayed by another caller → 403 cursor_binding_mismatch. |
| ListCredentialAssignments | limit (query) | integer | no | [1, 200], default 50. The handler clamps out-of-range values rather than rejecting them. |
Request schemas
CredentialAssignmentRequest
Body for POST /v1/projects/{id}/credential-assignments. additionalProperties is false.
| Field | Type | Required | Notes |
|---|---|---|---|
cloud_credential_id | string (uuid) | yes | Identifier of the Cloud Credential to bind to the Project. Must be a non-zero UUID; a malformed value is rejected with 400 invalid_cloud_credential_id. |
CredentialAssignmentDecisionRequest
Body for POST /v1/credential-assignments/{id}/reject and POST /v1/credential-assignments/{id}/revoke. additionalProperties is false. ApproveCredentialAssignment takes no body.
| Field | Type | Required | Notes |
|---|---|---|---|
reason | string | yes | Decision rationale recorded on the lifecycle outbox event as an approver- or operator-supplied audit string. minLength: 1, maxLength: 1024. An empty or whitespace-only value is rejected with 400 invalid_decision_reason. |
Response schemas
CredentialAssignmentResponse
The metadata projection of a Credential Assignment. The same shape is returned by all four mutating operations and as each element of a ListCredentialAssignments page, so a client needs only one binding. Every field is required.
| Field | Type | Notes |
|---|---|---|
id | string (uuid) | Credential Assignment identifier (UUIDv7). |
project_id | string (uuid) | Owning Project — the residency pivot the ReBAC gate authorises against. |
cloud_credential_id | string (uuid) | The Cloud Credential bound to the Project by this assignment. |
state | CredentialAssignmentState | Lifecycle state — see the enum below. |
materialised | boolean | Whether the binding is currently live. true only while state is approved; false for requested, rejected, and revoked. |
created_at | string (date-time) | Aggregate creation timestamp (UTC). |
updated_at | string (date-time) | Last-modified timestamp (UTC). Bumped by every lifecycle transition. |
CredentialAssignmentList
The page returned by GET /v1/projects/{id}/credential-assignments.
| Field | Type | Required | Notes |
|---|---|---|---|
items | array<CredentialAssignmentResponse> | yes | Credential Assignments in the current page, in creation order, filtered to the subset the caller may read. |
next_cursor | string | null | no | Continuation token for the next page. null or omitted at end-of-stream. HMAC-signed by the server. |
CredentialAssignmentState
Closed enum. The state is a stored column advanced only by the application service's transition rules.
| Value | Meaning |
|---|---|
requested | Opening state — the assignment has been asked for and awaits a decision. |
approved | A reviewer accepted the request; the cloudcredential#uses binding is materialised. |
rejected | A reviewer declined the request. Terminal. |
revoked | A previously approved assignment was withdrawn; the binding is torn down. Terminal. |
The legal transitions are requested → approved, requested → rejected, and approved → revoked. Any other transition is rejected with 409 illegal_transition.
Error taxonomy
All error responses use the shared Problem envelope (application/problem+json, RFC 9457). The 403 authorisation path uses the richer PermissionDenied shape carrying the ReBAC denial reason, relation_path, and request correlation_id.
The Credential Assignment surface adds the closed taxonomy below. Each code maps to exactly one repo / service / transport sentinel and one HTTP status.
| Code | Status | Where | Meaning |
|---|---|---|---|
invalid_project_id | 400 | Request / List | Path {id} on /v1/projects/{id}/credential-assignments was not a non-zero UUID. |
invalid_credential_assignment_id | 400 | Approve / Reject / Revoke | Path {id} on /v1/credential-assignments/{id}/… was not a non-zero UUID. |
invalid_cloud_credential_id | 400 | Request | Body cloud_credential_id was not a non-zero UUID. |
invalid_decision_reason | 400 | Reject / Revoke | Body reason was empty or whitespace-only. |
invalid_body | 400 | Request / Reject / Revoke | Body could not be read or did not parse as the typed request shape. |
invalid_cursor | 400 | List | Cursor HMAC verification failed or the version byte was unknown. |
invalid_limit | 400 | List | limit query parameter was non-numeric (out-of-range values are clamped, not rejected). |
unauthenticated | 401 | every operation | Request carries no authenticated principal. |
self_approval_denied | 403 | Approve | The caller is the principal that requested this assignment and may not approve their own request. |
cursor_binding_mismatch | 403 | List | The pagination cursor was minted by a different caller; the per-(caller, pepper) HMAC binding rejected the replay. |
credential_assignment_not_found | 404 | Approve / Reject / Revoke | No Credential Assignment with the given {id}. |
duplicate_live_assignment | 409 | Request | A live Credential Assignment already exists for the same (Project, Cloud Credential) pair. |
illegal_transition | 409 | Approve / Reject / Revoke | The requested lifecycle transition is not legal from the assignment's current state. |
credential_not_assignable | 422 | Request | The named Cloud Credential is not in an assignable lifecycle state. |
request_body_too_large | 413 | Request / Reject / Revoke | Body exceeded the 8 KiB ceiling. |
credential_assignments_not_provisioned | 501 | every operation | The composition root has not wired the Credential Assignment dependency bundle yet. |
internal | 500 | every operation | Server-side failure path. |
A 403 authorisation refusal (the caller lacks the ReBAC gate for the operation) carries Problem.code = permission_denied plus the extended PermissionDenied fields documented in ../api/authz.md. self_approval_denied and cursor_binding_mismatch are also 403 but use the plain Problem envelope.
Cross-references
../../../api/openapi/plexsphere-v1.yaml— OpenAPI 3.1 spec; the*CredentialAssignment*operations and theCredentialAssignmentRequest/CredentialAssignmentDecisionRequest/CredentialAssignmentResponse/CredentialAssignmentList/CredentialAssignmentStateschemas.../../../internal/transport/http/v1/credentialassignments/— the transport-tier implementation: the five handlers, the closedProblem.codetaxonomy, the body-cap and limit-clamp constants, and the per-row visibility filter onListCredentialAssignments.../../../internal/provisioning/credentialassignment/— the Credential Assignment bounded sub-context: the aggregate, the lifecycle transitions, the five domain events, and the application service that owns the credential-assignability and self-approval guards.../../../schema/authz.zed— ReBAC schema; thecloudcredentialdefinition declares theassignpermission and theusesrelation a materialised assignment writes.../../contexts/provisioning/rebac.md— the explanation page: thecloudcredential#usesrelation, the request/approve/reject/revoke state machine, the dual-write tuple sync, and the event-to-tuple mapping.../../how-to/provisioning/assign-a-cloud-credential.md— the operator how-to walking the request → approve → use → revoke flow withcurl../cloud-credential-pool.md— the Cloud Credentials Custodian reference; the lifecycle of thecloudcredentialaggregate this surface assigns.../api/authz.md— thePermissionDeniedshape returned on a 403 authorisation refusal.