Console RBAC
Moʻolelo's web console is multi-tenant and multi-role. Every operator who can reach the console signs in via the same OIDC flow, and what they can do once inside is governed by role bindings stored in Postgres.
This is not the same system that gates access to your data. Data ACL comes from each source system and is enforced via OpenFGA. Console RBAC gates access to the platform's administrative surface — who can configure connectors, manage tenants, see the audit log, etc.
The two are independent:
- A
user_adminwith no source-system ACL tuples can still manage role bindings, but won't see any documents in Search — OpenFGA filters them out. - A
viewerwith full source-system access can browse every document but can't edit a connector — RBAC blocks the write.
Default roles
| Role | Tenant scope | Summary |
|---|---|---|
global_admin | global | Superuser. Full access to every tenant, region, and platform setting. |
tenant_admin | per-tenant | Manages every aspect of a single tenant — connectors, identity, ACL, supersession, secrets. |
connector_admin | per-tenant | Manages a tenant's data-source connectors and related secrets. No identity / ACL / supersession write. |
user_admin | global or per-tenant | Onboards console users and assigns role bindings. Cannot read source data. |
billing_admin | global | Owns license keys and per-tenant seat limits. Read-only elsewhere. |
auditor | global | Read-only across audit log, documents, search, and pipeline. |
viewer | per-tenant | Default role: browse documents, run searches, view the live pipeline. No writes. |
Custom roles aren't supported in v1. If the predefined set doesn't fit, the operator-facing answer is to file an issue describing the missing persona; a new predefined role is a one-line catalog change.
Permission catalog
Permissions follow <resource>:<verb>. The full list:
| Resource | Permissions |
|---|---|
tenants | read, create, update, delete |
regions | read, create, update |
connectors | read, write, oauth |
secrets | read, write |
identity | read, write, erasure |
acl | read, write (tenant ACL policy hooks — not OpenFGA tuples) |
supersession | read, write |
documents | read |
search | query |
pipeline | read |
audit | read |
roles | read, assign, manage |
users | read, invite, disable |
license | read, write |
seats | read, write |
Permissions are Go constants in internal/webserver/rbac/permissions.go; the Postgres tables only store assignments. A typo in a permission name fails at compile time.
Scope
Every role binding has a scope:
- Global (
*) — the binding applies regardless of which tenant the request is for. Reserved for platform roles. - Tenant (
<tenant_id>) — the binding applies only when the request targets that specific tenant.
Decision matrix:
| Binding scope | Target | Result |
|---|---|---|
| Global | Global | allow |
| Global | Tenant X | allow |
| Tenant X | Tenant X | allow |
| Tenant X | Tenant Y | deny |
| Tenant X | Global | deny |
| Tenant X | (no tenant resolved) | deny (fail-closed) |
A global binding always satisfies a tenant-scoped request; a tenant binding never satisfies a global request. This is what keeps a tenant_admin from creating new tenants or reading another tenant's data.
Bootstrap break-glass
Every cluster ships with a bootstrap-admin account that has the equivalent of global_admin at global scope. It exists so the first administrator can sign in before any OIDC identities have been provisioned.
The bootstrap account:
- Has no row in
admin_usersreferenced by role bindings — it's resolved synthetically. - Surfaces a persistent warning banner in the console UI when in use.
- Every action it takes is recorded in the audit log as a privileged break-glass operation.
To disable it, assign at least one human OIDC identity the global_admin role and then flip the bootstrap_disabled setting in license config. The account never auto-disables — removing your last escape hatch should be deliberate.
First-login behavior
When a previously-unseen OIDC subject signs in, the tenant decides what happens by default:
reject(default) — the user lands on a "no access yet — ask your admin" screen. Most secure.auto_viewer— the user is automatically bound toviewerat the tenant scope. Friendly; relies on OpenFGA to filter what they actually see in Search.
The setting is per-tenant; the global default is reject.
How role bindings are managed
In the console: Access > Users lists every admin and the bindings they hold. The "Assign role" button creates a new binding; the × next to a binding revokes it.
Behind the scenes:
GET /api/console/me → caller capabilities
GET /api/console/roles → catalog
GET /api/console/role-bindings → all bindings (user_admin / global_admin)
POST /api/console/role-bindings → bind { admin_id, role, scope }
DELETE /api/console/role-bindings/{id} → revokeEvery create / delete is auditable; the audit row records the granting user, the target subject, the role, and the scope.

