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. GetResource reads the single backing row; ListProjectResources hydrates the whole page's provisioned rows through one batch read keyed by resource id, so a page stays at one broker round-trip regardless of its width — never a per-row fan-out. A reader fault on either path degrades to a response without the provisioning sub-object rather than failing the read: a flaky broker must not mask the canonical Resource projection.
  • 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>; needs the Credential Assignment and a Cloud Assignment on the parent Cloud)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, so the gate asks what the project may do rather than what the user may do.

The one check asks two questions, because the schema defines cloudcredential#use as (uses & parent->use) + owner + assigner. The project must hold both an approved Credential Assignment (which materialises cloudcredential:<id>#uses@project:<id>) and an approved Cloud Assignment on the credential's parent Cloud (which materialises cloud:<parent>#uses@project:<id>). A denial does not report which half is missing; the credential-assignment surface refuses a missing Cloud Assignment earlier, with 422 cloud_not_usable_in_project — see Credential Assignments.

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.
blueprint_provider_mismatch422Create (provisioned)Both ids resolve, but the Blueprint version accepts none of the provider kinds corresponding to the provider of the Cloud behind the credential. Refused before the ProvisionedResource is minted; the Resource created for the request is removed again. errors[].field names both blueprint_version_id and cloud_credential_id, since either can be changed to make the pair provisionable.
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.