113 lines
5.0 KiB
C#
113 lines
5.0 KiB
C#
using ZB.MOM.WW.Auth.ApiKeys.Admin;
|
|
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Security;
|
|
|
|
namespace ZB.MOM.WW.ScadaBridge.Security;
|
|
|
|
/// <summary>
|
|
/// Implements the Commons <see cref="IInboundApiKeyAdmin"/> management seam over the shared
|
|
/// <c>ZB.MOM.WW.Auth.ApiKeys</c> admin facade (<see cref="ApiKeyAdminCommands"/>). This is the
|
|
/// single shared path through which CLI and CentralUI create / list / enable / disable / delete
|
|
/// inbound keys and edit their method-scopes, so all front-ends drive identical library behaviour.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Mapping from the library projection to the app DTO:
|
|
/// <list type="bullet">
|
|
/// <item>A key is "enabled" iff its <c>RevokedUtc</c> is null.</item>
|
|
/// <item>"Methods" are the library's <c>Scopes</c>, sorted ordinally for a stable display order.</item>
|
|
/// <item>Delete is best-effort revoke-then-delete: the library only deletes already-revoked keys,
|
|
/// so we revoke first (a harmless no-op when already revoked) and the delete is authoritative.</item>
|
|
/// </list>
|
|
/// </remarks>
|
|
public sealed class LibraryInboundApiKeyAdmin : IInboundApiKeyAdmin
|
|
{
|
|
private readonly ApiKeyAdminCommands _admin;
|
|
|
|
/// <summary>Creates the seam over the supplied library admin command set.</summary>
|
|
/// <param name="admin">The shared library admin facade.</param>
|
|
public LibraryInboundApiKeyAdmin(ApiKeyAdminCommands admin)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(admin);
|
|
_admin = admin;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public async Task<InboundApiKeyCreated> CreateAsync(
|
|
string name, IReadOnlyCollection<string> methods, CancellationToken ct = default)
|
|
{
|
|
ArgumentException.ThrowIfNullOrWhiteSpace(name);
|
|
ArgumentNullException.ThrowIfNull(methods);
|
|
|
|
// "N" format = 32 hex chars, no hyphens/underscores — the library rejects underscores
|
|
// in keyId because they delimit the sbk_<keyId>_<secret> token.
|
|
var keyId = Guid.NewGuid().ToString("N");
|
|
var result = await _admin.CreateKeyAsync(
|
|
keyId, name, methods.ToHashSet(StringComparer.Ordinal),
|
|
constraintsJson: null, remoteAddress: null, ct).ConfigureAwait(false);
|
|
|
|
// Token is non-null on create success; CreateKeyAsync throws rather than returning a
|
|
// null token on the failure path, so a successful return always carries the secret.
|
|
return new InboundApiKeyCreated(result.KeyId, result.Token!);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public async Task<IReadOnlyList<InboundApiKeyInfo>> ListAsync(CancellationToken ct = default)
|
|
{
|
|
var items = await _admin.ListKeysAsync(ct).ConfigureAwait(false);
|
|
var result = new List<InboundApiKeyInfo>(items.Count);
|
|
foreach (var item in items)
|
|
{
|
|
result.Add(new InboundApiKeyInfo(
|
|
KeyId: item.KeyId,
|
|
Name: item.DisplayName,
|
|
Enabled: item.RevokedUtc is null,
|
|
Methods: item.Scopes.OrderBy(s => s, StringComparer.Ordinal).ToList(),
|
|
CreatedUtc: item.CreatedUtc,
|
|
LastUsedUtc: item.LastUsedUtc));
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public async Task<bool> SetEnabledAsync(string keyId, bool enabled, CancellationToken ct = default) =>
|
|
(await _admin.SetEnabledAsync(keyId, enabled, remoteAddress: null, ct).ConfigureAwait(false)).Succeeded;
|
|
|
|
/// <inheritdoc />
|
|
public async Task<bool> SetMethodsAsync(
|
|
string keyId, IReadOnlyCollection<string> methods, CancellationToken ct = default)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(methods);
|
|
return (await _admin.SetScopesAsync(
|
|
keyId, methods.ToHashSet(StringComparer.Ordinal), remoteAddress: null, ct)
|
|
.ConfigureAwait(false)).Succeeded;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public async Task<bool> DeleteAsync(string keyId, CancellationToken ct = default)
|
|
{
|
|
// Best-effort revoke first so the library permits the delete (it only deletes
|
|
// already-revoked keys). Revoking an already-disabled key is a harmless no-op;
|
|
// the delete result is authoritative.
|
|
await _admin.RevokeKeyAsync(keyId, remoteAddress: null, ct).ConfigureAwait(false);
|
|
return (await _admin.DeleteKeyAsync(keyId, remoteAddress: null, ct).ConfigureAwait(false)).Succeeded;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public async Task<IReadOnlyList<string>> GetMethodsForKeyAsync(string keyId, CancellationToken ct = default)
|
|
{
|
|
var keys = await ListAsync(ct).ConfigureAwait(false);
|
|
var match = keys.FirstOrDefault(k => string.Equals(k.KeyId, keyId, StringComparison.Ordinal));
|
|
return match?.Methods ?? (IReadOnlyList<string>)Array.Empty<string>();
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public async Task<IReadOnlyList<string>> GetKeysForMethodAsync(string methodName, CancellationToken ct = default)
|
|
{
|
|
var keys = await ListAsync(ct).ConfigureAwait(false);
|
|
return keys
|
|
.Where(k => k.Methods.Contains(methodName, StringComparer.Ordinal))
|
|
.Select(k => k.KeyId)
|
|
.ToList();
|
|
}
|
|
}
|