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
+37 -3
View File
@@ -8,7 +8,7 @@
| Last reviewed | 2026-05-28 |
| Reviewer | claude-agent |
| Commit reviewed | `1eb6e97` |
| Open findings | 6 (1 Deferred — see ManagementService-012) |
| Open findings | 4 (1 Deferred — see ManagementService-012) |
## Summary
@@ -796,7 +796,7 @@ Deployment user and an Admin user, in- and out-of-scope
|--|--|
| Severity | High |
| Category | Security |
| Status | Open |
| Status | Resolved |
| Location | `src/ScadaLink.ManagementService/ManagementActor.cs:153``:207`, `:336`, `:1302` |
**Description**
@@ -838,13 +838,26 @@ Recommended: option 1 plus a deprecation comment on `QueryAuditLogCommand` point
so the ManagementActor route is redundant. Add a regression test asserting that a
no-role / `Deployment`-only caller gets `ManagementUnauthorized` for `QueryAuditLogCommand`.
**Resolution**
Resolved 2026-05-28 (commit pending) per recommendation option 1. `QueryAuditLogCommand`
was added to the Admin-required group in `GetRequiredRole`, with an inline comment
documenting the deliberate strictness vs. the keyset-paged `/api/audit/query`
(`OperationalAuditRoles`) and pointing new audit consumers at the REST endpoint.
The CentralUI `ConfigurationAuditLog` page reads via `ICentralUiRepository` directly
(not through this command), so the gate tightening does not break any UI flow. Two
regression tests pin the new behaviour:
`QueryAuditLogCommand_WithNoRoles_ReturnsUnauthorized` and
`QueryAuditLogCommand_WithDeploymentRole_ReturnsUnauthorized` — both fail on the
pre-fix code (the command fell through to "any authenticated user") and pass after.
### ManagementService-019 — AuditEndpoints builds PermittedSiteIds but never enforces them
| | |
|--|--|
| Severity | Medium |
| Category | Security |
| Status | Open |
| Status | Resolved |
| Location | `src/ScadaLink.ManagementService/AuditEndpoints.cs:358``:368`, `:397``:437` |
**Description**
@@ -887,6 +900,27 @@ Recommended: option 1, mirroring the `ManagementActor` pattern — same security
across both surfaces. Add a regression test that a site-scoped `AuditReadOnly` user
filtering on an out-of-scope site gets a 403 (or an empty page).
**Resolution**
Resolved 2026-05-28 (commit pending) per recommendation option 1. Added a public
helper `AuditEndpoints.ApplySiteScope(AuditLogQueryFilter, AuthenticatedUser)` that
returns the restricted filter (or `null` when the caller explicitly asks for an
out-of-scope site). Three cases:
- Empty `PermittedSiteIds` (Admin or any unscoped role) → filter returned unchanged.
- Scoped user with empty caller filter → `SourceSiteIds` set to the permitted set.
- Scoped user with explicit `SourceSiteIds` → intersected with the permitted set;
empty intersection returns `null` so `HandleQuery` / `HandleExport` emit a 403
rather than silently producing an empty page.
Both `HandleQuery` and `HandleExport` now call the helper after the role check and
short-circuit to `Forbidden("OperationalAudit"|"AuditExport")` on a `null` result.
Audit roles remain non-site-scoped by design (the design doc unchanged), but the
helper honours scope rules if an operator attaches them via the LDAP-mapping UI,
matching the existing `ManagementActor` pattern. Regression tests added in
`AuditEndpointsTests.ApplySiteScope_*` (5 tests): system-wide unchanged,
empty-caller-filter restricted, in-scope kept verbatim, out-of-scope returns null,
mixed-set intersected.
### ManagementService-020 — UpdateSmtpConfig returns and audits the SMTP Credentials field verbatim
| | |