136 lines
5.2 KiB
C#
136 lines
5.2 KiB
C#
using Microsoft.Data.Sqlite;
|
|
|
|
namespace MxGateway.Server.Security.Authentication;
|
|
|
|
public sealed class SqliteAuthStoreMigrator(AuthSqliteConnectionFactory connectionFactory) : IAuthStoreMigrator
|
|
{
|
|
public async Task MigrateAsync(CancellationToken cancellationToken)
|
|
{
|
|
await using SqliteConnection connection = connectionFactory.CreateConnection();
|
|
await connection.OpenAsync(cancellationToken).ConfigureAwait(false);
|
|
|
|
await using SqliteTransaction transaction =
|
|
(SqliteTransaction)await connection.BeginTransactionAsync(cancellationToken).ConfigureAwait(false);
|
|
|
|
int existingVersion = await ReadExistingSchemaVersionAsync(connection, transaction, cancellationToken)
|
|
.ConfigureAwait(false);
|
|
|
|
if (existingVersion > SqliteAuthSchema.CurrentVersion)
|
|
{
|
|
throw new AuthStoreMigrationException(
|
|
$"Auth database schema version {existingVersion} is newer than supported version {SqliteAuthSchema.CurrentVersion}.");
|
|
}
|
|
|
|
await ApplyVersionOneAsync(connection, transaction, cancellationToken).ConfigureAwait(false);
|
|
|
|
await transaction.CommitAsync(cancellationToken).ConfigureAwait(false);
|
|
}
|
|
|
|
private static async Task<int> ReadExistingSchemaVersionAsync(
|
|
SqliteConnection connection,
|
|
SqliteTransaction transaction,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
await using SqliteCommand tableExistsCommand = connection.CreateCommand();
|
|
tableExistsCommand.Transaction = transaction;
|
|
tableExistsCommand.CommandText = """
|
|
SELECT COUNT(*)
|
|
FROM sqlite_master
|
|
WHERE type = 'table' AND name = $table_name;
|
|
""";
|
|
tableExistsCommand.Parameters.AddWithValue("$table_name", SqliteAuthSchema.SchemaVersionTable);
|
|
|
|
long tableCount = (long)(await tableExistsCommand.ExecuteScalarAsync(cancellationToken).ConfigureAwait(false) ?? 0L);
|
|
|
|
if (tableCount == 0)
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
await using SqliteCommand versionCommand = connection.CreateCommand();
|
|
versionCommand.Transaction = transaction;
|
|
versionCommand.CommandText = """
|
|
SELECT version
|
|
FROM schema_version
|
|
WHERE id = 1;
|
|
""";
|
|
|
|
object? version = await versionCommand.ExecuteScalarAsync(cancellationToken).ConfigureAwait(false);
|
|
|
|
return version is null || version == DBNull.Value
|
|
? 0
|
|
: Convert.ToInt32(version, System.Globalization.CultureInfo.InvariantCulture);
|
|
}
|
|
|
|
private static async Task ApplyVersionOneAsync(
|
|
SqliteConnection connection,
|
|
SqliteTransaction transaction,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
await ExecuteNonQueryAsync(
|
|
connection,
|
|
transaction,
|
|
"""
|
|
CREATE TABLE IF NOT EXISTS schema_version (
|
|
id INTEGER PRIMARY KEY CHECK (id = 1),
|
|
version INTEGER NOT NULL,
|
|
applied_utc TEXT NOT NULL
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS api_keys (
|
|
key_id TEXT PRIMARY KEY,
|
|
key_prefix TEXT NOT NULL,
|
|
secret_hash BLOB NOT NULL,
|
|
display_name TEXT NOT NULL,
|
|
scopes TEXT NOT NULL,
|
|
created_utc TEXT NOT NULL,
|
|
last_used_utc TEXT NULL,
|
|
revoked_utc TEXT NULL
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS api_key_audit (
|
|
audit_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
key_id TEXT NULL,
|
|
event_type TEXT NOT NULL,
|
|
remote_address TEXT NULL,
|
|
created_utc TEXT NOT NULL,
|
|
details TEXT NULL
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS ix_api_keys_revoked_utc
|
|
ON api_keys (revoked_utc);
|
|
|
|
CREATE INDEX IF NOT EXISTS ix_api_key_audit_key_id_created_utc
|
|
ON api_key_audit (key_id, created_utc);
|
|
""",
|
|
cancellationToken).ConfigureAwait(false);
|
|
|
|
await using SqliteCommand versionCommand = connection.CreateCommand();
|
|
versionCommand.Transaction = transaction;
|
|
versionCommand.CommandText = """
|
|
INSERT INTO schema_version (id, version, applied_utc)
|
|
VALUES (1, $version, $applied_utc)
|
|
ON CONFLICT(id) DO UPDATE SET
|
|
version = excluded.version,
|
|
applied_utc = excluded.applied_utc;
|
|
""";
|
|
versionCommand.Parameters.AddWithValue("$version", SqliteAuthSchema.CurrentVersion);
|
|
versionCommand.Parameters.AddWithValue("$applied_utc", DateTimeOffset.UtcNow.ToString("O"));
|
|
|
|
await versionCommand.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
|
|
}
|
|
|
|
private static async Task ExecuteNonQueryAsync(
|
|
SqliteConnection connection,
|
|
SqliteTransaction transaction,
|
|
string commandText,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
await using SqliteCommand command = connection.CreateCommand();
|
|
command.Transaction = transaction;
|
|
command.CommandText = commandText;
|
|
|
|
await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
|
|
}
|
|
}
|