feat(configdb): add AuditLog migration with monthly partitioning and DB roles (#23)
Bundle C of the #23 M1 foundation. Creates the centralized AuditLog table with the partition function, partition scheme, partition-aligned non-clustered indexes, and the two access-control roles documented in alog.md §4. Schema: - pf_AuditLog_Month: RANGE RIGHT, 24 monthly boundaries (Jan 2026 – Dec 2027). - ps_AuditLog_Month: ALL TO ([PRIMARY]) — dev/test parity. - dbo.AuditLog: created via raw SQL ON ps_AuditLog_Month(OccurredAtUtc). Composite clustered PK {EventId, OccurredAtUtc} (partition column must be part of the clustered key). 22 columns matching the EF AuditEvent model. - 5 reconciliation/query non-clustered indexes from alog.md §4 (Channel_Status_Occurred, CorrelationId filtered, OccurredAtUtc, Site_Occurred, Target_Occurred filtered) — all partition-aligned. - UX_AuditLog_EventId: non-aligned UNIQUE on EventId alone (preserves InsertIfNotExistsAsync idempotency from M1-T8). Non-aligned because partition-aligned unique indexes require the partition column in the key, which would weaken to composite uniqueness; the purge story (M2/M3) rebuilds this index around partition switches. Access control: - scadalink_audit_writer: GRANT INSERT + GRANT SELECT, DENY UPDATE + DENY DELETE on AuditLog. The explicit DENY guarantees later db_datawriter membership cannot quietly re-enable mutation. - scadalink_audit_purger: GRANT SELECT on AuditLog, GRANT ALTER on SCHEMA::dbo (enables ALTER PARTITION FUNCTION SWITCH and SWITCH PARTITION). Both role definitions are idempotent (IF DATABASE_PRINCIPAL_ID IS NULL). Down() drops in reverse dependency order with IF EXISTS guards. Integration tests (tests/ScadaLink.ConfigurationDatabase.Tests/Migrations/): - MsSqlMigrationFixture: connects to the running infra/mssql container (or the SCADALINK_MSSQL_TEST_CONN override), creates a unique per-fixture database, applies the migrations, drops the DB on dispose. Marks itself Available=false when MSSQL is unreachable so tests early-return cleanly on CI without the dev container. - AddAuditLogTableMigrationTests: 8 tests covering table existence, partition function/scheme, partition-aligned PK, the 5 named indexes, both roles' grants, and a smoke test that a writer-role user receives SqlException with "permission" on UPDATE AuditLog. ConfigurationDatabase tests: 142 passing -> 150 passing (8 new integration tests). Full solution builds clean. Package: tests project locally overrides Microsoft.Data.SqlClient to 6.1.1 (EF SqlServer 10.0.7 needs >= 6.1.1; central package version is pinned at 6.0.2 for the production ExternalSystemGateway).
This commit is contained in:
@@ -41,6 +41,123 @@ namespace ScadaLink.ConfigurationDatabase.Migrations
|
||||
b.ToTable("DataProtectionKeys");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ScadaLink.Commons.Entities.Audit.AuditEvent", b =>
|
||||
{
|
||||
b.Property<Guid>("EventId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<DateTime>("OccurredAtUtc")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<string>("Actor")
|
||||
.HasMaxLength(128)
|
||||
.IsUnicode(false)
|
||||
.HasColumnType("varchar(128)");
|
||||
|
||||
b.Property<string>("Channel")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.IsUnicode(false)
|
||||
.HasColumnType("varchar(32)");
|
||||
|
||||
b.Property<Guid?>("CorrelationId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<int?>("DurationMs")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("ErrorDetail")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("ErrorMessage")
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("nvarchar(1024)");
|
||||
|
||||
b.Property<string>("Extra")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("ForwardState")
|
||||
.HasMaxLength(32)
|
||||
.IsUnicode(false)
|
||||
.HasColumnType("varchar(32)");
|
||||
|
||||
b.Property<int?>("HttpStatus")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<DateTime?>("IngestedAtUtc")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<string>("Kind")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.IsUnicode(false)
|
||||
.HasColumnType("varchar(32)");
|
||||
|
||||
b.Property<bool>("PayloadTruncated")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<string>("RequestSummary")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("ResponseSummary")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("SourceInstanceId")
|
||||
.HasMaxLength(128)
|
||||
.IsUnicode(false)
|
||||
.HasColumnType("varchar(128)");
|
||||
|
||||
b.Property<string>("SourceScript")
|
||||
.HasMaxLength(128)
|
||||
.IsUnicode(false)
|
||||
.HasColumnType("varchar(128)");
|
||||
|
||||
b.Property<string>("SourceSiteId")
|
||||
.HasMaxLength(64)
|
||||
.IsUnicode(false)
|
||||
.HasColumnType("varchar(64)");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.IsUnicode(false)
|
||||
.HasColumnType("varchar(32)");
|
||||
|
||||
b.Property<string>("Target")
|
||||
.HasMaxLength(256)
|
||||
.IsUnicode(false)
|
||||
.HasColumnType("varchar(256)");
|
||||
|
||||
b.HasKey("EventId", "OccurredAtUtc");
|
||||
|
||||
b.HasIndex("CorrelationId")
|
||||
.HasDatabaseName("IX_AuditLog_CorrelationId")
|
||||
.HasFilter("[CorrelationId] IS NOT NULL");
|
||||
|
||||
b.HasIndex("EventId")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("UX_AuditLog_EventId");
|
||||
|
||||
b.HasIndex("OccurredAtUtc")
|
||||
.IsDescending()
|
||||
.HasDatabaseName("IX_AuditLog_OccurredAtUtc");
|
||||
|
||||
b.HasIndex("SourceSiteId", "OccurredAtUtc")
|
||||
.IsDescending(false, true)
|
||||
.HasDatabaseName("IX_AuditLog_Site_Occurred");
|
||||
|
||||
b.HasIndex("Target", "OccurredAtUtc")
|
||||
.IsDescending(false, true)
|
||||
.HasDatabaseName("IX_AuditLog_Target_Occurred")
|
||||
.HasFilter("[Target] IS NOT NULL");
|
||||
|
||||
b.HasIndex("Channel", "Status", "OccurredAtUtc")
|
||||
.IsDescending(false, false, true)
|
||||
.HasDatabaseName("IX_AuditLog_Channel_Status_Occurred");
|
||||
|
||||
b.ToTable("AuditLog", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ScadaLink.Commons.Entities.Audit.AuditLogEntry", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
|
||||
Reference in New Issue
Block a user