Roles And Permissions

What It Is

Jetstack combines several access-control layers:

  • role assignment
  • presenter and navigation ACL
  • system permissions
  • type permissions
  • property permissions
  • optional permission queries

That combination is powerful, but it also means access is not determined by a single yes-or-no matrix. The runtime resolves permissions cumulatively and in a specific order.

Why It Matters

This is one of the most important governance layers in the platform. It decides:

  • who can open certain areas of the application
  • who can create, show, edit, or delete objects
  • who can see or modify specific properties
  • whether access is global or narrowed to objects matching a permission query

To design access well, you need to understand not only what permissions exist, but how the runtime combines them.

Mental Model

Keep these layers separate:

  1. Base system roles provide built-in capabilities.
  2. Presenter ACL decides whether the user can access application areas such as presenters and tools.
  3. Stored permission rules add or narrow system, type, and property actions.
  4. Permission queries can turn an allow into an object-scoped allow.
  5. Surrounding availability rules for modules, views, widgets, and automations still affect what the user can actually reach.

That is why "the user has the role" is never the full answer on its own.

Roles

Roles define stable capability bundles and availability relationships. In practical terms, a role can influence:

  • permission matrices
  • module availability
  • view availability
  • widget availability
  • automation availability
  • which roles are available for assignment by another role

Role order also matters.

role_priority

What it is:

  • the numeric priority used to order roles

How it works:

  • lower numeric priority means higher importance
  • user roles are sorted by priority before permission evaluation

Why it matters:

  • the permission resolver iterates roles in priority order
  • because the resolver stops at the first matching rule, role priority affects which rule wins first

This is a key part of the cumulative logic.

Permission Layers

Jetstack evaluates permissions in three main stored layers:

System permissions

These govern broad application capabilities such as:

  • homepage access
  • navigation-level capabilities
  • filter-list, tag, user, version, and secret management
  • AI, integration, export, and tooling capabilities

Use system permissions when the action is not tied to one business type or one property.

Type permissions

These govern actions on a type, such as:

  • list
  • show
  • create
  • edit
  • delete
  • freeCreate
  • freeEdit
  • type-specific actions
  • tab actions such as showTab.*

Use type permissions when the rule is about objects of a type as a whole.

Property permissions

These govern field-level actions such as:

  • property visibility
  • property create-time access
  • property edit-time access

Use property permissions when access must differ at the field level.

Wildcard Rules And Specific Rules

The permissions manager supports broad wildcard-style rules through NULL values in storage:

  • type permissions can be defined for all types
  • actions can be defined for all actions
  • property permissions can be defined for all properties of the addressed scope
  • system permissions can be defined for all system actions

This is important because the runtime looks for matches in a fallback order:

  1. exact role + exact target + exact action
  2. exact role + wildcard target + exact action
  3. exact role + exact target + wildcard action
  4. exact role + wildcard target + wildcard action

That fallback order is one of the main reasons the permission model feels cumulative.

Cumulative Permission Logic As Implemented

The actual runtime logic in permissionsManager.php is not "collect all matching permissions and then merge them mathematically." It behaves more like ordered resolution with fallback.

Step 1: Roles are evaluated in priority order

The current user’s roles are sorted by role_priority in ascending order before the permission manager evaluates them.

That means:

  • lower priority number is checked first
  • if an earlier role produces a match, later roles may never matter for that permission check

This is the first important implementation detail.

Step 2: Built-in system-role shortcuts are checked first

Before stored permission tables are consulted, the permissions manager checks built-in implicit capabilities for system roles.

These built-in capabilities can immediately allow access without consulting stored permission rows.

This applies especially to:

  • Superuser
  • Reader
  • Publisher sometimes informally referred to as "Published"
  • Editor
  • Model Implementer

These are described in detail later in this chapter.

Step 3: The resolver looks for the first matching allow or query

If no built-in shortcut applies, the manager checks its caches in fallback order.

