Skip to main content
Version: v0.9.0a2
Integrator

Human Key Issuance — Garden-Scoped Permissions

3 min readNode operatorsTrack C · C2

What this page is

Track C — C2 adds a permission ceiling to the OIDC exchange: the scoped key issued to a human is capped by that person's garden membership role. Node operators no longer manage permission grants manually — they flow from the curator roster.

This page covers the garden membership scoping layer. For the full OIDC bridge configuration (env vars, IdP setup, token exchange flow), see OIDC / SSO Integration.

How garden membership gates key permissions

When a human calls POST /v1/auth/oidc/exchange, the node:

  1. Validates the id_token and derives entity_uri = oidc:<sub>.
  2. Looks up the caller's rows in garden_members to determine their permission ceiling: admin or writer in any garden → ceiling is write; reader only or no membership → ceiling is read.
  3. Intersects the caller's requested permissions with that ceiling.
  4. Returns the key scoped to the intersection.
Requested permissions: ["read", "write"]
Garden role: reader only
Issued permissions: ["read"] ← write silently dropped
Requested permissions: ["read", "write"]
Garden role: writer in at least one garden
Issued permissions: ["read", "write"] ← full grant

The federate permission is never grantable via OIDC exchange.

Garden membership or not. Operators who need federate must provision static keys out-of-band.

Exchange response includes granted permissions

The ExchangeResponse now includes a permissions field:

{
"api_key": "stgm_...",
"entity_uri": "oidc:alice@example.com",
"permissions": ["read", "write"],
"expires_at": "2026-05-03T17:00:00Z"
}

Clients should read permissions on each exchange to know what access they were granted — do not cache the previous value across token rotations.

Setting up garden membership for human users

Grant a human OIDC principal membership in a garden before they exchange a token:

# Add alice as a writer in the team garden
curl -s -X POST http://localhost:8000/v1/gardens/team/members \
-H 'Authorization: Bearer <admin-key>' \
-H 'Content-Type: application/json' \
-d '{
"entity_uri": "oidc:alice@example.com",
"role": "writer"
}'

Role values:

Role
Effect on key permissions
Notes
admin
ceiling: read + write
writer
ceiling: read + write
reader
ceiling: read only
(no membership)
ceiling: read only

A single admin or writer role in any garden grants the full ceiling — it is not per-garden.

Minimal example · OIDC exchange with garden membership

# 1. Obtain an id_token from your IdP (exact command is IdP-specific)
ID_TOKEN="<id_token from Google / GitHub / etc.>"

# 2. Exchange for a scoped Stigmem key
RESPONSE=$(curl -s -X POST http://localhost:8000/v1/auth/oidc/exchange \
-H 'Content-Type: application/json' \
-d "{\"id_token\": \"$ID_TOKEN\"}")

API_KEY=$(echo $RESPONSE | jq -r .api_key)
PERMS=$(echo $RESPONSE | jq -r '.permissions | join(",")')
echo "Issued permissions: $PERMS"

# 3. Use the key
curl -H "Authorization: Bearer $API_KEY" http://localhost:8000/v1/facts

Security notes

Membership checked at exchange time

Not at API request time. Changing a member's role in the garden does not immediately revoke an active key; the new ceiling applies on the next exchange.

Single-session-per-principal

Every successful exchange revokes all prior OIDC keys for the same sub. Revoking garden membership and waiting for the current token to expire (or exchanging immediately to pick up the reduced ceiling) is the recommended offboarding flow.

No-membership default

Users who are not in any garden receive read-only access. Safe default for pilot users or self-hosted nodes without curated gardens.

See also