Skip to main content

Roles and permissions

A role in Liberty is a name + a list of permission strings. Users get roles; roles grant or deny what users can do.

The Settings → Access → Roles tab is where you build them. The page hides every permission-string detail behind a PermissionPicker — you don't type sql:crm:customers_get; you pick Query → customers → customers_get from dropdowns and the picker generates the string.


The Roles tab

Settings · Access👤 Users🛡 Roles+ Add roleadminFull superuser equivalent — wildcard baseline.Full accessanalystFull access, except destructive operations.3 rulesreaderRead-only on the crm app.2 rulesClick a row to open the Role editor.

Each card shows the role's id, description, and a count of allow/deny rules (or a Full access chip when the baseline is * with no further restrictions).


The Role editor

Click a role row to open the editor. It has three pieces:

SectionNotes
Role nameRead-only after creation. Renaming requires deleting and recreating with the new name (no cross-file rename for roles — references in users update automatically because users carry role names, not ids).
DescriptionFree text. Shown on the card.
PermissionsThe PermissionPicker — baseline + allow / deny rules.

The PermissionPicker — anatomy

Role editor · analystBASELINE⊘ No access✓ Full accesseverything, minus the denies belowRULES!sql:crm:customers_delete!menu:crm:admin!ai:chatADD A RULE✓ Allow⊘ DenyMenu ▾Pick a menu…+ Add

The baseline

Two buttons, mutually exclusive:

BaselinePermissions listSemantics
No accessstarts emptyNothing is allowed except what you add via Allow rules. The right default for least-privilege roles.
Full accessstarts with ["*"]Everything is allowed except what you remove via Deny rules. The right default for superuser-like roles.

The hint below the toggle reads either "nothing, plus the allows below" or "everything, minus the denies below" depending on the choice.

The rule list

Each rule is a coloured chip:

Chip colourMeaning
GreenAn allow rule — sql:crm:customers_get, menu:crm:pipeline.
RedA deny rule — the same shapes but prefixed ! on storage (!sql:crm:customers_delete).

The ✕ on each chip removes the rule. The list at the top of the Picker is sorted by the order rules were added.

Adding a rule

Three controls below the chips:

ControlWhat it does
Effect (Allow / Deny)Two-button toggle. Allow adds the chip without the ! prefix; Deny adds it with !.
TypeDropdown — Menu / Dashboard / Connector (all queries) / Query / API endpoint / AI assistant. Drives the available items in the next dropdown.
ItemA search-select populated based on Type. For Menu it lists every menu item from every app; for Connector it lists SQL connectors; for Query it lists every named query; for Dashboard it lists every dashboard id; for API endpoint it lists every API connector's endpoints; for AI assistant there's a single entry.
+ AddResolves the picked item into one or more permission patterns, prefixes ! if Deny, appends to the rule list.

The picker handles the pattern composition so you don't have to remember the sql:<c>:<q> syntax. You think in terms of "allow this menu" or "deny this query"; the storage looks like menu:crm:reports or !sql:crm:customers_delete.

When you allow a Menu rule, the picker expands it into multiple permission patterns:

  • menu:<app>:<id> — so the menu item itself shows in the navigation.
  • menu:<app>:<descendant_id> — for each descendant leaf (so submenus show).
  • sql:<connector>:<target> or api:<connector>:<target> — for each descendant leaf's underlying query / endpoint, so the screens / endpoints actually run.

This means an Allow → Menu → Pipeline rule grants the user the whole Pipeline folder and every screen inside it, in one go. The same expansion happens on Deny — denying Menu → Admin hides the folder and refuses every screen inside.

Without this expansion, you'd have to add an allow for the menu and a separate allow for each underlying query — an error-prone manual chore. The picker does it for you.


Resolution order

When the framework checks a permission, it runs through this order:

  1. Superuser check — if the user has is_superuser = true, allow everything (deny rules ignored).
  2. Explicit deny — if any !<pattern> in the user's combined roles' permissions matches, reject.
  3. Explicit allow — if any non-! pattern matches, allow.
  4. Default deny — otherwise, reject.

The order matters. A ["*", "!sql:crm:customers_delete"] permission list:

  • sql:crm:customers_get → matches * (allow), no matching deny → allowed.
  • sql:crm:customers_delete → matches * (allow) and matches !sql:crm:customers_delete (deny) → deny wins → rejected.
  • sql:reporting:* → matches * (allow), no matching deny → allowed.

Pattern syntax — the full grammar

Permission strings use colon-segmented globs:

OperatorMeaningExample
<word>Literal segment.sql, crm, customers_get.
* (single segment)Matches one segment.sql:*:customers_get matches sql:crm:customers_get and sql:reporting:customers_get.
* (trailing)Matches the rest.sql:crm:* matches every sql:crm:<anything>.
!<pattern>Deny.!sql:crm:customers_delete.
* (alone)Wildcard everything.The Full access baseline.
!*Deny everything.Rare; effectively a kill switch.

The full surfaces:

PatternGrants
sql:<connector>:<query>Run one SQL query.
sql:<connector>:*Run every SQL query on a connector.
sql:*Run every SQL query everywhere.
api:<connector>:<endpoint>Call one API endpoint.
api:<connector>:*Call every endpoint on a connector.
menu:<app>:<id>See one menu item (folder or leaf).
menu:<app>:*See every menu item under an app.
dashboard:<id>Open one dashboard.
ai:chatUse the AI assistant.
*Everything.

Standard role recipes

RoleBaselineRules
SuperuserFull access(empty) — equivalent to flipping is_superuser on the user.
Reader on the CRM appNo accessAllow → Menu → CRM (which expands into menu:crm:* + sql:crm:* for the read queries).
Power user on CRM except deleteFull accessDeny → Query → customers_delete, deals_delete, activities_delete.
AI-only accessNo accessAllow → AI assistant. The user signs in and gets the AI chat surface but no menu / no screens.
Reports managerNo accessAllow → Menu → Reports + Allow → Dashboard → revenue_overview / pipeline_health / etc.
Admin minus AIFull accessDeny → AI assistant. Everything else allowed.

The Permissions Picker handles every case through the same Allow / Deny / Type / Item cascade.


How a user's effective permissions compose

A user can have several roles. The framework concatenates the permission lists from every role — deny wins across roles too.

Example: Alice has both analyst and reporter:

  • analyst.permissions = ["*", "!sql:crm:customers_delete"]
  • reporter.permissions = ["sql:reporting:*", "menu:reporting:*"]

Alice's effective permissions:

  • sql:crm:customers_get → allowed by * in analyst.
  • sql:crm:customers_delete → denied by analyst's !sql:crm:customers_delete even though * would allow it.
  • sql:reporting:monthly_revenue → allowed by both * (analyst) and sql:reporting:* (reporter) — redundant but fine.

There's no way for one role to "override" another role's deny — deny is final across the user's complete role set.


Common pitfalls

MistakeSymptomFix
Allow a Menu without realising it grants the underlying queries.A user has more SQL access than expected.Use Allow → Query for fine-grained control; reserve Allow → Menu for "grant the whole section".
Full access baseline + a few allow rules.The allows are redundant (the baseline already grants them).Either use Full access (and deny exceptions) or No access (and allow specifics). Not both.
!sql:crm:* thinking it'll override an inherited allow.The deny works — but the user has no SQL access on crm even when other roles grant it.Denies are absolute. Use them deliberately.
Renaming a role through editing the name.Name is read-only after creation.Delete the role and recreate with the new name. Re-assign users.
Forgetting that JWT changes lag.A user gets a new role but doesn't see it for an hour.The user signs out and back in, or waits for the access token TTL.

What's next