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:
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user