> ## Documentation Index
> Fetch the complete documentation index at: https://docs.coreflux.org/llms.txt
> Use this file to discover all available pages before exploring further.

# RBAC with LoT Rules

> Role-based access control with user groups, LoT Rules, permission flags, and Visu panel access

## Control Who Can Do What

Role-based access control (RBAC) in Coreflux combines **user groups**, **LoT Rules**, **permission flags**, and optional **Visu panel** sharing so you can lock down topics, management operations, and dashboards without external ACL servers.

<Tip>
  **Like badge groups in a factory.** Operators get a badge that opens production doors; supervisors get a badge that also opens the emergency-stop cabinet. Groups name the badges; Rules decide which doors each badge opens.
</Tip>

### When to Use This Guide

| Goal                                        | Use                                               |
| ------------------------------------------- | ------------------------------------------------- |
| Restrict MQTT publish/subscribe by role     | Groups + `DEFINE RULE` with `USER IN GROUP`       |
| Gate who can create users, routes, or rules | Permission flags + management scopes              |
| Hide or lock Visu panels/controls           | `WITH SHARE TO GROUP` / `WITH INTERACT FOR GROUP` |
| Understand which rule wins                  | Precedence model below                            |

<Note>
  Adding custom topic or management ACL rules (`-addRule`) requires a **Growth or Enterprise** license. Shipped default rules always apply. Group membership commands and permission flags are available on all tiers.
</Note>

***

Write rules with correct layout and conditions in [Rules Syntax](./syntax), then return here for groups, precedence, and operator patterns.

## User Groups

Each broker user can belong to one or more named groups. Group names are **case-insensitive** at evaluation time (`"Operators"` and `"operators"` match the same group). Prefer kebab-case or lowercase identifiers without spaces (`zone1-staff`, not `Zone 1 Staff`).

Groups are created by naming them — assign a user to a group and reference that name in rules or Visu clauses.

### Manage Groups via MQTT

Publish to `$SYS/Coreflux/Command` from a session authorized for user management (`AllowedUserManagement`, `AllowedSystemConfiguration`, or `root`). The requesting identity is the **authenticated MQTT session** — do not pass a requester in the payload.

```bash wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
-addUserToGroup alice operators
-removeUserFromGroup alice operators
-listUserGroups alice
```

Responses arrive on `$SYS/Coreflux/Command/Output`. See [Broker Commands](/latest/mqtt-broker/commands) for the full command set.

***

## Rule Precedence

**Lower priority number wins.** This is the same convention as BACnet write priority and ISA-18.2 alarm priority: the lower the number, the stronger the statement. The very strongest numbers are reserved for the broker's own rules, so `100` is the strongest priority you can write.

For a given operation (and topic, for topic operations), the broker considers only the rules with the **lowest** priority number among those that match. Within that winning band:

* If **any** matching rule evaluates to **DENY**, the operation is denied (**deny-wins**)
* Otherwise, if a rule allows, the operation is allowed
* If **no** rule matches at all, the operation is **denied** (default-deny)

Weaker (higher-numbered) rules are consulted only where no stronger rule matches the topic.

| Priority      | Use case                                                                                                        |
| ------------- | --------------------------------------------------------------------------------------------------------------- |
| `0–99`        | **Reserved** for locked system rules — rejected for user/project rules                                          |
| `100`         | The strongest rule you can write — use it for the most specific case                                            |
| `100–999 999` | The user band. Layer exceptions by stepping *down* toward `100` as rules get more specific                      |
| `1 000 000`   | Shipped permissive catch-alls (`AllowPublishTopic` / `AllowSubscribeTopic` / `AllowConnect`) — the weakest band |
| `> 1 000 000` | **Rejected** — weaker than the catch-alls, so the rule could never apply                                        |

Because the catch-alls sit at the weakest priority `1 000 000`, **any** rule you add in the user band overrides them for its topic (or, for Connect, for the whole connection). You do not need to remove or reprioritise a built-in to deny (or further allow) a topic.

<Tip>
  **Start broad rules high — you can only carve out downwards.** An exception needs a *lower* number than the rule it overrides, and `100` is the floor of the user band. Give a namespace-wide rule a large number (`1000`, or higher if you expect several layers) and keep `100` for the most specific case. A broad rule placed at `100` leaves no room to carve out later without renumbering what is already deployed.
</Tip>

<Warning>
  Prefer distinct priorities, or combine conditions with `OR` in one rule. Same-priority ties resolve **deny-wins**.
