Implemented admin pane + user permission
This commit is contained in:
+89
-22
@@ -164,6 +164,22 @@ Framing: JSON messages over a single persistent WebSocket connection per client.
|
||||
|
||||
// Request historical data
|
||||
{ "type": "history", "signal": "EPICS:PV1", "start": "2026-01-01T00:00:00Z", "end": "2026-01-02T00:00:00Z", "maxPoints": 5000 }
|
||||
|
||||
// Start a control-logic live-debug session (one per client; replaces any prior).
|
||||
// mode "live" — observe the running, enabled graph identified by graphId.
|
||||
// mode "simulate" — dry-run the unsaved `graph` in a server sandbox (no real
|
||||
// writes/config/dialogs); re-send on each edit to refresh it.
|
||||
{ "type": "debugSubscribe", "mode": "live", "graphId": "g1" }
|
||||
{ "type": "debugSubscribe", "mode": "simulate", "graph": { /* unsaved control-logic graph */ } }
|
||||
|
||||
// Stop the current debug session (tears down any simulate sandbox).
|
||||
{ "type": "debugUnsubscribe" }
|
||||
|
||||
// Force a trigger node of the current debug session (live or simulate) to run
|
||||
// now, as if it had fired. nodeId must be a trigger node of the watched graph;
|
||||
// best-effort (dropped if the session is gone). Drives the editor's
|
||||
// double-click-to-fire gesture on trigger nodes in debug mode.
|
||||
{ "type": "fireTrigger", "nodeId": "t" }
|
||||
```
|
||||
|
||||
**Server → Client messages:**
|
||||
@@ -178,6 +194,11 @@ Framing: JSON messages over a single persistent WebSocket connection per client.
|
||||
// Historical data response
|
||||
{ "type": "history", "signal": "EPICS:PV1", "points": [ { "ts": "...", "value": 1.2 }, ... ] }
|
||||
|
||||
// Control-logic node execution during a live-debug session. Emitted ~per node
|
||||
// run for the watched graph (live) or sandbox (simulate); value is meaningful
|
||||
// only when hasValue is true (e.g. an action.write's value, a flow.if branch 0/1).
|
||||
{ "type": "debugNode", "graphId": "g1", "nodeId": "w", "value": 42, "hasValue": true, "ts": 1750000000000 }
|
||||
|
||||
// Error
|
||||
{ "type": "error", "code": "NOT_FOUND", "message": "Signal not found" }
|
||||
```
|
||||
@@ -205,6 +226,7 @@ Base path: `/api/v1`
|
||||
| GET, PUT | `/groups` | Read or set group definitions |
|
||||
| GET, POST | `/synthetic` | List or create synthetic signal definitions |
|
||||
| GET, PUT, DELETE| `/synthetic/{name}` | Read, update, or delete a synthetic definition |
|
||||
| POST | `/synthetic/trace` | Stateless single-shot trace of an unsaved graph for the live-debug view — every node's value; stateful ops flagged `approx` |
|
||||
| GET, POST | `/controllogic` | List or create server-side control-logic graphs |
|
||||
| GET, PUT, DELETE| `/controllogic/{id}` | Read, update, or delete a control-logic graph |
|
||||
| GET, POST | `/config/sets` | List or create configuration sets (schemas) |
|
||||
@@ -360,14 +382,48 @@ instance's `setId` in its `Meta`.
|
||||
|
||||
### 3.10 Access Control
|
||||
|
||||
Identity and global policy live in `internal/access` (`Policy`, `Level`). The user identity
|
||||
is read per request from `server.trusted_user_header` (with a `default_user` fallback) and
|
||||
stored on the request context. `accessMiddleware` gates mutating HTTP methods by the user's
|
||||
global level. Per-panel ownership and ACL evaluation (with folder inheritance) live in
|
||||
`internal/panelacl`, backed by the `acl.json` sidecar. `Policy.CanEditLogic(user)` enforces
|
||||
the optional `server.logic_editors` allowlist over control-logic endpoints and over any
|
||||
change to a panel's `<logic>` block; it is surfaced to the frontend through `/api/v1/me`
|
||||
(`canEditLogic`) so the UI can hide logic-editing affordances.
|
||||
Identity and global policy live in `internal/access` (`Policy`, `Role`, `Level`). The user
|
||||
identity is read per request from `server.trusted_user_header` (with a `default_user`
|
||||
fallback) and stored on the request context. Access is **role-based** through group
|
||||
memberships: each `[[groups]]` block lists members by role along the cumulative ladder
|
||||
`viewer < operator < logiceditor < auditor < admin`, and a user's **effective** global
|
||||
capability is the highest role across all their memberships. Roles map to capabilities as:
|
||||
operator+ → write (`Level` is derived, `LevelWrite` for operator+, else `LevelRead`);
|
||||
logiceditor+ → `CanEditLogic`; auditor+ → `CanViewAudit`; admin → `CanAdmin`.
|
||||
`accessMiddleware` gates mutating HTTP methods by the derived global level. Per-panel
|
||||
ownership and ACL evaluation (with folder inheritance) live in `internal/panelacl`, backed
|
||||
by the `acl.json` sidecar.
|
||||
|
||||
A built-in **public** group is always present: every user (and anonymous) is an implicit
|
||||
viewer member, so an identified caller is at least read-only. Groups may **nest** via a
|
||||
`parent` pointer (a forest rooted at top-level groups); a member of a parent group inherits
|
||||
its role on every descendant group too, unless overridden lower. Cycles are rejected
|
||||
(offending parent dropped to root), and the public group is protected from rename, reparent,
|
||||
and delete. As a bootstrap convenience, a `Policy` with **no roles assigned anywhere** is
|
||||
treated as unconfigured → fully open (everyone is admin), matching trusted-LAN/dev use;
|
||||
assigning any role switches to strict mode where unlisted users are read-only viewers.
|
||||
|
||||
The capability checks (`CanEditLogic`, `CanViewAudit`, `CanAdmin`) are surfaced to the
|
||||
frontend through `/api/v1/me` (`canEditLogic`, `canViewAudit`, `canAdmin`) so the UI can
|
||||
hide affordances. The `Policy` is seeded from the TOML config at startup but is
|
||||
**runtime-mutable** through the admin pane. `Policy.EnablePersistence(storageDir)` points
|
||||
it at an `access.json` sidecar; once any admin mutation is made, that file is written (tmp +
|
||||
atomic rename) and, on a later startup, supersedes the TOML access config (which then only
|
||||
bootstraps an empty install). All policy state is guarded by an `RWMutex` (reads share,
|
||||
mutations are exclusive and persist), keeping the shared `*Policy` pointer wiring intact.
|
||||
|
||||
The admin REST routes live under `/api/v1/admin/*` (all `requireAdmin`-gated):
|
||||
`GET /admin/access` returns an `AccessSnapshot` (users with effective role + per-group
|
||||
roles, groups with members and parents, the role ladder, and the configured flag);
|
||||
`PUT /admin/users/{user}` replaces a user's full set of per-group roles (body
|
||||
`{roles: {group: role}}`, creating missing groups); `POST|PUT|DELETE /admin/groups[/{name}]`
|
||||
create (name + optional parent), rename + set parent and member roles, and delete groups;
|
||||
and `GET /admin/stats` reports live server statistics (the `internal/metrics` counters via
|
||||
`metrics.Snapshot`, the broker's observed-signal count and data-source list, Go runtime
|
||||
stats, and the Linux `/proc/loadavg` load average). The frontend `AdminPane.tsx` (a modal
|
||||
opened from the view-mode Tools dropdown when `canAdmin`) presents Users (per-group role
|
||||
assignment with an effective-role badge), Groups (nesting + per-member roles), and
|
||||
Server-stats tabs.
|
||||
|
||||
### 3.11 Configuration Manager
|
||||
|
||||
@@ -565,15 +621,25 @@ storage_dir = "./interfaces"
|
||||
# Access control (all optional)
|
||||
trusted_user_header = "" # header carrying the proxy-authenticated user
|
||||
default_user = "" # identity when the header is absent (LAN/dev)
|
||||
logic_editors = [] # users/groups allowed to edit panel & control logic
|
||||
|
||||
# [[server.blacklist]] # downgrade users: level = "readonly" | "noaccess"
|
||||
# user = "guest"
|
||||
# level = "readonly"
|
||||
|
||||
# [[groups]] # named user sets for per-panel sharing
|
||||
# name = "operators"
|
||||
# members = ["alice", "bob"]
|
||||
# Role-based access through group memberships. Roles (low→high):
|
||||
# viewer < operator < logiceditor < auditor < admin
|
||||
# Effective capability = highest role across all memberships. The built-in
|
||||
# "public" group makes every user an implicit viewer. Groups may nest via
|
||||
# "parent" (members inherit their role on descendants). No roles anywhere = open
|
||||
# (everyone admin); once set, unlisted users are read-only viewers.
|
||||
# [[groups]]
|
||||
# name = "public"
|
||||
# admins = ["alice"] # alice is a global admin
|
||||
# [[groups]]
|
||||
# name = "operations"
|
||||
# operators = ["bob"]
|
||||
# auditors = ["carol"]
|
||||
# [[groups]]
|
||||
# name = "engineers"
|
||||
# parent = "operations" # bob inherits operator here
|
||||
# logiceditors = ["dave"]
|
||||
# viewers = ["erin"]
|
||||
|
||||
[datasource.epics]
|
||||
enabled = true
|
||||
@@ -586,7 +652,7 @@ enabled = true
|
||||
```
|
||||
|
||||
All settings can also be overridden with `UOPI_*` environment variables (e.g.
|
||||
`UOPI_SERVER_LISTEN`, `UOPI_SERVER_LOGIC_EDITORS`, `UOPI_EPICS_CA_ADDR_LIST`).
|
||||
`UOPI_SERVER_LISTEN`, `UOPI_EPICS_CA_ADDR_LIST`).
|
||||
|
||||
---
|
||||
|
||||
@@ -611,11 +677,12 @@ All settings can also be overridden with `UOPI_*` environment variables (e.g.
|
||||
- Interface XML parsing: use strict schema validation to prevent XXE.
|
||||
- **Identity & access control:** the end-user identity is taken from a header set by a
|
||||
trusted authenticating reverse proxy (`trusted_user_header`), never from client-supplied
|
||||
values — the proxy MUST strip any inbound copy of that header or it can be spoofed. A
|
||||
global blacklist downgrades users (read-only/no-access); per-panel ACLs and the optional
|
||||
`logic_editors` allowlist provide finer control. An unidentified caller (no header, no
|
||||
`default_user`) is treated as a trusted-LAN user with full write access, preserving the
|
||||
unproxied/SSH-tunnel deployment model.
|
||||
values — the proxy MUST strip any inbound copy of that header or it can be spoofed.
|
||||
Authorisation is role-based through group memberships (viewer/operator/logiceditor/
|
||||
auditor/admin), with the highest role across memberships deciding global capability;
|
||||
per-panel ACLs provide finer per-panel control. When **no** roles are assigned anywhere
|
||||
the deployment is fully open (everyone admin), preserving the unproxied/SSH-tunnel/dev
|
||||
model; once any role is set, unlisted and anonymous callers are read-only viewers.
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user