feat(secrets): mount /admin/secrets + AddSecretsAuthorization in the dashboard (G-6)
This commit is contained in:
@@ -19,6 +19,7 @@
|
|||||||
</NavRailSection>
|
</NavRailSection>
|
||||||
<NavRailSection Title="Admin" Key="admin">
|
<NavRailSection Title="Admin" Key="admin">
|
||||||
<NavRailItem Href="/apikeys" Text="API Keys" />
|
<NavRailItem Href="/apikeys" Text="API Keys" />
|
||||||
|
<NavRailItem Href="/admin/secrets" Text="Secrets" />
|
||||||
<NavRailItem Href="/settings" Text="Settings" />
|
<NavRailItem Href="/settings" Text="Settings" />
|
||||||
</NavRailSection>
|
</NavRailSection>
|
||||||
</Nav>
|
</Nav>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<Router AppAssembly="@typeof(Routes).Assembly">
|
<Router AppAssembly="@typeof(Routes).Assembly"
|
||||||
|
AdditionalAssemblies="new[]{ typeof(ZB.MOM.WW.Secrets.Ui.SecretsPage).Assembly }">
|
||||||
<Found Context="routeData">
|
<Found Context="routeData">
|
||||||
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
|
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
|
||||||
<FocusOnNavigate RouteData="@routeData" Selector="h1" />
|
<FocusOnNavigate RouteData="@routeData" Selector="h1" />
|
||||||
|
|||||||
@@ -132,6 +132,7 @@ public static class DashboardEndpointRouteBuilderExtensions
|
|||||||
// cookie scheme and redirected to /login.
|
// cookie scheme and redirected to /login.
|
||||||
endpoints.MapRazorComponents<App>()
|
endpoints.MapRazorComponents<App>()
|
||||||
.AddInteractiveServerRenderMode()
|
.AddInteractiveServerRenderMode()
|
||||||
|
.AddAdditionalAssemblies(typeof(ZB.MOM.WW.Secrets.Ui.SecretsPage).Assembly)
|
||||||
.RequireAuthorization(DashboardAuthenticationDefaults.ViewerPolicy);
|
.RequireAuthorization(DashboardAuthenticationDefaults.ViewerPolicy);
|
||||||
|
|
||||||
return endpoints;
|
return endpoints;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System.Security.Cryptography.X509Certificates;
|
using System.Security.Cryptography.X509Certificates;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Hosting.StaticWebAssets;
|
using Microsoft.AspNetCore.Hosting.StaticWebAssets;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Extensions.Logging.Configuration;
|
using Microsoft.Extensions.Logging.Configuration;
|
||||||
@@ -19,6 +20,7 @@ using ZB.MOM.WW.Secrets.Abstractions;
|
|||||||
using ZB.MOM.WW.Secrets.Configuration;
|
using ZB.MOM.WW.Secrets.Configuration;
|
||||||
using ZB.MOM.WW.Secrets.DependencyInjection;
|
using ZB.MOM.WW.Secrets.DependencyInjection;
|
||||||
using ZB.MOM.WW.Secrets.Sqlite;
|
using ZB.MOM.WW.Secrets.Sqlite;
|
||||||
|
using ZB.MOM.WW.Secrets.Ui;
|
||||||
using ZB.MOM.WW.Telemetry;
|
using ZB.MOM.WW.Telemetry;
|
||||||
using ZB.MOM.WW.Telemetry.Serilog;
|
using ZB.MOM.WW.Telemetry.Serilog;
|
||||||
|
|
||||||
@@ -121,6 +123,10 @@ public static class GatewayApplication
|
|||||||
builder.Services.AddGatewaySessions();
|
builder.Services.AddGatewaySessions();
|
||||||
builder.Services.AddGatewayAlarms();
|
builder.Services.AddGatewayAlarms();
|
||||||
builder.Services.AddGatewayDashboard(builder.Configuration);
|
builder.Services.AddGatewayDashboard(builder.Configuration);
|
||||||
|
// Register the shared Secrets UI authorization policies (secrets:manage + secrets:reveal)
|
||||||
|
// additively so they compose with the dashboard's existing AddAuthorization block. The
|
||||||
|
// mounted /admin/secrets Blazor page carries [Authorize(Policy = "secrets:manage")].
|
||||||
|
builder.Services.Configure<AuthorizationOptions>(o => o.AddSecretsAuthorization());
|
||||||
// Register the gateway's browse-scope provider before AddZbGalaxyRepository so the
|
// Register the gateway's browse-scope provider before AddZbGalaxyRepository so the
|
||||||
// library's TryAddSingleton default (NullGalaxyBrowseScopeProvider) does not win.
|
// library's TryAddSingleton default (NullGalaxyBrowseScopeProvider) does not win.
|
||||||
builder.Services.AddSingleton<ZB.MOM.WW.GalaxyRepository.Grpc.IGalaxyBrowseScopeProvider,
|
builder.Services.AddSingleton<ZB.MOM.WW.GalaxyRepository.Grpc.IGalaxyBrowseScopeProvider,
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
using System.Security.Claims;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Builder;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using ZB.MOM.WW.MxGateway.Server;
|
||||||
|
using ZB.MOM.WW.MxGateway.Server.Dashboard;
|
||||||
|
|
||||||
|
namespace ZB.MOM.WW.MxGateway.Tests.Gateway;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Verifies the shared Secrets UI is wired into the dashboard: the
|
||||||
|
/// <c>AddSecretsAuthorization</c> policies are registered so the mounted
|
||||||
|
/// <c>/admin/secrets</c> Blazor page (<c>[Authorize(Policy = "secrets:manage")]</c>) and its
|
||||||
|
/// reveal action resolve, AND that the dashboard's real Administrator principal shape actually
|
||||||
|
/// satisfies them (the claim-type-compatibility risk class that bit HistorianGateway). The
|
||||||
|
/// browser reveal/redirect behaviour is verified separately on the box.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class DashboardSecretsMountTests
|
||||||
|
{
|
||||||
|
private const string ManagePolicy = "secrets:manage";
|
||||||
|
private const string RevealPolicy = "secrets:reveal";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Verifies that Build registers the shared Secrets authorization policies
|
||||||
|
/// (<c>secrets:manage</c> + <c>secrets:reveal</c>) added additively via
|
||||||
|
/// <c>Configure<AuthorizationOptions>(o => o.AddSecretsAuthorization())</c>.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||||
|
[Fact]
|
||||||
|
public async Task Build_RegistersSecretsAuthorizationPolicies()
|
||||||
|
{
|
||||||
|
await using WebApplication app = GatewayApplication.Build([]);
|
||||||
|
|
||||||
|
IAuthorizationPolicyProvider policyProvider =
|
||||||
|
app.Services.GetRequiredService<IAuthorizationPolicyProvider>();
|
||||||
|
|
||||||
|
AuthorizationPolicy? managePolicy = await policyProvider.GetPolicyAsync(ManagePolicy);
|
||||||
|
AuthorizationPolicy? revealPolicy = await policyProvider.GetPolicyAsync(RevealPolicy);
|
||||||
|
|
||||||
|
Assert.NotNull(managePolicy);
|
||||||
|
Assert.NotNull(revealPolicy);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Verifies that the dashboard's Administrator principal — constructed exactly the way
|
||||||
|
/// <see cref="DashboardAutoLoginAuthenticationHandler.CreatePrincipal(string?)"/> mints it
|
||||||
|
/// (role claim on <c>ZbClaimTypes.Role</c>, carrying <c>Administrator</c>, authenticated) —
|
||||||
|
/// satisfies BOTH secrets policies. This locks claim-type compatibility: if roles were ever
|
||||||
|
/// issued on a divergent claim type, <c>RequireRole("Administrator")</c> would no longer match
|
||||||
|
/// and this test would fail.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||||
|
[Fact]
|
||||||
|
public async Task AdministratorPrincipal_SatisfiesSecretsPolicies()
|
||||||
|
{
|
||||||
|
await using WebApplication app = GatewayApplication.Build([]);
|
||||||
|
IAuthorizationService authorization =
|
||||||
|
app.Services.GetRequiredService<IAuthorizationService>();
|
||||||
|
ClaimsPrincipal administrator = DashboardAutoLoginAuthenticationHandler.CreatePrincipal("multi-role");
|
||||||
|
|
||||||
|
AuthorizationResult manage = await authorization.AuthorizeAsync(administrator, resource: null, ManagePolicy);
|
||||||
|
AuthorizationResult reveal = await authorization.AuthorizeAsync(administrator, resource: null, RevealPolicy);
|
||||||
|
|
||||||
|
Assert.True(manage.Succeeded);
|
||||||
|
Assert.True(reveal.Succeeded);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Verifies that an anonymous, role-less principal is DENIED both secrets policies, proving the
|
||||||
|
/// policies actually gate on the Administrator role rather than admitting everyone.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||||
|
[Fact]
|
||||||
|
public async Task AnonymousPrincipal_IsDeniedSecretsPolicies()
|
||||||
|
{
|
||||||
|
await using WebApplication app = GatewayApplication.Build([]);
|
||||||
|
IAuthorizationService authorization =
|
||||||
|
app.Services.GetRequiredService<IAuthorizationService>();
|
||||||
|
ClaimsPrincipal anonymous = new(new ClaimsIdentity());
|
||||||
|
|
||||||
|
AuthorizationResult manage = await authorization.AuthorizeAsync(anonymous, resource: null, ManagePolicy);
|
||||||
|
AuthorizationResult reveal = await authorization.AuthorizeAsync(anonymous, resource: null, RevealPolicy);
|
||||||
|
|
||||||
|
Assert.False(manage.Succeeded);
|
||||||
|
Assert.False(reveal.Succeeded);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user