feat(secrets-ui): RCL + authz policies + metadata-only secrets list

This commit is contained in:
Joseph Doherty
2026-07-15 17:10:40 -04:00
parent 7f1db1b214
commit d7f8ca455b
7 changed files with 294 additions and 16 deletions
@@ -1,9 +0,0 @@
namespace ZB.MOM.WW.Secrets.Ui;
/// <summary>
/// Internal marker giving the assembly a compilable input while the Blazor
/// components are scaffolded. Replace with real components as they are added.
/// </summary>
internal static class AssemblyMarker
{
}
@@ -0,0 +1,84 @@
@namespace ZB.MOM.WW.Secrets.Ui
@implements IDisposable
@inject ISecretStore Store
@* Components/SecretsList.razor — metadata-only listing of secrets. Never renders a value or ciphertext. *@
<TechCard Title="Secrets">
@if (_secrets is null)
{
<p class="mono">Loading…</p>
}
else
{
<div class="table-wrap">
<table class="data-table">
<thead>
<tr>
<th>Name</th>
<th>Description</th>
<th>Content type</th>
<th>KEK</th>
<th class="num">Revision</th>
<th>Updated (UTC)</th>
<th>Updated by</th>
<th></th>
</tr>
</thead>
<tbody>
@if (_secrets.Count == 0)
{
<tr>
<td class="empty-row" colspan="8">No secrets.</td>
</tr>
}
else
{
@foreach (SecretMetadata secret in _secrets)
{
<tr>
<td class="mono">@secret.Name.Value</td>
<td>@secret.Description</td>
<td>@secret.ContentType</td>
<td class="mono">@secret.KekId</td>
<td class="num">@secret.Revision</td>
<td class="mono">@secret.UpdatedUtc.UtcDateTime.ToString("u")</td>
<td>@secret.UpdatedBy</td>
<td>
<AuthorizeView Policy="@SecretsAuthorization.RevealPolicy">
@* Placeholder — actual reveal wiring is a later task. Gated so it
renders only for principals holding the reveal policy. *@
<button type="button"
class="btn btn-outline-secondary reveal-secret"
disabled
title="Reveal (coming soon)"
data-secret-name="@secret.Name.Value">
Reveal
</button>
</AuthorizeView>
</td>
</tr>
}
}
</tbody>
</table>
</div>
}
</TechCard>
@code {
private readonly CancellationTokenSource _cts = new();
private IReadOnlyList<SecretMetadata>? _secrets;
/// <inheritdoc />
protected override async Task OnInitializedAsync()
{
_secrets = await Store.ListAsync(includeDeleted: false, _cts.Token);
}
/// <inheritdoc />
public void Dispose()
{
_cts.Cancel();
_cts.Dispose();
}
}
@@ -0,0 +1,9 @@
@page "/admin/secrets"
@namespace ZB.MOM.WW.Secrets.Ui
@attribute [Authorize(Policy = SecretsAuthorization.ManagePolicy)]
@* Pages/SecretsPage.razor — the host supplies the outer Theme shell/layout. *@
<h1>Secrets</h1>
<p>Manage stored secrets. Values are never shown here — reveal is a separate, more privileged action.</p>
<SecretsList />
@@ -0,0 +1,74 @@
using Microsoft.AspNetCore.Authorization;
using ZB.MOM.WW.Auth.AspNetCore;
namespace ZB.MOM.WW.Secrets.Ui;
/// <summary>
/// Authorization policies and the roles that back them for the secrets-management UI.
/// </summary>
/// <remarks>
/// Two policies gate the UI, layered by privilege:
/// <list type="bullet">
/// <item>
/// <description>
/// <see cref="ManagePolicy"/> — view and administer secret <em>metadata</em> (the list
/// page, create/rename/delete affordances). Satisfied by <see cref="ManageRole"/>,
/// <see cref="RevealRole"/>, or <see cref="AdminRole"/>.
/// </description>
/// </item>
/// <item>
/// <description>
/// <see cref="RevealPolicy"/> — the strictly more privileged right to reveal a secret's
/// plaintext value. Satisfied only by <see cref="RevealRole"/> or <see cref="AdminRole"/>.
/// Because both reveal-granting roles also satisfy <see cref="ManagePolicy"/>, holding
/// reveal always implies manage.
/// </description>
/// </item>
/// </list>
/// Both policies require an authenticated user. The requirements are expressed as role checks
/// against the canonical <see cref="ZbClaimTypes.Role"/> claim, so the host maps its LDAP
/// groups to these role names (via its <c>IGroupRoleMapper</c> / <c>Security:Ldap:GroupToRole</c>
/// configuration) exactly as it does for every other <c>ZB.MOM.WW</c> role. The role-name
/// constants are deliberately minimal and can be overridden by the host by registering its own
/// policies under the same policy names instead of calling <see cref="AddSecretsAuthorization"/>.
/// </remarks>
public static class SecretsAuthorization
{
/// <summary>Policy name for administering secret metadata (the list page and management actions).</summary>
public const string ManagePolicy = "secrets:manage";
/// <summary>Policy name for revealing a secret's plaintext value — strictly more privileged than <see cref="ManagePolicy"/>.</summary>
public const string RevealPolicy = "secrets:reveal";
/// <summary>Role that grants secret metadata management (satisfies <see cref="ManagePolicy"/>).</summary>
public const string ManageRole = "secrets-manager";
/// <summary>Role that grants secret value reveal (satisfies both <see cref="RevealPolicy"/> and <see cref="ManagePolicy"/>).</summary>
public const string RevealRole = "secrets-reveal";
/// <summary>Administrator role that satisfies every secrets policy.</summary>
public const string AdminRole = "administrator";
/// <summary>
/// Registers the <see cref="ManagePolicy"/> and <see cref="RevealPolicy"/> policies on the
/// supplied <see cref="AuthorizationOptions"/>. Each policy requires an authenticated user;
/// <see cref="RevealPolicy"/> is a strict superset-privilege of <see cref="ManagePolicy"/>.
/// </summary>
/// <param name="options">The authorization options to add the policies to.</param>
/// <returns>The same <paramref name="options"/> instance, to allow chaining.</returns>
/// <exception cref="ArgumentNullException"><paramref name="options"/> is <c>null</c>.</exception>
public static AuthorizationOptions AddSecretsAuthorization(this AuthorizationOptions options)
{
ArgumentNullException.ThrowIfNull(options);
options.AddPolicy(ManagePolicy, policy => policy
.RequireAuthenticatedUser()
.RequireRole(ManageRole, RevealRole, AdminRole));
options.AddPolicy(RevealPolicy, policy => policy
.RequireAuthenticatedUser()
.RequireRole(RevealRole, AdminRole));
return options;
}
}
@@ -0,0 +1,7 @@
@using Microsoft.AspNetCore.Components
@using Microsoft.AspNetCore.Components.Authorization
@using Microsoft.AspNetCore.Components.Web
@using Microsoft.AspNetCore.Authorization
@using ZB.MOM.WW.Theme
@using ZB.MOM.WW.Secrets.Abstractions
@using ZB.MOM.WW.Secrets.Ui