</Warning>

### Layout requirements

The LoT lexer is indentation-sensitive (one level = 4 spaces):

* Keep the full header on **one line**: `DEFINE RULE Name WITH PRIORITY n FOR …`
* Put each `IF` / `ELSE` / `ALLOW` / `DENY` on its own line (a single-line `THEN ALLOW ELSE DENY` body is rejected)
* Omitting `ELSE` is valid: `THEN DENY` allows everyone else, and `THEN ALLOW` denies everyone else. An explicit `ELSE` is still the clearer form.
* Rule names are bare identifiers — **no quotes** around the rule name

```lot wrap focus={2-5} theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE RULE Zone1Access WITH PRIORITY 100 FOR Subscribe TO TOPIC "zone1/#"
    IF USER IN GROUP "zone1-staff" THEN
        ALLOW
    ELSE
        DENY
```

***

## Quick Start

<Tabs>
  <Tab title="Restrict a topic to a group">
    Only members of `operators` may publish commands under `machines/+/cmd`:

    ```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
    DEFINE RULE OperatorControl WITH PRIORITY 100 FOR Publish TO TOPIC "machines/+/cmd"
        IF USER IN GROUP "operators" THEN
            ALLOW
        ELSE
            DENY
    ```
  </Tab>

  <Tab title="Allow multiple groups">
    Supervisors or operators may subscribe to production topics:

    ```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
    DEFINE RULE SupOrOp WITH PRIORITY 100 FOR Subscribe TO TOPIC "production/#"
        IF USER IN GROUP "supervisors" OR USER IN GROUP "operators" THEN
            ALLOW
        ELSE
            DENY
    ```
  </Tab>

  <Tab title="Layer deny then allow">
    Deny a config subtree at a coarse priority, then carve out one leaf for supervisors at a stronger (lower) one:

    ```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
    DEFINE RULE DenyConfig WITH PRIORITY 1000 FOR Publish TO TOPIC "site/config/#"
        DENY
    DEFINE RULE AllowSetpt WITH PRIORITY 100 FOR Publish TO TOPIC "site/config/setpoint"
        IF USER IN GROUP "supervisors" THEN
            ALLOW
        ELSE
            DENY
    ```
  </Tab>
</Tabs>

Deploy with `-addRule <definition>` on `$SYS/Coreflux/Command`, or run the cell in a [LoT Notebook](/latest/quick-start/vscode). Remove with `-removeRule <RuleName>`.

***

## Access-Control Cookbook

### Deny a whole subtree

```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE RULE DenyRawArea WITH PRIORITY 100 FOR Publish TO TOPIC "factory/raw/#"
    DENY
```

Beats the weakest-band publish catch-all (priority `1 000 000`) for `factory/raw/#`; other topics still fall through to the default allow.

### Restrict a topic to named users

```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE RULE ControlWriters WITH PRIORITY 100 FOR Publish TO TOPIC "line1/control/#"
    IF USER IS "operator1" OR USER IS "operator2" THEN
        ALLOW
    ELSE
        DENY
```

### Gate by permission flag

```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE RULE DiagnosticsReaders WITH PRIORITY 100 FOR Subscribe TO TOPIC "diagnostics/#"
    IF USER HAS AllowedSystemConfiguration OR USER IS "root" THEN
        ALLOW
    ELSE
        DENY
```

### Switch a namespace toward default-deny

Blanket-deny at a coarse priority, then allow only what you intend at a stronger (lower) one:

```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE RULE DenyAllPub WITH PRIORITY 1000 FOR Publish TO TOPIC "#"
    DENY
DEFINE RULE AllowApp WITH PRIORITY 100 FOR Publish TO TOPIC "app/#"
    ALLOW
```

