Skip to content

Grant access through a Group

In Build in your first Domain you created a Group and added a member to it — but you never used the Group to grant anything. This lesson does. You will grant a Group a relation on a Project, watch every member inherit that access, and then watch it disappear the moment you leave the Group. By the end you will understand how plexsphere delegates access to roles rather than to individuals.

This lesson takes about fifteen minutes.

Before you start

You need a running stack and a logged-in plexctl from Set up your local plexsphere, and you should have finished Build in your first Domain — this lesson assumes you recognise a Project, a Group, a member, and a ReBAC relation. You also need jq on your $PATH.

Recreate the shell environment and capture the Domain and your own user id:

bash
export PATH="$PWD/bin:$PATH"
export PLEXSPHERE_URL=http://localhost:8080

DOMAIN_ID=$(kubectl exec statefulset/postgres -- \
  env PGPASSWORD=plexsphere psql -U plexsphere -d plexsphere -tAc \
  "SELECT id FROM plexsphere.domains WHERE slug='acme-corp'")

USER_ID=$(plexctl identity list --domain "$DOMAIN_ID" --output json \
  | jq -r '.items[0].id')

Step 1 — Create a Project to share

Make a fresh Project — the resource whose access you will delegate:

bash
PROJECT=$(plexctl project create \
  --domain "$DOMAIN_ID" \
  --slug billing \
  --display-name "Billing" \
  --output json)
PROJECT_ID=$(echo "$PROJECT" | jq -r '.id')
echo "$PROJECT" | jq '{id, domain_id, slug, name}'
json
{
  "id": "<project-uuid>",
  "domain_id": "<acme-corp-uuid>",
  "slug": "billing",
  "name": "Billing"
}

Step 2 — Confirm you do not hold the access yet

You administer the Domain — but that does not make you a maintainer of every Project in it. maintainer is a relation that has to be granted; administering the Domain gives you management permissions through the tenancy hierarchy, not the maintainer role itself. Ask the authorization engine whether you hold it on the new Project:

bash
plexctl rebac check \
  --subject "user:$USER_ID" \
  --relation maintainer \
  --resource "project:$PROJECT_ID"
text
plexctl: rebac check: denied (reason=insufficient_relation, correlation_id=<correlation-id>)

denied, with a non-zero exit. Good — that is the baseline. Now you will grant the relation, but to a Group rather than to yourself.

Step 3 — Build a Group and join it

Create a Group and add yourself as a member:

bash
GROUP=$(plexctl group create \
  --domain "$DOMAIN_ID" \
  --slug billing-oncall \
  --display-name "Billing On-Call" \
  --source manual \
  --output json)
GROUP_ID=$(echo "$GROUP" | jq -r '.id')
echo "$GROUP" | jq '{id, domain_id, slug, display_name, source}'

plexctl group member add \
  --group "$GROUP_ID" \
  --principal "$USER_ID" \
  --kind user \
  --source manual
text
GROUP                                 PRINCIPAL                             KIND  SOURCE
<group-uuid>                          <seed-user-uuid>                      user  manual

Step 4 — Grant the Group the access

Now the key move. Grant the maintainer relation on the Project not to a user, but to the Group's member set — written group:<id>#member:

bash
plexctl rebac tuple add \
  --project "$PROJECT_ID" \
  --resource "project:$PROJECT_ID" \
  --relation maintainer \
  --subject "group:$GROUP_ID#member"
text
ID                                    SUBJECT                                            RELATION    RESOURCE                                      CREATED_AT
<tuple-uuid>                          group:<group-uuid>#member                          maintainer  project:<project-uuid>                        <timestamp>

If the grant returns Permission Denied on the first try, wait a couple of seconds and run it again — the Project you just created takes a moment to propagate to the authorization backend.

The #member suffix is what makes this a role grant: it points at "every member of this Group", not at the Group object itself.

Step 5 — Membership grants the access

Run the exact same check as Step 2 — but now you are a member of a Group that holds maintainer:

bash
plexctl rebac check \
  --subject "user:$USER_ID" \
  --relation maintainer \
  --resource "project:$PROJECT_ID"
text
DECISION  REASON  CORRELATION_ID
allowed           <correlation-id>

allowed. You did not grant yourself anything — your membership in the Group did. Every other member of billing-oncall has exactly the same access, and onboarding a new joiner is one group member add, not a tuple per Project.

If it still says denied, the grant from Step 4 is propagating — wait a moment and run the check again.

Step 6 — Ask the other two questions

rebac check answers one subject-and-resource question at a time. Two companion queries turn it around — what can a subject reach, and who can reach a resource:

bash
plexctl rebac lookup-resources \
  --subject "user:$USER_ID" \
  --relation maintainer \
  --resource-type project

plexctl rebac lookup-subjects \
  --resource "project:$PROJECT_ID" \
  --relation maintainer \
  --subject-type user
text
OBJECT
<project-uuid>
text
OBJECT
<seed-user-uuid>

The first answer lists the Billing Project — the access you inherited through the Group. The second lists you, the member who holds it. Notice that neither query names the Group: the engine has already expanded membership into concrete resources and subjects, which is exactly what an access review wants to see. Add a second member and they appear in the lookup-subjects answer; remove yourself — as you do next — and you drop out of it.

Step 7 — Leave the Group, lose the access

Group membership is live, not a snapshot. Remove yourself from the Group and check again:

bash
plexctl group member remove \
  --group "$GROUP_ID" \
  --principal "$USER_ID" \
  --kind user

plexctl rebac check \
  --subject "user:$USER_ID" \
  --relation maintainer \
  --resource "project:$PROJECT_ID"
text
plexctl: rebac check: denied (reason=insufficient_relation, correlation_id=<correlation-id>)

denied again. The Project's grant never changed — you simply stopped being a member of the Group it was granted to. Revoking a person's access across every Project a Group governs is a single group member remove.

The re-check can lag a second or two behind the removal, just as the grant did; if it still says allowed, wait and run it again.

What you learned

  • You grant access to roles, not just people. A relation granted to group:<id>#member is held by every member of the Group.
  • Membership is the control point. Adding or removing one member grants or revokes that access across every resource the Group governs — no per-resource tuple churn.
  • Administering a Domain is not the same as holding a Project relation. Broad tenancy permissions and specific role relations are separate — the same lesson Labels taught when a Domain admin still could not assign one.
  • Authorization is live. ReBAC re-evaluates membership on every check, so access follows the Group in real time.
  • You can ask in both directions. rebac check answers one subject-and-resource pairing; rebac lookup-resources and rebac lookup-subjects answer "what can this principal reach" and "who can reach this" — the queries an access review runs, with Group membership already expanded into concrete resources and subjects.

Where to go next