feat(db): V010 audit infrastructure + temporal tables
Applied to SIGCM2 (dev) and SIGCM2_Test.
V010__audit_infrastructure.sql (idempotent, ~280 LoC):
- Filegroups AUDIT_HOT + AUDIT_COLD with physical files (per-DB logical names
via DB_NAME() prefix to avoid collision in dev/test).
- pf/ps_AuditEvent_Monthly + pf/ps_SecurityEvent_Monthly (RANGE RIGHT,
DATETIME2(3), 14 boundaries 2026-01..2027-02 → 15 partitions). Job extends
forward monthly in B11.
- dbo.AuditEvent (partitioned, clustered PK on OccurredAt+Id) + 4 indexes
(Actor/Target/Action/Correlation) with PAGE compression.
- dbo.SecurityEvent (partitioned) + 3 indexes (Actor/Action_Result/Ip_Failure).
- CHECK constraints: Action LIKE '%.%', ISJSON(Metadata), Result IN (success|failure).
- SYSTEM_VERSIONING ON in Usuario/Rol/Permiso/RolPermiso with 10 YEARS retention +
PAGE compression in history tables.
- No hard FK on ActorUserId → Usuario.Id (soft FK — audit must survive user deletion).
V010_ROLLBACK.sql: emergency reversal (WARNING: destroys all audit history).
database/README.md: migration order + V010 prod-apply notes.
tests/SIGCM2.TestSupport/SqlTestFixture.cs:
- EnsureV010SchemaAsync() validates audit infra is applied (fails fast with
clear message if not — migration itself requires ALTER DATABASE privileges
and is applied manually via sqlcmd).
- Respawn TablesToIgnore extended with *_History (engine rejects direct DELETE
on system-versioned history tables).
tests/SIGCM2.Api.Tests/Audit/V010MigrationTests.cs — 5 smoke tests:
- AuditEvent insert+roundtrip with CorrelationId.
- CK_AuditEvent_Action rejects Action without '.'.
- CK_AuditEvent_Metadata rejects non-JSON.
- CK_SecurityEvent_Result rejects invalid Result.
- Usuario SYSTEM_VERSIONING: temporal query FOR SYSTEM_TIME AS OF returns
pre-update state + Usuario_History populated.
Suite: 130/130 passing (previous 124 + spike B0 + 5 new B1). No regressions.
Refs: sdd/udt-010-auditoria-trazabilidad/{spec#REQ-AUD-1,2, #REQ-SEC-1,
design#D-4, tasks}
This commit is contained in:
183
database/migrations/V010_ROLLBACK.sql
Normal file
183
database/migrations/V010_ROLLBACK.sql
Normal file
@@ -0,0 +1,183 @@
|
||||
-- V010_ROLLBACK.sql
|
||||
-- Reversa de V010__audit_infrastructure.sql.
|
||||
--
|
||||
-- ⚠️ ADVERTENCIA: ejecutar este script ELIMINA toda la historia auditada.
|
||||
-- - dbo.AuditEvent y dbo.SecurityEvent se dropean (junto con datos).
|
||||
-- - History tables (Usuario_History, Rol_History, Permiso_History, RolPermiso_History) se dropean.
|
||||
-- - Particionamiento, filegroups y archivos físicos se desmontan.
|
||||
--
|
||||
-- Uso intended: ROLLBACK de emergencia en entornos NO-productivos.
|
||||
-- En prod futuro, este script NO se ejecuta: si hace falta revertir, se hace
|
||||
-- restore de backup previo a V010.
|
||||
|
||||
SET QUOTED_IDENTIFIER ON;
|
||||
SET ANSI_NULLS ON;
|
||||
SET NOCOUNT ON;
|
||||
GO
|
||||
|
||||
-- ═══════════════════════════════════════════════════════════════════════
|
||||
-- 1. Apagar SYSTEM_VERSIONING + remover columnas PERIOD en las 4 tablas
|
||||
-- ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
-- Usuario
|
||||
IF EXISTS (SELECT 1 FROM sys.tables WHERE object_id = OBJECT_ID('dbo.Usuario') AND temporal_type = 2)
|
||||
BEGIN
|
||||
ALTER TABLE dbo.Usuario SET (SYSTEM_VERSIONING = OFF);
|
||||
PRINT 'Usuario: SYSTEM_VERSIONING OFF.';
|
||||
END
|
||||
GO
|
||||
|
||||
IF EXISTS (SELECT 1 FROM sys.periods WHERE object_id = OBJECT_ID('dbo.Usuario'))
|
||||
BEGIN
|
||||
ALTER TABLE dbo.Usuario DROP PERIOD FOR SYSTEM_TIME;
|
||||
PRINT 'Usuario: PERIOD FOR SYSTEM_TIME dropped.';
|
||||
END
|
||||
GO
|
||||
|
||||
IF COL_LENGTH('dbo.Usuario', 'ValidFrom') IS NOT NULL
|
||||
BEGIN
|
||||
ALTER TABLE dbo.Usuario DROP CONSTRAINT IF EXISTS DF_Usuario_ValidFrom;
|
||||
ALTER TABLE dbo.Usuario DROP CONSTRAINT IF EXISTS DF_Usuario_ValidTo;
|
||||
ALTER TABLE dbo.Usuario DROP COLUMN ValidFrom, ValidTo;
|
||||
PRINT 'Usuario: ValidFrom/ValidTo dropped.';
|
||||
END
|
||||
GO
|
||||
|
||||
IF OBJECT_ID(N'dbo.Usuario_History', N'U') IS NOT NULL
|
||||
BEGIN
|
||||
DROP TABLE dbo.Usuario_History;
|
||||
PRINT 'Usuario_History dropped.';
|
||||
END
|
||||
GO
|
||||
|
||||
-- Rol
|
||||
IF EXISTS (SELECT 1 FROM sys.tables WHERE object_id = OBJECT_ID('dbo.Rol') AND temporal_type = 2)
|
||||
ALTER TABLE dbo.Rol SET (SYSTEM_VERSIONING = OFF);
|
||||
GO
|
||||
|
||||
IF EXISTS (SELECT 1 FROM sys.periods WHERE object_id = OBJECT_ID('dbo.Rol'))
|
||||
ALTER TABLE dbo.Rol DROP PERIOD FOR SYSTEM_TIME;
|
||||
GO
|
||||
|
||||
IF COL_LENGTH('dbo.Rol', 'ValidFrom') IS NOT NULL
|
||||
BEGIN
|
||||
ALTER TABLE dbo.Rol DROP CONSTRAINT IF EXISTS DF_Rol_ValidFrom;
|
||||
ALTER TABLE dbo.Rol DROP CONSTRAINT IF EXISTS DF_Rol_ValidTo;
|
||||
ALTER TABLE dbo.Rol DROP COLUMN ValidFrom, ValidTo;
|
||||
END
|
||||
GO
|
||||
|
||||
IF OBJECT_ID(N'dbo.Rol_History', N'U') IS NOT NULL DROP TABLE dbo.Rol_History;
|
||||
GO
|
||||
|
||||
-- Permiso
|
||||
IF EXISTS (SELECT 1 FROM sys.tables WHERE object_id = OBJECT_ID('dbo.Permiso') AND temporal_type = 2)
|
||||
ALTER TABLE dbo.Permiso SET (SYSTEM_VERSIONING = OFF);
|
||||
GO
|
||||
|
||||
IF EXISTS (SELECT 1 FROM sys.periods WHERE object_id = OBJECT_ID('dbo.Permiso'))
|
||||
ALTER TABLE dbo.Permiso DROP PERIOD FOR SYSTEM_TIME;
|
||||
GO
|
||||
|
||||
IF COL_LENGTH('dbo.Permiso', 'ValidFrom') IS NOT NULL
|
||||
BEGIN
|
||||
ALTER TABLE dbo.Permiso DROP CONSTRAINT IF EXISTS DF_Permiso_ValidFrom;
|
||||
ALTER TABLE dbo.Permiso DROP CONSTRAINT IF EXISTS DF_Permiso_ValidTo;
|
||||
ALTER TABLE dbo.Permiso DROP COLUMN ValidFrom, ValidTo;
|
||||
END
|
||||
GO
|
||||
|
||||
IF OBJECT_ID(N'dbo.Permiso_History', N'U') IS NOT NULL DROP TABLE dbo.Permiso_History;
|
||||
GO
|
||||
|
||||
-- RolPermiso
|
||||
IF EXISTS (SELECT 1 FROM sys.tables WHERE object_id = OBJECT_ID('dbo.RolPermiso') AND temporal_type = 2)
|
||||
ALTER TABLE dbo.RolPermiso SET (SYSTEM_VERSIONING = OFF);
|
||||
GO
|
||||
|
||||
IF EXISTS (SELECT 1 FROM sys.periods WHERE object_id = OBJECT_ID('dbo.RolPermiso'))
|
||||
ALTER TABLE dbo.RolPermiso DROP PERIOD FOR SYSTEM_TIME;
|
||||
GO
|
||||
|
||||
IF COL_LENGTH('dbo.RolPermiso', 'ValidFrom') IS NOT NULL
|
||||
BEGIN
|
||||
ALTER TABLE dbo.RolPermiso DROP CONSTRAINT IF EXISTS DF_RolPermiso_ValidFrom;
|
||||
ALTER TABLE dbo.RolPermiso DROP CONSTRAINT IF EXISTS DF_RolPermiso_ValidTo;
|
||||
ALTER TABLE dbo.RolPermiso DROP COLUMN ValidFrom, ValidTo;
|
||||
END
|
||||
GO
|
||||
|
||||
IF OBJECT_ID(N'dbo.RolPermiso_History', N'U') IS NOT NULL DROP TABLE dbo.RolPermiso_History;
|
||||
GO
|
||||
|
||||
-- ═══════════════════════════════════════════════════════════════════════
|
||||
-- 2. Drop AuditEvent + SecurityEvent
|
||||
-- ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
IF OBJECT_ID(N'dbo.AuditEvent', N'U') IS NOT NULL
|
||||
BEGIN
|
||||
DROP TABLE dbo.AuditEvent;
|
||||
PRINT 'Table dbo.AuditEvent dropped.';
|
||||
END
|
||||
GO
|
||||
|
||||
IF OBJECT_ID(N'dbo.SecurityEvent', N'U') IS NOT NULL
|
||||
BEGIN
|
||||
DROP TABLE dbo.SecurityEvent;
|
||||
PRINT 'Table dbo.SecurityEvent dropped.';
|
||||
END
|
||||
GO
|
||||
|
||||
-- ═══════════════════════════════════════════════════════════════════════
|
||||
-- 3. Drop partition schemes + functions
|
||||
-- ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
IF EXISTS (SELECT 1 FROM sys.partition_schemes WHERE name = 'ps_AuditEvent_Monthly')
|
||||
DROP PARTITION SCHEME ps_AuditEvent_Monthly;
|
||||
GO
|
||||
|
||||
IF EXISTS (SELECT 1 FROM sys.partition_functions WHERE name = 'pf_AuditEvent_Monthly')
|
||||
DROP PARTITION FUNCTION pf_AuditEvent_Monthly;
|
||||
GO
|
||||
|
||||
IF EXISTS (SELECT 1 FROM sys.partition_schemes WHERE name = 'ps_SecurityEvent_Monthly')
|
||||
DROP PARTITION SCHEME ps_SecurityEvent_Monthly;
|
||||
GO
|
||||
|
||||
IF EXISTS (SELECT 1 FROM sys.partition_functions WHERE name = 'pf_SecurityEvent_Monthly')
|
||||
DROP PARTITION FUNCTION pf_SecurityEvent_Monthly;
|
||||
GO
|
||||
|
||||
-- ═══════════════════════════════════════════════════════════════════════
|
||||
-- 4. Remover archivos físicos y filegroups
|
||||
-- ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
DECLARE @dbName NVARCHAR(128) = DB_NAME();
|
||||
DECLARE @hotLogical NVARCHAR(128) = @dbName + N'_AUDIT_HOT';
|
||||
DECLARE @coldLogical NVARCHAR(128) = @dbName + N'_AUDIT_COLD';
|
||||
DECLARE @sql NVARCHAR(MAX);
|
||||
|
||||
IF EXISTS (SELECT 1 FROM sys.database_files WHERE name = @hotLogical)
|
||||
BEGIN
|
||||
SET @sql = N'ALTER DATABASE CURRENT REMOVE FILE [' + @hotLogical + N'];';
|
||||
EXEC sp_executesql @sql;
|
||||
PRINT 'File ' + @hotLogical + ' removed.';
|
||||
END
|
||||
|
||||
IF EXISTS (SELECT 1 FROM sys.database_files WHERE name = @coldLogical)
|
||||
BEGIN
|
||||
SET @sql = N'ALTER DATABASE CURRENT REMOVE FILE [' + @coldLogical + N'];';
|
||||
EXEC sp_executesql @sql;
|
||||
PRINT 'File ' + @coldLogical + ' removed.';
|
||||
END
|
||||
|
||||
IF EXISTS (SELECT 1 FROM sys.filegroups WHERE name = 'AUDIT_HOT')
|
||||
EXEC sp_executesql N'ALTER DATABASE CURRENT REMOVE FILEGROUP AUDIT_HOT;';
|
||||
|
||||
IF EXISTS (SELECT 1 FROM sys.filegroups WHERE name = 'AUDIT_COLD')
|
||||
EXEC sp_executesql N'ALTER DATABASE CURRENT REMOVE FILEGROUP AUDIT_COLD;';
|
||||
GO
|
||||
|
||||
PRINT '';
|
||||
PRINT 'V010 rolled back. Audit infrastructure removed. All audit history is permanently LOST.';
|
||||
GO
|
||||
434
database/migrations/V010__audit_infrastructure.sql
Normal file
434
database/migrations/V010__audit_infrastructure.sql
Normal file
@@ -0,0 +1,434 @@
|
||||
-- V010__audit_infrastructure.sql
|
||||
-- UDT-010: Infraestructura de Auditoría y Trazabilidad (Fase 0.5 — transversal).
|
||||
--
|
||||
-- Cambios:
|
||||
-- 1. Filegroups AUDIT_HOT y AUDIT_COLD (archivos físicos en default data path).
|
||||
-- 2. Partition functions + schemes mensuales (RANGE RIGHT) para AuditEvent y SecurityEvent.
|
||||
-- 3. dbo.AuditEvent particionada + 4 índices + CHECK constraints.
|
||||
-- 4. dbo.SecurityEvent particionada + 3 índices + CHECK constraints.
|
||||
-- 5. SYSTEM_VERSIONING en dbo.Usuario, dbo.Rol, dbo.Permiso, dbo.RolPermiso
|
||||
-- con HISTORY_RETENTION_PERIOD = 10 YEARS y PAGE compression en history tables.
|
||||
--
|
||||
-- Source of truth del diseño: Obsidian/02-ARQUITECTURA-y-TECH-STACK/2.5 📋 Auditoría.md
|
||||
-- Idempotente: seguro para re-ejecutar.
|
||||
-- Reversa: V010_ROLLBACK.sql (pierde TODA la historia auditada).
|
||||
-- Run on: SIGCM2 (dev) y SIGCM2_Test (integration tests). Para producción futuro,
|
||||
-- revisar database/README.md (ventana de mantenimiento + backup previo).
|
||||
|
||||
SET QUOTED_IDENTIFIER ON;
|
||||
SET ANSI_NULLS ON;
|
||||
SET NOCOUNT ON;
|
||||
GO
|
||||
|
||||
-- ═══════════════════════════════════════════════════════════════════════
|
||||
-- 1. FILEGROUPS + ARCHIVOS FÍSICOS
|
||||
-- ═══════════════════════════════════════════════════════════════════════
|
||||
-- Usamos el default data path del server + DB_NAME como prefijo lógico,
|
||||
-- así SIGCM2 y SIGCM2_Test coexisten sin colisión de logical file names.
|
||||
|
||||
DECLARE @dbName NVARCHAR(128) = DB_NAME();
|
||||
DECLARE @dataPath NVARCHAR(260) = CAST(SERVERPROPERTY('InstanceDefaultDataPath') AS NVARCHAR(260));
|
||||
DECLARE @hotLogical NVARCHAR(128) = @dbName + N'_AUDIT_HOT';
|
||||
DECLARE @coldLogical NVARCHAR(128) = @dbName + N'_AUDIT_COLD';
|
||||
DECLARE @hotPhysical NVARCHAR(260) = @dataPath + @hotLogical + N'.ndf';
|
||||
DECLARE @coldPhysical NVARCHAR(260) = @dataPath + @coldLogical + N'.ndf';
|
||||
DECLARE @sql NVARCHAR(MAX);
|
||||
|
||||
-- Filegroup AUDIT_HOT
|
||||
IF NOT EXISTS (SELECT 1 FROM sys.filegroups WHERE name = 'AUDIT_HOT')
|
||||
BEGIN
|
||||
EXEC sp_executesql N'ALTER DATABASE CURRENT ADD FILEGROUP AUDIT_HOT;';
|
||||
PRINT 'Filegroup AUDIT_HOT created.';
|
||||
END
|
||||
ELSE
|
||||
PRINT 'Filegroup AUDIT_HOT already exists — skip.';
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM sys.database_files WHERE name = @hotLogical)
|
||||
BEGIN
|
||||
SET @sql = N'ALTER DATABASE CURRENT ADD FILE (
|
||||
NAME = N''' + @hotLogical + N''',
|
||||
FILENAME = N''' + @hotPhysical + N''',
|
||||
SIZE = 64MB,
|
||||
FILEGROWTH = 64MB
|
||||
) TO FILEGROUP AUDIT_HOT;';
|
||||
EXEC sp_executesql @sql;
|
||||
PRINT 'File ' + @hotLogical + ' added to filegroup AUDIT_HOT.';
|
||||
END
|
||||
ELSE
|
||||
PRINT 'File ' + @hotLogical + ' already exists — skip.';
|
||||
|
||||
-- Filegroup AUDIT_COLD
|
||||
IF NOT EXISTS (SELECT 1 FROM sys.filegroups WHERE name = 'AUDIT_COLD')
|
||||
BEGIN
|
||||
EXEC sp_executesql N'ALTER DATABASE CURRENT ADD FILEGROUP AUDIT_COLD;';
|
||||
PRINT 'Filegroup AUDIT_COLD created.';
|
||||
END
|
||||
ELSE
|
||||
PRINT 'Filegroup AUDIT_COLD already exists — skip.';
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM sys.database_files WHERE name = @coldLogical)
|
||||
BEGIN
|
||||
SET @sql = N'ALTER DATABASE CURRENT ADD FILE (
|
||||
NAME = N''' + @coldLogical + N''',
|
||||
FILENAME = N''' + @coldPhysical + N''',
|
||||
SIZE = 64MB,
|
||||
FILEGROWTH = 64MB
|
||||
) TO FILEGROUP AUDIT_COLD;';
|
||||
EXEC sp_executesql @sql;
|
||||
PRINT 'File ' + @coldLogical + ' added to filegroup AUDIT_COLD.';
|
||||
END
|
||||
ELSE
|
||||
PRINT 'File ' + @coldLogical + ' already exists — skip.';
|
||||
GO
|
||||
|
||||
-- ═══════════════════════════════════════════════════════════════════════
|
||||
-- 2. PARTITION FUNCTIONS + SCHEMES (mensuales, RANGE RIGHT)
|
||||
-- ═══════════════════════════════════════════════════════════════════════
|
||||
-- Boundaries iniciales: 14 valores de 2026-01-01 a 2027-02-01 → 15 particiones.
|
||||
-- AuditPartitionManagerJob (B11) extiende mes a mes automáticamente.
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM sys.partition_functions WHERE name = 'pf_AuditEvent_Monthly')
|
||||
BEGIN
|
||||
CREATE PARTITION FUNCTION pf_AuditEvent_Monthly (DATETIME2(3))
|
||||
AS RANGE RIGHT FOR VALUES (
|
||||
'2026-01-01T00:00:00.000', '2026-02-01T00:00:00.000', '2026-03-01T00:00:00.000',
|
||||
'2026-04-01T00:00:00.000', '2026-05-01T00:00:00.000', '2026-06-01T00:00:00.000',
|
||||
'2026-07-01T00:00:00.000', '2026-08-01T00:00:00.000', '2026-09-01T00:00:00.000',
|
||||
'2026-10-01T00:00:00.000', '2026-11-01T00:00:00.000', '2026-12-01T00:00:00.000',
|
||||
'2027-01-01T00:00:00.000', '2027-02-01T00:00:00.000'
|
||||
);
|
||||
PRINT 'Partition function pf_AuditEvent_Monthly created.';
|
||||
END
|
||||
ELSE
|
||||
PRINT 'Partition function pf_AuditEvent_Monthly already exists — skip.';
|
||||
GO
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM sys.partition_schemes WHERE name = 'ps_AuditEvent_Monthly')
|
||||
BEGIN
|
||||
CREATE PARTITION SCHEME ps_AuditEvent_Monthly
|
||||
AS PARTITION pf_AuditEvent_Monthly ALL TO ([AUDIT_HOT]);
|
||||
PRINT 'Partition scheme ps_AuditEvent_Monthly created.';
|
||||
END
|
||||
ELSE
|
||||
PRINT 'Partition scheme ps_AuditEvent_Monthly already exists — skip.';
|
||||
GO
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM sys.partition_functions WHERE name = 'pf_SecurityEvent_Monthly')
|
||||
BEGIN
|
||||
CREATE PARTITION FUNCTION pf_SecurityEvent_Monthly (DATETIME2(3))
|
||||
AS RANGE RIGHT FOR VALUES (
|
||||
'2026-01-01T00:00:00.000', '2026-02-01T00:00:00.000', '2026-03-01T00:00:00.000',
|
||||
'2026-04-01T00:00:00.000', '2026-05-01T00:00:00.000', '2026-06-01T00:00:00.000',
|
||||
'2026-07-01T00:00:00.000', '2026-08-01T00:00:00.000', '2026-09-01T00:00:00.000',
|
||||
'2026-10-01T00:00:00.000', '2026-11-01T00:00:00.000', '2026-12-01T00:00:00.000',
|
||||
'2027-01-01T00:00:00.000', '2027-02-01T00:00:00.000'
|
||||
);
|
||||
PRINT 'Partition function pf_SecurityEvent_Monthly created.';
|
||||
END
|
||||
ELSE
|
||||
PRINT 'Partition function pf_SecurityEvent_Monthly already exists — skip.';
|
||||
GO
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM sys.partition_schemes WHERE name = 'ps_SecurityEvent_Monthly')
|
||||
BEGIN
|
||||
CREATE PARTITION SCHEME ps_SecurityEvent_Monthly
|
||||
AS PARTITION pf_SecurityEvent_Monthly ALL TO ([AUDIT_HOT]);
|
||||
PRINT 'Partition scheme ps_SecurityEvent_Monthly created.';
|
||||
END
|
||||
ELSE
|
||||
PRINT 'Partition scheme ps_SecurityEvent_Monthly already exists — skip.';
|
||||
GO
|
||||
|
||||
-- ═══════════════════════════════════════════════════════════════════════
|
||||
-- 3. dbo.AuditEvent (eventos de dominio, retention 10 años)
|
||||
-- ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
IF OBJECT_ID(N'dbo.AuditEvent', N'U') IS NULL
|
||||
BEGIN
|
||||
CREATE TABLE dbo.AuditEvent (
|
||||
Id BIGINT IDENTITY(1,1) NOT NULL,
|
||||
OccurredAt DATETIME2(3) NOT NULL CONSTRAINT DF_AuditEvent_OccurredAt DEFAULT(SYSUTCDATETIME()),
|
||||
ActorUserId INT NULL, -- NULL solo para eventos del sistema (jobs)
|
||||
ActorRoleId INT NULL, -- rol efectivo al momento del evento (denormalizado)
|
||||
Action VARCHAR(100) NOT NULL, -- "usuario.create", "cliente.deactivate"
|
||||
TargetType VARCHAR(50) NOT NULL, -- "Usuario", "Cliente", "Factura"
|
||||
TargetId VARCHAR(100) NOT NULL, -- PK del target como string (soporta INT/GUID)
|
||||
CorrelationId UNIQUEIDENTIFIER NULL, -- linkea eventos de una misma operación
|
||||
IpAddress VARCHAR(45) NULL, -- IPv4/IPv6
|
||||
UserAgent VARCHAR(500) NULL,
|
||||
Metadata NVARCHAR(MAX) NULL, -- JSON libre (ya sanitizado por la app)
|
||||
CONSTRAINT PK_AuditEvent PRIMARY KEY CLUSTERED (OccurredAt, Id)
|
||||
ON ps_AuditEvent_Monthly(OccurredAt),
|
||||
CONSTRAINT CK_AuditEvent_Action CHECK (Action LIKE '%.%'),
|
||||
CONSTRAINT CK_AuditEvent_Metadata CHECK (Metadata IS NULL OR ISJSON(Metadata) = 1)
|
||||
) ON ps_AuditEvent_Monthly(OccurredAt);
|
||||
PRINT 'Table dbo.AuditEvent created (partitioned monthly on OccurredAt).';
|
||||
END
|
||||
ELSE
|
||||
PRINT 'Table dbo.AuditEvent already exists — skip.';
|
||||
GO
|
||||
|
||||
-- Índices (cubren 95% de queries: actor / target / action / correlation)
|
||||
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = 'IX_AuditEvent_Actor' AND object_id = OBJECT_ID('dbo.AuditEvent'))
|
||||
BEGIN
|
||||
CREATE NONCLUSTERED INDEX IX_AuditEvent_Actor
|
||||
ON dbo.AuditEvent(ActorUserId, OccurredAt DESC)
|
||||
INCLUDE (Action, TargetType, TargetId, CorrelationId)
|
||||
WITH (DATA_COMPRESSION = PAGE)
|
||||
ON ps_AuditEvent_Monthly(OccurredAt);
|
||||
PRINT 'Index IX_AuditEvent_Actor created.';
|
||||
END
|
||||
GO
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = 'IX_AuditEvent_Target' AND object_id = OBJECT_ID('dbo.AuditEvent'))
|
||||
BEGIN
|
||||
CREATE NONCLUSTERED INDEX IX_AuditEvent_Target
|
||||
ON dbo.AuditEvent(TargetType, TargetId, OccurredAt DESC)
|
||||
INCLUDE (ActorUserId, Action, CorrelationId)
|
||||
WITH (DATA_COMPRESSION = PAGE)
|
||||
ON ps_AuditEvent_Monthly(OccurredAt);
|
||||
PRINT 'Index IX_AuditEvent_Target created.';
|
||||
END
|
||||
GO
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = 'IX_AuditEvent_Action' AND object_id = OBJECT_ID('dbo.AuditEvent'))
|
||||
BEGIN
|
||||
CREATE NONCLUSTERED INDEX IX_AuditEvent_Action
|
||||
ON dbo.AuditEvent(Action, OccurredAt DESC)
|
||||
INCLUDE (ActorUserId, TargetType, TargetId)
|
||||
WITH (DATA_COMPRESSION = PAGE)
|
||||
ON ps_AuditEvent_Monthly(OccurredAt);
|
||||
PRINT 'Index IX_AuditEvent_Action created.';
|
||||
END
|
||||
GO
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = 'IX_AuditEvent_Correlation' AND object_id = OBJECT_ID('dbo.AuditEvent'))
|
||||
BEGIN
|
||||
CREATE NONCLUSTERED INDEX IX_AuditEvent_Correlation
|
||||
ON dbo.AuditEvent(CorrelationId)
|
||||
WHERE CorrelationId IS NOT NULL
|
||||
ON ps_AuditEvent_Monthly(OccurredAt);
|
||||
PRINT 'Index IX_AuditEvent_Correlation (filtered) created.';
|
||||
END
|
||||
GO
|
||||
|
||||
-- ═══════════════════════════════════════════════════════════════════════
|
||||
-- 4. dbo.SecurityEvent (eventos de seguridad, retention 5 años)
|
||||
-- ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
IF OBJECT_ID(N'dbo.SecurityEvent', N'U') IS NULL
|
||||
BEGIN
|
||||
CREATE TABLE dbo.SecurityEvent (
|
||||
Id BIGINT IDENTITY(1,1) NOT NULL,
|
||||
OccurredAt DATETIME2(3) NOT NULL CONSTRAINT DF_SecurityEvent_OccurredAt DEFAULT(SYSUTCDATETIME()),
|
||||
ActorUserId INT NULL, -- NULL si login falló y user no existe
|
||||
AttemptedUsername VARCHAR(256) NULL, -- para login failures
|
||||
SessionId UNIQUEIDENTIFIER NULL,
|
||||
Action VARCHAR(100) NOT NULL, -- "login", "logout", "refresh.reuse_detected", "permission.denied"
|
||||
Result VARCHAR(20) NOT NULL, -- "success" | "failure"
|
||||
FailureReason VARCHAR(200) NULL, -- "invalid_password", "account_locked"
|
||||
IpAddress VARCHAR(45) NULL,
|
||||
UserAgent VARCHAR(500) NULL,
|
||||
Metadata NVARCHAR(MAX) NULL,
|
||||
CONSTRAINT PK_SecurityEvent PRIMARY KEY CLUSTERED (OccurredAt, Id)
|
||||
ON ps_SecurityEvent_Monthly(OccurredAt),
|
||||
CONSTRAINT CK_SecurityEvent_Result CHECK (Result IN ('success','failure')),
|
||||
CONSTRAINT CK_SecurityEvent_Metadata CHECK (Metadata IS NULL OR ISJSON(Metadata) = 1)
|
||||
) ON ps_SecurityEvent_Monthly(OccurredAt);
|
||||
PRINT 'Table dbo.SecurityEvent created (partitioned monthly on OccurredAt).';
|
||||
END
|
||||
ELSE
|
||||
PRINT 'Table dbo.SecurityEvent already exists — skip.';
|
||||
GO
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = 'IX_SecurityEvent_Actor' AND object_id = OBJECT_ID('dbo.SecurityEvent'))
|
||||
BEGIN
|
||||
CREATE NONCLUSTERED INDEX IX_SecurityEvent_Actor
|
||||
ON dbo.SecurityEvent(ActorUserId, OccurredAt DESC)
|
||||
INCLUDE (Action, Result, SessionId)
|
||||
WITH (DATA_COMPRESSION = PAGE)
|
||||
ON ps_SecurityEvent_Monthly(OccurredAt);
|
||||
PRINT 'Index IX_SecurityEvent_Actor created.';
|
||||
END
|
||||
GO
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = 'IX_SecurityEvent_Action_Result' AND object_id = OBJECT_ID('dbo.SecurityEvent'))
|
||||
BEGIN
|
||||
CREATE NONCLUSTERED INDEX IX_SecurityEvent_Action_Result
|
||||
ON dbo.SecurityEvent(Action, Result, OccurredAt DESC)
|
||||
INCLUDE (ActorUserId, IpAddress)
|
||||
WITH (DATA_COMPRESSION = PAGE)
|
||||
ON ps_SecurityEvent_Monthly(OccurredAt);
|
||||
PRINT 'Index IX_SecurityEvent_Action_Result created.';
|
||||
END
|
||||
GO
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = 'IX_SecurityEvent_Ip_Failure' AND object_id = OBJECT_ID('dbo.SecurityEvent'))
|
||||
BEGIN
|
||||
CREATE NONCLUSTERED INDEX IX_SecurityEvent_Ip_Failure
|
||||
ON dbo.SecurityEvent(IpAddress, OccurredAt DESC)
|
||||
WHERE Result = 'failure'
|
||||
ON ps_SecurityEvent_Monthly(OccurredAt);
|
||||
PRINT 'Index IX_SecurityEvent_Ip_Failure (filtered) created.';
|
||||
END
|
||||
GO
|
||||
|
||||
-- ═══════════════════════════════════════════════════════════════════════
|
||||
-- 5. SYSTEM_VERSIONING — Usuario, Rol, Permiso, RolPermiso
|
||||
-- ═══════════════════════════════════════════════════════════════════════
|
||||
-- Patrón: (a) agregar PERIOD FOR SYSTEM_TIME con columnas HIDDEN,
|
||||
-- (b) activar SYSTEM_VERSIONING con HISTORY_RETENTION_PERIOD 10 YEARS,
|
||||
-- (c) rebuild history con PAGE compression.
|
||||
-- Tablas con datos: los registros existentes reciben ValidFrom = instante del ALTER.
|
||||
|
||||
-- ─── Usuario ───────────────────────────────────────────────────────────
|
||||
IF COL_LENGTH('dbo.Usuario', 'ValidFrom') IS NULL
|
||||
BEGIN
|
||||
ALTER TABLE dbo.Usuario
|
||||
ADD
|
||||
ValidFrom DATETIME2(3) GENERATED ALWAYS AS ROW START HIDDEN NOT NULL
|
||||
CONSTRAINT DF_Usuario_ValidFrom DEFAULT(SYSUTCDATETIME()),
|
||||
ValidTo DATETIME2(3) GENERATED ALWAYS AS ROW END HIDDEN NOT NULL
|
||||
CONSTRAINT DF_Usuario_ValidTo DEFAULT(CONVERT(DATETIME2(3), '9999-12-31 23:59:59.999')),
|
||||
PERIOD FOR SYSTEM_TIME (ValidFrom, ValidTo);
|
||||
PRINT 'Usuario: PERIOD FOR SYSTEM_TIME added.';
|
||||
END
|
||||
GO
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM sys.tables WHERE object_id = OBJECT_ID('dbo.Usuario') AND temporal_type = 2)
|
||||
BEGIN
|
||||
ALTER TABLE dbo.Usuario
|
||||
SET (SYSTEM_VERSIONING = ON (
|
||||
HISTORY_TABLE = dbo.Usuario_History,
|
||||
HISTORY_RETENTION_PERIOD = 10 YEARS
|
||||
));
|
||||
PRINT 'Usuario: SYSTEM_VERSIONING = ON (history: dbo.Usuario_History, retention: 10 years).';
|
||||
END
|
||||
ELSE
|
||||
PRINT 'Usuario: SYSTEM_VERSIONING already ON — skip.';
|
||||
GO
|
||||
|
||||
IF EXISTS (SELECT 1 FROM sys.tables WHERE name = 'Usuario_History' AND schema_id = SCHEMA_ID('dbo'))
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM sys.partitions p
|
||||
JOIN sys.tables t ON t.object_id = p.object_id
|
||||
WHERE t.name = 'Usuario_History' AND p.data_compression = 2
|
||||
)
|
||||
BEGIN
|
||||
ALTER TABLE dbo.Usuario_History REBUILD WITH (DATA_COMPRESSION = PAGE);
|
||||
PRINT 'Usuario_History: rebuilt with PAGE compression.';
|
||||
END
|
||||
GO
|
||||
|
||||
-- ─── Rol ───────────────────────────────────────────────────────────────
|
||||
IF COL_LENGTH('dbo.Rol', 'ValidFrom') IS NULL
|
||||
BEGIN
|
||||
ALTER TABLE dbo.Rol
|
||||
ADD
|
||||
ValidFrom DATETIME2(3) GENERATED ALWAYS AS ROW START HIDDEN NOT NULL
|
||||
CONSTRAINT DF_Rol_ValidFrom DEFAULT(SYSUTCDATETIME()),
|
||||
ValidTo DATETIME2(3) GENERATED ALWAYS AS ROW END HIDDEN NOT NULL
|
||||
CONSTRAINT DF_Rol_ValidTo DEFAULT(CONVERT(DATETIME2(3), '9999-12-31 23:59:59.999')),
|
||||
PERIOD FOR SYSTEM_TIME (ValidFrom, ValidTo);
|
||||
PRINT 'Rol: PERIOD FOR SYSTEM_TIME added.';
|
||||
END
|
||||
GO
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM sys.tables WHERE object_id = OBJECT_ID('dbo.Rol') AND temporal_type = 2)
|
||||
BEGIN
|
||||
ALTER TABLE dbo.Rol
|
||||
SET (SYSTEM_VERSIONING = ON (
|
||||
HISTORY_TABLE = dbo.Rol_History,
|
||||
HISTORY_RETENTION_PERIOD = 10 YEARS
|
||||
));
|
||||
PRINT 'Rol: SYSTEM_VERSIONING = ON.';
|
||||
END
|
||||
GO
|
||||
|
||||
IF EXISTS (SELECT 1 FROM sys.tables WHERE name = 'Rol_History' AND schema_id = SCHEMA_ID('dbo'))
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM sys.partitions p JOIN sys.tables t ON t.object_id = p.object_id
|
||||
WHERE t.name = 'Rol_History' AND p.data_compression = 2
|
||||
)
|
||||
BEGIN
|
||||
ALTER TABLE dbo.Rol_History REBUILD WITH (DATA_COMPRESSION = PAGE);
|
||||
PRINT 'Rol_History: rebuilt with PAGE compression.';
|
||||
END
|
||||
GO
|
||||
|
||||
-- ─── Permiso ───────────────────────────────────────────────────────────
|
||||
IF COL_LENGTH('dbo.Permiso', 'ValidFrom') IS NULL
|
||||
BEGIN
|
||||
ALTER TABLE dbo.Permiso
|
||||
ADD
|
||||
ValidFrom DATETIME2(3) GENERATED ALWAYS AS ROW START HIDDEN NOT NULL
|
||||
CONSTRAINT DF_Permiso_ValidFrom DEFAULT(SYSUTCDATETIME()),
|
||||
ValidTo DATETIME2(3) GENERATED ALWAYS AS ROW END HIDDEN NOT NULL
|
||||
CONSTRAINT DF_Permiso_ValidTo DEFAULT(CONVERT(DATETIME2(3), '9999-12-31 23:59:59.999')),
|
||||
PERIOD FOR SYSTEM_TIME (ValidFrom, ValidTo);
|
||||
PRINT 'Permiso: PERIOD FOR SYSTEM_TIME added.';
|
||||
END
|
||||
GO
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM sys.tables WHERE object_id = OBJECT_ID('dbo.Permiso') AND temporal_type = 2)
|
||||
BEGIN
|
||||
ALTER TABLE dbo.Permiso
|
||||
SET (SYSTEM_VERSIONING = ON (
|
||||
HISTORY_TABLE = dbo.Permiso_History,
|
||||
HISTORY_RETENTION_PERIOD = 10 YEARS
|
||||
));
|
||||
PRINT 'Permiso: SYSTEM_VERSIONING = ON.';
|
||||
END
|
||||
GO
|
||||
|
||||
IF EXISTS (SELECT 1 FROM sys.tables WHERE name = 'Permiso_History' AND schema_id = SCHEMA_ID('dbo'))
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM sys.partitions p JOIN sys.tables t ON t.object_id = p.object_id
|
||||
WHERE t.name = 'Permiso_History' AND p.data_compression = 2
|
||||
)
|
||||
BEGIN
|
||||
ALTER TABLE dbo.Permiso_History REBUILD WITH (DATA_COMPRESSION = PAGE);
|
||||
PRINT 'Permiso_History: rebuilt with PAGE compression.';
|
||||
END
|
||||
GO
|
||||
|
||||
-- ─── RolPermiso ────────────────────────────────────────────────────────
|
||||
IF COL_LENGTH('dbo.RolPermiso', 'ValidFrom') IS NULL
|
||||
BEGIN
|
||||
ALTER TABLE dbo.RolPermiso
|
||||
ADD
|
||||
ValidFrom DATETIME2(3) GENERATED ALWAYS AS ROW START HIDDEN NOT NULL
|
||||
CONSTRAINT DF_RolPermiso_ValidFrom DEFAULT(SYSUTCDATETIME()),
|
||||
ValidTo DATETIME2(3) GENERATED ALWAYS AS ROW END HIDDEN NOT NULL
|
||||
CONSTRAINT DF_RolPermiso_ValidTo DEFAULT(CONVERT(DATETIME2(3), '9999-12-31 23:59:59.999')),
|
||||
PERIOD FOR SYSTEM_TIME (ValidFrom, ValidTo);
|
||||
PRINT 'RolPermiso: PERIOD FOR SYSTEM_TIME added.';
|
||||
END
|
||||
GO
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM sys.tables WHERE object_id = OBJECT_ID('dbo.RolPermiso') AND temporal_type = 2)
|
||||
BEGIN
|
||||
ALTER TABLE dbo.RolPermiso
|
||||
SET (SYSTEM_VERSIONING = ON (
|
||||
HISTORY_TABLE = dbo.RolPermiso_History,
|
||||
HISTORY_RETENTION_PERIOD = 10 YEARS
|
||||
));
|
||||
PRINT 'RolPermiso: SYSTEM_VERSIONING = ON.';
|
||||
END
|
||||
GO
|
||||
|
||||
IF EXISTS (SELECT 1 FROM sys.tables WHERE name = 'RolPermiso_History' AND schema_id = SCHEMA_ID('dbo'))
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM sys.partitions p JOIN sys.tables t ON t.object_id = p.object_id
|
||||
WHERE t.name = 'RolPermiso_History' AND p.data_compression = 2
|
||||
)
|
||||
BEGIN
|
||||
ALTER TABLE dbo.RolPermiso_History REBUILD WITH (DATA_COMPRESSION = PAGE);
|
||||
PRINT 'RolPermiso_History: rebuilt with PAGE compression.';
|
||||
END
|
||||
GO
|
||||
|
||||
PRINT '';
|
||||
PRINT 'V010 applied successfully — audit infrastructure + temporal tables active.';
|
||||
PRINT 'Next: runs in B2 onwards (Application.Audit abstractions).';
|
||||
GO
|
||||
Reference in New Issue
Block a user