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:
@@ -8,7 +8,7 @@
|
||||
| Last reviewed | 2026-05-28 |
|
||||
| Reviewer | claude-agent |
|
||||
| Commit reviewed | `1eb6e97` |
|
||||
| Open findings | 6 (1 deferred — Security-008) |
|
||||
| Open findings | 2 (Security-020, Security-021); 1 deferred (Security-008) |
|
||||
|
||||
## Summary
|
||||
|
||||
@@ -706,7 +706,7 @@ use the single canonical identity. Regression tests
|
||||
|--|--|
|
||||
| Severity | Medium |
|
||||
| Category | Correctness & logic bugs |
|
||||
| Status | Open |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.Security/RoleMapper.cs:30-31`, `:41-55`, `:59` |
|
||||
|
||||
**Description**
|
||||
@@ -742,13 +742,29 @@ scopedSiteIds` (left empty for system-wide users). Add a regression test
|
||||
`MapGroupsToRoles_UserInBothSystemWideAndScopedDeploymentGroup_IsSystemWide` covering
|
||||
the design's example pair `SCADA-Deploy-All` + `SCADA-Deploy-SiteA`.
|
||||
|
||||
**Resolution**
|
||||
|
||||
Resolved 2026-05-28 (commit pending). `RoleMapper.MapGroupsToRolesAsync` now tracks two
|
||||
independent flags per matched Deployment mapping: `hasUnscopedDeploymentMapping` (any
|
||||
matched mapping with no scope rules) and `hasScopedDeploymentMapping` (any matched
|
||||
mapping with scope rules). `isSystemWide` is `hasUnscopedDeploymentMapping ||
|
||||
(hasDeploymentRole && !hasScopedDeploymentMapping)` — so a user in both
|
||||
`SCADA-Deploy-All` and `SCADA-Deploy-SiteA` is now correctly system-wide, with the
|
||||
accumulated scope ids cleared. Magic string `"Deployment"` was replaced with the new
|
||||
`Roles.Deployment` constant (Security-018). Regression test
|
||||
`MapGroupsToRoles_UserInBothSystemWideAndScopedDeploymentGroup_IsSystemWide`
|
||||
(seeds Site A, attaches a scope rule to the seeded `SCADA-Deploy-SiteA` mapping, and
|
||||
asserts a user mapped via both `SCADA-Deploy-All` and `SCADA-Deploy-SiteA` resolves
|
||||
to system-wide with empty `PermittedSiteIds`) fails on the pre-fix code and passes
|
||||
after.
|
||||
|
||||
### Security-017 — `SiteScopeRequirement` / `SiteScopeAuthorizationHandler` are dead code from production callers — `[Authorize(Policy = RequireDeployment)]` does NOT enforce site scoping
|
||||
|
||||
| | |
|
||||
|--|--|
|
||||
| Severity | Medium |
|
||||
| Category | Design-document adherence |
|
||||
| Status | Open |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.Security/SiteScopeAuthorizationHandler.cs:8-58`; `src/ScadaLink.Security/AuthorizationPolicies.cs:113-143` |
|
||||
|
||||
**Description**
|
||||
@@ -787,13 +803,25 @@ the request — that is a meaningful design extension and would need to be plann
|
||||
alongside the Central UI's existing `SiteScopeService` usage rather than replacing it
|
||||
piecemeal.
|
||||
|
||||
**Resolution**
|
||||
|
||||
Resolved 2026-05-28 (commit pending) per recommendation option (a): deleted
|
||||
`SiteScopeRequirement` and `SiteScopeAuthorizationHandler` outright, along with the
|
||||
unwired `services.AddSingleton<IAuthorizationHandler, SiteScopeAuthorizationHandler>()`
|
||||
registration in `AuthorizationPolicies.AddScadaLinkAuthorization` and the four
|
||||
isolation tests in `SecurityTests.cs`. `SiteScopeService` (CentralUI-002) is now
|
||||
documented as the sole site-scoping mechanism — the stale "mirrors
|
||||
SiteScopeAuthorizationHandler" comment in `SiteScopeService.ResolveAsync` was rewritten
|
||||
to say so. The cross-module partner CentralUI-028 is fixed in the same batch (the two
|
||||
new report pages now consume `SiteScopeService`).
|
||||
|
||||
### Security-018 — Role names are hard-coded magic strings duplicated across `RoleMapper`, `SiteScopeAuthorizationHandler`, and `AuthorizationPolicies`
|
||||
|
||||
| | |
|
||||
|--|--|
|
||||
| Severity | Low |
|
||||
| Category | Code organization & conventions |
|
||||
| Status | Open |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.Security/RoleMapper.cs:41`; `src/ScadaLink.Security/SiteScopeAuthorizationHandler.cs:36`; `src/ScadaLink.Security/AuthorizationPolicies.cs:118,121,124,95,107` |
|
||||
|
||||
**Description**
|
||||
@@ -822,13 +850,26 @@ project and replace every magic-string occurrence — including the elements of
|
||||
`OperationalAuditRoles` and `AuditExportRoles` — with the constants. A single rename
|
||||
will then either succeed everywhere or fail to compile.
|
||||
|
||||
**Resolution**
|
||||
|
||||
Resolved 2026-05-28 (commit pending). Added `src/ScadaLink.Security/Roles.cs` holding
|
||||
`Admin`/`Design`/`Deployment`/`Audit`/`AuditReadOnly` as `public const string`
|
||||
fields. Replaced every magic-string occurrence in this module:
|
||||
`RoleMapper.MapGroupsToRolesAsync` now compares against `Roles.Deployment`;
|
||||
`AuthorizationPolicies.AddScadaLinkAuthorization` binds `RequireClaim(...)` to
|
||||
`Roles.{Admin,Design,Deployment}`; `OperationalAuditRoles` /
|
||||
`AuditExportRoles` are now built from `Roles.Admin`, `Roles.Audit`, `Roles.AuditReadOnly`.
|
||||
`SiteScopeAuthorizationHandler.cs` was deleted under Security-017, so its
|
||||
`"Deployment"` literal is gone with it. A future rename now propagates by a
|
||||
single edit or fails to compile.
|
||||
|
||||
### Security-019 — Service-account rebind failure is reported as "Invalid username or password" — masks misconfiguration as a user-credential error
|
||||
|
||||
| | |
|
||||
|--|--|
|
||||
| Severity | Low |
|
||||
| Category | Error handling & resilience |
|
||||
| Status | Open |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.Security/LdapAuthService.cs:85-89`, `:147-151` |
|
||||
|
||||
**Description**
|
||||
@@ -859,6 +900,25 @@ Add a regression test that exercises the service-account-bind failure path (a mo
|
||||
or seamed `LdapConnection.Bind` that throws on the second call) and asserts the
|
||||
distinct error message.
|
||||
|
||||
**Resolution**
|
||||
|
||||
Resolved 2026-05-28 (commit pending). Added a new `ServiceAccountBindException`
|
||||
(`src/ScadaLink.Security/ServiceAccountBindException.cs`) — deliberately NOT an
|
||||
`LdapException` subtype so it short-circuits the generic LDAP catch — and a private
|
||||
`BindServiceAccountAsync` helper on `LdapAuthService` that wraps both service-account
|
||||
rebind sites (the post-user-bind group-lookup rebind AND the `ResolveUserDnAsync`
|
||||
DN-search rebind). On `LdapException`, the helper logs Error
|
||||
("Service-account rebind failed; check LdapServiceAccountDn /
|
||||
LdapServiceAccountPassword configuration") and rethrows as `ServiceAccountBindException`,
|
||||
which the outer `AuthenticateAsync` catch chain maps to the distinct user-facing message
|
||||
"Authentication service is misconfigured. Contact an administrator." Service-account
|
||||
faults no longer surface as "Invalid username or password". Regression test
|
||||
`ServiceAccountBindException_DoesNotInheritLdapException_SoCatchOrderIsCorrect` pins the
|
||||
load-bearing inheritance contract; a full end-to-end auth test that exercises the
|
||||
service-account-bind failure path is not feasible without an LDAP seam in
|
||||
`LdapAuthService` (the `LdapConnection` is constructed in-method), so the structural test
|
||||
is the closest meaningful unit-level coverage.
|
||||
|
||||
### Security-020 — `SecurityOptions` has no startup validation for required fields (`LdapServer`, `LdapSearchBase`)
|
||||
|
||||
| | |
|
||||
|
||||
Reference in New Issue
Block a user