Files
scadaproj/ZB.MOM.WW.Secrets/src/ZB.MOM.WW.Secrets.Ui/Components/RevealButton.razor
T

128 lines
3.9 KiB
Plaintext

@namespace ZB.MOM.WW.Secrets.Ui.Components
@implements IDisposable
@inject ISecretStore Store
@inject ISecretCipher Cipher
@inject IAuditWriter Audit
@inject IServiceProvider Services
@* Components/RevealButton.razor — a policy-gated, audited reveal. The plaintext is loaded and
shown only AFTER the click, straight from the store+cipher (a human reveal shows the CURRENT
value, distinct from any cached machine resolve). The value never enters an audit field. *@
@if (!_revealed)
{
<button type="button" class="btn btn-outline-secondary reveal-secret"
disabled="@_busy" data-secret-name="@Name.Value"
data-testid="reveal-button" @onclick="RevealAsync">
Reveal
</button>
}
else
{
<div class="reveal-panel" data-testid="reveal-panel">
@if (_value is not null)
{
<code class="revealed-value" data-testid="revealed-value">@_value</code>
}
else
{
<span class="field-error s-bad" data-testid="reveal-error">@(_error ?? "Unavailable.")</span>
}
<button type="button" class="btn btn-link hide-secret"
data-testid="hide-button" @onclick="Hide">
Hide
</button>
</div>
}
@code {
private readonly CancellationTokenSource _cts = new();
private bool _revealed;
private string? _value;
private string? _error;
private bool _busy;
/// <summary>The secret whose plaintext this button reveals.</summary>
[Parameter] public SecretName Name { get; set; }
/// <summary>The cascading authentication state, used as the fallback source of the audit actor.</summary>
[CascadingParameter] private Task<AuthenticationState>? AuthState { get; set; }
private async Task RevealAsync()
{
_busy = true;
AuditOutcome outcome;
try
{
StoredSecret? row = await Store.GetAsync(Name, _cts.Token);
if (row is null)
{
_value = null;
_error = "Secret not found.";
outcome = AuditOutcome.Failure;
}
else
{
_value = Cipher.Decrypt(row);
_error = null;
outcome = AuditOutcome.Success;
}
}
catch (SecretDecryptionException)
{
_value = null;
_error = "Unable to decrypt secret.";
outcome = AuditOutcome.Failure;
}
catch (Exception)
{
// Any other unexpected store/cipher fault (e.g. master key unavailable, timeout) must
// still be audited as a Failure and surfaced to the user, not left to tear down the
// circuit. The value is never shown.
_value = null;
_error = "Unable to reveal secret.";
outcome = AuditOutcome.Failure;
}
finally
{
_busy = false;
_revealed = true;
}
string actor = await SecretActorResolver.ResolveAsync(Services, AuthState);
try
{
await Audit.WriteAsync(
new AuditEvent
{
EventId = Guid.NewGuid(),
OccurredAtUtc = DateTimeOffset.UtcNow,
Actor = actor,
Action = "secret.reveal",
Outcome = outcome,
Category = "Secrets",
Target = Name.Value,
DetailsJson = outcome == AuditOutcome.Failure ? "{\"reason\":\"error\"}" : null,
},
_cts.Token);
}
catch
{
// Best-effort audit; never let an audit-write fault surface into the event pipeline.
}
}
private void Hide()
{
_revealed = false;
_value = null;
_error = null;
}
/// <inheritdoc />
public void Dispose()
{
_cts.Cancel();
_cts.Dispose();
}
}