feat(ft546): enforce shadow database isolation

This commit is contained in:
zgw
2026-08-13 14:23:50 +08:00
parent 364668c939
commit b364831c4a
23 changed files with 2516 additions and 14 deletions

View File

@@ -0,0 +1,77 @@
:setvar ShadowSourceDatabase "FT546-wcs"
SET NOCOUNT ON;
SET XACT_ABORT ON;
GO
IF DB_ID() <> DB_ID(N'$(ShadowSourceDatabase)')
THROW 51000, 'Run the shadow read-view script only in ShadowSourceDatabase.', 1;
GO
IF SCHEMA_ID(N'shadow_ro') IS NULL
EXEC(N'CREATE SCHEMA shadow_ro AUTHORIZATION dbo');
GO
CREATE OR ALTER VIEW shadow_ro.v_wcs_srgv_queue
AS
SELECT guid, task_no, task_type, priority, cargo, issued, path_info,
execution_status, cargo_pick_site, cargo_place_site, start_site,
end_site, the_floor, trun_path, up_down_site, create_date,
start_date, finish_date, agv_id, default02, default03, default04,
default05, default06, default07, default08
FROM dbo.wcs_srgv_queue;
GO
CREATE OR ALTER VIEW shadow_ro.v_wcs_srgv_queue_finish
AS
SELECT guid, task_no, task_type, priority, cargo, issued, path_info,
execution_status, cargo_pick_site, cargo_place_site, start_site,
end_site, the_floor, trun_path, up_down_site, create_date,
start_date, finish_date, agv_id, default02, default03, default04,
default05, default06, default07, default08
FROM dbo.wcs_srgv_queue_finish;
GO
CREATE OR ALTER VIEW shadow_ro.v_device_srgv
AS
SELECT guid, device_id, device_name, type, set_floor, power_time,
last_time, agv_status, task_no, src_site, tar_site, curr_site,
task_type, battery, work_mode, alarm, status, finish, charge_flag,
work_phase, err_msg, ssj_request, change_layer, cargo_flag,
manual_charge, path, allow_send, curr_floor, offline_site, locked,
spare1, spare2, spare3, spare4, spare5
FROM dbo.device_srgv;
GO
CREATE OR ALTER VIEW shadow_ro.v_wcs_srgv_map
AS
SELECT guid, x, y, z, axy, value, ux, uy, uz, uxy, space_name, remark,
cargo_space_id, the_row, the_column, the_layer, site_id, plc_row,
plc_column, plc_layer, warehouse, space_area
FROM dbo.wcs_srgv_map;
GO
CREATE OR ALTER VIEW shadow_ro.v_bas_cargo_space
AS
SELECT cargo_space_id, warehouse_id, roadway_id, cargo_area_id, the_type,
the_type_name, cargo_space_name, the_platoon, the_column, the_floor,
status, locked, tray_code, cargo_hight, iss_double, group_coding_no,
group_coding, cargo_space_logo, this_cargo_area, wms_cargo_space_id
FROM dbo.bas_cargo_space;
GO
CREATE OR ALTER VIEW shadow_ro.v_tsj_execution
AS
SELECT guid, device_id, device_name, task_no, src_row, src_column,
src_layer, tar_row, tar_column, tar_layer, src_site, tar_site,
send_flag, send_type, task_type, create_date, start_date,
finish_date, spare1, spare2, spare3, rwlx
FROM dbo.tsj_device_execution_queue;
GO
CREATE OR ALTER VIEW shadow_ro.v_current_database_clock
AS
SELECT SYSUTCDATETIME() AS database_utc_time,
DB_NAME() AS database_name,
@@SERVERNAME AS server_name;
GO

View File

