Access Control
Access control is used in Kittox to manage user access to resources. Resources in this context are everything that the user can access in any way, such as models, views and fields. Each resource is uniquely identified by a string in URI format, and supports one or more access modes.
A complete worked example with three roles (admin / user / viewer), six grant rows and a layered design that covers menu visibility, CRUD, field-level READ and Users-table protection ships with TasKitto — read this page first for the conceptual model, then jump there for the data you can copy-paste into your own app.
Resources, Privileges and Access Modes
A user can perform a given operation (access mode) on an object (resource) if she has the corresponding privilege(s). For example, in order to be able to change a field's value, you need to own the MODIFY privilege on both the ModelField and the ViewField that represents it. Plus, you need the MODIFY (if editing an existing record) or ADD (if creating a new record) privileges on the ViewTable and Model.
The following tables summarize the current built-in AC-enabled resource types in Kittox and the access modes each of them supports. Since resource URIs and access modes are nothing but strings, you can add your own resource types and modes if you want, and hook them to the system
Resource Type: Model
Sample URI: metadata://Model/Party
| Access Mode | Meaning |
|---|---|
| READ | User can see the data |
| ADD | User can add new records |
| MODIFY | User can modify existing records |
| DELETE | User can delete records |
Resource Type: ModelField
Sample URI: metadata://Model/Party/Party_Name
| Access Mode | Meaning |
|---|---|
| READ | User can see the data (otherwise the particular field/column is not visible) |
| MODIFY | When editing or adding records, user can set the field's value |
When you don't grant MODIFY access to particular fields to a user that can add records, you must make sure that these fields, which will be read-only for the user, receive suitable default or otherwise computed values.
Resource Type: View Sample URI: metadata://View/Customers
| Access Mode | Meaning |
|---|---|
| VIEW | User sees the view (in a menu or toolbar, for example) |
| RUN | User can launch the view by clicking it (otherwise the item will look disabled) |
If you want a user to see a feature that is not available to him, then you grant the VIEW mode but not the RUN mode to the relevant view. If you want to completely hide a feature, you grant neither of them.
Resource Type: ViewTable (part of a DataView)
Sample URI: metadata://View/Parties metadata://View/Parties/Invitation
| Access Mode | Meaning |
|---|---|
| READ | User can see the data |
| ADD | User can add new records |
| MODIFY | User can modify existing records |
| DELETE | User can delete records |
Please note that a DataView's MainTable shares the same URI as the view. IOW, you don't need to individually grant privileges for a DataView and its MainTable.
You need to grant privileges both at the view and model level for them to be effective. This is so to allow you to make different views for different kinds of users based on the same models.
Resource Type: ViewField (a field in a ViewTable)
Sample URI: metadata://View/Parties/Party_Name
| Access Mode | Meaning |
|---|---|
| READ | User can see the data (otherwise the particular field/column is not visible) |
| MODIFY | When editing or adding records, user can set the field's value |
Access Controllers
Access control is implemented by a class that you can optionally enable. By default, the Null access controller class is used, which always grants access to everything. You can use a predefined access controller or create your own.
In order to enable an access controller you need to:
- Specify its name and settings in the config file.
- Include/use the relevant unit in your project.
Kittox ships two access controllers (besides the default Null one):
| Name | Unit | Description |
|---|---|---|
| DB | Kitto.AccessControl.DB | RBAC implementation reading KITTO_PERMISSIONS and KITTO_USER_ROLES from a database. Hits the DB on first access per user/process and caches the result in memory for the session. |
| JWT | Kitto.AccessControl.JWT | Reads the grant rows from the kx_acl claim that TKJWTAuthenticator snapshots into the JWT at login. Closed-world: if the claim does not cover a (resource, mode), access is denied. Zero DB hit per IsAccessGranted call once the user is logged in. Requires Auth: JWT. For DB-driven evaluation use AccessControl: DB instead — Auth: JWT can still be used independently. |
The DB controller implements a simplified RBAC (Role Based Access Control) strategy. The JWT controller reuses the same matching logic but the rows live in the JWT envelope instead of the database — see JWT Authenticator: Access control via kx_acl claim.
Example (DB):
AccessControl: DB
ReadRolesCommandText: |
select ROLE_NAME from MY_USER_ROLES
where USER_NAME = :USER_NAME or USER_NAME = '*'
order by ROLE_NAMEExample (JWT — closed-world, claim is the sole source of truth):
Auth: JWT
Inner: DB
...
AccessControl: JWTThe framework auto-populates the kx_acl claim at login when AccessControl: JWT is configured. There is no opt-in flag and no DB fallback — anything not covered by the claim is denied.
Users, Roles and the Permissions Table
The DB Access Controller stores privileges in two tables — KITTO_USER_ROLES and KITTO_PERMISSIONS — which the JWT controller also reads at login time (when AccessControl: JWT is configured) to build the kx_acl claim snapshot — see JWT Authenticator.
KITTO_USER_ROLES
Maps users to roles. A user can have multiple roles; they are all active simultaneously and their permissions are unioned.
| USER_NAME | ROLE_NAME |
|---|---|
admin | admin |
user | user |
guest | viewer |
A row with USER_NAME = '*' is loaded for every user — useful for app-wide defaults that survive role assignment.
KITTO_PERMISSIONS
The grant table. Each row is a (pattern, grantee, modes, value) tuple:
| RESOURCE_URI_PATTERN | GRANTEE_NAME | ACCESS_MODES | GRANT_VALUE |
|---|---|---|---|
metadata://View/Customers | admin | VIEW,MODIFY,ADD,DELETE,RUN | 1 |
RESOURCE_URI_PATTERN— pattern matched against the URI built byTKMetadata.BuildURI(see the resource tables above). Supports*and?wildcards,~to negate,REGEX:/~REGEX:for regular expressions.GRANTEE_NAME— either a user name (matchesUSER_NAMEinKITTO_USERS) or a role name (matchesROLE_NAMEinKITTO_USER_ROLES). The wildcard'*'matches every grantee — use it for app-wide defaults.ACCESS_MODES— comma-separated list of modes (e.g.VIEW,MODIFY,ADD,DELETE,RUN,READ). The grant applies to each mode in the list.GRANT_VALUE—'1'to allow,'0'to deny. For the standard modes (VIEW,READ,MODIFY,ADD,DELETE,RUN) a'0'deny dominates any'1'allow on the same(URI, mode)pair (see FALSE-priority semantics).
Pattern examples
| Pattern | Matches |
|---|---|
* | Everything (typical wildcard for the read-only baseline). |
metadata://Model/* | All models and model fields. |
metadata://Model/Party/* | All fields of model Party. |
metadata://Model/*/* | All fields of all models. |
metadata://View/* | All views, view tables and fields. |
metadata://View/Customers | The Customers view (exact match — does not match field URIs under it). |
~metadata://View/* | Everything but views (negation). |
| `REGEX:^metadata://View/(Customers | Activities)$` |
Pattern matching is case-sensitive
StrMatchesPatternOrRegex (in EF.RegEx.pas) compares characters byte-by-byte. The pattern view/customers does not match the URI metadata://View/Customers (lowercase view vs capital View). Always copy the URI shape from the resource tables above (capital first letter for the class name: View, Model, Tool, ...).
The same case sensitivity applies to mode codes: View is not the same as VIEW (the ACM_* constant). Use VIEW, READ, MODIFY, ADD, DELETE, RUN — uppercase.
You can also use a regular expression (regex) as a pattern, provided you add the REGEX: prefix so that Kittox knows how to interpret the rest. You can use the ~REGEX: prefix if you want to negate the regex instead.
A role is a container of related privileges. Each user can have many roles, which are all active at the same time. Users and roles are identified by unique names of your choosing.
When defining your access control strategy, you would normally want to group privileges into roles and then assign as many roles as required to each user.
Layered design pattern (recommended)
Rather than enumerating every grant for every (role, view) pair, KittoX apps typically use a three-layer pattern that takes advantage of the matching loop's FALSE-priority semantics:
| Layer | Purpose | Cardinality | Example |
|---|---|---|---|
| 1. Wildcard baseline | Read-only minimum for everyone | 1 row | ('*', '*', 'VIEW,READ', '1') |
| 2. Role-specific allows | Grant write modes per role | 1 row per role | ('*', 'admin', 'MODIFY,ADD,DELETE,RUN', '1') |
| 3. Per-resource denies | Hide or read-only specific views per role | as needed | ('metadata://View/Users', 'viewer', 'VIEW,READ,MODIFY,ADD,DELETE,RUN', '0') |
Layer 1 makes every view visible and every cell readable for any authenticated user (this is what makes data values appear in the grid — see the READ vs VIEW note below). Layer 2 lifts a role above the baseline by granting CRUD modes on top. Layer 3 carves out exceptions: a deny at layer 3 wins over an allow at layer 1 or 2 because of the FALSE-priority break.
The full TasKitto demo implements exactly this pattern with three roles — see TasKitto: Sample ACL design.
Field-level access: READ vs VIEW
Two modes control visibility at different levels of the metadata tree:
VIEWis checked on the view itself (metadata://View/<Name>) when the menu is rendered (TKMetadata.IsAccessGranted(ACM_VIEW)inTreePanel/TilePanel/ToolBar). AVIEW=0deny on a view URI hides the menu entry entirely.READis checked on each field (TKViewField.IsAccessGranted(ACM_READ)inKitto.Metadata.DataView.pas) when the data row is serialized to the client. AREAD=0deny on a field URI shows the column header but blanks out the cell value.
If your wildcard baseline grants only VIEW and not READ, every grid will render with empty cells — the columns appear (because the view is visible) but the values do not (because field-level READ is denied by default). The TasKitto demo's wildcard baseline ('*', '*', 'VIEW,READ', '1') enables both at once, which is the right starting point for almost every app.
FALSE-priority semantics and deny rules
When the access controller evaluates IsAccessGranted(user, URI, mode), it walks the loaded permission rows and applies a layered evaluation:
- Iterate all rows for the user (and for the user's roles, and for the wildcard
'*'grantee). Each row whose pattern matches the URI and whoseACCESS_MODESincludes the requested mode contributes a tentative grant value. - Last-row-wins for non-standard modes — a custom mode like
EXPORTkeeps the value of the most recent matching row. - FALSE wins for standard modes — for the six standard modes (
VIEW,READ,MODIFY,ADD,DELETE,RUN) the loop breaks early as soon as it sees aGRANT_VALUE='0'. An explicit deny on a specific URI dominates any allow on a wildcard pattern, regardless of insertion order. This is what makes the layered design work: layer-3 denies are guaranteed to override layer-1 and layer-2 allows. - Default to deny — if no row matches, the result is
NullandIsAccessGrantedreturnsFalse(closed-world default).
The same evaluator runs in TKDBAccessController (TKUserPermissionStorage.GetAccessGrantValue in Kitto.AccessControl.DB.pas) and TKJWTAccessController (EvaluateFromClaim in Kitto.AccessControl.JWT.pas), so the semantics are identical whether the rows live in the database or in the JWT envelope.
Server-side ACL enforcement on route handlers
UI components (TreePanel, ToolBar, TabPanel, TilePanel, …) filter visible/clickable items via IsAccessGranted so denied resources never appear in the navigation. The framework also enforces the same ACL at the route handler level, so a hostile client cannot bypass the menu by typing a URL directly.
Every HandleKX* route in TKWebApplication checks the appropriate access mode before running:
| Route prefix | Mode |
|---|---|
/kx/view/<X> | ACM_VIEW |
/kx/data/<X> | ACM_VIEW |
/kx/delete/<X> | ACM_DELETE |
/kx/save/<X> | ACM_ADD (new/dup) or ACM_MODIFY (else) |
/kx/form/<X>?op=... | ACM_ADD / ACM_VIEW / ACM_MODIFY per op |
/kx/lookup/<X> | ACM_READ |
/kx/blob/<X>/<field> | ACM_READ |
/kx/upload/<X>/<field> | ACM_MODIFY |
/kx/view/<X>/tool/<T> | ACM_RUN |
/kx/view/<X>/detail/<i>/... | per-operation: ACM_VIEW/ACM_MODIFY/ACM_DELETE |
A request that fails the check returns 404 with empty body. The same response shape is used for "view does not exist" and "user not authenticated" (a separate gate in DoHandleRequest blocks unauthenticated requests on protected routes), so a probe cannot distinguish the three states. The denial is logged at LOG_DETAILED with entries like 'ACL deny: user "<name>" requested view "<View>" mode <MODE>'.
The server-side enforcement is independent of AccessControl: DB vs JWT: both controllers go through TKAccessController.IsAccessGranted with the same evaluation logic.
Public views: ACName: empty
The unauthenticated gate in DoHandleRequest honours TKMetadata.GetACURI: when a view's YAML declares an empty ACName:, GetACURI returns '' and TKAccessController.GetAccessGrantValue short-circuits to ACV_TRUE for any user (authenticated or not). The gate detects this and lets every related route through (/kx/view/<X>, /kx/save/<X>, /kx/blob/<X>/<field>, /kx/view/<X>/tool/<T>, …) without further code change.
# Views/RegisterNewUser.yaml
ACName: # explicit empty value → public view
Controller: Form
MainTable:
Model: KITTO_USERSIdiomatic uses: pre-login landing pages reachable from the Login form (ResetPassword, RegisterNewUser, PrivacyPolicy), public legal/marketing views, anonymous-feedback forms. Adding ACName: to a single YAML opens the view to the world; nothing else needs to be changed in the framework or the application code.
Toolbar buttons and ACL deny
The Add, Dup, Edit, Delete, and View buttons on a list view's toolbar are rendered with the HTML disabled attribute when the active session lacks the corresponding mode (ACM_ADD, ACM_MODIFY, ACM_DELETE, ACM_VIEW). The buttons are also stripped of the kx-requires-selection CSS class in that case, so the client-side selection handler that toggles disabled = !hasSelection cannot re-enable them when the user picks a row. This keeps the read-only experience consistent: a viewer never finds a CRUD button clickable, regardless of grid interaction.
Common pitfalls
| Symptom | Likely cause | Fix |
|---|---|---|
Menu is empty except for Logout and ChangePassword | Pattern or mode case mismatch — every view URI gets denied by default; only inline controllers with empty PersistentName (and thus empty URI) pass the empty-URI shortcut in TKAccessController.GetAccessGrantValue. | Use metadata://View/<Name> (capital V) and uppercase mode codes (VIEW, not View). |
Deny rule on a View: Build AutoList (auto-built view) is ignored, the menu entry is visible to all roles | Auto-built views have no PersistentName until the menu is rendered for the first time — the access check sees an empty URI and the empty-URI shortcut grants access. Once the framework caches the view it assigns MainTable.ModelName as the PersistentName (Kitto.Html.TreePanel.pas:122-130), so subsequent renders use metadata://View/<ModelName> (uppercase, exactly as the model name). | Write the deny against metadata://View/<MODEL_NAME> — for View: Build AutoList / Model: KITTO_PERMISSIONS the URI is metadata://View/KITTO_PERMISSIONS. The deny activates from the second render onward; if you need it from the very first render of a fresh process, declare the view in a dedicated YAML file with an explicit PersistentName instead. |
| Menu is correct but data grids show empty cells | Wildcard grants VIEW but not READ. | Add READ to the wildcard: ('*', '*', 'VIEW,READ', '1'). |
| Permission change in the DB is not reflected for the logged-in user | AccessControl: JWT snapshots grants into kx_acl at login, by design. | Force a re-login (delete the user's session or reduce TokenLifetime). To evaluate via DB on every check switch to AccessControl: DB — Auth: JWT can still be used independently for authentication. |
| Custom mode is silently denied | The check in IsStandardMode returns False for non-standard modes, so the FALSE-priority break does not apply, but the last matching row wins. Make sure the last row in iteration order grants the mode. | Use mode-specific patterns and a single allow row to avoid relying on iteration order. |
| Some users see a view, others do not | The matching cache is per-user (and per-process for TKDBAccessController). A new permission row is reloaded only on the next user's first request after login. | For AccessControl: DB, restart the app to flush the per-user cache; for AccessControl: JWT, force a re-login. |
/kx/view/<X> returns sensitive data after logout | Pre-Auth: JWT build without the post-logout authentication gate in DoHandleRequest. | Upgrade to a build that includes the server-side ACL enforcement — the gate now returns 404 with empty body for any unauthenticated request to a protected route. |
Additional features
All Access controllers (including the Null AC) support logging. When logging to file is enabled, the AC logs for every request received a timestamp, the user, the resource URI, the access mode and the result (1 for access granted, 0 for access denied). This is convenient both to troubleshoot privilege definition issues and to see how resource URIs are built so you can create correct matching patterns.
Example:
AccessControl: Null
Log:
FileName: %HOME_PATH%AC_log.txt
# By default fields are not delimited
# and separated by the Tab character.
FieldSeparator: ,
# Only the first character is used as delimiter.
FieldDelimiter: "
# By default the first line output in a file
# contains field titles and not data.
IncludeHeader: False