feat(ft546): enforce shadow database isolation
This commit is contained in:
312
sql/shadow/V003__ft546_shadow_evidence.sql
Normal file
312
sql/shadow/V003__ft546_shadow_evidence.sql
Normal file
@@ -0,0 +1,312 @@
|
||||
:setvar ShadowSourceDatabase "FT546-wcs"
|
||||
:setvar ShadowEvidenceLogin "ft546_shadow_evidence"
|
||||
:setvar ShadowEvidenceUser "ft546_shadow_evidence"
|
||||
:setvar ShadowEvidencePassword "REPLACE_AT_DEPLOY_TIME"
|
||||
|
||||
SET NOCOUNT ON;
|
||||
SET XACT_ABORT ON;
|
||||
|
||||
IF DB_ID() = DB_ID(N'$(ShadowSourceDatabase)')
|
||||
THROW 51001, 'Evidence objects must be installed in a separate database.', 1;
|
||||
|
||||
IF N'$(ShadowEvidencePassword)' = N'REPLACE_AT_DEPLOY_TIME'
|
||||
THROW 51002, 'Supply ShadowEvidencePassword through the approved secret channel.', 1;
|
||||
|
||||
DECLARE @evidenceLoginCreated bit = 0;
|
||||
IF NOT EXISTS (SELECT 1 FROM sys.server_principals
|
||||
WHERE name = N'$(ShadowEvidenceLogin)')
|
||||
BEGIN
|
||||
DECLARE @createEvidenceLogin nvarchar(max) =
|
||||
N'CREATE LOGIN ' + QUOTENAME(N'$(ShadowEvidenceLogin)')
|
||||
+ N' WITH PASSWORD = N'''
|
||||
+ REPLACE(N'$(ShadowEvidencePassword)', N'''', N'''''')
|
||||
+ N''', CHECK_POLICY = ON, CHECK_EXPIRATION = ON';
|
||||
EXEC sys.sp_executesql @createEvidenceLogin;
|
||||
SET @evidenceLoginCreated = 1;
|
||||
END;
|
||||
|
||||
BEGIN TRY
|
||||
IF EXISTS (
|
||||
SELECT 1
|
||||
FROM sys.server_role_members rm
|
||||
JOIN sys.server_principals login_principal
|
||||
ON login_principal.principal_id = rm.member_principal_id
|
||||
WHERE login_principal.name = N'$(ShadowEvidenceLogin)'
|
||||
)
|
||||
THROW 51003, 'The shadow evidence login must not belong to a server role.', 1;
|
||||
|
||||
DECLARE @serverContextSet bit = 0;
|
||||
BEGIN TRY
|
||||
EXECUTE AS LOGIN = N'$(ShadowEvidenceLogin)';
|
||||
SET @serverContextSet = 1;
|
||||
IF EXISTS (
|
||||
SELECT 1 FROM sys.fn_my_permissions(NULL, N'SERVER')
|
||||
WHERE permission_name NOT IN (N'CONNECT SQL', N'VIEW ANY DATABASE')
|
||||
)
|
||||
THROW 51009, 'The shadow evidence login retains forbidden server permissions.', 1;
|
||||
REVERT;
|
||||
SET @serverContextSet = 0;
|
||||
END TRY
|
||||
BEGIN CATCH
|
||||
IF @serverContextSet = 1
|
||||
REVERT;
|
||||
THROW;
|
||||
END CATCH;
|
||||
|
||||
BEGIN TRANSACTION;
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM sys.database_principals
|
||||
WHERE name = N'$(ShadowEvidenceUser)')
|
||||
BEGIN
|
||||
DECLARE @createEvidenceUser nvarchar(max) =
|
||||
N'CREATE USER ' + QUOTENAME(N'$(ShadowEvidenceUser)')
|
||||
+ N' FOR LOGIN ' + QUOTENAME(N'$(ShadowEvidenceLogin)');
|
||||
EXEC sys.sp_executesql @createEvidenceUser;
|
||||
END
|
||||
ELSE IF SUSER_SID(N'$(ShadowEvidenceLogin)')
|
||||
<> USER_SID(N'$(ShadowEvidenceUser)')
|
||||
THROW 51004, 'The existing shadow evidence user maps to a different login.', 1;
|
||||
|
||||
DECLARE @evidencePrincipal nvarchar(258) =
|
||||
QUOTENAME(N'$(ShadowEvidenceUser)');
|
||||
DECLARE @evidenceRole sysname;
|
||||
DECLARE evidence_roles CURSOR LOCAL FAST_FORWARD FOR
|
||||
SELECT role_principal.name
|
||||
FROM sys.database_principals role_principal
|
||||
JOIN sys.database_role_members rm
|
||||
ON rm.role_principal_id = role_principal.principal_id
|
||||
JOIN sys.database_principals member_principal
|
||||
ON member_principal.principal_id = rm.member_principal_id
|
||||
WHERE role_principal.type = 'R'
|
||||
AND member_principal.name = N'$(ShadowEvidenceUser)';
|
||||
|
||||
OPEN evidence_roles;
|
||||
FETCH NEXT FROM evidence_roles INTO @evidenceRole;
|
||||
WHILE @@FETCH_STATUS = 0
|
||||
BEGIN
|
||||
DECLARE @dropEvidenceRole nvarchar(max) = N'ALTER ROLE '
|
||||
+ QUOTENAME(@evidenceRole) + N' DROP MEMBER ' + @evidencePrincipal;
|
||||
EXEC sys.sp_executesql @dropEvidenceRole;
|
||||
FETCH NEXT FROM evidence_roles INTO @evidenceRole;
|
||||
END;
|
||||
CLOSE evidence_roles;
|
||||
DEALLOCATE evidence_roles;
|
||||
|
||||
DECLARE @revokeEvidenceDatabase nvarchar(max) =
|
||||
N'REVOKE CONTROL, ALTER, TAKE OWNERSHIP, SELECT, INSERT, UPDATE, DELETE, '
|
||||
+ N'EXECUTE FROM ' + @evidencePrincipal + N';';
|
||||
EXEC sys.sp_executesql @revokeEvidenceDatabase;
|
||||
|
||||
IF OBJECT_ID(N'dbo.shadow_run', N'U') IS NULL
|
||||
CREATE TABLE dbo.shadow_run (
|
||||
run_id uniqueidentifier NOT NULL PRIMARY KEY,
|
||||
started_at datetime2(3) NOT NULL,
|
||||
artifact_sha256 char(64) NOT NULL,
|
||||
configuration_sha256 char(64) NOT NULL,
|
||||
source_server nvarchar(256) NOT NULL,
|
||||
source_database sysname NOT NULL,
|
||||
source_login sysname NOT NULL,
|
||||
status varchar(32) NOT NULL
|
||||
);
|
||||
|
||||
IF OBJECT_ID(N'dbo.shadow_cycle', N'U') IS NULL
|
||||
CREATE TABLE dbo.shadow_cycle (
|
||||
cycle_id uniqueidentifier NOT NULL PRIMARY KEY,
|
||||
run_id uniqueidentifier NOT NULL,
|
||||
input_batch_hash char(64) NOT NULL,
|
||||
parent_task_id nvarchar(128) NOT NULL,
|
||||
phase varchar(64) NOT NULL,
|
||||
route_version bigint NOT NULL,
|
||||
status varchar(64) NOT NULL,
|
||||
reason_code nvarchar(512) NULL,
|
||||
database_captured_at datetime2(3) NULL,
|
||||
completed_at datetime2(3) NOT NULL,
|
||||
evidence_hash char(64) NOT NULL,
|
||||
CONSTRAINT fk_shadow_cycle_run FOREIGN KEY (run_id)
|
||||
REFERENCES dbo.shadow_run(run_id),
|
||||
CONSTRAINT uq_shadow_cycle_identity UNIQUE
|
||||
(run_id, input_batch_hash, parent_task_id, phase, route_version)
|
||||
);
|
||||
|
||||
IF OBJECT_ID(N'dbo.shadow_input_batch', N'U') IS NULL
|
||||
CREATE TABLE dbo.shadow_input_batch (
|
||||
cycle_id uniqueidentifier NOT NULL PRIMARY KEY,
|
||||
content_hash char(64) NOT NULL,
|
||||
captured_json nvarchar(max) NOT NULL,
|
||||
CONSTRAINT fk_shadow_input_cycle FOREIGN KEY (cycle_id)
|
||||
REFERENCES dbo.shadow_cycle(cycle_id)
|
||||
);
|
||||
|
||||
IF OBJECT_ID(N'dbo.shadow_legacy_queue', N'U') IS NULL
|
||||
CREATE TABLE dbo.shadow_legacy_queue (
|
||||
cycle_id uniqueidentifier NOT NULL PRIMARY KEY,
|
||||
vehicle_id nvarchar(64) NULL,
|
||||
path_info nvarchar(max) NULL,
|
||||
result_status varchar(64) NOT NULL,
|
||||
result_json nvarchar(max) NOT NULL,
|
||||
content_hash char(64) NOT NULL,
|
||||
CONSTRAINT fk_shadow_legacy_cycle FOREIGN KEY (cycle_id)
|
||||
REFERENCES dbo.shadow_cycle(cycle_id)
|
||||
);
|
||||
|
||||
IF OBJECT_ID(N'dbo.shadow_decision', N'U') IS NULL
|
||||
CREATE TABLE dbo.shadow_decision (
|
||||
cycle_id uniqueidentifier NOT NULL PRIMARY KEY,
|
||||
decision varchar(64) NOT NULL,
|
||||
vehicle_id nvarchar(64) NULL,
|
||||
reason_code nvarchar(512) NULL,
|
||||
decision_json nvarchar(max) NOT NULL,
|
||||
content_hash char(64) NOT NULL,
|
||||
CONSTRAINT fk_shadow_decision_cycle FOREIGN KEY (cycle_id)
|
||||
REFERENCES dbo.shadow_cycle(cycle_id)
|
||||
);
|
||||
|
||||
IF OBJECT_ID(N'dbo.shadow_encoded_write', N'U') IS NULL
|
||||
CREATE TABLE dbo.shadow_encoded_write (
|
||||
cycle_id uniqueidentifier NOT NULL,
|
||||
write_sequence int NOT NULL,
|
||||
register_address varchar(16) NOT NULL,
|
||||
data_type varchar(16) NOT NULL,
|
||||
values_json nvarchar(max) NOT NULL,
|
||||
trigger_value bigint NULL,
|
||||
CONSTRAINT pk_shadow_encoded_write PRIMARY KEY
|
||||
(cycle_id, write_sequence),
|
||||
CONSTRAINT fk_shadow_write_cycle FOREIGN KEY (cycle_id)
|
||||
REFERENCES dbo.shadow_cycle(cycle_id)
|
||||
);
|
||||
|
||||
IF OBJECT_ID(N'dbo.shadow_difference', N'U') IS NULL
|
||||
CREATE TABLE dbo.shadow_difference (
|
||||
difference_id bigint IDENTITY(1, 1) NOT NULL PRIMARY KEY,
|
||||
cycle_id uniqueidentifier NOT NULL,
|
||||
category varchar(64) NOT NULL,
|
||||
reason_code nvarchar(512) NOT NULL,
|
||||
expected_json nvarchar(max) NULL,
|
||||
actual_json nvarchar(max) NULL,
|
||||
CONSTRAINT fk_shadow_difference_cycle FOREIGN KEY (cycle_id)
|
||||
REFERENCES dbo.shadow_cycle(cycle_id)
|
||||
);
|
||||
|
||||
IF OBJECT_ID(N'dbo.shadow_traffic_state', N'U') IS NULL
|
||||
CREATE TABLE dbo.shadow_traffic_state (
|
||||
state_key nvarchar(256) NOT NULL PRIMARY KEY,
|
||||
snapshot_version bigint NOT NULL,
|
||||
state_json nvarchar(max) NOT NULL,
|
||||
updated_at datetime2(3) NOT NULL
|
||||
);
|
||||
|
||||
IF OBJECT_ID(N'dbo.shadow_hold_history', N'U') IS NULL
|
||||
CREATE TABLE dbo.shadow_hold_history (
|
||||
parent_task_id nvarchar(128) NOT NULL,
|
||||
vehicle_id nvarchar(64) NOT NULL,
|
||||
resource_key nvarchar(256) NOT NULL,
|
||||
reason_code nvarchar(512) NOT NULL,
|
||||
consecutive_count int NOT NULL,
|
||||
updated_at datetime2(3) NOT NULL,
|
||||
CONSTRAINT pk_shadow_hold_history PRIMARY KEY
|
||||
(parent_task_id, vehicle_id, resource_key, reason_code)
|
||||
);
|
||||
|
||||
IF OBJECT_ID(N'dbo.shadow_startup_check', N'U') IS NULL
|
||||
CREATE TABLE dbo.shadow_startup_check (
|
||||
check_id bigint IDENTITY(1, 1) NOT NULL PRIMARY KEY,
|
||||
check_name varchar(128) NOT NULL,
|
||||
status varchar(32) NOT NULL,
|
||||
checked_at datetime2(3) NOT NULL
|
||||
);
|
||||
|
||||
DECLARE @grantEvidence nvarchar(max) = N'GRANT CONNECT TO '
|
||||
+ @evidencePrincipal + N';';
|
||||
DECLARE @evidenceTable sysname;
|
||||
DECLARE evidence_tables CURSOR LOCAL FAST_FORWARD FOR
|
||||
SELECT name
|
||||
FROM sys.tables
|
||||
WHERE schema_id = SCHEMA_ID(N'dbo')
|
||||
AND name IN (N'shadow_run', N'shadow_cycle', N'shadow_input_batch',
|
||||
N'shadow_legacy_queue', N'shadow_decision',
|
||||
N'shadow_encoded_write', N'shadow_difference',
|
||||
N'shadow_traffic_state', N'shadow_hold_history',
|
||||
N'shadow_startup_check');
|
||||
OPEN evidence_tables;
|
||||
FETCH NEXT FROM evidence_tables INTO @evidenceTable;
|
||||
WHILE @@FETCH_STATUS = 0
|
||||
BEGIN
|
||||
SET @grantEvidence += N'GRANT SELECT, INSERT, UPDATE, DELETE ON OBJECT::dbo.'
|
||||
+ QUOTENAME(@evidenceTable) + N' TO ' + @evidencePrincipal + N';';
|
||||
FETCH NEXT FROM evidence_tables INTO @evidenceTable;
|
||||
END;
|
||||
CLOSE evidence_tables;
|
||||
DEALLOCATE evidence_tables;
|
||||
EXEC sys.sp_executesql @grantEvidence;
|
||||
|
||||
DECLARE @databaseContextSet bit = 0;
|
||||
BEGIN TRY
|
||||
EXECUTE AS USER = N'$(ShadowEvidenceUser)';
|
||||
SET @databaseContextSet = 1;
|
||||
|
||||
IF EXISTS (
|
||||
SELECT 1
|
||||
FROM sys.database_role_members rm
|
||||
JOIN sys.database_principals member_principal
|
||||
ON member_principal.principal_id = rm.member_principal_id
|
||||
WHERE member_principal.name = USER_NAME()
|
||||
)
|
||||
THROW 51005, 'The shadow evidence login retains a database role.', 1;
|
||||
|
||||
IF EXISTS (
|
||||
SELECT 1 FROM sys.fn_my_permissions(NULL, N'DATABASE')
|
||||
WHERE permission_name <> N'CONNECT'
|
||||
)
|
||||
THROW 51006, 'The shadow evidence login retains forbidden database permissions.', 1;
|
||||
|
||||
IF EXISTS (
|
||||
SELECT 1
|
||||
FROM sys.schemas s
|
||||
CROSS APPLY sys.fn_my_permissions(QUOTENAME(s.name), N'SCHEMA') p
|
||||
WHERE s.name NOT IN (N'sys', N'INFORMATION_SCHEMA')
|
||||
)
|
||||
THROW 51007, 'The shadow evidence login retains forbidden schema permissions.', 1;
|
||||
|
||||
IF EXISTS (
|
||||
SELECT 1
|
||||
FROM sys.objects o
|
||||
JOIN sys.schemas s ON s.schema_id = o.schema_id
|
||||
CROSS APPLY sys.fn_my_permissions(
|
||||
QUOTENAME(s.name) + N'.' + QUOTENAME(o.name), N'OBJECT') p
|
||||
WHERE o.is_ms_shipped = 0
|
||||
AND NOT (
|
||||
s.name = N'dbo'
|
||||
AND o.name IN (
|
||||
N'shadow_run', N'shadow_cycle', N'shadow_input_batch',
|
||||
N'shadow_legacy_queue', N'shadow_decision',
|
||||
N'shadow_encoded_write', N'shadow_difference',
|
||||
N'shadow_traffic_state', N'shadow_hold_history',
|
||||
N'shadow_startup_check')
|
||||
AND p.permission_name IN (
|
||||
N'SELECT', N'INSERT', N'UPDATE', N'DELETE')
|
||||
)
|
||||
)
|
||||
THROW 51008, 'The shadow evidence login retains forbidden object permissions.', 1;
|
||||
|
||||
REVERT;
|
||||
SET @databaseContextSet = 0;
|
||||
END TRY
|
||||
BEGIN CATCH
|
||||
IF @databaseContextSet = 1
|
||||
REVERT;
|
||||
THROW;
|
||||
END CATCH;
|
||||
|
||||
COMMIT TRANSACTION;
|
||||
END TRY
|
||||
BEGIN CATCH
|
||||
IF XACT_STATE() <> 0
|
||||
ROLLBACK TRANSACTION;
|
||||
IF @evidenceLoginCreated = 1
|
||||
BEGIN
|
||||
DECLARE @dropEvidenceLogin nvarchar(max) =
|
||||
N'DROP LOGIN ' + QUOTENAME(N'$(ShadowEvidenceLogin)');
|
||||
EXEC sys.sp_executesql @dropEvidenceLogin;
|
||||
END;
|
||||
THROW;
|
||||
END CATCH;
|
||||
Reference in New Issue
Block a user