Skip to content

Credential Assignments HTTP API

This is the reference for the Credential Assignments 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.

A Credential Assignment binds a Cloud Credential to a Project through an approver-gated lifecycle. A requester opens an assignment in the requested state; an approver who is not the requester moves it to approved (which materialises the binding) or rejected; an operator later moves an approved assignment to revoked (which tears the binding down). The surface adds five operations: a request, a cursor-paginated list, and the three decision verbs. The carrying OpenAPI tag is cloud — Credential Assignments sit alongside the Cloud Inventory and Cloud Credentials surfaces.

Every gate authorises against the owning Project: manage is the mutation grant and observe is the lowest-privilege read-equivalent. The ReBAC chain the gates resolve against is the project definition in schema/authz.zed. For the cursor-paginated list idiom this surface inherits, see cloud-credentials.md — Cloud Credentials and Credential Assignments share the same HMAC-signed caller-bound cursor mechanism.

Operations

MethodPathOperation IDReBAC gateAudit relationOutbox eventBody cap
POST/v1/projects/{id}/credential-assignmentsRequestCredentialAssignmentproject#manage on the parent Project (BEFORE the persistence write)credential_assignment.request (on the denial path)CredentialAssignmentRequested (emitted by the application service)8 KiB
GET/v1/projects/{id}/credential-assignmentsListCredentialAssignmentstop-level project#observe on the parent Project (BEFORE the persistence read) + per-row project#observe filtercredential_assignment.list (granted, with post-filter cohort size)(none)n/a
POST/v1/credential-assignments/{id}/approveApproveCredentialAssignmentproject#manage on the owning Project (AFTER an unavoidable pre-authz row read)credential_assignment.approve (on the denial path)CredentialAssignmentApproved (emitted by the application service)n/a
POST/v1/credential-assignments/{id}/rejectRejectCredentialAssignmentproject#manage on the owning Project (AFTER an unavoidable pre-authz row read)credential_assignment.reject (on the denial path)CredentialAssignmentRejected (emitted by the application service)8 KiB
POST/v1/credential-assignments/{id}/revokeRevokeCredentialAssignmentproject#manage on the owning Project (AFTER an unavoidable pre-authz row read)credential_assignment.revoke (on the denial path)CredentialAssignmentRevoked (emitted by the application service)8 KiB
  • body_cap = 8 KiB (MaxCredentialAssignmentRequestBodyBytes in internal/transport/http/v1/credentialassignments/wiring.go) is enforced before the JSON decoder runs on the request and decision bodies; an over-cap body surfaces as 413 request_body_too_large.
  • ListCredentialAssignments.limit is clamped at the handler to [1, 200] with default 50.
  • ListCredentialAssignments.cursor is opaque, HMAC-signed by the server 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 page renders in creation order.
  • RequestCredentialAssignment runs the project#manage gate before the persistence write, then delegates to the Credential Assignment application service, which records the requested row and appends the CredentialAssignmentRequested outbox event in a single transaction.
  • ListCredentialAssignments runs a top-level project#observe check on the parent Project before the persistence read so an unauthorised caller never observes the existence side-channel of the Project's assignment set, then layers a per-row project#observe filter so the items array is the subset the caller is authorised to see.
  • ApproveCredentialAssignment, RejectCredentialAssignment, and RevokeCredentialAssignment must read the persistence row before the ReBAC check because the assignment id on the path does not encode its owning Project — the gate target is unknowable until the row is read.
  • The application service does not emit transport-level audit rows for the successful mutations; success is observable through the lifecycle outbox events. The handlers stamp the credential_assignment.* audit relations on their permission-denied paths so a probe is recorded.

Lifecycle and the self-approval rule

The assignment state is a stored column (CredentialAssignmentState) advanced only by the application service's transition rules:

FromVerbToEffect
(new)requestrequestedOpens the assignment; binding not yet live.
requestedapproveapprovedMaterialises the binding (materialised: true).
requestedrejectrejectedCloses an unapproved request.
approvedrevokerevokedTears the materialised binding down.
  • A verb attempted from any source state other than the one above returns 409 illegal_transition. Approval and rejection are legal only from requested; revocation is legal only from approved.
  • The materialised flag is true only while the assignment is in the approved state; false for requested, rejected, and revoked.
  • Self-approval is forbidden. The caller who requested an assignment may not approve their own request — self_approval_denied is returned with 403 so the request/approval split stays a true separation-of-duties control.
  • Opening a second request for the same (Project, Cloud Credential) pair while an earlier one is still live is rejected with 409 duplicate_live_assignment. A Cloud Credential that is not in an assignable lifecycle state is rejected with 422 credential_not_assignable.

