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/domains/{domain_id}/resourcesListDomainResourcesdomain#read on the Domain (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.
  • ListDomainResources pages the Resources of every Project in the Domain in ascending id order; each item names its Project in project_id. The Domain is not looked up before the domain#read check, so an absent Domain answers the same 403 as one the caller cannot read, and the operation declares no 404. The per-row filter, the provisioning hydrate, the paging and the audit row are the ones ListProjectResources applies.
  • ListProjectResources, ListDomainResources 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.
  • limit on both lists must lie in [1, 200] (default 50); a value outside the window answers 400 invalid_limit.
  • cursor on both lists 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.
ListDomainResourcesdomain_id (path)string (uuid)yesUUIDv7. Non-zero. The nil UUID → 400 invalid_domain_id.
GetResource / DeleteResourceid (path)string (uuid)yesUUIDv7. Non-zero. Malformed → 400 invalid_resource_id.
ListProjectResources / ListDomainResourcescursor (query)stringnoOpaque HMAC-signed continuation.
ListProjectResources / ListDomainResourceslimit (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>
List by Domain (top-level)pre-fetchdomain#readdomain:<domain_id>
List by Domain (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), permission_denied; the object is project:<id> on the Project list and domain:<id> on the Domain list.
  • 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_domain_id400List by DomainThe nil UUID as domain_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, ListDomainResources, GetResource, DeleteResource operations and the ResourceCreateRequest / ResourceResponse / ResourceList / ResourceProvisioning schemas.
  • ../../../internal/transport/http/v1/resources/ — the transport-tier implementation: the five 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 domain, 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.