Skip to content

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_admin with no source-system ACL tuples can still manage role bindings, but won't see any documents in Search — OpenFGA filters them out.
  • A viewer with full source-system access can browse every document but can't edit a connector — RBAC blocks the write.

Default roles ​

RoleTenant scopeSummary
global_adminglobalSuperuser. Full access to every tenant, region, and platform setting.
tenant_adminper-tenantManages every aspect of a single tenant — connectors, identity, ACL, supersession, secrets.
connector_adminper-tenantManages a tenant's data-source connectors and related secrets. No identity / ACL / supersession write.
user_adminglobal or per-tenantOnboards console users and assigns role bindings. Cannot read source data.
billing_adminglobalOwns license keys and per-tenant seat limits. Read-only elsewhere.
auditorglobalRead-only across audit log, documents, search, and pipeline.
viewerper-tenantDefault 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:

ResourcePermissions
tenantsread, create, update, delete
regionsread, create, update
connectorsread, write, oauth
secretsread, write
identityread, write, erasure
aclread, write (tenant ACL policy hooks — not OpenFGA tuples)
supersessionread, write
documentsread
searchquery
pipelineread
auditread
rolesread, assign, manage
usersread, invite, disable
licenseread, write
seatsread, 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 scopeTargetResult
GlobalGlobalallow
GlobalTenant Xallow
Tenant XTenant Xallow
Tenant XTenant Ydeny
Tenant XGlobaldeny
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_users referenced 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 to viewer at 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}          → revoke

Every create / delete is auditable; the audit row records the granting user, the target subject, the role, and the scope.

Moʻolelo — self-hosted federated enterprise knowledge platform.