fix(security): close auth & site-scoping gaps across 8 findings

Resolves the auth-theme batch from the 2026-05-28 baseline review (8 findings
across Security/CentralUI/ManagementService/CLI). The most consequential gaps:
NotificationReport + SiteCallsReport now route through SiteScopeService so a
site-scoped Deployment user cannot see or act on other sites' rows (CUI-028);
QueryAuditLogCommand is no longer "any authenticated user" — gated Admin-only
to match /api/audit/query's strictness (MS-018); RoleMapper preserves the
broader grant when a user is in both an unscoped and scoped Deployment LDAP
group, instead of silently narrowing to the scoped set (Sec-016); and the
dead SiteScopeRequirement/Handler are deleted so SiteScopeService is
unambiguously the sole site-scoping mechanism (Sec-017). Pending findings:
172 → 164.
This commit is contained in:
Joseph Doherty
2026-05-28 03:35:29 -04:00
parent f93b7b99bb
commit e536178323
28 changed files with 814 additions and 196 deletions
+25 -2
View File
@@ -8,7 +8,7 @@
| Last reviewed | 2026-05-28 |
| Reviewer | claude-agent |
| Commit reviewed | `1eb6e97` |
| Open findings | 8 |
| Open findings | 7 |
## Summary
@@ -1341,7 +1341,7 @@ at least one representative page so the helper's continued use is enforced.
|--|--|
| Severity | High |
| Category | Security |
| Status | Open |
| Status | Resolved |
| Location | `src/ScadaLink.CentralUI/Components/Pages/Notifications/NotificationReport.razor:2,434,472,502`; `src/ScadaLink.CentralUI/Components/Pages/SiteCalls/SiteCallsReport.razor:2,52-59`; `src/ScadaLink.CentralUI/Components/Pages/SiteCalls/SiteCallsReport.razor.cs:97-110,201,250-251,278-279` |
**Description**
@@ -1378,6 +1378,29 @@ Add `Site_ScopedDeploymentUser_OnlySeesPermittedRows` and
`Site_ScopedDeploymentUser_CannotRetryRowOnNonPermittedSite` regression tests modelled
on `TopologyPageTests.SiteScoping_*`.
**Resolution**
Resolved 2026-05-28 (commit pending). Both pages now inject `SiteScopeService` and apply
three layers of restriction. (1) `OnInitializedAsync` keeps an unfiltered `_allSites`
list as the source of truth for site-identifier → Site.Id lookups, runs the dropdown
through `SiteScope.FilterSitesAsync`, and caches `IsSystemWideAsync` + permitted-site
ids so the row-level filter is synchronous. (2) The query response is run through a new
`FilterPermittedAsync` helper that drops any row whose `SourceSiteId` / `SourceSite`
resolves (via the unfiltered list) to a Site.Id outside the permitted set — a stale
source-site identifier not present in the loaded list defaults to allowed, mirroring
the existing tolerance for deleted-site rows. (3) `RetryNotification` /
`DiscardNotification` / `RetrySiteCall` / `DiscardSiteCall` each re-check
`IsRowSiteAllowedAsync` against the row's site BEFORE relaying, surfacing
"You are not permitted to act on …" via toast on failure. Cross-module partner
Security-017 was resolved in the same batch (the dead `SiteScopeAuthorizationHandler`
was deleted; `SiteScopeService` is now documented as the sole site-scoping mechanism).
Regression test `SiteCallsReportPageTests.SiteScoping_ScopedDeploymentUser_HidesOutOfScopeRows`
seeds a Deployment user with a single `SiteId=1` claim, asserts only the Plant-A row
renders, and verifies the Plant-B row is dropped (the page's row count drops from 2 to
1). All three existing report-page test fixtures register `SiteScopeService` so the
default system-wide path is unaffected — the full `ScadaLink.CentralUI.Tests` suite
still passes (568 / 568).
### CentralUI-029 — `ConfigurationAuditLog` uses `JS.InvokeAsync<int>("eval", ...)` instead of a dedicated JS module
| | |