Delegate Administration
Let a customer's own people administer their organization, or one office inside it, without your backend in the loop. Two reserved permissions, each evaluated at a node, decide who runs what.
The Reserved Keys
Delegation is a permission, scoped like any other. Canopy seeds two keys into every Environment with the organizations container on, as system rows in its catalog; you attach them to roles of your own, and the engine evaluates them at a node the same way it evaluates reports.view. In the Developer Console both sit under Canopy capabilities on the Roles page, above your own permissions and tinted apart from them.
A customer cannot create a key inside the canopy: namespace, so nothing of theirs can collide with these, and a permission they define under one of Canopy's own management names (their own identity.manage, say) grants nothing on these routes.
Where Each Key Is Evaluated
Every check names a node, and the engine walks that node's lineage upward: a key held on an ancestor counts, a key held on a sibling does not. The two keys differ only in which node the check names.
The Grant Ceiling
Delegation without a limit is an escalation path: an office administrator who may assign any role could make themselves, or a friend, the organization's administrator. So what an identity may hand out is bounded by what it holds.
The rule needs nothing configured and cannot be misconfigured. Whoever can delegate can only delegate what they have, which is what keeps an office's administrator an office's administrator.
An Office Administrator, Step by Step
Acme is an organization with two offices, EU Office and UK Office, each with teams beneath it. Sam should run access for EU Office. Everything here is the developer's own setup over the API; from then on Sam acts alone.
Make the role
Create a role, Office Admin, and give it canopy:access.manage beside whatever an office administrator needs in your product. The write replaces the role's whole set, so send its other keys alongside.
curl -X PUT https://auth.canopy-io.com/api/v1/roles/{role_id}/permissions \ -H "X-API-Key: $CANOPY_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "permission_keys": ["canopy:access.manage", "reports.view"] }'
Hold it at the office
Sam is already a member of Acme (a membership is a record of its own; the roles are assignments on top of it). Assign Office Admin to Sam at EU Office's node, not at Acme's: the node the key is held at is the node it reaches from.
curl -X POST https://auth.canopy-io.com/api/v1/assignments \ -H "X-API-Key: $CANOPY_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "identity_id": "<sam>", "role_id": "<office admin>", "node_id": "<eu office>" }'
What Sam can do now
To hand Sam the whole organization later, hold Manage organization at Acme's node instead; the access key is then implied everywhere in it. To take EU Office away, remove the assignment; the next check at that node is refused.
In the Hosted Portal
A delegated administrator needs nothing built. They sign in to the hosted portal like any administrator, and the portal shows only what they administer: someone holding Manage access alone sees the Access page and nothing else, where they grant access to members and invite new people straight to the places they administer. In a flat Environment the organization is the only place, so the page does not appear and roles are changed on Members for a member holding one role there. A member holding several roles at the organization is changed over the assignment routes; the portal does not add a second role at the organization node.
Building Your Own Admin Screen
The hosted portal renders a delegated administrator's controls from these reads, and so can a screen inside your own product. None of them decides anything: they report what the write routes would accept, so no control you render leads to a refusal.
What they administer
GET /api/v1/organizations/{org_id}/me, with the administrator's own token, answers administers_organization and access_roots: the topmost places where Manage access is held, each with its name, type and the path of names from the organization down to it. Everything the screen shows hangs off this: which places to offer, whether the organization's own pages exist for this person. has_hierarchy is false in a flat Environment, where the organization is its only place. Computed from roles held at or inside the organization, so a role held above every organization opens nothing here.
curl https://auth.canopy-io.com/api/v1/organizations/{org_id}/me \ -H "Authorization: Bearer <identity access token>"
{ "data": { "administers_organization": false, "access_roots": [ { "node_id": "…", "name": "EU Office", "node_type": "office", "path": ["Acme", "Europe"] } ], "structure_roots": [], "roles_roots": [], "has_hierarchy": true } }
What they may do at a place
curl https://auth.canopy-io.com/api/v1/organizations/{org_id}/nodes/{node_id}/grantable-roles \ -H "Authorization: Bearer <identity access token>"
The writes
POST, PATCH and DELETE /api/v1/organizations/{org_id}/assignments grant, change and take away a role at the organization node or at a place beneath it, with the administrator's own token. Membership itself is admission, not a role: a member is added and removed through the members routes, and a role at the organization node is an assignment like any other. A grant beneath the organization needs the membership first (409 organization.membership_required); the last administrator's role at the organization node cannot be changed away or removed (409 organization.last_admin). Each write asks the same questions the developer's assignment routes ask: the delegation at the place, the grant ceiling, the organization's own rules. The portal is built on exactly these routes. Every assignment carries a version. Send it back as If-Match on PATCH, and a change somebody made since you read the row answers 409 assignment.version_mismatch instead of overwriting it: read the row again and retry. Removing takes no version.
Next Step
Tell us how we can improve this guide.