Bilko RBAC -- Users / Roles / Permissions

Overview

Bilko uses a flat RBAC model: users have one role per organisation; roles map to a permission catalog via a DB seed table. Permission resolution is live from the database on every request (no JWT role claim for authorisation). The system was built in WP1 (MC #103141, branch feat/rbac-wp1-permissions-catalog).

Roles

RoleLevelScope
owner3All permissions including billing, account deletion, user management
admin2All permissions except billing and account deletion; can manage users and roles
accountant1Create and manage financial records; cannot delete; cannot manage users
viewer0Read-only access; default for newly JIT-provisioned Entra users

Roles are stored in users.role (VARCHAR 50) with a CHECK constraint added in V67 limiting values to these four. Single role per user per organisation (multi-role/multi-org deferred, MC #103089).

Permissions Catalog (V67 — 52 keys)

Source: apps/api/src/main/resources/db/migration/V67__rbac_permissions_catalog.sql (commit 66629bd). The catalog is stored in the permissions table; all application code references permission keys as string constants.

Format: <resource>:<verb> enforced by a DB CHECK constraint (permission_key_format). Example keys by resource group:

Resource groupExample keys
Invoicesinvoice:read, invoice:create, invoice:update, invoice:delete, invoice:submit
Expensesexpense:read, expense:create, expense:update, expense:delete
Contactscontact:read, contact:create, contact:update, contact:delete
Transactionstransaction:read, transaction:create, transaction:reconcile
Reportsreport:read, report:export
Settings / billingsettings:read, settings:update, billing:read, billing:update
Usersusers:read, users:manage, users:invite
Account adminaccount:delete
Documentsdocument:read, document:upload, document:delete
Articles / productsarticle:read, article:create, article:update, article:delete

Full 52-key baseline stored in: apps/api/src/main/resources/rbac/requireRole-baseline-v67.tsv (commit 0bf18fd, 51 data rows).

Role-to-Permission Seed (Strategy A — Flat Inheritance)

Source: role_permissions table seeded in V67. Each row: (role, permission_key). No runtime inheritance logic — the seed embeds the full flattened set for each role.

RolePermissions countPrinciple
viewer13Read-only: all :read + :export keys
accountant40viewer permissions + create/update on financial resources; no delete, no user management
admin49accountant permissions + delete + user management; no billing:update, no account:delete
owner52All 52 permissions (complete set)

The seed exactly reproduces the behaviour of the legacy requireRole() numeric hierarchy — verified by 204 RbacMatrixTest cases (0 failures). No behaviour regression.

PermissionService — Live DB Resolution

Source: apps/api/src/main/kotlin/no/alai/bilko/services/PermissionService.kt (commit dee4fb1)

BilkoPrincipal + requirePermission

Source: apps/api/src/main/kotlin/no/alai/bilko/auth/BilkoPrincipal.kt and RbacHelper.kt (commit dee4fb1)

Role-to-Permission Matrix

Permission keyvieweraccountantadminowner
invoice:readYYYY
invoice:create-YYY
invoice:update-YYY
invoice:delete--YY
invoice:submit-YYY
expense:readYYYY
expense:create-YYY
expense:delete--YY
users:readYYYY
users:manage--YY
users:invite--YY
billing:read---Y
billing:update---Y
account:delete---Y
settings:readYYYY
settings:update--YY
report:readYYYY
report:exportYYYY
... (52 total)Full catalog in V67 seed

Full read-only matrix visible to admins/owners in the web admin UI at /admin/users (component: lib/permissions.ts ROLE_PERMISSION_MATRIX).

Authorization Audit Log

Source: apps/api/src/main/kotlin/no/alai/bilko/auth/AuthzAuditLogger.kt (commit dee4fb1)

V67/V68 Migration Summary

MigrationContents
V67 (V67__rbac_permissions_catalog.sql)Creates permissions table (52 keys, format CHECK); role_permissions table with full 4-role seed; adds users.role CHECK constraint; GRANT SELECT to bilko_app; no RLS (global catalog)
V68 (V68__rbac_user_provisioning.sql)Adds users:manage and users:invite permission keys; SECURITY DEFINER function bilko_auth.provision_user_with_org(issuer, oid, email, fullName) returning new user UUID; seeds new permissions to admin + owner roles

Test Coverage

Out of Scope (v1)


Revision #1
Created 2026-06-08 07:41:03 UTC by John
Updated 2026-06-08 07:41:03 UTC by John