Alternatively, remove the removable publish seed (`-removeRule AllowPublishTopic`) so unmatched publishes fall through to default-deny. Locked system rules and the root backstop still protect recovery. Restore the three shipped seeds later with [`-restoreRules`](/latest/mqtt-broker/commands#restoring-seeded-rules). Do **not** expect `-removeRule AllowConnect` alone to lock CONNECT — see [Connection admission](#connection-admission).

### Precedence walk-through

The table reads top-down in precedence order — the first band that matches decides:

| Rule                           | Priority  | Topic             | Decision |
| ------------------------------ | --------- | ----------------- | -------- |
| `AllowSensors`                 | 100       | `plant/sensors/#` | ALLOW    |
| `DenyArea`                     | 1 000     | `plant/#`         | DENY     |
| `AllowPublishTopic` (built-in) | 1 000 000 | `#`               | ALLOW    |

* Publish to `office/x` → only the catch-all matches → **ALLOW**
* Publish to `plant/motor` → strongest match is `DenyArea` (1 000) → **DENY**
* Publish to `plant/sensors/t1` → strongest match is `AllowSensors` (100) → **ALLOW**

***

## Permission Flags vs Groups

| Mechanism            | What it is for                                                     |
| -------------------- | ------------------------------------------------------------------ |
| **Permission flags** | Coarse system access — checked with `USER HAS`                     |
| **Groups**           | Fine-grained topic and panel access — checked with `USER IN GROUP` |

| Flag                         | Gates                                                           |
| ---------------------------- | --------------------------------------------------------------- |
| `AllowedSystemConfiguration` | System config, creating routes/models/actions, many admin paths |
| `AllowedUserManagement`      | User CRUD and group membership commands                         |
| `AllowedLogManagement`       | Log access                                                      |

Set a flag via MQTT:

```bash wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
-changeUserSettings alice AllowedUserManagement true
```

In Coreflux HUB User Management, **Full Access** maps to `AllowedSystemConfiguration`; **User Management** and **Log Management** map to the matching flags. Manage groups with the MQTT commands above (see also [User Management](/hub/system/user-management)).

| Scenario                              | Prefer                    |
| ------------------------------------- | ------------------------- |
| Can this user create LoT actions?     | Permission flag           |
| Can this user see Zone 1 data/panels? | Group + rule / Visu share |
| Can this user manage other users?     | `AllowedUserManagement`   |

`USER IN GROUP`, `USER IS` / `USER EQUALS`, and `USER HAS` also work inside LoT Action bodies (for topic-triggered actions). Scheduled actions (`ON EVERY` / `ON START`) have no triggering user — group checks return false there.

***

## Default Rules and System Access

### Locked system rules

Management gates (`CommandCall`, user/rule/route/action management, and so on) and `$SYS` access rules are **locked** built-ins in the reserved band (`0–99`) — the strongest priorities on the number line. They cannot be removed, cannot be overridden by a same-named rule, and outrank every user rule (which are floored at `100`), so no rule you write can revoke admin access and lock the broker.

The band is split the same way user rules are — more specific means a lower number: base locked rules sit at `10`, and more-specific `$SYS` carve-outs at `1`.

The flip side of that protection: a custom rule on a management scope, or on `$SYS/#` subscribe, never decides either — the locked rule always forms the winning band. Adjust those with permission flags instead. See [Operation Scopes](./syntax#operation-scopes).

### Seed catch-alls

Three permissive defaults sit at priority `1 000 000` — the weakest possible band — so MQTT clients keep working out of the box while losing to every rule an operator writes:

```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE RULE AllowPublishTopic WITH PRIORITY 1000000 FOR Publish TO TOPIC "#"
    ALLOW
DEFINE RULE AllowSubscribeTopic WITH PRIORITY 1000000 FOR Subscribe TO TOPIC "#"
    ALLOW
DEFINE RULE AllowConnect WITH PRIORITY 1000000 FOR Connect
    ALLOW
```

These are the only defaults an operator may remove or replace via MQTT. To change a seed catch-all's body, `-removeRule` it and then `-addRule` your replacement — rule names must be unique. Project merges skip every built-in name — including these seeds — so redefine them with MQTT commands, not project files.

To put the three seeds back (including resetting an override of those names to the shipped ALLOW body), run [`-restoreRules`](/latest/mqtt-broker/commands#restoring-seeded-rules). That command requires `root` or `AllowedSystemConfiguration` and does not require an RBAC license. Locked built-ins and your other custom rules are left alone.

Loading or unloading a project keeps locked built-ins and operator-authored rules. Only rules the broker attributes to the outgoing project are removed.

### Connection admission

A rule written `FOR Connect` decides whether the broker accepts an MQTT CONNECT after authentication succeeds. Connect rules take no `TO TOPIC` clause. Any user-band Connect rule (`100`–`999 999`) outranks `AllowConnect`.

```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE RULE DenyGuestConnect WITH PRIORITY 100 FOR Connect
    IF USER IS "guest" THEN
        DENY
```

A rule set with **no** `Connect` rule at all leaves CONNECT ungated — that is how upgrades from brokers that had no Connect operation keep accepting clients. `-removeRule AllowConnect` without adding a replacement therefore does **not** lock the door. To control who may connect, add an explicit `FOR Connect` policy. `root` remains admitted by the anti-lockout backstop.

### Rejected priorities

`-addRule` and project rule merges reject both ends of the number line, and report the reason per rule:

| Priority      | Result                                                                                                       |
| ------------- | ------------------------------------------------------------------------------------------------------------ |
| `0–99`        | **Rejected** — nothing user-defined may tie with or outrank a locked system rule                             |
| `100–999 999` | Accepted — the user band                                                                                     |
| `1 000 000`   | Accepted, so you can re-add a replaced catch-all in its own band                                             |
| `> 1 000 000` | **Rejected** — weaker than the catch-alls, so the rule would never be consulted and would silently fail open |

### Root anti-lockout

Independently of all rules, `root` can always connect, run commands, manage rules, and reach `$SYS/Coreflux/Command` and `$SYS/Coreflux/Command/Output`.

### Upgrade note

**Review any custom rule that used the `1–99` range.** That band is now reserved for locked system rules, so such rules are **clamped to `100`** when the broker loads them (each clamp is logged with its old and new priority). Clamping keeps them in force and keeps them from outranking the locked safety rules, but several rules that used to be distinct can land on `100` together — where ties resolve **deny-wins**. Re-space them deliberately across the user band, putting the *more specific* rule at the *lower* number.

Locked built-ins are rewritten to their canonical definitions and priorities on every start, and a seed catch-all you kept has its body preserved but its priority normalized to `1 000 000`. A seed you deliberately removed stays removed. Fresh brokers also ship `AllowConnect` at `1 000 000`; see [Connection admission](#connection-admission).

***

## Visu Panel RBAC

LoTV panels can restrict who sees content and who can interact with controls.

### Visibility

```lot wrap focus={4-5} theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE PANEL "Zone1Control"
    WITH TITLE "Zone 1 Control"
    WITH VISIBILITY SHARED
    WITH SHARE TO GROUP "zone1-staff"
    WITH SHARE TO GROUP "supervisors"
    WITH STATE PUBLISHED
```

Published shared panels generate Layer 1 subscribe rules so only listed groups (or users via `WITH SHARE TO USER`) receive the panel. Others do not see it in `$Visu/Catalog`.

### Interaction

| Keyword                         | Who can interact                  |
| ------------------------------- | --------------------------------- |
| `WITH INTERACT PUBLIC`          | Any client who can view the panel |
| `WITH INTERACT FOR GROUP "<g>"` | Members of group `<g>`            |
| `WITH INTERACT FOR USER "<u>"`  | Named user `<u>`                  |
| *(omitted)*                     | View-only — no Interact topic     |

Stack multiple `FOR GROUP` lines or multiple `FOR USER` lines. Do **not** mix `FOR GROUP` and `FOR USER` on the same component — only the last scope is enforced. Put named users into a group when both are needed.

Denial modes: `WITH INTERACT_DENY SILENT` (default) or `WITH INTERACT_DENY NOTIFY` (publishes to `$Visu/Session/<id>/Denied`).

### Two-layer enforcement

| Layer   | What                                                                    |
| ------- | ----------------------------------------------------------------------- |
| Layer 1 | MQTT subscribe/publish to `$Visu` topics from panel visibility          |
| Layer 2 | Runtime check of `WITH INTERACT FOR GROUP` / `FOR USER` per interaction |

***

## Best Practices

* Start from the shipped defaults; every rule you add already overrides the catch-alls for its topic
* Give namespace-wide rules a coarse number (`1000` or higher) and reserve `100` for the most specific carve-outs
* Prefer groups over hard-coded usernames for topic and panel access
* Keep rule headers on one line; use consistent 4-space indentation
* Keep custom priorities inside the user band (`100`–`1 000 000`)
* Document who owns each group name in your operations runbook

<Warning>
  Never attempt to override locked system rules or rely on removing `root` protections. The reserved priority band and root backstop exist so the broker remains recoverable.
</Warning>

***

## Next Steps

<CardGroup cols={2}>
  <Card title="Rules Syntax" icon="code" href="/latest/lot-language/rules/syntax">
    Conditions, operation scopes, and complete rule patterns.
  </Card>

  <Card title="Broker Commands" icon="terminal" href="/latest/mqtt-broker/commands">
    Group, user, and rule management command reference.
  </Card>
</CardGroup>
