Resolve all 46 low-severity architecture review findings

This commit is contained in:
Joseph Doherty
2026-09-09 22:56:36 -04:00
parent fef3e7675f
commit 9ee402bbcd
139 changed files with 2621 additions and 1082 deletions
@@ -1,3 +1,9 @@
using NSubstitute;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.Extensions.Options;
using ZB.MOM.WW.Auth.Abstractions.Ldap;
using ZB.MOM.WW.Auth.Abstractions.Roles;
using ZB.MOM.WW.ScadaBridge.Security;
using Microsoft.AspNetCore.Antiforgery;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
@@ -67,16 +73,51 @@ public class AuthEndpointsCsrfTests
}
[Fact]
public void PostAuthLogin_StillDisablesAntiforgery_PreAuthIsAcceptable()
public async Task PostAuthLogin_ValidTokenAuthenticatesAndSetsCookie()
{
// Login is a pre-auth endpoint; disabling antiforgery there is acceptable
// and intentional. This pins that the fix did not over-correct.
var endpoints = BuildEndpoints();
var login = Find(endpoints, "/auth/login", "POST");
var endpoint = Find(BuildEndpoints(), "/auth/login", "POST")!;
var services = new ServiceCollection();
services.AddLogging();
services.AddAntiforgery();
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme).AddCookie();
services.AddSingleton(new LoginThrottle(TimeProvider.System, Options.Create(new SecurityOptions())));
var ldap = Substitute.For<ILdapAuthService>();
ldap.AuthenticateAsync("alice", "password", Arg.Any<CancellationToken>())
.Returns(LdapAuthResult.Success("alice", "Alice", []));
services.AddSingleton(ldap);
var mapper = Substitute.For<IGroupRoleMapper<string>>();
mapper.MapAsync(Arg.Any<IReadOnlyList<string>>(), Arg.Any<CancellationToken>())
.Returns(new GroupRoleMapping<string>([Roles.Viewer], Scope: null));
services.AddSingleton(mapper);
using var provider = services.BuildServiceProvider();
var tokenContext = new DefaultHttpContext { RequestServices = provider };
var tokens = provider.GetRequiredService<IAntiforgery>().GetAndStoreTokens(tokenContext);
var cookie = tokenContext.Response.Headers.SetCookie.Single()!.Split(';')[0];
var context = new DefaultHttpContext { RequestServices = provider };
context.Request.Headers.Cookie = cookie;
context.Request.Method = "POST";
context.Request.ContentType = "application/x-www-form-urlencoded";
var form = $"username=alice&password=password&{tokens.FormFieldName}={Uri.EscapeDataString(tokens.RequestToken!)}";
context.Request.Body = new MemoryStream(System.Text.Encoding.UTF8.GetBytes(form));
await endpoint.RequestDelegate!(context);
Assert.Equal(StatusCodes.Status302Found, context.Response.StatusCode);
Assert.Equal("/", context.Response.Headers.Location.ToString());
Assert.Contains(context.Response.Headers.SetCookie, value => value!.StartsWith(".AspNetCore.Cookies="));
}
Assert.NotNull(login);
var antiforgery = login!.Metadata.GetMetadata<IAntiforgeryMetadata>();
Assert.NotNull(antiforgery);
Assert.False(antiforgery!.RequiresValidation);
[Fact]
public async Task PostAuthLogin_MissingTokenRejectsBeforeCredentialLookup()
{
var endpoint = Find(BuildEndpoints(), "/auth/login", "POST")!;
var services = new ServiceCollection();
services.AddLogging();
services.AddAntiforgery();
using var provider = services.BuildServiceProvider();
var context = new DefaultHttpContext { RequestServices = provider };
context.Request.Method = "POST";
context.Request.ContentType = "application/x-www-form-urlencoded";
context.Request.Body = new MemoryStream(System.Text.Encoding.UTF8.GetBytes("username=test&password=password"));
await endpoint.RequestDelegate!(context);
Assert.Equal(StatusCodes.Status400BadRequest, context.Response.StatusCode);
}
}
@@ -0,0 +1,89 @@
using System.Reflection;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Components;
using ZB.MOM.WW.ScadaBridge.CentralUI.Components.Layout;
using ZB.MOM.WW.ScadaBridge.Security;
namespace ZB.MOM.WW.ScadaBridge.CentralUI.Tests.Auth;
public class RoutePolicyCoverageTests
{
[Fact]
public void EveryRoutableComponent_HasItsReviewedPolicy_IncludingExternalSecrets()
{
var expected = new Dictionary<string, string>
{
["/"] = "authenticated",
["/admin/api-keys"] = AuthorizationPolicies.RequireAdmin,
["/admin/api-keys/create"] = AuthorizationPolicies.RequireAdmin,
["/admin/api-keys/{KeyId}/edit"] = AuthorizationPolicies.RequireAdmin,
["/admin/ldap-mappings"] = AuthorizationPolicies.RequireAdmin,
["/admin/ldap-mappings/create"] = AuthorizationPolicies.RequireAdmin,
["/admin/ldap-mappings/{Id:int}/edit"] = AuthorizationPolicies.RequireAdmin,
["/admin/sites"] = AuthorizationPolicies.RequireAdmin,
["/admin/sites/create"] = AuthorizationPolicies.RequireAdmin,
["/admin/sites/{Id:int}/edit"] = AuthorizationPolicies.RequireAdmin,
["/audit/configuration"] = AuthorizationPolicies.OperationalAudit,
["/audit/execution-tree"] = AuthorizationPolicies.OperationalAudit,
["/audit/log"] = AuthorizationPolicies.OperationalAudit,
["/deployment/debug-view"] = AuthorizationPolicies.RequireDeployment,
["/deployment/deployments"] = AuthorizationPolicies.RequireDeployment,
["/deployment/flow-debugger"] = AuthorizationPolicies.RequireFlowDebugger,
["/deployment/instances"] = AuthorizationPolicies.RequireDeployment,
["/deployment/instances/create"] = AuthorizationPolicies.RequireDeployment,
["/deployment/instances/{Id:int}/configure"] = AuthorizationPolicies.RequireDeployment,
["/deployment/topology"] = AuthorizationPolicies.RequireDeployment,
["/design/api-methods/create"] = AuthorizationPolicies.RequireDesign,
["/design/api-methods/{Id:int}/edit"] = AuthorizationPolicies.RequireDesign,
["/design/connections"] = AuthorizationPolicies.RequireDesign,
["/design/connections/create"] = AuthorizationPolicies.RequireDesign,
["/design/connections/{Id:int}/certificates"] = AuthorizationPolicies.RequireAdmin,
["/design/connections/{Id:int}/edit"] = AuthorizationPolicies.RequireDesign,
["/design/data-connections"] = AuthorizationPolicies.RequireDesign,
["/design/data-connections/create"] = AuthorizationPolicies.RequireDesign,
["/design/data-connections/{Id:int}/edit"] = AuthorizationPolicies.RequireDesign,
["/design/db-connections/create"] = AuthorizationPolicies.RequireDesign,
["/design/db-connections/{Id:int}/edit"] = AuthorizationPolicies.RequireDesign,
["/design/external-systems"] = AuthorizationPolicies.RequireDesign,
["/design/external-systems/create"] = AuthorizationPolicies.RequireDesign,
["/design/external-systems/{Id:int}/edit"] = AuthorizationPolicies.RequireDesign,
["/design/schema-library"] = AuthorizationPolicies.RequireDesign,
["/design/shared-scripts"] = AuthorizationPolicies.RequireDesign,
["/design/shared-scripts/create"] = AuthorizationPolicies.RequireDesign,
["/design/shared-scripts/{Id:int}/edit"] = AuthorizationPolicies.RequireDesign,
["/design/templates"] = AuthorizationPolicies.RequireDesign,
["/design/templates/create"] = AuthorizationPolicies.RequireDesign,
["/design/templates/{Id:int}"] = AuthorizationPolicies.RequireDesign,
["/design/transport/export"] = AuthorizationPolicies.RequireDesign,
["/design/transport/import"] = AuthorizationPolicies.RequireAdmin,
["/login"] = "anonymous",
["/monitoring/alarms"] = AuthorizationPolicies.RequireDeployment,
["/monitoring/event-logs"] = AuthorizationPolicies.RequireDeployment,
["/monitoring/health"] = "authenticated",
["/monitoring/parked-messages"] = AuthorizationPolicies.RequireDeployment,
["/notifications/kpis"] = AuthorizationPolicies.RequireDeployment,
["/notifications/lists"] = AuthorizationPolicies.RequireDesign,
["/notifications/lists/create"] = AuthorizationPolicies.RequireDesign,
["/notifications/lists/{Id:int}/edit"] = AuthorizationPolicies.RequireDesign,
["/notifications/report"] = AuthorizationPolicies.RequireDeployment,
["/notifications/sms"] = AuthorizationPolicies.RequireAdmin,
["/notifications/smtp"] = AuthorizationPolicies.RequireAdmin,
["/operations/secured-writes"] = AuthorizationPolicies.RequireSecuredWriteAccess,
["/site-calls/report"] = AuthorizationPolicies.RequireDeployment,
["/admin/secrets"] = "secrets:manage",
};
var actual = new Dictionary<string, string>();
foreach (var type in new[] { typeof(MainLayout).Assembly, typeof(ZB.MOM.WW.Secrets.Ui.SecretsPage).Assembly }
.SelectMany(a => a.GetTypes()))
{
foreach (var route in type.GetCustomAttributes<RouteAttribute>())
{
var anonymous = type.IsDefined(typeof(AllowAnonymousAttribute));
var authorization = type.GetCustomAttributes<AuthorizeAttribute>().ToArray();
Assert.True(anonymous || authorization.Length > 0, $"Missing authorization: {route.Template}");
actual.Add(route.Template, anonymous ? "anonymous" : Assert.Single(authorization).Policy ?? "authenticated");
}
}
Assert.Equal(expected.OrderBy(x => x.Key), actual.OrderBy(x => x.Key));
}
}