Kanzo UI
Auth

Roles

can, Gate and useSession().can — asking a role question inside the current organization, where the roles in it are read from, and why the predicate has no hierarchy.

import { Gate, can, organizationOf, useSession } from "@kanzo-tech/auth";

Everything on this page decides what to draw, and none of it decides what is allowed — see the one rule.

Asking the question

can(session, "warden");            // inside session.organization, the hall this request is in
can(session, "warden", "amber");   // inside the hall named
can(session, "gold");              // with no current hall: a realm or client role — Ravenna's rank

The organization defaults to session.organization, the current tenant the server resolved for this request, so can(session, "editor") asks the question a page means: here. Clerk's auth().has() binds to the active organization the same way. Named explicitly, the question moves to that organization. With no current tenant at all, the realm and client roles answer.

Inside an organization the question is asked against the person's roles there, which is a different set from their realm and client roles and is deliberately not merged with them. A role held in one organization says nothing about another, and the day those two sets are unioned for convenience is the day one organization's owner is every organization's owner. A realm role is still on session.roles for the page that needs it while a tenant is current.

In a component, useSession().can(role, organization?) is the same predicate bound to the session the provider holds.

It is closed by default. No session, or no membership of the organization — current or named — is false rather than a throw — a predicate that raises is a predicate every call site has to guard, and a guard somebody forgets is a control drawn for a stranger.

organizationOf(session, alias) is the lookup underneath, for when you want the membership itself rather than a yes or no.

Gate

Ravenna is a warden in her own hall and an archivist or a scout in the two she visits, so the control appears and disappears without her memberships changing at all. That is the current organization, drawn — and the predicate is on the screen beside the thing it decides, with the roles the token carries for that hall above it, because why a gate is shut is the part worth reading. Two of the five halls are ones she holds nothing in: those answer false because there is no membership at all, which is a different sentence from being a member and not a warden.

<Gate fallback={<p>Only a warden signs for a party.</p>} role="warden">
  <Button>Post a contract</Button>
</Gate>

Gate asks through useSession().can, so it asks inside the current organization unless its organization prop names another.

While the session is loading, neither branch is drawn. Neither answer is known to be true yet, and both of the alternatives are worse: rendering the fallback flashes you cannot do this at somebody who can, for as long as the session endpoint takes, and rendering the children offers a control and then retracts it.

No hierarchy, and no way to configure one

That admin outranks editor is a fact about a product, not about the predicate, so can never ranks: it asks whether a role is in a set, and nothing else. A hierarchy wired into the library would apply to every consumer at once, including the ones with three roles that order differently or not at all.

The product declares its hierarchy once, as Keycloak composite roles, in its own client registration — admin containing editor, editor containing reader. Keycloak expands a composite into every token, so an admin's token says admin, editor and reader, and the question a product asks is the one it means:

can(session, "editor", "amber"); // true for an editor and for an admin, because the token says so

The order then lives in one place both sides read — the realm — rather than in a predicate the browser runs and a second copy the server runs, which is the one deciding. services/auth/modules/app is where an application declares its roles and their composites; the Keycloak page has the rest of what the realm owes.

Roles inside an organization are read from its entry

Each entry of the organization claim carries what the person holds there, keyed by application exactly as the top-level claim is:

"organization": {
  "amber": {
    "id": "hall-amber",
    "groups": ["/Wardens"],
    "resource_access": { "board": { "roles": ["warden"] }, "ledger": { "roles": ["reader"] } }
  }
}

organization.<alias>.resource_access.<clientId>.roles is the set can(session, role, alias) asks against. Keycloak writes it from the roles an organization's administrator mapped onto the groups the person is in, with composites already expanded — so the organization decides who is a warden, the application decides what a warden contains, and neither has to know how the other arranged it. Reading under board sees warden and nothing of the ledger's reader, which is another application's role sitting inside the very same organization; without the client id, a role granted in the ledger would authorise its holder on the board.

Group names are never read. /Wardens is how the Amber Hall arranged its people, and it is the organization's own data: it may be renamed, split or called something else in the next hall, and a role inferred from it would change with it. An application learns its roles and never a group's name — what grants anything is a role mapped onto a group, not the group.

They are not merged into session.roles either, for the reason above: a role in one organization says nothing about another. Keycloak keeps them out of the top-level resource_access too, and the reader does not undo that.

What the token actually carries is a recorded token in this shape, and the live reading under it lets you switch the asking client and watch the same claim set change hands.

Which organization a page is in

The server decides, per request, with kanzoAuth's organization resolver — a hostname's first label, a path segment, a cookie — and the answer arrives as session.organization.

That answer is an address and never an authority. Anyone can send Host: amber.kanzo.tech; what makes an organization real for a session is the claim in a token Keycloak signed. So can resolves the current organization against session.organizations, and a miss is false — never a throw, and never the first organization instead. Falling back is how somebody ends up reading another customer's data believing it is their own. A product that wants to say you are not a member of this hall checks organizationOf(session, session.organization).

API Reference

can

can(session: Session | null | undefined, role: string, organization?: string): boolean
ArgumentTypeDescription
sessionSession | null | undefinednull is false, not an error
rolestringThe role name, as the token carries it
organizationstring?Defaults to session.organization; with neither, asks against session.roles

organizationOf

organizationOf(session: Session | null | undefined, alias: string): Organization | undefined

Gate

PropTypeDefault
rolestring—
organizationstring?session.organization, as for can
fallbackReact.ReactNodenull
childrenReact.ReactNode—

On this page