Merge fix/c3-dashboard-acl: SEC-41 ACL re-resolves tag grants from group claims, SEC-40 alarms hub/page behind non-bypassable authenticated-role policy, GWC-38 browse scope fails closed without identity under ApiKey mode
This commit is contained in:
@@ -180,6 +180,21 @@ Supported constraints are:
|
||||
| `read_historized_only` | Read/subscription commands must target objects with historized attributes. |
|
||||
| `dashboard_tags` | Dashboard event-visibility tags. **Not a data-access constraint** — see below. |
|
||||
|
||||
`browse_subtrees` is applied by `GatewayBrowseScopeProvider`, the host seam the
|
||||
shared `ZB.MOM.WW.GalaxyRepository` package calls through `IGalaxyBrowseScopeProvider`;
|
||||
it reads the constraint off the ambient per-call identity that
|
||||
`GatewayGrpcAuthorizationInterceptor` pushed earlier in the same call. An empty
|
||||
`browse_subtrees` list is the library's "no scoping" signal, so the provider has to
|
||||
distinguish two states that look alike (GWC-38): an authenticated key with no browse
|
||||
constraint is legitimately unscoped and returns the empty list, whereas **no ambient
|
||||
identity at all** means the interceptor did not run before the Galaxy service and the
|
||||
request's scope cannot be established. In that second case, with
|
||||
`MxGateway:Authentication:Mode = ApiKey`, the provider logs a warning naming the broken
|
||||
ordering invariant and fails the call with `PermissionDenied` rather than serving the full
|
||||
hierarchy — a narrowing control's silent absence is invisible in normal operation, so it is
|
||||
made loud instead. Under `Mode = Disabled` an absent identity is the expected steady state
|
||||
(nothing authenticates, so no key and no constraints exist) and browse stays unscoped.
|
||||
|
||||
`dashboard_tags` is the one member of the blob that constrains nothing on the
|
||||
gRPC data path. No read, write, browse, or subscribe check consults it, and
|
||||
`HasReadConstraints` / `HasWriteConstraints` deliberately ignore it: adding a tag
|
||||
|
||||
@@ -186,7 +186,7 @@ events (a "gap") and must re-snapshot; whatever is still retained is replayed.
|
||||
| Option | Default | Description |
|
||||
|--------|---------|-------------|
|
||||
| `MxGateway:Dashboard:Enabled` | `true` | Enables Blazor Server dashboard route mapping. The dashboard mounts at the host root (`/`); there is no separate path-base prefix. |
|
||||
| `MxGateway:Dashboard:AllowAnonymousLocalhost` | `true` | Allows loopback dashboard requests to bypass the dashboard cookie requirement for local development. Remote requests still require dashboard authentication. |
|
||||
| `MxGateway:Dashboard:AllowAnonymousLocalhost` | `true` | Allows loopback dashboard requests to bypass the dashboard cookie requirement for local development. Remote requests still require dashboard authentication. The bypass is **read-only** — it satisfies a requirement including the `Viewer` role but never `AdminOnly`, so API-key CRUD and session Close/Kill stay closed — and since SEC-40 it does **not** reach the alarm feed: `/alarms` and `/hubs/alarms` carry `MxGateway.Dashboard.Alarms`, a policy built from a requirement type the bypass handler does not handle, so an anonymous loopback caller is denied the page, denied the hub, and not offered the Alarms link. The alarm monitor is gateway-central and its messages carry no session provenance, so alarm metadata cannot be tag-scoped the way session events are — requiring a real role claim is the narrowing that exists. Sign in (or use `DisableLogin` in dev, which mints a genuine principal) to see alarms; no setting re-opens the feed to the bypass. The same exclusion applies to `Authentication:Mode=Disabled`. |
|
||||
| `MxGateway:Dashboard:RequireHttpsCookie` | `true` | Sets the dashboard auth cookie's secure policy. `true` keeps `CookieSecurePolicy.Always` — the cookie is only sent over HTTPS, which matches a production HTTPS deployment. Set to `false` for plain-HTTP dev deployments to use `CookieSecurePolicy.SameAsRequest`; the cookie is still flagged Secure on HTTPS requests, but it can round-trip over HTTP. Browsers drop Secure cookies set over HTTP from non-localhost hosts, so leaving this `true` while serving the dashboard over plain HTTP will break login from any remote browser. |
|
||||
| `MxGateway:Dashboard:CookieName` | _(unset)_ | Dashboard auth cookie name. Leave unset (null/blank) to use the resolved default: `__Host-MxGatewayDashboard` when `RequireHttpsCookie` is `true` (Secure cookie, so the `__Host-` prefix's Secure/no-Domain/`Path=/` guarantees apply), else the plain `MxGatewayDashboard` (a `__Host-` cookie without Secure is silently dropped by browsers). Override it to give a distinct name to a gateway that shares a hostname with another gateway instance: browser cookies are scoped by host+path but **not** by port, so two instances on the same host would otherwise clobber each other's dashboard session under a shared cookie name. An explicit override always wins over the `__Host-` default. Changing it signs out existing dashboard sessions on next deploy. |
|
||||
| `MxGateway:Dashboard:SnapshotIntervalMilliseconds` | `1000` | Dashboard snapshot refresh interval used by the snapshot SignalR hub and the pages that subscribe to it. |
|
||||
@@ -194,7 +194,7 @@ events (a "gap") and must re-snapshot; whatever is still retained is replayed.
|
||||
| `MxGateway:Dashboard:RecentSessionLimit` | `200` | Maximum number of session summaries projected into each dashboard snapshot. |
|
||||
| `MxGateway:Dashboard:ShowTagValues` | `false` | Controls whether tag values reach the dashboard at all. `false` (the default) suppresses them on all three seams that carry one: (1) the **events hub mirror** — `DashboardEventBroadcaster` blanks `MxEvent.value` plus the alarm body's `current_value`/`limit_value` from a deep-cloned copy before it reaches any hub subscriber (see `docs/GatewayDashboardDesign.md`'s `EventsHub` row); (2) the **alarms hub** — `AlarmsHubPublisher` clears `current_value`/`limit_value` from a deep-cloned copy of each `AlarmFeedMessage`, on both value-bearing payload arms (`transition` and `active_alarm`), before broadcasting to `/hubs/alarms`; (3) the **`/browse` live-value panel** — `DashboardLiveDataService` substitutes the literal `[redacted]` for the value text of each **successfully read** tag; a failed read keeps its `-` placeholder (there was no value to suppress, and the row's error is left untouched, so the two cannot contradict each other). Both hub redactions clone: the source `MxEvent` is shared with the gRPC event stream and the replay ring, and the source `AlarmFeedMessage` fans out to gRPC `StreamAlarms` subscribers, so neither is mutated in place and **no gRPC client is affected by this flag** — it is a dashboard-display control only. Everything that is not the value survives on every seam: tag reference, alarm reference/severity/state/operator, data type, quality, status, and timestamps still render, so the dashboard stays diagnostic without disclosing process values. This is one of two independent layers: `IDashboardSessionAcl` decides *which* sessions a caller may subscribe to at all (see `GroupToTag` / `UntaggedSessionVisibility` below), while this flag decides what a permitted subscriber sees. Setting it `true` exposes tag values to everyone the ACL admits — every Administrator, plus each Viewer holding a matching tag — and, because the alarms hub is session-less, exposes alarm values to every dashboard client that can reach `/hubs/alarms`. |
|
||||
| `MxGateway:Dashboard:GroupToRole` | _(empty)_ | LDAP group → dashboard role mapping. Keys must be **short** LDAP group names — the CN / first-RDN value, e.g. `GwAdmin`, not `ou=GwAdmin,ou=groups,dc=zb,dc=local` — matched case-insensitively. The shared `ZB.MOM.WW.Auth.Ldap` provider delivers a user's groups already stripped to short names, so a full-DN key can never match and the group silently maps to nothing. (The mapper does try the full literal string before falling back to the leading-RDN value, but on the live login path both branches see the same short name; the fallback only matters to non-library callers of the `IGroupRoleMapper<string>` seam.) Values must be `Administrator` (read/write, API-key CRUD) or `Viewer` (read-only) — matched ordinally by the startup validator, so the spelling is exact and `Admin` is rejected. A user whose LDAP groups don't intersect this map cannot sign in; with no mapping at all, only the loopback bypass admits anyone. |
|
||||
| `MxGateway:Dashboard:GroupToTag` | _(empty)_ | LDAP group → dashboard visibility tags. Keys follow the same convention as `GroupToRole` — short LDAP group names (CN / first-RDN value), matched case-insensitively; a full-DN key never matches the pre-stripped groups the LDAP provider returns. Values are tag lists. A dashboard user's granted tag set is the union over the groups they belong to; an unmapped group contributes nothing. **Visibility only:** tags scope which sessions' event streams a Viewer may observe on the dashboard — they never grant or deny data access, which stays with the API key's scopes and constraints. Independent of `GroupToRole`: a group may appear in either map, both, or neither. Empty (the default) means Viewers hold no tags, so under the default `UntaggedSessionVisibility` they observe no session's events. |
|
||||
| `MxGateway:Dashboard:GroupToTag` | _(empty)_ | LDAP group → dashboard visibility tags. Keys follow the same convention as `GroupToRole` — short LDAP group names (CN / first-RDN value), matched case-insensitively; a full-DN key never matches the pre-stripped groups the LDAP provider returns. Values are tag lists. A dashboard user's granted tag set is the union over the groups they belong to; an unmapped group contributes nothing. **Visibility only:** tags scope which sessions' event streams a Viewer may observe on the dashboard — they never grant or deny data access, which stays with the API key's scopes and constraints. Independent of `GroupToRole`: a group may appear in either map, both, or neither. Empty (the default) means Viewers hold no tags, so under the default `UntaggedSessionVisibility` they observe no session's events. **Applies to session event streams only:** the alarm feed (`/alarms`, `/hubs/alarms`) is deliberately *not* tag-scoped — the alarm monitor is gateway-central and session-less, so an `AlarmFeedMessage` carries no session or owning key to intersect a tag grant against. Alarm metadata (reference, state, severity, category, area, acknowledging operator) is therefore galaxy-wide for every authenticated dashboard role, regardless of tag grant; alarm *values* still follow `ShowTagValues`. What SEC-40 narrowed there is admission rather than scope: the environment bypasses (`AllowAnonymousLocalhost`, `Authentication:Mode=Disabled`) no longer receive the feed at all. **Revocation:** the ACL re-resolves a caller's grant from their LDAP group claims against this map on every subscribe, so removing an entry takes effect at the next subscribe after the gateway restart that rebinds options (options bind once at startup); a change to the user's LDAP group membership takes effect when their principal is rebuilt — at re-login for cookie sessions, within one 5-minute hub-token lifetime for token clients. An already-open subscription is never re-checked and lives until it drops. |
|
||||
| `MxGateway:Dashboard:UntaggedSessionVisibility` | `AdminOnly` | Who may observe a session that carries no tags (its owning API key declared none). `AdminOnly` (default, fail-closed) restricts untagged sessions to dashboard Administrators. `AllViewers` shows them to every Viewer — opt-in for a single-tenant deployment that wants the pre-tag behaviour. Administrators always see every session regardless of tags. |
|
||||
| `MxGateway:Dashboard:DisableLogin` | `false` | Dev/test only. When `true`, replaces the cookie authentication handler with `DashboardAutoLoginAuthenticationHandler`, which auto-authenticates every dashboard request — including requests from remote browsers, not just loopback — as `AutoLoginUser` holding both `Administrator` and `Viewer` roles. No login form, LDAP bind, or cookie is involved. A loud one-time startup warning is logged. Differs from `AllowAnonymousLocalhost`: `DisableLogin` mints a real authenticated principal (so role-gated write affordances appear), whereas `AllowAnonymousLocalhost` satisfies the authorization requirement on loopback only without minting a principal (write affordances stay hidden). Never enable in production. **Production hard-stop (SEC-04):** when the host runs in the `Production` environment and `DisableLogin` is `true`, startup validation fails and the process aborts — the flag is only accepted outside Production, where the one-time startup warning still fires. |
|
||||
| `MxGateway:Dashboard:AutoLoginUser` | `(null)` | Username stamped on the synthetic principal when `DisableLogin` is `true`. Default `(null)` — a null or blank value falls back to `multi-role`. Has no effect when `DisableLogin` is `false`. |
|
||||
|
||||
@@ -274,7 +274,7 @@ bearer). Each hub class is `[Authorize(Policy = HubClientsPolicy)]`.
|
||||
| Hub | Path | Producer | Payload | Routing |
|
||||
|---|---|---|---|---|
|
||||
| `DashboardSnapshotHub` | `/hubs/snapshot` | `DashboardSnapshotPublisher` (BackgroundService consuming `IDashboardSnapshotService.WatchSnapshotsAsync`) | `DashboardSnapshot` | Sent to all connected clients on every snapshot tick, but only while at least one client is connected (see "Idle gating" below); new connections receive the current snapshot synchronously in `OnConnectedAsync`. |
|
||||
| `AlarmsHub` | `/hubs/alarms` | `AlarmsHubPublisher` (BackgroundService consuming `IGatewayAlarmService.StreamAsync(filter: null)`) | `AlarmFeedMessage` (`active_alarm` / `snapshot_complete` / `transition` / `provider_status` / `snapshot_status`) | Connected clients auto-join `__alarms__`; all clients receive every message. Publisher auto-reconnects every 5s on stream faults. The alarm value fields (`current_value` / `limit_value`) are stripped from a deep-cloned copy of the message when `Dashboard:ShowTagValues` is false (the default) — on both value-bearing payload arms, so neither a live transition nor a snapshot record leaks a process value; every other field still renders. The source message is never mutated: it fans out from the same feed to gRPC `StreamAlarms` subscribers, which this dashboard-display flag does not govern. Arms carrying no value are forwarded as-is, uncloned. |
|
||||
| `AlarmsHub` | `/hubs/alarms` | `AlarmsHubPublisher` (BackgroundService consuming `IGatewayAlarmService.StreamAsync(filter: null)`) | `AlarmFeedMessage` (`active_alarm` / `snapshot_complete` / `transition` / `provider_status` / `snapshot_status`) | Guarded by `MxGateway.Dashboard.Alarms`, not `HubClients`: a real authenticated dashboard role is required, so the `Authentication:Mode=Disabled` and anonymous-localhost bypasses are excluded (SEC-40). Connected clients auto-join `__alarms__`; all admitted clients receive every message — the feed is galaxy-wide and carries no session provenance, so no per-caller scoping applies (see "Alarm-feed visibility"). Publisher auto-reconnects every 5s on stream faults. The alarm value fields (`current_value` / `limit_value`) are stripped from a deep-cloned copy of the message when `Dashboard:ShowTagValues` is false (the default) — on both value-bearing payload arms, so neither a live transition nor a snapshot record leaks a process value; every other field still renders. The source message is never mutated: it fans out from the same feed to gRPC `StreamAlarms` subscribers, which this dashboard-display flag does not govern. Arms carrying no value are forwarded as-is, uncloned. |
|
||||
| `EventsHub` | `/hubs/events` | `DashboardEventBroadcaster` invoked by each session's internal dashboard-mirror subscriber on its `SessionEventDistributor` (registered when the session becomes Ready) | `MxEvent` | Clients call `SubscribeSession(sessionId)` to join `session:{id}`, which also registers them in `EventsHubViewerRegistry` — the mirror is gated on that registry, which counts hub and in-process viewers alike (see "Mirror gating" below). The dashboard is a first-class distributor subscriber, so it receives the session's events whether or not a gRPC client is streaming. It sees RAW session events — not the per-gRPC-subscriber `AfterWorkerSequence` filtering that `EventStreamService` applies at its own boundary — because the dashboard is a separate LDAP-authenticated monitoring view meant to show the session's full event activity. Tag values are stripped from the mirrored `MxEvent` copy by `DashboardEventBroadcaster` when `Dashboard:ShowTagValues` is false (the default) — event metadata (tag reference, quality, status, timestamps) still renders, but the value fields are blanked, so no value leaks through this seam. `SubscribeSession` is gated by `IDashboardSessionAcl` (SEC-25 / TST-15): a denied caller gets a `HubException`, is not joined to the group, and is not registered as a viewer, so the mirror stays off for a session nobody is legitimately watching. The same ACL gates the in-process seam the session-details page uses, so neither path is the weaker one. Value redaction remains an independent layer — it bounds what a *permitted* subscriber sees. |
|
||||
|
||||
### Default cadences
|
||||
@@ -556,6 +556,14 @@ re-asserts the truncation verdict as the reconcile baseline. If `MxGateway:Alarm
|
||||
false the central monitor never starts, and the page says so instead of showing
|
||||
an empty list with no explanation.
|
||||
|
||||
The page carries `[Authorize(Policy = MxGateway.Dashboard.Alarms)]` rather than relying on the
|
||||
`Viewer` policy `MapRazorComponents<App>()` applies to every component route (SEC-40), so it
|
||||
needs a real authenticated dashboard role: the `Authentication:Mode=Disabled` and
|
||||
anonymous-localhost bypasses reach every other read-only page but not this one. It is the same
|
||||
policy `AlarmsHub` carries — the two read one central monitor and must not disagree about who
|
||||
may see it — and the side rail hides the Alarms link for callers the policy denies. What a
|
||||
permitted caller sees is galaxy-wide and not tag-scoped; see "Alarm-feed visibility".
|
||||
|
||||
### Live data source
|
||||
|
||||
Both the Browse subscription panel and the Alarms page read live MXAccess data
|
||||
@@ -706,14 +714,25 @@ Implemented behavior:
|
||||
generic credential-rejected message;
|
||||
- antiforgery tokens guard the login and logout POSTs.
|
||||
|
||||
Three authorization policies are registered:
|
||||
Four authorization policies are registered:
|
||||
|
||||
- `MxGateway.Dashboard.Viewer` — Razor component routes. Satisfied by Admin or
|
||||
Viewer.
|
||||
- `MxGateway.Dashboard.Admin` — Admin-only write surfaces (API-key CRUD).
|
||||
- `MxGateway.Dashboard.HubClients` — SignalR hubs. Accepts the dashboard
|
||||
cookie OR a `MxGateway.Dashboard.HubToken` bearer (used by WebSocket upgrades
|
||||
where the cookie can't be forwarded).
|
||||
- `MxGateway.Dashboard.HubClients` — the snapshot and events SignalR hubs. Accepts
|
||||
the dashboard cookie OR a `MxGateway.Dashboard.HubToken` bearer (used by WebSocket
|
||||
upgrades where the cookie can't be forwarded).
|
||||
- `MxGateway.Dashboard.Alarms` — the alarm feed: `AlarmsHub` and the `/alarms`
|
||||
page. Same two schemes as `HubClients`, but built from
|
||||
`AuthenticatedDashboardRoleRequirement` instead of
|
||||
`DashboardAuthorizationRequirement` (SEC-40). Because `DashboardAuthorizationHandler`
|
||||
handles only the latter type, the read-only environment bypasses below cannot
|
||||
satisfy this policy — see "Alarm-feed visibility".
|
||||
|
||||
The first three requirement types are satisfiable by the environment bypasses;
|
||||
`MxGateway.Dashboard.Alarms` and the mounted Secrets UI's `secrets:manage` are not,
|
||||
both for the same structural reason — the bypass handler is typed to a requirement
|
||||
they do not use.
|
||||
|
||||
The in-process page feeds carry no authentication of their own, and need none:
|
||||
`MapRazorComponents<App>()` applies `RequireAuthorization(ViewerPolicy)` to the
|
||||
@@ -733,12 +752,30 @@ caller's granted tags, ordinal-ignore-case. A session's tags are inherited from
|
||||
owning API key's `--dashboard-tags` constraint and are immutable for the session's
|
||||
life, so a subscribe-time decision cannot go stale while the subscription lives and
|
||||
no per-event re-check is needed. A Viewer's grant comes from
|
||||
`Dashboard:GroupToTag` applied to their LDAP groups, stamped as
|
||||
`zb:dashboardtag` claims at cookie login and re-resolved (not copied) at hub-token
|
||||
mint, so the token's 5-minute lifetime bounds how long a revoked grant survives. A
|
||||
principal carrying no tag claims — anonymous localhost included — is an
|
||||
empty-grant Viewer. The mirror's value redaction is an independent layer: it bounds
|
||||
what a permitted subscriber sees, not who may subscribe.
|
||||
`Dashboard:GroupToTag` applied to their LDAP groups. Since SEC-41 the ACL **re-resolves**
|
||||
that grant on every subscribe, from the caller's `mxgateway:ldap_group` claims against the
|
||||
currently bound `GroupToTag` map — the same one `DashboardGroupTagMapping.MapGroupsToTags`
|
||||
call the hub-token mint makes — rather than trusting the `zb:dashboardtag` claims stamped
|
||||
into the cookie at login. Those stamped claims were resolved under whatever map was bound
|
||||
when the user signed in, and the auth cookie outlives the gateway restart that applies a
|
||||
`GroupToTag` edit, so trusting them let a revoked grant survive the restart meant to remove
|
||||
it. The stamped claims remain the **fallback** for a principal carrying no group claims,
|
||||
which in practice means the hub-token principal: the token payload carries roles and tags
|
||||
but no LDAP groups, and the mint already re-resolved them. That fallback cannot re-open the
|
||||
cookie hole, because `DashboardAuthenticator` stamps group and tag claims from one group
|
||||
list — a cookie principal holding a tag claim always holds the groups it came from, and a
|
||||
login yielding no groups yields no tags. A principal carrying neither kind of claim —
|
||||
anonymous localhost included — is an empty-grant Viewer. The mirror's value redaction is an
|
||||
independent layer: it bounds what a permitted subscriber sees, not who may subscribe.
|
||||
|
||||
**Revocation bounds.** Three different revocations propagate on three different clocks, and
|
||||
the difference is intrinsic to where each fact lives:
|
||||
|
||||
| Revocation | Takes effect |
|
||||
|---|---|
|
||||
| A `GroupToTag` entry removed from config | At the next subscribe after the gateway restart that rebinds options — identically on both seams, since both re-resolve against current options. `GatewayOptions` binds once at startup, so the restart is what applies the edit; nothing further is needed. |
|
||||
| An LDAP group-membership revocation | When the principal is next rebuilt: at re-login for the cookie seam, bounded by the cookie's sliding 8-hour idle life (renewable by activity); within one 5-minute token lifetime for the hub-token seam. Neither seam re-queries LDAP per subscribe. |
|
||||
| Either, for an **already-open** subscription | Never — the subscription lives until it drops. The ACL is a subscribe-time gate by design (session tags are immutable, so no per-event re-check is warranted); closing an in-flight stream on a grant change would need a revocation channel the dashboard does not have. Force it by closing the session, or by restarting the gateway, which tears down every circuit and hub connection. |
|
||||
|
||||
Two environmental bypasses still apply, both scoped to **read-only** access:
|
||||
`MxGateway:Authentication:Mode = Disabled` and `MxGateway:Dashboard:AllowAnonymousLocalhost`
|
||||
@@ -746,11 +783,50 @@ Two environmental bypasses still apply, both scoped to **read-only** access:
|
||||
role (`AnyDashboardRole`) but **never** the `AdminOnly` requirement. Destructive/admin
|
||||
surfaces (API-key CRUD, session Close, worker Kill) still demand a real `Administrator`
|
||||
role claim, so the "anonymous localhost is read-only" contract holds at the policy layer
|
||||
rather than only in downstream service re-checks. Remote requests (outside the `Disabled`
|
||||
rather than only in downstream service re-checks. Since SEC-40 the bypasses also do not
|
||||
reach the **alarm feed** (`MxGateway.Dashboard.Alarms`) — read-only is not the same as
|
||||
"may see the whole plant's alarm metadata"; see "Alarm-feed visibility" below. Remote requests (outside the `Disabled`
|
||||
bypass) always require an authenticated principal carrying at least the Viewer role. The
|
||||
loopback test trusts `Connection.RemoteIpAddress`; if forwarded-headers middleware is ever
|
||||
added upstream, `DashboardAuthorizationHandler.IsLoopbackRequest()` must be revisited.
|
||||
|
||||
### Alarm-feed visibility
|
||||
|
||||
The alarm feed is **not** tag-scoped, and that is a deliberate, bounded decision rather than
|
||||
an oversight in the SEC-25 ACL.
|
||||
|
||||
The gateway's alarm monitor is central and session-less: one subscription expression, one
|
||||
`IGatewayAlarmService` feed, consumed by `AlarmsHubPublisher` (which re-broadcasts every
|
||||
message to the `__alarms__` group) and by the `/alarms` page in process. An `AlarmFeedMessage`
|
||||
carries no session id and no owning API key, so there is nothing for the per-session tag
|
||||
intersection to intersect — a tag ACL cannot be applied to it without inventing provenance the
|
||||
feed does not have. Deriving a filter from the caller's tags would also mean equating a
|
||||
visibility tag with an alarm-reference prefix, which `GroupToTag` deliberately does not do
|
||||
(tags are visibility labels, never data-access constraints).
|
||||
|
||||
So **alarm metadata stays galaxy-wide for every authenticated dashboard Viewer**: alarm
|
||||
reference (a full tag path), state, severity, priority, category, area, and acknowledging
|
||||
operator are all visible to any real dashboard role, regardless of which sessions their tag
|
||||
grant covers. Alarm *values* (`current_value` / `limit_value`) are still stripped when
|
||||
`Dashboard:ShowTagValues` is false (the default), on the hub seam as on the events seam.
|
||||
|
||||
What SEC-40 tightened is the entry gate rather than the scope. Both readers of the monitor —
|
||||
`AlarmsHub` and the `/alarms` page — carry `MxGateway.Dashboard.Alarms`, whose requirement type
|
||||
the environment-bypass handler does not handle. Consequences:
|
||||
|
||||
- `MxGateway:Authentication:Mode = Disabled` and anonymous localhost (`AllowAnonymousLocalhost`,
|
||||
default `true`) **no longer receive the alarm feed at all**. They keep read-only access to every
|
||||
other dashboard page; `/alarms` and `/hubs/alarms` deny, and the side rail hides the Alarms link
|
||||
rather than offering a dead end.
|
||||
- `Dashboard:DisableLogin` auto-login is unaffected: it mints a genuinely authenticated principal
|
||||
holding both roles, so it satisfies the policy like any signed-in user.
|
||||
- Gating only the hub was rejected: the page reads the same monitor in process, so the disclosure
|
||||
would have been one navigation away.
|
||||
|
||||
Operators who *want* the alarm feed on an unauthenticated local console must sign in (or, in a dev
|
||||
environment, set `Dashboard:DisableLogin=true`); there is deliberately no config flag that re-opens
|
||||
the feed to the bypasses.
|
||||
|
||||
### DisableLogin dev bypass
|
||||
|
||||
`MxGateway:Dashboard:DisableLogin` (default `false`) is a third bypass for
|
||||
@@ -824,13 +900,16 @@ the deliberate mitigation: it bounds that exposure window without the cost of a
|
||||
revocation store. It now bounds a stale *tag* grant the same way: the token carries
|
||||
the tags resolved from the caller's LDAP groups at mint time, so removing a
|
||||
`GroupToTag` entry takes effect for token-authenticated hub connections within one
|
||||
lifetime. That 5-minute staleness bound covers token-authenticated connections only:
|
||||
a cookie principal carries the `zb:dashboardtag` claims stamped at login for the
|
||||
cookie's whole life, so for cookie-authenticated (in-process page) subscriptions a
|
||||
revoked `GroupToTag` grant takes effect at the user's next login, not within five
|
||||
minutes. That is where the per-session ACL's revocation need landed — a jti
|
||||
denylist stays deferred, since the short lifetime already bounds every grant the
|
||||
token carries.
|
||||
lifetime. Since SEC-41 the cookie seam is no longer the weaker one: the ACL
|
||||
re-resolves a cookie principal's grant from its group claims against current
|
||||
options on every subscribe, so a `GroupToTag` edit takes effect on both seams at
|
||||
the next subscribe after the restart that rebinds options — the stamped
|
||||
`zb:dashboardtag` claims are consulted only for a principal with no group claims
|
||||
to re-resolve from, i.e. the hub-token principal itself. What the token's lifetime
|
||||
still bounds uniquely is a stale **role** set and a stale LDAP **group** list, since
|
||||
both are copied into the payload at mint. See the revocation-bounds table under
|
||||
"Authentication And Authorization". A jti denylist stays deferred — the short
|
||||
lifetime already bounds every claim the token carries.
|
||||
|
||||
## Configuration
|
||||
|
||||
|
||||
Reference in New Issue
Block a user