Skip to content

Resources HTTP API

This is the reference for the Tenancy Resource HTTP surface. It maps each operation to its OpenAPI schema, ReBAC gate, audit emission, 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 Resource aggregate spans two origins:

  • Adopted (BYO) — the operator registers an already-running workload. Create persists a row and emits a ResourceCreated outbox event; the Broker is NEVER called.
  • Provisioned — the operator selects a BlueprintVersion + Cloud Credential and declares the desired workload. Create persists the Resource row, then asks the Broker to mint a Pending ProvisionedResource; the operator polls GET /v1/resources/{id} to observe phase advances.

Operations

MethodPathOperation IDReBAC gateAudit relationOutbox eventBody cap
POST/v1/projects/{project_id}/resourcesCreateResourceproject#deploy (pre-decode); plus cloudcredential#use (project subject) on provisioned flowresource.create.{adopted,provisioned}ResourceCreated (+ ProvisionedResourceRequested on provisioned)8 KiB
GET/v1/projects/{project_id}/resourcesListProjectResourcesproject#observe on the parent Project (top-level) + per-row resource#observe filterresource.list (granted, with post-filter item_count)(none)n/a
GET/v1/resources/{id}GetResourceresource#observe (BEFORE persistence read)resource.get(none)n/a
DELETE/v1/resources/{id}DeleteResourceresource#manageresource.delete.{adopted,provisioned}ResourceRemoved (adopted) / ProvisionedResourceDeleting (provisioned, emitted by the broker)n/a
  • body_cap = 8 KiB is enforced before the JSON decoder runs; an over-cap body surfaces as 413 request_body_too_large.
  • CreateResource returns:
    • 201 Created with a ResourceResponse body on the adopted flow.
    • 202 Accepted with a Location: /v1/resources/{id} header on the provisioned flow; the body carries the hydrated ResourceResponse with an embedded provisioning.phase = pending.
  • ListProjectResources and GetResource attach a provisioning sub-object only when the Resource origin is provisioned and the composition root wired a ProvisioningReader port.
  • DeleteResource branches on origin:
    • Adopted → remove the Resource row + ResourceRemoved outbox event; responds 204 No Content.
    • Provisioned → record a Broker Deprovision request and respond 202 Accepted; the row stays until the broker finalises.
  • ListProjectResources.limit is clamped at the handler to [1, 200] with default 50.
  • ListProjectResources.cursor is opaque, HMAC-signed by the server through the CursorCodec port; tampered → 400 invalid_cursor; cross-user replay → 403 cursor_binding_mismatch.

Path & query parameters

OperationParameterTypeRequiredNotes
CreateResource / ListProjectResourcesproject_id (path)string (uuid)yesUUIDv7. Non-zero. Malformed → 400 invalid_project_id.
GetResource / DeleteResourceid (path)string (uuid)yesUUIDv7. Non-zero. Malformed → 400 invalid_resource_id.
ListProjectResourcescursor (query)stringnoOpaque HMAC-signed continuation.
ListProjectResourceslimit (query)integerno[1, 200], default 50.

Schemas

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

  • Request: ResourceCreateRequest (origin discriminator selects between the adopted and provisioned bodies).
  • Response: ResourceResponse (single, with optional provisioning sub-object), ResourceList (paged).
  • Embedded: ResourceProvisioning (provisioned_resource_id, phase).

The origin discriminator is a closed enum: adopted | provisioned. An adopted body sets kind and external_ref. A provisioned body sets kind, cloud_credential_id, blueprint_version_id, and a parameters map validated by the BlueprintVersion's parameter schema. A body whose origin is not in the closed set surfaces as 400 invalid_resource_origin.

ReBAC gates

OperationPre-conditionSchema permissionObject
Create (any origin)pre-decodeproject#deployproject:<project_id>
Create (provisioned only)post-decode, pre-persistcloudcredential#use (subject is project:<project_id>)cloudcredential:<cloud_credential_id>
List (top-level)pre-fetchproject#observeproject:<project_id>
List (per row)post-fetchresource#observeresource:<id>
Getpre-persistresource#observeresource:<id>
Deletepre-persistresource#manageresource:<id>

The Create handler runs the project#deploy gate before decoding the body so a probe cannot exercise the JSON decoder. The provisioned flow layers a secondary cloudcredential#use check before the Resource row is persisted, so an unauthorised caller never produces a Resource row OR a broker-side ProvisionedResource. That secondary check uses the consuming project:<project_id> as its subject, not the calling user: the schema's uses relation admits only project (or project#operator) subjects, and an approved Credential Assignment materialises exactly the cloudcredential:<id>#uses@project:<id> tuple, so the gate asks "does this project hold an approved assignment?" rather than "does this user?".

Audit emission

Every grant and every denial emits an audit row through the transport-local AuditSink; the tenancy repository does NOT emit on writes. The relation strings are pinned in internal/transport/http/v1/resources/wiring.go:

  • resource.create.adopted / resource.create.provisioned — granted, permission_denied, invariant_violation.
  • resource.list — granted (carries item_count).
  • resource.get — granted, permission_denied.
  • resource.delete.adopted / resource.delete.provisioned — granted, permission_denied, invariant_violation.

Error taxonomy

All error responses use the shared Problem envelope. The 403 path uses the richer PermissionDenied shape.

CodeStatusWhereMeaning
invalid_project_id400Create / ListMalformed or zero project_id.
invalid_resource_id400Get / DeleteMalformed or zero resource id.
invalid_resource_origin400Createorigin outside {adopted, provisioned}.
invalid_resource400CreateAggregate invariant rejected the body (empty kind, provisioned-branch missing cloud_credential_id/blueprint_version_id, etc.).
invalid_body400CreateBody could not be read or did not parse as the typed request shape.
invalid_cursor400ListHMAC verification failed.
invalid_limit400ListOut of [1, 200].
unauthenticated401every operationRequest carries no authenticated principal.
cursor_binding_mismatch403ListCursor was minted by a different caller; per-(caller, pepper) HMAC binding rejected the replay.
resource_not_found404Get / DeleteNo Resource with the given id. The pre-persistence ReBAC gate returns 403; this surface returns 404 only after the gate has passed.
cloud_credential_not_found422Create (provisioned)cloud_credential_id does not resolve.
blueprint_not_found422Create (provisioned)blueprint_version_id does not resolve.
request_body_too_large413CreateBody exceeded 8 KiB.
resources_not_provisioned501every operationComposition root did not wire the Resources port.
internal500every operationServer-side failure path.

Cross-references

  • ../../../api/openapi/plexsphere-v1.yaml — OpenAPI 3.1 spec; the CreateResource, ListProjectResources, GetResource, DeleteResource operations and the ResourceCreateRequest / ResourceResponse / ResourceList / ResourceProvisioning schemas.
  • ../../../internal/transport/http/v1/resources/ — the transport-tier implementation: the four handlers, the closed Problem.code taxonomy, the body-cap and limit-clamp constants, the per-row visibility filter, and the origin-branch dispatch.
  • ../../../schema/authz.zed — ReBAC schema; the project, resource, and cloudcredential definitions declare the gates this surface enforces.
  • ./blueprints.md — companion surface for the Blueprint Catalog the provisioned flow consumes.
  • ./authz.md — the PermissionDenied shape returned on 403.