Skip to content

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

FlagTypeRequiredDefaultDescription
--subjectstringyesSubject reference, e.g. user:<uuid>.
--relationstringyesRelation name to test, e.g. viewer.
--resourcestringyesResource reference, e.g. project:<uuid>.
--caveat-contextstringnoOptional caveat context as a JSON object. A malformed value is a local exit 2 failure, not a round-trip.

plexctl rebac tuple add

FlagTypeRequiredDefaultDescription
--projectUUIDyesProject the tuple is scoped to. Maps to the required project_id query parameter.
--subjectstringyesSubject reference, e.g. user:<uuid>.
--relationstringyesRelation name, e.g. viewer.
--resourcestringyesResource reference, e.g. project:<uuid>.
--caveat-namestringnoOptional caveat name to attach to the tuple.
--caveat-contextstringnoOptional caveat context as a JSON object.

plexctl rebac tuple list

FlagTypeRequiredDefaultDescription
--projectUUIDyesProject to list tuples for. Maps to the required project_id query parameter.
--limitintnoserver defaultMaximum items per page. 0 lets the server pick.
--cursorstringnoContinuation token from a previous call's next_cursor. Mutually exclusive with --all.
--allboolnofalseFollow next_cursor until exhausted. Capped at 100 000 items.

plexctl rebac tuple update

FlagTypeRequiredDefaultDescription
<id> (positional)UUIDyesRelation-tuple UUID.
--subjectstringyesSubject reference (full re-statement).
--relationstringyesRelation name (full re-statement).
--resourcestringyesResource reference (full re-statement).
--caveat-namestringnoOptional caveat name to attach.
--caveat-contextstringnoOptional caveat context as a JSON object.

plexctl rebac tuple delete

FlagTypeRequiredDefaultDescription
<id> (positional)UUIDyesRelation-tuple UUID.
--yes (persistent)boolyesfalseRequired confirmation for the destructive operation. The CLI refuses to call DeleteRelationTuple without --yes.

plexctl rebac lookup-resources

FlagTypeRequiredDefaultDescription
--subjectstringyesSubject reference whose reachable resources are listed, e.g. user:<uuid>.
--relationstringyesRelation name to evaluate, e.g. read.
--resource-typestringyesObject type to enumerate, e.g. project. Returned items are <resource-type>:<id>.
--caveat-contextstringnoOptional caveat context as a JSON object.

plexctl rebac lookup-subjects

FlagTypeRequiredDefaultDescription
--subject-typestringyesObject type to enumerate, e.g. user. Returned items are <subject-type>:<id>.
--relationstringyesRelation name to evaluate, e.g. read.
--resourcestringyesResource reference whose authorised subjects are listed, e.g. project:<uuid>.
--caveat-contextstringnoOptional 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:

  • check renders RebacCheckResponse on an allowed decision — see the schema definition in ../../../api/openapi/plexsphere-v1.yaml.
  • tuple add / tuple update render RelationTuple.
  • tuple list renders RelationTupleList, with next_cursor preserved verbatim so a caller can resume pagination from the response shape.
  • lookup-resources / lookup-subjects render RebacLookupResponse (items + correlation_id). There is no cursor — items is 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.

CodeReachableMeaning
0yesThe API returned the expected status (200 for an allowed check / list / update, 201 for add, 204 for delete).
1yesRuntime / API error: transport failure, unexpected status code, malformed response body, the --all safety cap fired, or tuple delete invoked without --yes.
2yesFlag-parse / misconfiguration: missing required flag, malformed UUID, mutually exclusive --all / --cursor, or a malformed --caveat-context JSON object.
3yesMissing or insecure credentials, or 401 Unauthorized from the API.
4yesPermission denied or resource not addressable: 403 Forbidden (non-ReBAC denial path) or 404 Not Found from the API.
64noNot reachable — rebac is fully wired and never returns *NotImplementedError.
77yesReBAC 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 allowed

A 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 $?  # 77

Create 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-b0b0b0b0b0b0

Page through every tuple with --all

shell
plexctl rebac tuple list \
  --server  "${PLEXSPHERE_URL}" \
  --project 0190a8b8-a0c0-7a0a-8a0a-c0c0c0c0c0c0 \
  --all \
  --output  json

The 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-b0b0b0b0b0b0

All 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 $?  # 0

The 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-b0b0b0b0b0b0

An 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.yamlPostAuthzCheck, ListRelationTuples, CreateRelationTuple, PatchRelationTuple, DeleteRelationTuple operation definitions and the RebacCheckResponse, RelationTuple, RelationTupleList schemas.
  • ../../../cmd/plexctl/commands/rebac.go — source of truth for the cobra command, flag wiring, the --all cap, the denied-check exit mapping, and the table projection.