@@ -0,0 +1,192 @@
:setvar ShadowSourceLogin "ft546_shadow_ro"
:setvar ShadowSourceUser "ft546_shadow_ro"
:setvar ShadowSourcePassword "REPLACE_AT_DEPLOY_TIME"
:setvar ShadowSourceDatabase "FT546-wcs"
SET NOCOUNT ON;
SET XACT_ABORT ON;
IF N'$(ShadowSourcePassword)' = N'REPLACE_AT_DEPLOY_TIME'
THROW 51000, 'Supply ShadowSourcePassword through the approved secret channel.', 1;
IF DB_ID() <> DB_ID(N'$(ShadowSourceDatabase)')
THROW 51001, 'Run the shadow source-login script only in ShadowSourceDatabase.', 1;
DECLARE @sourceLoginCreated bit = 0;
IF NOT EXISTS (SELECT 1 FROM sys.server_principals
WHERE name = N'$(ShadowSourceLogin)')
BEGIN
DECLARE @createLogin nvarchar(max) =
N'CREATE LOGIN ' + QUOTENAME(N'$(ShadowSourceLogin)')
+ N' WITH PASSWORD = N'''
+ REPLACE(N'$(ShadowSourcePassword)', N'''', N'''''')
+ N''', CHECK_POLICY = ON, CHECK_EXPIRATION = ON';
EXEC sys.sp_executesql @createLogin;
SET @sourceLoginCreated = 1;
END;
BEGIN TRY
IF EXISTS (
SELECT 1
FROM sys.server_role_members rm
JOIN sys.server_principals role_principal
ON role_principal.principal_id = rm.role_principal_id
JOIN sys.server_principals login_principal
ON login_principal.principal_id = rm.member_principal_id
WHERE login_principal.name = N'$(ShadowSourceLogin)'
)
THROW 51002, 'The shadow source login must not belong to a server role.', 1;
DECLARE @serverContextSet bit = 0;
BEGIN TRY
EXECUTE AS LOGIN = N'$(ShadowSourceLogin)';
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 51007, 'The shadow source 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'$(ShadowSourceUser)')
BEGIN
DECLARE @createUser nvarchar(max) =
N'CREATE USER ' + QUOTENAME(N'$(ShadowSourceUser)')
+ N' FOR LOGIN ' + QUOTENAME(N'$(ShadowSourceLogin)');
EXEC sys.sp_executesql @createUser;
END
ELSE IF SUSER_SID(N'$(ShadowSourceLogin)') <> USER_SID(N'$(ShadowSourceUser)')
THROW 51003, 'The existing shadow source user maps to a different login.', 1;
DECLARE @principal nvarchar(258) = QUOTENAME(N'$(ShadowSourceUser)');
DECLARE @permissions nvarchar(max) =
N'GRANT CONNECT TO ' + @principal + N';'
+ N'REVOKE CONTROL FROM ' + @principal + N';'
+ N'REVOKE SELECT FROM ' + @principal + N';'
+ N'GRANT SELECT ON SCHEMA::shadow_ro TO ' + @principal + N';'
+ N'DENY INSERT, UPDATE, DELETE, EXECUTE, ALTER, TAKE OWNERSHIP '
+ N'TO ' + @principal + N';'
+ N'DENY CONTROL ON OBJECT::dbo.wcs_srgv_queue TO ' + @principal + N';'
+ N'DENY CONTROL ON OBJECT::dbo.wcs_srgv_queue_finish TO ' + @principal + N';'
+ N'DENY CONTROL ON OBJECT::dbo.device_srgv TO ' + @principal + N';'
+ N'DENY CONTROL ON OBJECT::dbo.wcs_srgv_map TO ' + @principal + N';'
+ N'DENY CONTROL ON OBJECT::dbo.bas_cargo_space TO ' + @principal + N';'
+ N'DENY CONTROL ON OBJECT::dbo.tsj_device_execution_queue TO '
+ @principal + N';'
+ N'DENY SELECT ON OBJECT::dbo.wcs_srgv_queue TO ' + @principal + N';'
+ N'DENY SELECT ON OBJECT::dbo.wcs_srgv_queue_finish TO ' + @principal + N';'
+ N'DENY SELECT ON OBJECT::dbo.device_srgv TO ' + @principal + N';'
+ N'DENY SELECT ON OBJECT::dbo.wcs_srgv_map TO ' + @principal + N';'
+ N'DENY SELECT ON OBJECT::dbo.bas_cargo_space TO ' + @principal + N';'
+ N'DENY SELECT ON OBJECT::dbo.tsj_device_execution_queue TO '
+ @principal + N';';
EXEC sys.sp_executesql @permissions;
DECLARE @role sysname;
DECLARE forbidden_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'$(ShadowSourceUser)';
OPEN forbidden_roles;
FETCH NEXT FROM forbidden_roles INTO @role;
WHILE @@FETCH_STATUS = 0
BEGIN
IF IS_ROLEMEMBER(@role, N'$(ShadowSourceUser)') = 1
BEGIN
DECLARE @dropRole nvarchar(max) = N'ALTER ROLE '
+ QUOTENAME(@role) + N' DROP MEMBER ' + @principal;
EXEC sys.sp_executesql @dropRole;
END;
FETCH NEXT FROM forbidden_roles INTO @role;
END;
CLOSE forbidden_roles;
DEALLOCATE forbidden_roles;
DECLARE @databaseContextSet bit = 0;
BEGIN TRY
EXECUTE AS USER = N'$(ShadowSourceUser)';
SET @databaseContextSet = 1;
IF HAS_PERMS_BY_NAME(DB_NAME(), N'DATABASE', N'SELECT') <> 0
OR HAS_PERMS_BY_NAME(DB_NAME(), N'DATABASE', N'INSERT') <> 0
OR HAS_PERMS_BY_NAME(DB_NAME(), N'DATABASE', N'UPDATE') <> 0
OR HAS_PERMS_BY_NAME(DB_NAME(), N'DATABASE', N'DELETE') <> 0
OR HAS_PERMS_BY_NAME(DB_NAME(), N'DATABASE', N'EXECUTE') <> 0
OR HAS_PERMS_BY_NAME(DB_NAME(), N'DATABASE', N'ALTER') <> 0
OR HAS_PERMS_BY_NAME(DB_NAME(), N'DATABASE', N'CONTROL') <> 0
OR HAS_PERMS_BY_NAME(DB_NAME(), N'DATABASE', N'TAKE OWNERSHIP') <> 0
THROW 51004, 'The shadow source 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')
AND NOT (s.name = N'shadow_ro' AND p.permission_name = N'SELECT')
)
THROW 51005, 'The shadow source 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'shadow_ro'
AND o.name IN (
N'v_wcs_srgv_queue', N'v_wcs_srgv_queue_finish',
N'v_device_srgv', N'v_wcs_srgv_map',
N'v_bas_cargo_space', N'v_tsj_execution',
N'v_current_database_clock')
AND p.permission_name = N'SELECT'
)
)
THROW 51006, 'The shadow source login retains forbidden object permissions.', 1;
SELECT TOP (0) * FROM shadow_ro.v_wcs_srgv_queue;
SELECT TOP (0) * FROM shadow_ro.v_wcs_srgv_queue_finish;
SELECT TOP (0) * FROM shadow_ro.v_device_srgv;
SELECT TOP (0) * FROM shadow_ro.v_wcs_srgv_map;
SELECT TOP (0) * FROM shadow_ro.v_bas_cargo_space;
SELECT TOP (0) * FROM shadow_ro.v_tsj_execution;
SELECT TOP (0) * FROM shadow_ro.v_current_database_clock;
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 @sourceLoginCreated = 1
BEGIN
DECLARE @dropSourceLogin nvarchar(max) =
N'DROP LOGIN ' + QUOTENAME(N'$(ShadowSourceLogin)');
EXEC sys.sp_executesql @dropSourceLogin;
END;
THROW;
END CATCH;

