Tenant selector & administration
When a principal belongs to several tenants, two jobs appear: letting a user switch between their own organizations, and letting an admin manage organizations and their members. Both are projector-driven route sets that sit on top of the authn, authz & tenancy wiring — no custom handler code. The tenancy model itself is in Multi-tenancy.
Tenant selector ("switch organization")¶
Ship a switcher with one extra projector. build_tenancy_registry(spec) reuses
the same authn spec (so it can re-mint tokens through that route's lifecycle)
and gives two operations that attach_tenancy_routes projects:
GET /tenants— the principal's active memberships (tenant_id,tenant_key,is_current).POST /tenants/{id}/activate— validates the choice against membership via theTenantResolverPort, then returns a new token pair scoped to the selected tenant (the same body as/login); the client swaps to the new token.DELETE /tenants/{id}— leave a tenant. Keyed on the bound principal, so a caller can only drop their own membership; leaving the tenant on the current token makes itstidstop matching a live membership, so the next request fails closed (switch or re-authenticate).
from forze_fastapi.routes import attach_tenancy_routes
from forze_kits.aggregates.tenancy import build_tenancy_registry
attach_tenancy_routes(
router,
registry=build_tenancy_registry(spec).freeze(),
ns=spec.default_namespace,
ctx_dep=ctx_dep,
)
The active tenant rides the signed tid claim and is re-validated against live membership on
every request, so removing a principal from a tenant invalidates their scoped token at once.
Requires a TenancyDepsModule wiring a tenant_resolver and tenant_management route
(see Wire the planes).
Tenant administration¶
The selector is self-service — every op acts on the caller's own membership. Managing
other tenants and members (create an org, invite/remove a member, list members, deactivate a
tenant) is the privileged inverse, in a separate aggregate:
build_tenancy_admin_registry(ns) → attach_tenancy_admin_routes (POST /tenants,
GET /tenants/{id}/members, POST /tenants/{id}/deactivate, POST/DELETE /memberships).
Because who may administer a tenant is your authorization model — not something the framework
can define — these ops ship unguarded, exactly like deactivate_principal in the
authn recipe. Bind AuthnRequired plus
an AuthzBeforeAuthorize on each operation before exposing the router (or keep ops off it with
include=):
from forze.application.hooks.authn import AuthnRequired
from forze.application.hooks.authz import AuthzBeforeAuthorize
from forze_fastapi.routes import attach_tenancy_admin_routes
from forze_kits.aggregates.tenancy_admin import TenancyAdminKernelOp, build_tenancy_admin_registry
reg = build_tenancy_admin_registry(spec.default_namespace)
for op in TenancyAdminKernelOp:
reg = (
reg.bind(spec.default_namespace.key(op))
.bind_outer()
.before(
AuthnRequired().to_step(),
AuthzBeforeAuthorize(spec=AUTHZ, action=f"tenants:{op}").to_step(),
)
.finish(deep=True)
)
admin_router = APIRouter(prefix="/admin", tags=["tenant-admin"])
attach_tenancy_admin_routes(
admin_router, registry=reg.freeze(), ns=spec.default_namespace, ctx_dep=ctx_dep
)
list_members returns principal ids only (TenantManagementPort.list_tenant_principals); join
them with identity-plane details out of band.
Notes¶
- Both build on the base wiring. The middleware, deps modules, and enforcement hooks come from Authn, authz & tenancy — these route sets only add the org-switch and admin surfaces.
- The selector is safe to ship as-is; the admin plane is not. Selector ops are keyed on the caller's own principal; admin ops ship unguarded and must carry your authz hooks first.