ACL matrix and per-module ACL editing
Background
pyobs-core 2.0 added per-module access control: an acl: block in a module’s own
YAML config, enforced at runtime by that module itself (Module.execute()), independent
of any fleet-wide state. That keeps enforcement simple and legible per-module, but it means
that once a fleet has a dozen-plus modules each with their own acl: block, “who can
reach the telescope, and with what” is scattered across a dozen files with no single place
to read it back. The ACL matrix is a visibility-and-authoring tool over exactly that
problem – it never touches enforcement, only the config files enforcement reads.
What the matrix shows
One page, one table: rows are target modules, columns are callers. A caller is any
name that appears in any module’s resolved allow/deny list – not necessarily a
module this installation itself runs (it could be another app’s connecting identity, or a
module on a different host under hub mode). A caller name that happens to match a known
module links to that module’s own page.
Target’s |
Cell |
|---|---|
no |
open – every caller, surfaced prominently rather than left blank, since finding modules that should have a policy and don’t is the matrix’s main value |
|
all methods |
|
m1, m2 |
|
denied |
|
denied |
|
all methods |
any of the above with |
same value, flagged as not yet enforced |
An allow entry may itself be an interface name (e.g. ICamera) rather than a bare
method name – pyobs-core expands this at runtime into that interface’s full method
list. The matrix does not perform this expansion; it shows the entry as-is, badged
(interface) to distinguish it from a literal method name. Reproducing the expansion
statically would mean importing the concrete driver class named in every module’s
class: key – potentially every device package in the fleet – which reintroduces
exactly the pyobs-core coupling this app’s “no pyobs-core dependency” design avoids for
a purely cosmetic gain.
Editing from the matrix
An edit has to land in the file the rule actually came from:
If the target’s
acl:is not behind an{include}, it’s edited and saved directly – a structured modal (mode: enforce/log, policy: allow-list/deny-list, add/ remove caller rows) that writes back via a text splice, not a full-file YAML round-trip: only theacl:block’s own line range is touched (serialized withruamel.yaml’s round-trip dumper, so it reads like hand-written YAML), and everything else in the file – including unrelated{include ...}lines a generic parser couldn’t even load – is left byte-identical. After writing, the block is re-resolved and rolled back to the original content if it doesn’t match what was requested, as a safety net for the line-detection heuristic rather than trying to make that heuristic exhaustively correct.If the target’s
acl:is pulled in from a shared fragment, editing it in place would silently change every other module that includes the same fragment. The matrix instead shows a badge (“this rule comes fromacl.shared.yaml, included by 4 modules”) linking to that fragment’s own editor – it does not offer a one-click “detach into a module-local override,” by design; the operator decides by hand.
Two editing surfaces, one backend. Besides the matrix’s own per-row modal, each module’s detail page has its own ACL tab – a full editor listing every other managed module as a click-to-allow/deny row, plus a free-text row for callers that aren’t a known module. Both surfaces call the same underlying save path and storage format; they’re kept as two independent UIs (not one shared component) because the two pages have different host-context models – the matrix aggregates every host on one page, while a module’s own detail page always shows one host at a time, matching its other tabs.
Hub mode
Unlike most of this app’s hub-mode views (dashboard/config/logs), which show one host at a time via the session’s active-host switcher, the matrix’s whole point is fleet-wide visibility – it always queries every configured host and merges the results into one table, tagging each row with its host and recomputing cells against the union of all hosts’ callers. An unreachable host is shown as a warning banner, with the rest of the matrix still usable, rather than failing the whole page. See Hub mode for the underlying proxying mechanism this reuses.