For type permissions, the matching order is effectively:

  1. role + exact type + exact action
  2. role + all types + exact action
  3. role + exact type + all actions
  4. role + all types + all actions

If a match is found:

  • true means allowed
  • a numeric query ID becomes a permission Query
  • false means denied for that checked path

The first matching result wins.

Step 4: Explicit denies are not a universal global veto

This is one of the most important implementation details.

The manager stores deny rules separately and uses them mainly to stop fallback from broader rules for the same role and exact target/action combination.

What this means in practice:

  • an explicit deny on a specific role/type/action blocks that role from inheriting a broader allow through wildcard fallback
  • but another role can still allow the same action later
  • an earlier matching allow can effectively win before a later role is even considered

So the implementation is cumulative and additive across roles, but not in the sense of "any deny anywhere always overrides everything."

The safer implementation-facing rule is:

  • exact denies stop inherited fallback for the same role path
  • later roles can still allow
  • earlier higher-priority role matches win first

Step 5: Permission queries are not merged

If the match is a query-backed allow, the manager returns that Query object.

Important consequence:

  • multiple matching permission queries are not combined into one composite query
  • the first matching query-backed allow wins

So query-scoped permissions are resolved as a single winning rule, not as an intersection or union of all matching permission queries.

Step 6: If no match exists, access fails

If no built-in shortcut and no stored permission match applies, the permission resolver returns false.

Once caches are fully loaded, the manager does not keep probing the database for missing cases during the same resolution pattern. A non-match is treated as a deny.

Privileged Actions: freeCreate And freeEdit

The manager treats freeCreate and freeEdit as privileged actions.

These actions cannot be allowed through the broad wildcard action fallbacks in the same way as ordinary actions.

In practice, this means:

  • privileged actions need a more explicit rule
  • they should not be assumed to come from broad "all actions" permissions
  • Superuser is still unrestricted

This is an important guardrail in the implementation.

Property Permission Logic

Property permissions use the same general pattern as type permissions:

  • roles are checked in order
  • exact match is preferred
  • broader fallback is used if not blocked by an exact deny
  • query-backed allows return a Query

Property permissions have two extra implementation details:

duplicated_from_id special-case

The manager implicitly allows property action create on fields ending with .duplicated_from_id.

This supports duplication flows without requiring extra explicit property rules for that field.

Model schema property shortcut

If the user has the Model Implementer system role and the property belongs to the Type or Property schema types, the manager implicitly allows access through the schema-property shortcut.

This is part of the built-in implementer behavior.

System Permission Logic

System permissions are simpler.

The manager checks:

  1. exact role + exact system action
  2. exact role + wildcard system action

As with other layers:

  • the first matching allow or query wins
  • explicit denies mainly stop fallback for the same exact role/action path

Permission Queries

Permission queries are what turn "allowed" into "allowed only for matching objects."

If a permission rule contains a query:

  • the permission manager returns the query object instead of plain true
  • the caller can then apply that query to the working object selection

How query application works

When a permission query is applied to a database selection, the manager:

  1. resets the query parameters
  2. applies the query parameters
  3. reads the query conditions
  4. pushes the resulting where and whereOr conditions into the target selection

The manager also keeps an internal recursion guard so the same permission query is not re-applied endlessly while already being processed.

This is an important implementation detail when permission queries reference data structures that may themselves trigger permission-aware loading.

Presenter ACL And System Roles

In addition to the permissions manager, Jetstack has presenter/resource ACL defined in AuthorizatorFactory.

This gives system roles some baseline application capabilities even before stored permission rows are considered.

User

User is the base authenticated system role.

It gets baseline access to application infrastructure such as:

  • Homepage
  • Module
  • Search
  • ActiveResource
  • Upload
  • user and object action endpoints
  • object and user canvas/checklist interaction endpoints

This is the foundation that the more specific system roles build on.

Reader

Reader inherits from User.

Presenter-level implicit capability:

  • gains access to the Data area

