Appearance
plexctl rebac
Synopsis
plexctl rebac is the operator surface for the ReBAC authorization model. It wraps the typed /v1/authz operations (PostAuthzCheck, ListRelationTuples, CreateRelationTuple, PatchRelationTuple, DeleteRelationTuple) so an operator can answer a single authorization question or manage relation tuples without hand-rolling curl calls.
The expand verb is intentionally not provided: there is no /v1/authz/expand operation in the public v1 contract, so wiring it would require a contract addition first.
Invocation
text
plexctl rebac check [flags]
plexctl rebac tuple <add|list|update|delete> [flags]
plexctl rebac lookup-resources [flags]
plexctl rebac lookup-subjects [flags]Every subcommand is a private cobra constructor; flags are validated at parse time so an invalid UUID or a missing required flag surfaces as exit 2 instead of a wire round-trip.
plexctl rebac check
Answers a single subject→relation→resource question via POST /v1/authz/check. --subject, --relation, and --resource are required. A successful query whose decision is allowed renders the typed RebacCheckResponse and exits 0. A successful query whose decision is denied is not exit 0: it maps to the dedicated ReBAC-denial exit code (77) — the same code an enforced 403 rebac_denied response yields — so a runbook branches on one "you lack the relation" signal regardless of which path produced it. The reason and correlation id are written to stderr on a denial.
plexctl rebac tuple add
Creates a relation tuple via POST /v1/authz/relation-tuples. The project_id query parameter is required by the wire operation (a non-pointer UUID), so it surfaces as a required --project flag rather than a server round-trip. --subject, --relation, and --resource are required; --caveat-name and --caveat-context are optional. The handler renders the 201 RelationTuple.
plexctl rebac tuple list
Lists relation tuples via GET /v1/authz/relation-tuples. The --project flag is required (the wire project_id query parameter is mandatory). Supports keyset pagination via --cursor and an opt-in --all flag that walks every page until next_cursor is empty. The --all accumulator is capped at 100 000 items as defence-in-depth against a runaway server.
plexctl rebac tuple update
Updates a relation tuple via PATCH /v1/authz/relation-tuples/{id}. The wire RelationTuplePatchRequest marks subject, relation, and resource as required, so the PATCH is a full re-statement of the triple — not a sparse update. All three flags are therefore required; --caveat-name and --caveat-context are optional.
plexctl rebac tuple delete
Deletes a relation tuple via DELETE /v1/authz/relation-tuples/{id}. The destructive operation is gated behind the root persistent --yes flag — mirroring domain delete, group delete, and audit erase-identity — so a single typo of the tuple id cannot wipe a grant without explicit confirmation. The server returns 204 No Content on success.
plexctl rebac lookup-resources
Enumerates every resource of --resource-type the --subject can reach via --relation, calling POST /v1/authz/lookup-resources. The result is materialised data — an empty list is a normal answer and exits 0, never an error. There is no --all/cursor: the server drains SpiceDB's pagination internally and returns the full set.
plexctl rebac lookup-subjects
The dual of lookup-resources: enumerates every subject of --subject-type that can reach --resource via --relation, calling POST /v1/authz/lookup-subjects. Same materialised-data posture.
Flags
plexctl rebac check
| Flag | Type | Required | Default | Description |
|---|---|---|---|---|
--subject | string | yes | — | Subject reference, e.g. user:<uuid>. |
--relation | string | yes | — | Relation name to test, e.g. viewer. |
--resource | string | yes | — | Resource reference, e.g. project:<uuid>. |
--caveat-context | string | no | — | Optional caveat context as a JSON object. A malformed value is a local exit 2 failure, not a round-trip. |
plexctl rebac tuple add
| Flag | Type | Required | Default | Description |
|---|---|---|---|---|
--project | UUID | yes | — | Project the tuple is scoped to. Maps to the required project_id query parameter. |
--subject | string | yes | — | Subject reference, e.g. user:<uuid>. |
--relation | string | yes | — | Relation name, e.g. viewer. |
--resource | string | yes | — | Resource reference, e.g. project:<uuid>. |
--caveat-name | string | no | — | Optional caveat name to attach to the tuple. |
--caveat-context | string | no | — | Optional caveat context as a JSON object. |
plexctl rebac tuple list
| Flag | Type | Required | Default | Description |
|---|---|---|---|---|
--project | UUID | yes | — | Project to list tuples for. Maps to the required project_id query parameter. |
--limit | int | no | server default | Maximum items per page. 0 lets the server pick. |
--cursor | string | no | — | Continuation token from a previous call's next_cursor. Mutually exclusive with --all. |
--all | bool | no | false | Follow next_cursor until exhausted. Capped at 100 000 items. |
plexctl rebac tuple update
| Flag | Type | Required | Default | Description |
|---|---|---|---|---|
<id> (positional) | UUID | yes | — | Relation-tuple UUID. |
--subject | string | yes | — | Subject reference (full re-statement). |
--relation | string | yes | — | Relation name (full re-statement). |
--resource | string | yes | — | Resource reference (full re-statement). |
--caveat-name | string | no | — | Optional caveat name to attach. |
--caveat-context | string | no | — | Optional caveat context as a JSON object. |
plexctl rebac tuple delete
| Flag | Type | Required | Default | Description |
|---|---|---|---|---|
<id> (positional) | UUID | yes | — | Relation-tuple UUID. |
--yes (persistent) | bool | yes | false | Required confirmation for the destructive operation. The CLI refuses to call DeleteRelationTuple without --yes. |
plexctl rebac lookup-resources
| Flag | Type | Required | Default | Description |
|---|---|---|---|---|
--subject | string | yes | — | Subject reference whose reachable resources are listed, e.g. user:<uuid>. |
--relation | string | yes | — | Relation name to evaluate, e.g. read. |
--resource-type | string | yes | — | Object type to enumerate, e.g. project. Returned items are <resource-type>:<id>. |
--caveat-context | string | no | — | Optional caveat context as a JSON object. |
plexctl rebac lookup-subjects
| Flag | Type | Required | Default | Description |
|---|---|---|---|---|
--subject-type | string | yes | — | Object type to enumerate, e.g. user. Returned items are <subject-type>:<id>. |
--relation | string | yes | — | Relation name to evaluate, e.g. read. |
--resource | string | yes | — | Resource reference whose authorised subjects are listed, e.g. project:<uuid>. |
--caveat-context | string | no | — | Optional caveat context as a JSON object. |
Persistent flags inherited from root
--server, --profile, --token-file, --output, --yes, --reveal-secrets. The --yes flag is consumed by tuple delete; the others apply unchanged. See ../plexctl.md for the canonical list.
Output JSON schema reference
JSON and YAML output mirror the typed wire shapes verbatim:
checkrendersRebacCheckResponseon analloweddecision — see the schema definition in../../../api/openapi/plexsphere-v1.yaml.tuple add/tuple updaterenderRelationTuple.tuple listrendersRelationTupleList, withnext_cursorpreserved verbatim so a caller can resume pagination from the response shape.lookup-resources/lookup-subjectsrenderRebacLookupResponse(items+correlation_id). There is no cursor —itemsis the full materialised set.
The text output mode emits a tabwriter grid: DECISION, REASON, CORRELATION_ID for check, ID, SUBJECT, RELATION, RESOURCE, CREATED_AT for the tuple operations, and a single OBJECT column (one object reference per row) for the lookup operations. Reach for --output json when the caveat context or the full relation path matters.
Exit codes
plexctl collapses every failure into the taxonomy below; the single source of truth is exitCodeFor in ../../../cmd/plexctl/app.go.
| Code | Reachable | Meaning |
|---|---|---|
0 | yes | The API returned the expected status (200 for an allowed check / list / update, 201 for add, 204 for delete). |
1 | yes | Runtime / API error: transport failure, unexpected status code, malformed response body, the --all safety cap fired, or tuple delete invoked without --yes. |
2 | yes | Flag-parse / misconfiguration: missing required flag, malformed UUID, mutually exclusive --all / --cursor, or a malformed --caveat-context JSON object. |
3 | yes | Missing or insecure credentials, or 401 Unauthorized from the API. |
4 | yes | Permission denied or resource not addressable: 403 Forbidden (non-ReBAC denial path) or 404 Not Found from the API. |
64 | no | Not reachable — rebac is fully wired and never returns *NotImplementedError. |
77 | yes | ReBAC denial. Either a successful rebac check whose decision is denied, or a 403 Forbidden whose typed *output.APIError reports IsRebacDenied() == true. Both map here so callers branch on one "you lack the relation" code. |
Examples
Check whether a subject has a relation
shell
export PLEXSPHERE_URL="${PLEXSPHERE_URL:-https://localhost:8080}"
plexctl rebac check \
--server "${PLEXSPHERE_URL}" \
--subject user:0190a8b8-a0c0-7a0a-8a0a-a0a0a0a0a0a0 \
--relation viewer \
--resource project:0190a8b8-a0c0-7a0a-8a0a-b0b0b0b0b0b0 \
--output json
echo $? # 0 when allowedA denied decision writes the reason to stderr and exits 77:
shell
plexctl rebac check \
--server "${PLEXSPHERE_URL}" \
--subject user:0190a8b8-a0c0-7a0a-8a0a-a0a0a0a0a0a0 \
--relation editor \
--resource project:0190a8b8-a0c0-7a0a-8a0a-b0b0b0b0b0b0
# stderr: plexctl: rebac check: denied (reason=insufficient_relation, correlation_id=...)
echo $? # 77Create a relation tuple
shell
plexctl rebac tuple add \
--server "${PLEXSPHERE_URL}" \
--project 0190a8b8-a0c0-7a0a-8a0a-c0c0c0c0c0c0 \
--subject user:0190a8b8-a0c0-7a0a-8a0a-a0a0a0a0a0a0 \
--relation owner \
--resource project:0190a8b8-a0c0-7a0a-8a0a-b0b0b0b0b0b0Page through every tuple with --all
shell
plexctl rebac tuple list \
--server "${PLEXSPHERE_URL}" \
--project 0190a8b8-a0c0-7a0a-8a0a-c0c0c0c0c0c0 \
--all \
--output jsonThe walk follows next_cursor until empty and aborts with exit 1 if the accumulator passes 100 000 items.
Re-state a relation tuple
shell
plexctl rebac tuple update 0190a8b8-a0c0-7a0a-8a0a-d0d0d0d0d0d0 \
--server "${PLEXSPHERE_URL}" \
--subject user:0190a8b8-a0c0-7a0a-8a0a-a0a0a0a0a0a0 \
--relation viewer \
--resource project:0190a8b8-a0c0-7a0a-8a0a-b0b0b0b0b0b0All three of --subject, --relation, --resource are required — the PATCH body re-states the full triple because the wire schema marks them required.
Delete with --yes
shell
plexctl rebac tuple delete 0190a8b8-a0c0-7a0a-8a0a-d0d0d0d0d0d0 \
--server "${PLEXSPHERE_URL}" \
--yes
echo $? # 0The server returns 204 No Content on success; the empty body is treated as a no-op and the dispatcher reports exit 0.
Enumerate reachable resources / authorised subjects
shell
plexctl rebac lookup-resources \
--server "${PLEXSPHERE_URL}" \
--subject user:0190a8b8-a0c0-7a0a-8a0a-a0a0a0a0a0a0 \
--relation read \
--resource-type project \
--output json
plexctl rebac lookup-subjects \
--server "${PLEXSPHERE_URL}" \
--subject-type user \
--relation read \
--resource project:0190a8b8-a0c0-7a0a-8a0a-b0b0b0b0b0b0An empty items array is a normal answer and still exits 0 — the lookup succeeded, the set is just empty.
Cross-references
../../../api/openapi/plexsphere-v1.yaml—PostAuthzCheck,ListRelationTuples,CreateRelationTuple,PatchRelationTuple,DeleteRelationTupleoperation definitions and theRebacCheckResponse,RelationTuple,RelationTupleListschemas.../../../cmd/plexctl/commands/rebac.go— source of truth for the cobra command, flag wiring, the--allcap, the denied-check exit mapping, and the table projection.