feat(secrets-ui): RCL + authz policies + metadata-only secrets list
This commit is contained in:
@@ -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
|
||||
Reference in New Issue
Block a user