Permissions-manager implicit capabilities:

  • can list
  • can show
  • can access showTab.*

This applies to:

  • Module
  • User
  • Group
  • custom types
  • type-neutral calls represented by typeId = null in the evaluator

Practical meaning:

  • Reader is the built-in role for read-oriented access to ordinary data areas

Publisher

The codebase defines the role as Publisher. If your internal terminology says "Published," this is the role being referred to.

Publisher inherits from Reader.

Presenter-level implicit capability:

  • inherits Reader presenter access, including Data

Permissions-manager implicit capabilities:

  • can list
  • can show
  • can create
  • can access showTab.*

This applies to:

  • Module
  • User
  • Group
  • custom types
  • evaluator calls with typeId = null

Practical meaning:

  • Publisher is a lightweight creator role
  • it can create but does not get the full broad edit/delete shortcut of Editor

Editor

Editor inherits from Reader.

Presenter-level implicit capability:

  • inherits Reader presenter access, including Data

Permissions-manager implicit capabilities:

  • can list
  • can show
  • can create
  • can freeCreate
  • can edit
  • can freeEdit
  • can delete
  • can access showTab.*

This applies to:

  • Module
  • User
  • Group
  • custom types
  • evaluator calls with typeId = null

Practical meaning:

  • Editor is the built-in broad object-maintenance role for ordinary data resources

Model Implementer

Model Implementer inherits from User, not from Reader.

Presenter-level implicit capability:

  • it does not inherit the Reader presenter shortcut to Data through the ACL hierarchy
  • its special power comes mainly from schema-level implicit permissions in the permissions manager

Permissions-manager implicit capabilities on schema types:

  • on Type, can list, show, create, edit, delete
  • on Property, can list, show, create, edit, delete

Permissions-manager implicit capabilities on schema properties:

  • can access schema properties belonging to the Type and Property types

Practical meaning:

  • Model Implementer is the built-in schema builder role
  • it is meant for designing types and properties rather than acting as a general data editor

Superuser

Superuser is unrestricted.

It gets:

  • full presenter/resource ACL access
  • full implicit allow in the permissions manager

Use this role sparingly because it bypasses the normal complexity of the permission model.

Why The Logic Feels Cumulative

The model feels cumulative because several things stack:

  • users can have several roles
  • each role can have exact and wildcard rules
  • system roles add built-in capabilities
  • stored rules can add further access
  • permission queries can narrow allowed object scope

But the implementation is not "sum all permissions and compute a final union-minus-deny equation."

It is better described as:

  • ordered role evaluation
  • built-in shortcuts first
  • first matching rule wins
  • exact denies block fallback for the same role path
  • later roles can still allow

That is the most accurate summary of the implementation.

Design Guidance

Use this model carefully:

  • Use role priority intentionally, because it affects first-match evaluation.
  • Use built-in system roles for broad baseline capability, not for every special case.
  • Use explicit stored permissions when built-in shortcuts are too broad.
  • Use permission queries when scope depends on object data.
  • Do not assume a deny on one role automatically cancels an allow from another role.
  • Treat Model Implementer as a schema role, not as a business-data editor by default.

Example

Suppose a user has two roles:

  • a high-priority restricted role with an exact deny on one type/action
  • a lower-priority broader role with an allow on the same type/action

In the current implementation:

  • the exact deny prevents wildcard fallback for the restricted role
  • but the broader lower-priority role may still allow the action if no earlier positive match already resolved the result

That is why access design should be tested with real role combinations, not only by reading the matrix in isolation.

Best Practices

  • Keep the number of roles understandable.
  • Document the meaning of role priority, not just the role name.
  • Use permission queries to express scope, not to hide core business logic.
  • Prefer explicit permission design over relying blindly on system-role shortcuts.
  • Be careful when combining Reader, Publisher, Editor, and custom roles on one user.
  • Review freeCreate and freeEdit separately because they are privileged actions.