View Markdown

Build a team hierarchy

On this page

What you get at the end: an organisation with a team scope below it, a team admin who can manage that team while retaining tenant-wide read access, a key homed at the team, and a budget authored there - with the server telling you which rule binds, and an honest note on when it binds.

The examples use the administrative API to create scopes, delegate roles and attach a key. Replace names, IDs and credentials with your own values. See the admin API reference for request and response contracts.

For the architecture and precise role/policy inheritance rules, read policy-based authorization. Scoped administrative grants are enabled by default; the stored tree request path remains opt-in.

All admin calls use Basic auth:

bash
A="$POINTSMAN_ADMIN_USERNAME:$POINTSMAN_ADMIN_PASSWORD"

1. The organisation, and the scope it brings for free#

bash
ORG=$(curl -s -u "$A" -X POST localhost:8080/admin/organizations \
  -H 'Content-Type: application/json' -d '{"name":"acme"}' | jq -r .id)

Creating an org also creates its TENANT scope in the tree - mirrored automatically, nothing to ask for. Find it by walking the tree from the root:

bash
ROOT=$(curl -s -u "$A" localhost:8080/admin/scopes/root | jq -r .id)
TENANT=$(curl -s -u "$A" "localhost:8080/admin/scopes/$ROOT/tree" \
  | jq -r '.children[] | select(.name=="acme") | .id')

2. Author the team scope#

bash
TEAM=$(curl -s -u "$A" -X POST localhost:8080/admin/scopes \
  -H 'Content-Type: application/json' \
  -d "{\"parent_id\":\"$TENANT\",\"name\":\"ml-team\",\"slug\":\"ml-team\"}" | jq -r .id)

A row, not a release. You did not say what kind of scope this is - position decides (below a tenant means GROUP), and the door refuses rather than guesses everything else: an unknown parent, a sibling slug collision, and depth beyond the configured maximum (pointsman.scopes.max-depth-below-tenant, default 2 - the refusal names the setting, and past three levels a warning rides the response even when it succeeds). Creating scopes needs OWNER at the parent: handing out structure is delegation, not configuration.

3. A team admin, granted at the team#

Create an explicit tenant VIEWER account for the team lead. Do not omit role: the account-creation API defaults an omitted role to global ADMIN. This starting role projects a read-only grant across the tenant; the team grant below adds management authority only within the team.

bash
LEAD=$(curl -s -u "$A" -X POST localhost:8080/admin/users \
  -H 'Content-Type: application/json' \
  -d "{\"username\":\"ml-lead\",\"password\":\"change-me-234567\",\"role\":\"VIEWER\",\"org_id\":\"$ORG\"}" \
  | jq -r .id)

curl -s -u "$A" -X POST "localhost:8080/admin/scopes/$TEAM/grants" \
  -H 'Content-Type: application/json' \
  -d "{\"principal_id\":\"$LEAD\",\"role\":\"ADMIN\"}"

201, and the grant row names who granted it. The lead now holds ADMIN at ml-team and everywhere below it. That grant adds no management authority above or beside the team. The account's separate tenant VIEWER grant still permits tenant-wide reads; inherited authority is additive. This recipe therefore does not create a team-only visibility boundary.

Two delegation boundaries to check:

  • Granting above your own strength. An ADMIN may grant only DEVELOPER or VIEWER at an authorized target; granting ADMIN or OWNER is refused with 403. An OWNER may grant any role, including OWNER.
  • Granting across the tenant wall. Granting a platform account a role inside a tenant's subtree gets a 400 spelling out the geography - platform staff act through grants at the root; tenant members are granted inside their tenant.

4. A key homed at the team#

bash
MINT=$(curl -s -u "$A" -X POST "localhost:8080/admin/organizations/$ORG/keys" \
  -H 'Content-Type: application/json' \
  -d "{\"label\":\"ml-team-key\",\"kind\":\"service\",\"scope_id\":\"$TEAM\"}")
KEY=$(echo "$MINT" | jq -r .key)
echo "$MINT" | jq .scope_id     # the response disclosing the home

Leave scope_id out and the key homes at the org's mirror scope instead - the old model's position, deliberately not an error. Name a scope and the door validates it hard: the platform root is refused by name, and a scope that does not exist or belongs to another tenant's subtree answers the same 404 unknown scope, so the door confirms nothing about trees that are not yours.

5. A budget at the team, and who binds#

bash
curl -s -u "$A" -X POST "localhost:8080/admin/scopes/$TEAM/rules" \
  -H 'Content-Type: application/json' \
  -d '{"amount":"requests","budget":3,
       "window":{"kind":"rolling","count":1,"unit":"minute"},
       "action":"block"}'

Then ask the server which rule actually governs this scope, rather than working it out:

bash
curl -s -u "$A" "localhost:8080/admin/scopes/$TEAM/limits/preview" | jq .binding
json
[{"amount": "REQUESTS", "scope_id": "...", "rule_id": "...",
  "value": "3", "scope_name": "ml-team"}]

The full response lists every level from your scope to the root with each level's rules; binding names, per amount, the one that wins (every level is checked independently and the minimum decides). Add a looser org-level rule and the binding entry stays on ml-team; tighten the org below the team and it moves. You never have to deduce it.

6. When does it bind - the honest note#

Today, with the shipped defaults, that team budget binds in the preview and not on live traffic: requests walk a path derived from the fixed org/app/user/key chain, and pointsman.datapath.tree.enabled (default false) is what would put the key's stored home on the walked path. In a flag-on window the same setup refuses the fourth request with a 429 from exactly this rule - scripts/demo-deep-key.sh runs that end to end, verifies the flag actually reached the process before claiming anything, and restores the default afterwards.

So what you have built is real and inert by one flag: structure authored ahead of need, a team admin with the stated tenant read access, a key with its stored home, and a budget whose binding you can already watch. Why the flag is off, and what evidence flips it: authorization defaults and rollout boundaries.

Pointsman v1 candidate documentation Source: 0e09f5e7

Search guides, reference pages and architecture.

Documentation

Open full-size image