feat: add audited one-shot prepare gate
This commit is contained in:
@@ -0,0 +1,100 @@
|
||||
#nullable enable
|
||||
|
||||
using MMoneyCoderSharp.Data;
|
||||
|
||||
namespace MBN_STOCK_WEBVIEW.Infrastructure;
|
||||
|
||||
/// <summary>
|
||||
/// Identifies the native database-write boundary that is requesting
|
||||
/// authorization. The request deliberately contains no operator-entered data.
|
||||
/// </summary>
|
||||
public enum DatabaseMutationTarget
|
||||
{
|
||||
OperatorCatalog,
|
||||
NamedPlaylist,
|
||||
ManualFinancial
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Describes one validated transaction immediately before its executor can
|
||||
/// create a database connection.
|
||||
/// </summary>
|
||||
public sealed record DatabaseMutationAuthorizationRequest(
|
||||
DatabaseMutationTarget Target,
|
||||
DataSourceKind Source,
|
||||
Guid OperationId,
|
||||
string MutationKind);
|
||||
|
||||
/// <summary>
|
||||
/// Final authorization seam for state-changing database operations. Returning
|
||||
/// false denies the transaction before a connection or transaction is created.
|
||||
/// Throwing also fails closed and is never converted into an allow decision.
|
||||
/// </summary>
|
||||
public interface IDatabaseMutationAuthorization
|
||||
{
|
||||
bool IsAuthorized(DatabaseMutationAuthorizationRequest request);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Provides the compatibility policy used when existing constructors do not
|
||||
/// supply an explicit database-mutation authorization policy.
|
||||
/// </summary>
|
||||
public static class DatabaseMutationAuthorization
|
||||
{
|
||||
public static IDatabaseMutationAuthorization AllowAll { get; } =
|
||||
new AllowAllDatabaseMutationAuthorization();
|
||||
|
||||
private sealed class AllowAllDatabaseMutationAuthorization
|
||||
: IDatabaseMutationAuthorization
|
||||
{
|
||||
public bool IsAuthorized(DatabaseMutationAuthorizationRequest request)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(request);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Raised when the active policy denies a database mutation before any native
|
||||
/// database connection is created.
|
||||
/// </summary>
|
||||
public sealed class DatabaseMutationAuthorizationException : InvalidOperationException
|
||||
{
|
||||
public DatabaseMutationAuthorizationException(
|
||||
DatabaseMutationAuthorizationRequest request,
|
||||
Exception? innerException = null)
|
||||
: base(
|
||||
$"The {request?.Target} database mutation was denied before a connection was opened.",
|
||||
innerException)
|
||||
{
|
||||
Request = request ?? throw new ArgumentNullException(nameof(request));
|
||||
}
|
||||
|
||||
public DatabaseMutationAuthorizationRequest Request { get; }
|
||||
}
|
||||
|
||||
internal static class DatabaseMutationAuthorizationGuard
|
||||
{
|
||||
internal static void Demand(
|
||||
IDatabaseMutationAuthorization authorization,
|
||||
DatabaseMutationAuthorizationRequest request)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(authorization);
|
||||
ArgumentNullException.ThrowIfNull(request);
|
||||
bool authorized;
|
||||
try
|
||||
{
|
||||
authorized = authorization.IsAuthorized(request);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
throw new DatabaseMutationAuthorizationException(request, exception);
|
||||
}
|
||||
|
||||
if (!authorized)
|
||||
{
|
||||
throw new DatabaseMutationAuthorizationException(request);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,7 @@ public sealed class OperatorCatalogMutationExecutor : IOperatorCatalogMutationEx
|
||||
{
|
||||
private readonly IDatabaseConnectionFactory _connectionFactory;
|
||||
private readonly ITransientDatabaseErrorDetector _errorDetector;
|
||||
private readonly IDatabaseMutationAuthorization _mutationAuthorization;
|
||||
private readonly TimeSpan _operationTimeout;
|
||||
private readonly Action<DataSourceKind, DbCommand> _configureCommand;
|
||||
|
||||
@@ -28,7 +29,22 @@ public sealed class OperatorCatalogMutationExecutor : IOperatorCatalogMutationEx
|
||||
connectionFactory,
|
||||
resilienceOptions,
|
||||
errorDetector ?? new TransientDatabaseErrorDetector(),
|
||||
ConfigureProviderCommand)
|
||||
ConfigureProviderCommand,
|
||||
DatabaseMutationAuthorization.AllowAll)
|
||||
{
|
||||
}
|
||||
|
||||
public OperatorCatalogMutationExecutor(
|
||||
IDatabaseConnectionFactory connectionFactory,
|
||||
DatabaseResilienceOptions resilienceOptions,
|
||||
ITransientDatabaseErrorDetector? errorDetector,
|
||||
IDatabaseMutationAuthorization mutationAuthorization)
|
||||
: this(
|
||||
connectionFactory,
|
||||
resilienceOptions,
|
||||
errorDetector ?? new TransientDatabaseErrorDetector(),
|
||||
ConfigureProviderCommand,
|
||||
mutationAuthorization)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -37,6 +53,21 @@ public sealed class OperatorCatalogMutationExecutor : IOperatorCatalogMutationEx
|
||||
DatabaseResilienceOptions resilienceOptions,
|
||||
ITransientDatabaseErrorDetector errorDetector,
|
||||
Action<DataSourceKind, DbCommand> configureCommand)
|
||||
: this(
|
||||
connectionFactory,
|
||||
resilienceOptions,
|
||||
errorDetector,
|
||||
configureCommand,
|
||||
DatabaseMutationAuthorization.AllowAll)
|
||||
{
|
||||
}
|
||||
|
||||
internal OperatorCatalogMutationExecutor(
|
||||
IDatabaseConnectionFactory connectionFactory,
|
||||
DatabaseResilienceOptions resilienceOptions,
|
||||
ITransientDatabaseErrorDetector errorDetector,
|
||||
Action<DataSourceKind, DbCommand> configureCommand,
|
||||
IDatabaseMutationAuthorization mutationAuthorization)
|
||||
{
|
||||
_connectionFactory = connectionFactory ??
|
||||
throw new ArgumentNullException(nameof(connectionFactory));
|
||||
@@ -44,6 +75,8 @@ public sealed class OperatorCatalogMutationExecutor : IOperatorCatalogMutationEx
|
||||
_errorDetector = errorDetector ?? throw new ArgumentNullException(nameof(errorDetector));
|
||||
_configureCommand = configureCommand ??
|
||||
throw new ArgumentNullException(nameof(configureCommand));
|
||||
_mutationAuthorization = mutationAuthorization ??
|
||||
throw new ArgumentNullException(nameof(mutationAuthorization));
|
||||
|
||||
var validationOptions = new DatabaseOptions { Resilience = resilienceOptions };
|
||||
validationOptions.ValidateRuntimeSettings();
|
||||
@@ -56,6 +89,23 @@ public sealed class OperatorCatalogMutationExecutor : IOperatorCatalogMutationEx
|
||||
{
|
||||
ValidateTransaction(transaction);
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
try
|
||||
{
|
||||
DatabaseMutationAuthorizationGuard.Demand(
|
||||
_mutationAuthorization,
|
||||
new DatabaseMutationAuthorizationRequest(
|
||||
DatabaseMutationTarget.OperatorCatalog,
|
||||
transaction.Source,
|
||||
transaction.OperationId,
|
||||
transaction.Kind.ToString()));
|
||||
}
|
||||
catch (DatabaseMutationAuthorizationException exception)
|
||||
{
|
||||
throw new OperatorCatalogMutationException(
|
||||
$"The {transaction.Kind} transaction was denied before it started and did not commit.",
|
||||
outcomeUnknown: false,
|
||||
exception);
|
||||
}
|
||||
var commandTimeoutSeconds = _connectionFactory.GetCommandTimeoutSeconds(transaction.Source);
|
||||
if (commandTimeoutSeconds is < 1 or > 600)
|
||||
{
|
||||
|
||||
@@ -19,6 +19,7 @@ public sealed partial class OracleManualFinancialMutationExecutor
|
||||
{
|
||||
private readonly IDatabaseConnectionFactory _connectionFactory;
|
||||
private readonly ITransientDatabaseErrorDetector _errorDetector;
|
||||
private readonly IDatabaseMutationAuthorization _mutationAuthorization;
|
||||
private readonly TimeSpan _operationTimeout;
|
||||
private readonly int _commandTimeoutSeconds;
|
||||
private readonly Action<DbCommand> _configureCommand;
|
||||
@@ -31,7 +32,22 @@ public sealed partial class OracleManualFinancialMutationExecutor
|
||||
connectionFactory,
|
||||
resilienceOptions,
|
||||
errorDetector ?? new TransientDatabaseErrorDetector(),
|
||||
ConfigureOracleCommand)
|
||||
ConfigureOracleCommand,
|
||||
DatabaseMutationAuthorization.AllowAll)
|
||||
{
|
||||
}
|
||||
|
||||
public OracleManualFinancialMutationExecutor(
|
||||
IDatabaseConnectionFactory connectionFactory,
|
||||
DatabaseResilienceOptions resilienceOptions,
|
||||
ITransientDatabaseErrorDetector? errorDetector,
|
||||
IDatabaseMutationAuthorization mutationAuthorization)
|
||||
: this(
|
||||
connectionFactory,
|
||||
resilienceOptions,
|
||||
errorDetector ?? new TransientDatabaseErrorDetector(),
|
||||
ConfigureOracleCommand,
|
||||
mutationAuthorization)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -40,12 +56,29 @@ public sealed partial class OracleManualFinancialMutationExecutor
|
||||
DatabaseResilienceOptions resilienceOptions,
|
||||
ITransientDatabaseErrorDetector errorDetector,
|
||||
Action<DbCommand> configureCommand)
|
||||
: this(
|
||||
connectionFactory,
|
||||
resilienceOptions,
|
||||
errorDetector,
|
||||
configureCommand,
|
||||
DatabaseMutationAuthorization.AllowAll)
|
||||
{
|
||||
}
|
||||
|
||||
internal OracleManualFinancialMutationExecutor(
|
||||
IDatabaseConnectionFactory connectionFactory,
|
||||
DatabaseResilienceOptions resilienceOptions,
|
||||
ITransientDatabaseErrorDetector errorDetector,
|
||||
Action<DbCommand> configureCommand,
|
||||
IDatabaseMutationAuthorization mutationAuthorization)
|
||||
{
|
||||
_connectionFactory = connectionFactory ??
|
||||
throw new ArgumentNullException(nameof(connectionFactory));
|
||||
ArgumentNullException.ThrowIfNull(resilienceOptions);
|
||||
_errorDetector = errorDetector ?? throw new ArgumentNullException(nameof(errorDetector));
|
||||
_configureCommand = configureCommand ?? throw new ArgumentNullException(nameof(configureCommand));
|
||||
_mutationAuthorization = mutationAuthorization ??
|
||||
throw new ArgumentNullException(nameof(mutationAuthorization));
|
||||
|
||||
var validationOptions = new DatabaseOptions { Resilience = resilienceOptions };
|
||||
validationOptions.ValidateRuntimeSettings();
|
||||
@@ -73,6 +106,23 @@ public sealed partial class OracleManualFinancialMutationExecutor
|
||||
|
||||
ValidateTransaction(transaction);
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
try
|
||||
{
|
||||
DatabaseMutationAuthorizationGuard.Demand(
|
||||
_mutationAuthorization,
|
||||
new DatabaseMutationAuthorizationRequest(
|
||||
DatabaseMutationTarget.ManualFinancial,
|
||||
DataSourceKind.Oracle,
|
||||
transaction.OperationId,
|
||||
transaction.Kind.ToString()));
|
||||
}
|
||||
catch (DatabaseMutationAuthorizationException exception)
|
||||
{
|
||||
throw new ManualFinancialMutationException(
|
||||
$"The {transaction.Kind} transaction was denied before it started and did not commit.",
|
||||
outcomeUnknown: false,
|
||||
exception);
|
||||
}
|
||||
using var timeoutSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
||||
timeoutSource.CancelAfter(_operationTimeout);
|
||||
var operationToken = timeoutSource.Token;
|
||||
|
||||
@@ -17,6 +17,7 @@ public sealed class OracleNamedPlaylistMutationExecutor : INamedPlaylistMutation
|
||||
{
|
||||
private readonly IDatabaseConnectionFactory _connectionFactory;
|
||||
private readonly ITransientDatabaseErrorDetector _errorDetector;
|
||||
private readonly IDatabaseMutationAuthorization _mutationAuthorization;
|
||||
private readonly TimeSpan _operationTimeout;
|
||||
private readonly int _commandTimeoutSeconds;
|
||||
private readonly Action<DbCommand> _configureCommand;
|
||||
@@ -29,7 +30,22 @@ public sealed class OracleNamedPlaylistMutationExecutor : INamedPlaylistMutation
|
||||
connectionFactory,
|
||||
resilienceOptions,
|
||||
errorDetector ?? new TransientDatabaseErrorDetector(),
|
||||
ConfigureOracleCommand)
|
||||
ConfigureOracleCommand,
|
||||
DatabaseMutationAuthorization.AllowAll)
|
||||
{
|
||||
}
|
||||
|
||||
public OracleNamedPlaylistMutationExecutor(
|
||||
IDatabaseConnectionFactory connectionFactory,
|
||||
DatabaseResilienceOptions resilienceOptions,
|
||||
ITransientDatabaseErrorDetector? errorDetector,
|
||||
IDatabaseMutationAuthorization mutationAuthorization)
|
||||
: this(
|
||||
connectionFactory,
|
||||
resilienceOptions,
|
||||
errorDetector ?? new TransientDatabaseErrorDetector(),
|
||||
ConfigureOracleCommand,
|
||||
mutationAuthorization)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -38,6 +54,21 @@ public sealed class OracleNamedPlaylistMutationExecutor : INamedPlaylistMutation
|
||||
DatabaseResilienceOptions resilienceOptions,
|
||||
ITransientDatabaseErrorDetector errorDetector,
|
||||
Action<DbCommand> configureCommand)
|
||||
: this(
|
||||
connectionFactory,
|
||||
resilienceOptions,
|
||||
errorDetector,
|
||||
configureCommand,
|
||||
DatabaseMutationAuthorization.AllowAll)
|
||||
{
|
||||
}
|
||||
|
||||
internal OracleNamedPlaylistMutationExecutor(
|
||||
IDatabaseConnectionFactory connectionFactory,
|
||||
DatabaseResilienceOptions resilienceOptions,
|
||||
ITransientDatabaseErrorDetector errorDetector,
|
||||
Action<DbCommand> configureCommand,
|
||||
IDatabaseMutationAuthorization mutationAuthorization)
|
||||
{
|
||||
_connectionFactory = connectionFactory ??
|
||||
throw new ArgumentNullException(nameof(connectionFactory));
|
||||
@@ -45,6 +76,8 @@ public sealed class OracleNamedPlaylistMutationExecutor : INamedPlaylistMutation
|
||||
_errorDetector = errorDetector ?? throw new ArgumentNullException(nameof(errorDetector));
|
||||
_configureCommand = configureCommand ??
|
||||
throw new ArgumentNullException(nameof(configureCommand));
|
||||
_mutationAuthorization = mutationAuthorization ??
|
||||
throw new ArgumentNullException(nameof(mutationAuthorization));
|
||||
|
||||
var validationOptions = new DatabaseOptions { Resilience = resilienceOptions };
|
||||
validationOptions.ValidateRuntimeSettings();
|
||||
@@ -72,6 +105,23 @@ public sealed class OracleNamedPlaylistMutationExecutor : INamedPlaylistMutation
|
||||
|
||||
ValidateTransaction(transaction);
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
try
|
||||
{
|
||||
DatabaseMutationAuthorizationGuard.Demand(
|
||||
_mutationAuthorization,
|
||||
new DatabaseMutationAuthorizationRequest(
|
||||
DatabaseMutationTarget.NamedPlaylist,
|
||||
DataSourceKind.Oracle,
|
||||
transaction.OperationId,
|
||||
transaction.Kind.ToString()));
|
||||
}
|
||||
catch (DatabaseMutationAuthorizationException exception)
|
||||
{
|
||||
throw new NamedPlaylistMutationException(
|
||||
$"The {transaction.Kind} transaction was denied before it started and did not commit.",
|
||||
outcomeUnknown: false,
|
||||
exception);
|
||||
}
|
||||
|
||||
using var timeoutSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
||||
timeoutSource.CancelAfter(_operationTimeout);
|
||||
|
||||
Reference in New Issue
Block a user