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.

--relation takes a relation, not a permission. Relations are stored — they are what a tuple is — while permissions are computed from them by the schema, so there is nothing to write a permission into. Naming one (act, manage, observe, …) is refused with 400 and code: invalid_triple; grant a relation the permission derives from instead. For a Resource, act derives from owner, maintainer, and operator, so --relation operator is the smallest grant that confers it. The ReBAC schema reference lists the relations and permissions of every object type.

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.