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.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>) | 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, 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 (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. |
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.