Skip to content

How to set up role-based access control

This how-to walks you through designing a complete access control matrix for a Kittox app using the layered RBAC pattern: a wildcard read-only baseline that grants every authenticated user the right to see menu items and read data, role-specific rules that lift each role above the baseline by adding write modes, and per-resource denies that carve out exceptions (a view hidden from non-admins, a read-only table for a specific role, etc.).

The result fits in a handful of KITTO_PERMISSIONS rows and scales to dozens of roles and views without a permission-table explosion. The same matrix works whether you serve grants from AccessControl: DB (DB hit per check, mid-session changes immediately effective) or AccessControl: JWT (no DB hit, snapshot in the JWT — re-login to see grant changes). The decision depends on the volatility of your grants, not on the matrix structure.

For the underlying concepts (resource URIs, access modes, FALSE-priority semantics) read Access Control first.

1. Create the ACL tables

KITTO_USER_ROLES and KITTO_PERMISSIONS are the two tables KittoX expects to find. Their schema is dialect-specific in trivial ways (column types) but the data shape is identical across SQL Server, PostgreSQL and Firebird:

sql
CREATE TABLE KITTO_USER_ROLES (
  USER_NAME varchar(50) NOT NULL,
  ROLE_NAME varchar(50) NOT NULL,
  CONSTRAINT PK_KITTO_USER_ROLES PRIMARY KEY (USER_NAME, ROLE_NAME)
);

CREATE TABLE KITTO_PERMISSIONS (
  RESOURCE_URI_PATTERN varchar(200) NOT NULL,
  GRANTEE_NAME         varchar(50)  NOT NULL,
  ACCESS_MODES         varchar(100) NOT NULL,
  GRANT_VALUE          varchar(50)  NOT NULL,
  CONSTRAINT PK_KITTO_PERMISSIONS PRIMARY KEY (RESOURCE_URI_PATTERN, GRANTEE_NAME, ACCESS_MODES)
);

The ready-to-use scripts ship with TasKitto for all three engines — see TasKitto: Database scripts.

2. Define your roles

A role is just a name in KITTO_USER_ROLES. Don't over-engineer: most apps need only three or four roles. The TasKitto demo uses three:

RoleIntent
adminFull CRUD on every view, including user-management views
userFull CRUD on every view, except user-management views
viewerRead-only on every view, except user-management views

Map your users to roles in KITTO_USER_ROLES:

sql
INSERT INTO KITTO_USER_ROLES (USER_NAME, ROLE_NAME) VALUES ('admin',   'admin');
INSERT INTO KITTO_USER_ROLES (USER_NAME, ROLE_NAME) VALUES ('user',    'user');
INSERT INTO KITTO_USER_ROLES (USER_NAME, ROLE_NAME) VALUES ('guest',   'viewer');

A user can have multiple rows here — the permissions are unioned across all assigned roles.

3. Apply the layered grants

Three layers, evaluated together by the FALSE-priority loop. Always copy URIs and mode codes verbatim from the resource URI tables — pattern matching is byte-by-byte case-sensitive, so view/customers will not match the URI metadata://View/Customers.

Layer 1 — Wildcard read-only baseline

One row that grants VIEW and READ to every authenticated user (GRANTEE_NAME = '*'). VIEW makes views appear in menus; READ makes the cell values render in grids and forms (without READ the columns appear but the cells are blank — see READ vs VIEW).

sql
INSERT INTO KITTO_PERMISSIONS VALUES ('*', '*', 'VIEW,READ', '1');

Layer 2 — Role-specific allows

One row per role that lifts the role above the baseline by granting the write modes (MODIFY, ADD, DELETE, RUN). The wildcard pattern '*' means "on every URI"; combined with the layer-3 denies, you carve out exceptions instead of enumerating every (role, view) pair.

sql
INSERT INTO KITTO_PERMISSIONS VALUES ('*', 'admin', 'MODIFY,ADD,DELETE,RUN', '1');
INSERT INTO KITTO_PERMISSIONS VALUES ('*', 'user',  'MODIFY,ADD,DELETE,RUN', '1');
-- 'viewer' has no row here — it stays at the layer-1 read-only level.

Layer 3 — Per-resource denies

Specific URIs you want to hide or restrict for some roles. Use GRANT_VALUE='0' to deny — the FALSE-priority break in TKUserPermissionStorage.GetAccessGrantValue ensures the deny dominates the wildcard allow at layer 1 and 2, regardless of insertion order.

sql
-- Hide the Users table from non-admins.
-- Listing all six standard modes makes the deny watertight against any
-- future allow that might appear (e.g. promoting 'viewer' to 'user' later).
INSERT INTO KITTO_PERMISSIONS VALUES ('metadata://View/Users', 'user',   'VIEW,READ,MODIFY,ADD,DELETE,RUN', '0');
INSERT INTO KITTO_PERMISSIONS VALUES ('metadata://View/Users', 'viewer', 'VIEW,READ,MODIFY,ADD,DELETE,RUN', '0');

You can also use wildcards in patterns — metadata://View/Activity* matches every view name starting with Activity — or regex with the REGEX: prefix.

4. Configure the access controller in Config.yaml

Option A — AccessControl: DB (classic)

The DB controller hits the database on the first IsAccessGranted call per user (process-wide cache afterwards). Permission changes mid-session are visible after the per-process cache is invalidated (typically: app restart). Best fit when grants change rarely or you can tolerate a restart.

yaml
AccessControl: DB

Default queries against KITTO_PERMISSIONS and KITTO_USER_ROLES work out of the box. Add Kitto.AccessControl.DB to your UseKitto.pas so the 'DB' controller class id is registered.

Option B — AccessControl: JWT (stateless)

The JWT controller reads grant rows from the kx_acl claim that TKJWTAuthenticator snapshots into the JWT at login. Zero DB hit per check — perfect for high-traffic apps and clusters with no shared cache. Trade-off: changes to KITTO_PERMISSIONS mid-session take effect only after the user logs in again.

The kx_acl claim is auto-populated when AccessControl: JWT is configured — no opt-in flag. Closed-world: anything not covered by the claim is denied; for DB-driven evaluation use AccessControl: DB instead. Requires Auth: JWT (see JWT Authenticator) and pulls in the delphi-jose-jwt library.

yaml
Auth: JWT
  Inner: DB
    ...

AccessControl: JWT

Add Kitto.Auth.JWT, Kitto.AccessControl.JWT and Kitto.AccessControl.DB (the latter still provides the SQL templates / class id reused at login to build the claim) to UseKitto.pas. See Access control via kx_acl claim for the full reference.

5. Verify

Log in with each role and confirm:

  • admin sees everything, every grid is editable, all menu entries visible (including user-management views).
  • user sees and edits everything except the views denied at layer 3 (e.g. Users).
  • viewer sees everything (including data values in grids), no Add/Edit/Delete buttons (those modes are not granted), and the views denied at layer 3 are hidden from the menu.

If the menu is empty or grids show empty cells, check the Common pitfalls table — almost always a case mismatch in pattern or mode code.

Enable AC logging while debugging

Add a Log block to log every check to a file:

yaml
AccessControl: DB
  Log:
    FileName: %APP_PATH%AC_log.txt
    IncludeHeader: True

Each line shows (timestamp, user, URI, mode, default, result) and is the fastest way to find a typo in your patterns.

See also

Released under Apache License, Version 2.0.