Path & query parameters

OperationParameterTypeRequiredNotes
RequestCredentialAssignment / ListCredentialAssignmentsid (path)string (uuid)yesOwning Project UUIDv7. The shared ProjectID parameter component. Malformed → 400 invalid_project_id.
ApproveCredentialAssignment / RejectCredentialAssignment / RevokeCredentialAssignmentid (path)string (uuid)yesCredential Assignment UUIDv7. The shared CredentialAssignmentID parameter component. Malformed → 400 invalid_credential_assignment_id.
ListCredentialAssignmentscursor (query)stringnoOpaque HMAC-signed continuation. Tampered → 400 invalid_cursor; cross-caller replay → 403 cursor_binding_mismatch.
ListCredentialAssignmentslimit (query)integerno[1, 200], default 50. Out-of-range → 400 invalid_limit.

Schemas

The OpenAPI spec is the authoritative source for field shapes. The schemas this surface uses are:

  • Request: CredentialAssignmentRequest (a single cloud_credential_id UUID).
  • Decision: CredentialAssignmentDecisionRequest (a single non-empty reason string, 1..1024 characters; recorded on the reject / revoke outbox event as an audit string).
  • Response: CredentialAssignmentResponse (single), CredentialAssignmentList (paged, with optional nullable next_cursor).
  • Embedded: CredentialAssignmentState (closed enum: requested, approved, rejected, revoked).

The CredentialAssignmentResponse carries the resolved id, project_id, cloud_credential_id, state, the derived materialised flag, and the created_at / updated_at lifecycle timestamps. The shape is shared by all five operations so clients only need one binding.

RequestCredentialAssignment returns 201 with a Location header naming the canonical URL of the freshly opened assignment (/v1/credential-assignments/{id}); the three decision verbs return 200 with the updated projection.

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; the self_approval_denied and cursor_binding_mismatch 403s are plain Problem bodies.

CodeStatusWhereMeaning
invalid_project_id400Request / ListProject {id} was not a non-zero UUID.
invalid_credential_assignment_id400Approve / Reject / RevokeAssignment {id} was not a non-zero UUID.
invalid_cloud_credential_id400Requestcloud_credential_id was not a non-zero UUID.
invalid_body400Request / Reject / RevokeBody could not be read or did not parse as the expected schema.
invalid_decision_reason400Reject / Revokereason was empty or whitespace-only.
invalid_limit400ListOut of [1, 200].
invalid_cursor400ListHMAC verification or structural decode failed.
unauthenticated401every operationRequest carries no authenticated principal.
self_approval_denied403ApproveThe caller is the requester of this assignment and may not approve their own request.
cursor_binding_mismatch403ListCursor was minted for a different caller (per-(caller, pepper) HMAC binding rejected the replay).
credential_assignment_not_found404Approve / Reject / RevokeNo Credential Assignment with the given {id}.
duplicate_live_assignment409RequestA live assignment already exists for the same (Project, Cloud Credential) pair.
illegal_transition409Approve / Reject / RevokeThe assignment is not in a state from which the verb is legal.
credential_not_assignable422RequestThe named Cloud Credential is not in an assignable lifecycle state.
request_body_too_large413Request / Reject / RevokeBody exceeded the 8 KiB Credential Assignment ceiling.
credential_assignments_not_provisioned501every operationThe composition root has not wired the Credential Assignment dependency bundle yet.
internal500every operationServer-side failure path.

A 403 permission-denied response carries the extended PermissionDenied fields documented in authz.md.

Cross-references

  • ../../../api/openapi/plexsphere-v1.yaml — OpenAPI 3.1 spec; the *CredentialAssignment* operations and the CredentialAssignmentRequest / CredentialAssignmentDecisionRequest / CredentialAssignmentResponse / CredentialAssignmentList / CredentialAssignmentState schemas.
  • ../../../internal/transport/http/v1/credentialassignments/ — the transport-tier implementation: the five handlers, the closed Problem.code taxonomy, the metadata projection, the body-cap and limit-clamp constants, and the per-row visibility filter on the list path.
  • ../../../schema/authz.zed — ReBAC schema; the project definition declares the manage and observe permissions this surface gates on.
  • cloud-credentials.md — the Cloud Credentials read + revoke surface that supplies the Cloud Credential an assignment binds, with the same HMAC-signed caller-bound cursor pagination idiom.
  • clouds.md — the Cloud Inventory CRUD surface that anchors the Cloud a credential belongs to.
  • authz.md — the PermissionDenied shape returned on 403.