fix(config-db): grant CREATE TABLE + scoped DELETE to scadabridge_audit_purger so a segregated maintenance principal can actually run the purge
This commit is contained in:
@@ -0,0 +1,29 @@
|
|||||||
|
BEGIN TRANSACTION;
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT * FROM [__EFMigrationsHistory]
|
||||||
|
WHERE [MigrationId] = N'20260709105112_FixAuditPurgerRoleGrants'
|
||||||
|
)
|
||||||
|
BEGIN
|
||||||
|
|
||||||
|
IF DATABASE_PRINCIPAL_ID('scadabridge_audit_purger') IS NULL
|
||||||
|
EXEC sp_executesql N'CREATE ROLE scadabridge_audit_purger';
|
||||||
|
-- The switch-out dance CREATEs a staging table; ALTER ON SCHEMA::dbo alone does not
|
||||||
|
-- confer the database-level CREATE TABLE permission (review finding S3).
|
||||||
|
GRANT CREATE TABLE TO scadabridge_audit_purger;
|
||||||
|
-- The per-channel retention override (PurgeChannelOlderThanAsync) is a bounded row
|
||||||
|
-- DELETE on the maintenance path; a segregated purger principal needs this grant.
|
||||||
|
GRANT DELETE ON dbo.AuditLog TO scadabridge_audit_purger;
|
||||||
|
END;
|
||||||
|
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT * FROM [__EFMigrationsHistory]
|
||||||
|
WHERE [MigrationId] = N'20260709105112_FixAuditPurgerRoleGrants'
|
||||||
|
)
|
||||||
|
BEGIN
|
||||||
|
INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion])
|
||||||
|
VALUES (N'20260709105112_FixAuditPurgerRoleGrants', N'10.0.7');
|
||||||
|
END;
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
|
GO
|
||||||
|
|
||||||
+2009
File diff suppressed because it is too large
Load Diff
+47
@@ -0,0 +1,47 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Migrations
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Grants the segregated maintenance principal <c>scadabridge_audit_purger</c> the two
|
||||||
|
/// permissions its purge path actually needs but the original role creation
|
||||||
|
/// (<c>CollapseAuditLogToCanonical</c>) omitted (arch-review 04, S3):
|
||||||
|
/// <list type="bullet">
|
||||||
|
/// <item><c>CREATE TABLE</c> — the switch-out dance CREATEs a staging table; the
|
||||||
|
/// role's existing <c>ALTER ON SCHEMA::dbo</c> does NOT confer the database-level
|
||||||
|
/// <c>CREATE TABLE</c> permission.</item>
|
||||||
|
/// <item><c>DELETE ON dbo.AuditLog</c> — the per-channel retention override
|
||||||
|
/// (<c>PurgeChannelOlderThanAsync</c>) is a bounded row DELETE on the maintenance path.</item>
|
||||||
|
/// </list>
|
||||||
|
/// Permission DDL only — no schema/data change. Idempotent and safe to run standalone.
|
||||||
|
/// </summary>
|
||||||
|
public partial class FixAuditPurgerRoleGrants : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.Sql(@"
|
||||||
|
IF DATABASE_PRINCIPAL_ID('scadabridge_audit_purger') IS NULL
|
||||||
|
EXEC sp_executesql N'CREATE ROLE scadabridge_audit_purger';
|
||||||
|
-- The switch-out dance CREATEs a staging table; ALTER ON SCHEMA::dbo alone does not
|
||||||
|
-- confer the database-level CREATE TABLE permission (review finding S3).
|
||||||
|
GRANT CREATE TABLE TO scadabridge_audit_purger;
|
||||||
|
-- The per-channel retention override (PurgeChannelOlderThanAsync) is a bounded row
|
||||||
|
-- DELETE on the maintenance path; a segregated purger principal needs this grant.
|
||||||
|
GRANT DELETE ON dbo.AuditLog TO scadabridge_audit_purger;"); // AUDIT-PURGE-ALLOWED: role grant for the maintenance principal
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.Sql(@"
|
||||||
|
IF DATABASE_PRINCIPAL_ID('scadabridge_audit_purger') IS NOT NULL
|
||||||
|
BEGIN
|
||||||
|
REVOKE CREATE TABLE FROM scadabridge_audit_purger;
|
||||||
|
REVOKE DELETE ON dbo.AuditLog FROM scadabridge_audit_purger;
|
||||||
|
END"); // AUDIT-PURGE-ALLOWED: role grant reversal for the maintenance principal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+74
@@ -0,0 +1,74 @@
|
|||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Tests.Migrations;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Content guard for the <c>FixAuditPurgerRoleGrants</c> migration (arch-review 04, S3).
|
||||||
|
/// The switch-out dance CREATEs a staging table and the per-channel override runs a bounded
|
||||||
|
/// row DELETE — but the shipped <c>scadabridge_audit_purger</c> role only carried
|
||||||
|
/// <c>SELECT</c> + <c>ALTER ON SCHEMA::dbo</c>, which does NOT confer the database-level
|
||||||
|
/// <c>CREATE TABLE</c> permission nor row-level <c>DELETE</c>. This test asserts the migration
|
||||||
|
/// source grants both, so a segregated maintenance principal can genuinely execute the purge.
|
||||||
|
/// A pure source-content scan (no MSSQL required) — the schema-application sanity check lives
|
||||||
|
/// in the MSSQL migration suite.
|
||||||
|
/// </summary>
|
||||||
|
public class FixAuditPurgerRoleGrantsTests
|
||||||
|
{
|
||||||
|
private static string ReadMigrationSource()
|
||||||
|
{
|
||||||
|
var migrationsDir = Path.Combine(
|
||||||
|
GetConfigurationDatabaseSourceDirectory(), "Migrations");
|
||||||
|
|
||||||
|
// Timestamp-prefixed filename; match the non-Designer partial only.
|
||||||
|
var file = Directory.GetFiles(migrationsDir, "*_FixAuditPurgerRoleGrants.cs")
|
||||||
|
.FirstOrDefault(f => !f.EndsWith(".Designer.cs", StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
|
Assert.True(file is not null,
|
||||||
|
"Expected a *_FixAuditPurgerRoleGrants.cs migration under " + migrationsDir +
|
||||||
|
" — scaffold it with `dotnet ef migrations add FixAuditPurgerRoleGrants` (build first, NO --no-build).");
|
||||||
|
return File.ReadAllText(file!);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Migration_Grants_CreateTable_To_Purger()
|
||||||
|
{
|
||||||
|
var src = ReadMigrationSource();
|
||||||
|
Assert.Contains("GRANT CREATE TABLE TO scadabridge_audit_purger", src);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Migration_Grants_Delete_On_AuditLog_To_Purger()
|
||||||
|
{
|
||||||
|
var src = ReadMigrationSource();
|
||||||
|
Assert.Contains("GRANT DELETE ON dbo.AuditLog TO scadabridge_audit_purger", src);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Down_Revokes_Both_Grants()
|
||||||
|
{
|
||||||
|
var src = ReadMigrationSource();
|
||||||
|
// Down must reverse both grants so the migration is cleanly reversible.
|
||||||
|
Assert.Contains("REVOKE CREATE TABLE", src);
|
||||||
|
Assert.Contains("REVOKE DELETE ON dbo.AuditLog", src);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Same walk-up anchor pattern the append-only guard uses.
|
||||||
|
private static string GetConfigurationDatabaseSourceDirectory()
|
||||||
|
{
|
||||||
|
var dir = new DirectoryInfo(AppContext.BaseDirectory);
|
||||||
|
while (dir != null)
|
||||||
|
{
|
||||||
|
var candidate = Path.Combine(
|
||||||
|
dir.FullName, "src",
|
||||||
|
"ZB.MOM.WW.ScadaBridge.ConfigurationDatabase",
|
||||||
|
"ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.csproj");
|
||||||
|
if (File.Exists(candidate))
|
||||||
|
{
|
||||||
|
return Path.GetDirectoryName(candidate)!;
|
||||||
|
}
|
||||||
|
dir = dir.Parent;
|
||||||
|
}
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
"Could not locate ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.csproj by walking up from the test output directory.");
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user