View 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;

View File

@@ -0,0 +1,137 @@
:setvar ShadowSourceLogin "ft546_shadow_ro"
:setvar ShadowSourceDatabase "FT546-wcs"
SET NOCOUNT ON;
SET XACT_ABORT ON;
IF DB_ID() <> DB_ID(N'$(ShadowSourceDatabase)')
THROW 51010, 'Run source permission verification only in ShadowSourceDatabase.', 1;
IF SUSER_ID(N'$(ShadowSourceLogin)') IS NULL
THROW 51011, 'ShadowSourceLogin does not exist.', 1;
DECLARE @checks TABLE (
check_name nvarchar(512) NOT NULL,
expected_value int NOT NULL,
actual_value int NULL
);
INSERT INTO @checks (check_name, expected_value, actual_value)
SELECT N'server_role_membership_count', 0, COUNT(*)
FROM sys.server_role_members rm
JOIN sys.server_principals member_principal
ON member_principal.principal_id = rm.member_principal_id
WHERE member_principal.name = N'$(ShadowSourceLogin)';
BEGIN TRY
EXECUTE AS LOGIN = N'$(ShadowSourceLogin)';
DECLARE @objects TABLE (
object_name nvarchar(512) NOT NULL,
expected_select int NOT NULL,
is_base_table bit NOT NULL
);
INSERT INTO @objects (object_name, expected_select, is_base_table)
VALUES
(N'shadow_ro.v_wcs_srgv_queue', 1, 0),
(N'shadow_ro.v_wcs_srgv_queue_finish', 1, 0),
(N'shadow_ro.v_device_srgv', 1, 0),
(N'shadow_ro.v_wcs_srgv_map', 1, 0),
(N'shadow_ro.v_bas_cargo_space', 1, 0),
(N'shadow_ro.v_tsj_execution', 1, 0),
(N'shadow_ro.v_current_database_clock', 1, 0),
(N'dbo.wcs_srgv_queue', 0, 1),
(N'dbo.wcs_srgv_queue_finish', 0, 1),
(N'dbo.device_srgv', 0, 1),
(N'dbo.wcs_srgv_map', 0, 1),
(N'dbo.bas_cargo_space', 0, 1),
(N'dbo.tsj_device_execution_queue', 0, 1);
INSERT INTO @checks (check_name, expected_value, actual_value)
SELECT N'select:' + object_name, expected_select,
HAS_PERMS_BY_NAME(object_name, N'OBJECT', N'SELECT')
FROM @objects;
INSERT INTO @checks (check_name, expected_value, actual_value)
SELECT LOWER(permission_name) + N':' + object_name, 0,
HAS_PERMS_BY_NAME(object_name, N'OBJECT', permission_name)
FROM @objects
CROSS JOIN (VALUES (N'INSERT'), (N'UPDATE'), (N'DELETE'),
(N'ALTER'), (N'CONTROL')) AS operations(permission_name)
WHERE is_base_table = 1;
INSERT INTO @checks (check_name, expected_value, actual_value)
SELECT N'database_permission:' + permission_name, 0,
HAS_PERMS_BY_NAME(DB_NAME(), N'DATABASE', permission_name)
FROM (VALUES (N'SELECT'), (N'INSERT'), (N'UPDATE'), (N'DELETE'),
(N'EXECUTE'), (N'ALTER'), (N'CONTROL'),
(N'TAKE OWNERSHIP')) AS permissions(permission_name);
INSERT INTO @checks (check_name, expected_value, actual_value)
SELECT N'database_role:' + role_name, 0, IS_MEMBER(role_name)
FROM (VALUES (N'db_owner'), (N'db_datareader'),
(N'db_datawriter'), (N'db_ddladmin'),
(N'db_securityadmin'), (N'db_accessadmin'))
AS roles(role_name);
INSERT INTO @checks (check_name, expected_value, actual_value)
SELECT N'forbidden_effective_database_permission_count', 0, COUNT(*)
FROM sys.fn_my_permissions(NULL, N'DATABASE')
WHERE permission_name IN (N'SELECT', N'INSERT', N'UPDATE', N'DELETE',
N'EXECUTE', N'ALTER', N'CONTROL',
N'TAKE OWNERSHIP');
INSERT INTO @checks (check_name, expected_value, actual_value)
SELECT N'forbidden_effective_server_permission_count', 0, COUNT(*)
FROM sys.fn_my_permissions(NULL, N'SERVER')
WHERE permission_name NOT IN (N'CONNECT SQL', N'VIEW ANY DATABASE');
INSERT INTO @checks (check_name, expected_value, actual_value)
SELECT N'forbidden_effective_schema_permission_count', 0, COUNT(*)
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')
AND NOT (s.name = N'shadow_ro' AND p.permission_name = N'SELECT');
INSERT INTO @checks (check_name, expected_value, actual_value)
SELECT N'forbidden_effective_object_permission_count', 0, COUNT(*)
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'shadow_ro'
AND o.name IN (N'v_wcs_srgv_queue', N'v_wcs_srgv_queue_finish',
N'v_device_srgv', N'v_wcs_srgv_map',
N'v_bas_cargo_space', N'v_tsj_execution',
N'v_current_database_clock')
AND p.permission_name = N'SELECT'
);
SELECT TOP (0) * FROM shadow_ro.v_wcs_srgv_queue;
SELECT TOP (0) * FROM shadow_ro.v_wcs_srgv_queue_finish;
SELECT TOP (0) * FROM shadow_ro.v_device_srgv;
SELECT TOP (0) * FROM shadow_ro.v_wcs_srgv_map;
SELECT TOP (0) * FROM shadow_ro.v_bas_cargo_space;
SELECT TOP (0) * FROM shadow_ro.v_tsj_execution;
SELECT TOP (0) * FROM shadow_ro.v_current_database_clock;
REVERT;
END TRY
BEGIN CATCH
IF ORIGINAL_LOGIN() <> SUSER_SNAME()
REVERT;
THROW;
END CATCH;
SELECT check_name, expected_value, actual_value,
CASE WHEN actual_value = expected_value THEN N'PASS' ELSE N'FAIL' END
AS status
FROM @checks
ORDER BY check_name;
IF EXISTS (SELECT 1 FROM @checks
WHERE actual_value IS NULL OR actual_value <> expected_value)
THROW 51012, 'FT546 shadow source permission verification failed.', 1;