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:
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:
| Role | Intent |
|---|---|
admin | Full CRUD on every view, including user-management views |
user | Full CRUD on every view, except user-management views |
viewer | Read-only on every view, except user-management views |
Map your users to roles in KITTO_USER_ROLES:
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).
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.
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.
-- 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.
AccessControl: DBDefault 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.
Auth: JWT
Inner: DB
...
AccessControl: JWTAdd 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:
adminsees everything, every grid is editable, all menu entries visible (including user-management views).usersees and edits everything except the views denied at layer 3 (e.g.Users).viewersees 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:
AccessControl: DB
Log:
FileName: %APP_PATH%AC_log.txt
IncludeHeader: TrueEach line shows (timestamp, user, URI, mode, default, result) and is the fastest way to find a typo in your patterns.
See also
- Access Control — concepts, URI tables, FALSE-priority semantics, common pitfalls
- JWT Authenticator — stateless authentication and the
kx_aclclaim - TasKitto: Sample ACL design — the running example that ships with the framework, reproducing exactly the matrix described here
