Skip to content

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 ModeMeaning
READUser can see the data
ADDUser can add new records
MODIFYUser can modify existing records
DELETEUser can delete records

Resource Type: ModelField

Sample URI: metadata://Model/Party/Party_Name

Access ModeMeaning
READUser can see the data (otherwise the particular field/column is not visible)
MODIFYWhen 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 ModeMeaning
VIEWUser sees the view (in a menu or toolbar, for example)
RUNUser 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 ModeMeaning
READUser can see the data
ADDUser can add new records
MODIFYUser can modify existing records
DELETEUser 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 ModeMeaning
READUser can see the data (otherwise the particular field/column is not visible)
MODIFYWhen 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):

NameUnitDescription
DBKitto.AccessControl.DBRBAC 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.
JWTKitto.AccessControl.JWTReads 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):

yaml
AccessControl: DB
  ReadRolesCommandText: |
    select ROLE_NAME from MY_USER_ROLES
    where USER_NAME = :USER_NAME or USER_NAME = '*'
    order by ROLE_NAME

Example (JWT — closed-world, claim is the sole source of truth):

yaml
Auth: JWT
  Inner: DB
    ...

AccessControl: JWT

The 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_NAMEROLE_NAME
adminadmin
useruser
guestviewer

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_PATTERNGRANTEE_NAMEACCESS_MODESGRANT_VALUE
metadata://View/CustomersadminVIEW,MODIFY,ADD,DELETE,RUN1
  • RESOURCE_URI_PATTERN — pattern matched against the URI built by TKMetadata.BuildURI (see the resource tables above). Supports * and ? wildcards, ~ to negate, REGEX:/~REGEX: for regular expressions.
  • GRANTEE_NAME — either a user name (matches USER_NAME in KITTO_USERS) or a role name (matches ROLE_NAME in KITTO_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

PatternMatches
*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/CustomersThe Customers view (exact match — does not match field URIs under it).
~metadata://View/*Everything but views (negation).
`REGEX:^metadata://View/(CustomersActivities)$`

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.

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:

LayerPurposeCardinalityExample
1. Wildcard baselineRead-only minimum for everyone1 row('*', '*', 'VIEW,READ', '1')
2. Role-specific allowsGrant write modes per role1 row per role('*', 'admin', 'MODIFY,ADD,DELETE,RUN', '1')
3. Per-resource deniesHide or read-only specific views per roleas 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:

  • VIEW is checked on the view itself (metadata://View/<Name>) when the menu is rendered (TKMetadata.IsAccessGranted(ACM_VIEW) in TreePanel/TilePanel/ToolBar). A VIEW=0 deny on a view URI hides the menu entry entirely.
  • READ is checked on each field (TKViewField.IsAccessGranted(ACM_READ) in Kitto.Metadata.DataView.pas) when the data row is serialized to the client. A READ=0 deny 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:

  1. 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 whose ACCESS_MODES includes the requested mode contributes a tentative grant value.
  2. Last-row-wins for non-standard modes — a custom mode like EXPORT keeps the value of the most recent matching row.
  3. 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 a GRANT_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.
  4. Default to deny — if no row matches, the result is Null and IsAccessGranted returns False (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 prefixMode
/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.

yaml
# Views/RegisterNewUser.yaml
ACName:                   # explicit empty value → public view
Controller: Form
MainTable:
  Model: KITTO_USERS

Idiomatic 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

SymptomLikely causeFix
Menu is empty except for Logout and ChangePasswordPattern 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 rolesAuto-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 cellsWildcard 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 userAccessControl: 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: DBAuth: JWT can still be used independently for authentication.
Custom mode is silently deniedThe 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 notThe 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 logoutPre-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:

yaml
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

Released under Apache License, Version 2.0.