Initial commit: scadaproj umbrella — sister-project index, auth component normalization (design + GAPS), and the built ZB.MOM.WW.Auth shared library (0.1.0, flattened in).

This commit is contained in:
dohertj2
2026-06-01 03:59:23 -04:00
commit 37e23cf9f2
73 changed files with 6836 additions and 0 deletions
@@ -0,0 +1,143 @@
using Microsoft.Data.Sqlite;
using ZB.MOM.WW.Auth.Abstractions.ApiKeys;
namespace ZB.MOM.WW.Auth.ApiKeys.Sqlite;
/// <summary>
/// SQLite-backed administration store for API keys (create, revoke, rotate, delete).
/// </summary>
public sealed class SqliteApiKeyAdminStore(AuthSqliteConnectionFactory connectionFactory) : IApiKeyAdminStore
{
/// <inheritdoc />
public async Task CreateAsync(ApiKeyRecord record, CancellationToken ct)
{
ArgumentNullException.ThrowIfNull(record);
await using SqliteConnection connection =
await connectionFactory.OpenConnectionAsync(ct).ConfigureAwait(false);
await using SqliteCommand command = connection.CreateCommand();
command.CommandText = """
INSERT INTO api_keys (
key_id, key_prefix, secret_hash, display_name, scopes,
constraints, created_utc, last_used_utc, revoked_utc)
VALUES (
$key_id, $key_prefix, $secret_hash, $display_name, $scopes,
$constraints, $created_utc, $last_used_utc, $revoked_utc);
""";
command.Parameters.AddWithValue("$key_id", record.KeyId);
command.Parameters.AddWithValue("$key_prefix", record.KeyPrefix);
command.Parameters.Add("$secret_hash", SqliteType.Blob).Value = record.SecretHash;
command.Parameters.AddWithValue("$display_name", record.DisplayName);
command.Parameters.AddWithValue("$scopes", ScopeSerializer.Serialize(record.Scopes));
command.Parameters.AddWithValue("$constraints", (object?)record.ConstraintsJson ?? DBNull.Value);
command.Parameters.AddWithValue("$created_utc", record.CreatedUtc.ToString("O"));
command.Parameters.AddWithValue("$last_used_utc", (object?)record.LastUsedUtc?.ToString("O") ?? DBNull.Value);
command.Parameters.AddWithValue("$revoked_utc", (object?)record.RevokedUtc?.ToString("O") ?? DBNull.Value);
await command.ExecuteNonQueryAsync(ct).ConfigureAwait(false);
}
/// <inheritdoc />
public async Task<bool> RevokeAsync(string keyId, DateTimeOffset whenUtc, CancellationToken ct)
{
ArgumentException.ThrowIfNullOrWhiteSpace(keyId);
await using SqliteConnection connection =
await connectionFactory.OpenConnectionAsync(ct).ConfigureAwait(false);
await using SqliteCommand command = connection.CreateCommand();
command.CommandText = """
UPDATE api_keys
SET revoked_utc = $revoked_utc
WHERE key_id = $key_id AND revoked_utc IS NULL;
""";
command.Parameters.AddWithValue("$key_id", keyId);
command.Parameters.AddWithValue("$revoked_utc", whenUtc.ToString("O"));
int rows = await command.ExecuteNonQueryAsync(ct).ConfigureAwait(false);
return rows > 0;
}
/// <inheritdoc />
public async Task<bool> RotateAsync(string keyId, byte[] newSecretHash, CancellationToken ct)
{
ArgumentException.ThrowIfNullOrWhiteSpace(keyId);
ArgumentNullException.ThrowIfNull(newSecretHash);
await using SqliteConnection connection =
await connectionFactory.OpenConnectionAsync(ct).ConfigureAwait(false);
await using SqliteCommand command = connection.CreateCommand();
command.CommandText = """
UPDATE api_keys
SET secret_hash = $secret_hash,
last_used_utc = NULL,
revoked_utc = NULL
WHERE key_id = $key_id;
""";
command.Parameters.AddWithValue("$key_id", keyId);
command.Parameters.Add("$secret_hash", SqliteType.Blob).Value = newSecretHash;
int rows = await command.ExecuteNonQueryAsync(ct).ConfigureAwait(false);
return rows > 0;
}
/// <inheritdoc />
public async Task<bool> DeleteAsync(string keyId, CancellationToken ct)
{
ArgumentException.ThrowIfNullOrWhiteSpace(keyId);
await using SqliteConnection connection =
await connectionFactory.OpenConnectionAsync(ct).ConfigureAwait(false);
await using SqliteCommand command = connection.CreateCommand();
command.CommandText = """
DELETE FROM api_keys
WHERE key_id = $key_id AND revoked_utc IS NOT NULL;
""";
command.Parameters.AddWithValue("$key_id", keyId);
int rows = await command.ExecuteNonQueryAsync(ct).ConfigureAwait(false);
return rows > 0;
}
/// <inheritdoc />
public async Task<IReadOnlyList<ApiKeyListItem>> ListAsync(CancellationToken ct)
{
await using SqliteConnection connection =
await connectionFactory.OpenConnectionAsync(ct).ConfigureAwait(false);
await using SqliteCommand command = connection.CreateCommand();
// Deliberately omits secret_hash so listing can never leak secret material.
command.CommandText = """
SELECT key_id, key_prefix, display_name, scopes, constraints,
created_utc, last_used_utc, revoked_utc
FROM api_keys
ORDER BY created_utc DESC, key_id DESC;
""";
List<ApiKeyListItem> items = [];
await using SqliteDataReader reader = await command.ExecuteReaderAsync(ct).ConfigureAwait(false);
while (await reader.ReadAsync(ct).ConfigureAwait(false))
{
items.Add(new ApiKeyListItem(
KeyId: reader.GetString(0),
KeyPrefix: reader.GetString(1),
DisplayName: reader.GetString(2),
Scopes: ScopeSerializer.Deserialize(reader.GetString(3)),
ConstraintsJson: reader.IsDBNull(4) ? null : reader.GetString(4),
CreatedUtc: SqliteValueParsing.ParseUtc(reader.GetString(5)),
LastUsedUtc: SqliteValueParsing.ReadNullableUtc(reader, 6),
RevokedUtc: SqliteValueParsing.ReadNullableUtc(reader, 7)));
}
return items;
}
}