Appearance
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.
Createpersists a row and emits aResourceCreatedoutbox event; the Broker is NEVER called. - Provisioned — the operator selects a BlueprintVersion + Cloud Credential and declares the desired workload.
Createpersists the Resource row, then asks the Broker to mint a Pending ProvisionedResource; the operator pollsGET /v1/resources/{id}to observe phase advances.
Operations
| Method | Path | Operation ID | ReBAC gate | Audit relation | Outbox event | Body cap |
|---|---|---|---|---|---|---|
| POST | /v1/projects/{project_id}/resources | CreateResource | project#deploy (pre-decode); plus cloudcredential#use (project subject) on provisioned flow | resource.create.{adopted,provisioned} | ResourceCreated (+ ProvisionedResourceRequested on provisioned) | 8 KiB |
| GET | /v1/projects/{project_id}/resources | ListProjectResources | project#observe on the parent Project (top-level) + per-row resource#observe filter | resource.list (granted, with post-filter item_count) | (none) | n/a |
| GET | /v1/resources/{id} | GetResource | resource#observe (BEFORE persistence read) | resource.get | (none) | n/a |
| DELETE | /v1/resources/{id} | DeleteResource | resource#manage | resource.delete.{adopted,provisioned} | ResourceRemoved (adopted) / ProvisionedResourceDeleting (provisioned, emitted by the broker) | n/a |
body_cap = 8 KiBis enforced before the JSON decoder runs; an over-cap body surfaces as413 request_body_too_large.CreateResourcereturns:201 Createdwith aResourceResponsebody on the adopted flow.202 Acceptedwith aLocation: /v1/resources/{id}header on the provisioned flow; the body carries the hydratedResourceResponsewith an embeddedprovisioning.phase = pending.
ListProjectResourcesandGetResourceattach aprovisioningsub-object only when the Resource origin isprovisionedand the composition root wired aProvisioningReaderport.GetResourcereads the single backing row;ListProjectResourceshydrates 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 theprovisioningsub-object rather than failing the read: a flaky broker must not mask the canonical Resource projection.DeleteResourcebranches on origin:- Adopted → remove the Resource row +
ResourceRemovedoutbox event; responds204 No Content. - Provisioned → record a Broker
Deprovisionrequest and respond202 Accepted; the row stays until the broker finalises.
- Adopted → remove the Resource row +
ListProjectResources.limitis clamped at the handler to[1, 200]with default50.ListProjectResources.cursoris opaque, HMAC-signed by the server through theCursorCodecport; tampered →400 invalid_cursor; cross-user replay →403 cursor_binding_mismatch.
Path & query parameters
| Operation | Parameter | Type | Required | Notes |
|---|---|---|---|---|
| CreateResource / ListProjectResources | project_id (path) | string (uuid) | yes | UUIDv7. Non-zero. Malformed → 400 invalid_project_id. |
| GetResource / DeleteResource | id (path) | string (uuid) | yes | UUIDv7. Non-zero. Malformed → 400 invalid_resource_id. |
| ListProjectResources | cursor (query) | string | no | Opaque HMAC-signed continuation. |
| ListProjectResources | limit (query) | integer | no | [1, 200], default 50. |
Schemas
The OpenAPI spec is the authoritative source for field shapes. The schemas this surface uses are:
- Request:
ResourceCreateRequest(origindiscriminator selects between the adopted and provisioned bodies). - Response:
ResourceResponse(single, with optionalprovisioningsub-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
| Operation | Pre-condition | Schema permission | Object |
|---|---|---|---|
| Create (any origin) | pre-decode | project#deploy | project:<project_id> |
| Create (provisioned only) | post-decode, pre-persist | cloudcredential#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-fetch | project#observe | project:<project_id> |
| List (per row) | post-fetch | resource#observe | resource:<id> |
| Get | pre-persist | resource#observe | resource:<id> |
| Delete | pre-persist | resource#manage | resource:<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 (carriesitem_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.
| Code | Status | Where | Meaning |
|---|---|---|---|
invalid_project_id | 400 | Create / List | Malformed or zero project_id. |
invalid_resource_id | 400 | Get / Delete | Malformed or zero resource id. |
invalid_resource_origin | 400 | Create | origin outside {adopted, provisioned}. |
invalid_resource | 400 | Create | Aggregate invariant rejected the body (empty kind, provisioned-branch missing cloud_credential_id/blueprint_version_id, etc.). |
invalid_body | 400 | Create | Body could not be read or did not parse as the typed request shape. |
invalid_cursor | 400 | List | HMAC verification failed. |
invalid_limit | 400 | List | Out of [1, 200]. |
unauthenticated | 401 | every operation | Request carries no authenticated principal. |
cursor_binding_mismatch | 403 | List | Cursor was minted by a different caller; per-(caller, pepper) HMAC binding rejected the replay. |
resource_not_found | 404 | Get / Delete | No 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_found | 422 | Create (provisioned) | cloud_credential_id does not resolve. |
blueprint_not_found | 422 | Create (provisioned) | blueprint_version_id does not resolve. |
blueprint_provider_mismatch | 422 | Create (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_large | 413 | Create | Body exceeded 8 KiB. |
resources_not_provisioned | 501 | every operation | Composition root did not wire the Resources port. |
internal | 500 | every operation | Server-side failure path. |
Cross-references
../../../api/openapi/plexsphere-v1.yaml— OpenAPI 3.1 spec; theCreateResource,ListProjectResources,GetResource,DeleteResourceoperations and theResourceCreateRequest/ResourceResponse/ResourceList/ResourceProvisioningschemas.../../../internal/transport/http/v1/resources/— the transport-tier implementation: the four handlers, the closedProblem.codetaxonomy, the body-cap and limit-clamp constants, the per-row visibility filter, and the origin-branch dispatch.../../../schema/authz.zed— ReBAC schema; theproject,resource, andcloudcredentialdefinitions declare the gates this surface enforces../blueprints.md— companion surface for the Blueprint Catalog the provisioned flow consumes../authz.md— thePermissionDeniedshape returned on 403.