Created
March 26, 2026 22:11
-
-
Save JosiahSiegel/27aa135fce8af547cf76615ad53ad158 to your computer and use it in GitHub Desktop.
SQL Instance Permissions Management
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| SET ANSI_NULLS ON | |
| GO | |
| SET QUOTED_IDENTIFIER ON | |
| GO | |
| -- ============================================================================ | |
| -- Table type for the security matrix TVP | |
| -- Must drop SP first (it references the type), then drop/recreate the type. | |
| -- ============================================================================ | |
| IF EXISTS (SELECT 1 FROM sys.procedures WHERE name = 'usp_ConfigureSecurity' AND schema_id = SCHEMA_ID('dbo')) | |
| DROP PROCEDURE dbo.usp_ConfigureSecurity; | |
| GO | |
| IF EXISTS (SELECT 1 FROM sys.types WHERE name = 'SecurityMatrixType' AND is_table_type = 1) | |
| DROP TYPE dbo.SecurityMatrixType; | |
| GO | |
| CREATE TYPE dbo.SecurityMatrixType AS TABLE ( | |
| PrincipalName SYSNAME NOT NULL, | |
| PrincipalType VARCHAR(10) NOT NULL, -- 'SQL' or 'WINDOWS' (or 'WIN' shorthand) | |
| ServerPattern VARCHAR(50) NOT NULL, | |
| DatabaseName SYSNAME NOT NULL, | |
| DefaultSchema SYSNAME NOT NULL DEFAULT 'dbo', | |
| Flags VARCHAR(50) NOT NULL DEFAULT '', -- dash-delimited: R W A O Ex VD SP | |
| ServerRole VARCHAR(50) NULL, | |
| ServerGrant VARCHAR(100) NULL, | |
| CustomRole SYSNAME NULL, | |
| ExtraGrants NVARCHAR(MAX) NULL | |
| ); | |
| GO | |
| CREATE PROCEDURE [dbo].[usp_ConfigureSecurity] | |
| @Matrix SecurityMatrixType READONLY, | |
| @DryRun BIT = 0, | |
| @TargetDatabase SYSNAME = NULL, | |
| @Verbose INT = 0, | |
| @EnforceMatrix BIT = 0 | |
| /******************************************************************************* | |
| EXEC dbo.usp_ConfigureSecurity @Matrix = @SecurityMatrix; -- apply all (additive only) | |
| EXEC dbo.usp_ConfigureSecurity @Matrix = @SecurityMatrix, @EnforceMatrix = 1; -- apply all + revoke extras + drop extra users | |
| EXEC dbo.usp_ConfigureSecurity @Matrix = @SecurityMatrix, @DryRun = 1; -- preview changes only | |
| EXEC dbo.usp_ConfigureSecurity @Matrix = @SecurityMatrix, @DryRun = 1, @Verbose = 1; -- preview all (incl. OK) | |
| EXEC dbo.usp_ConfigureSecurity @Matrix = @SecurityMatrix, @TargetDatabase = 'MyAppDB'; -- single DB | |
| EXEC dbo.usp_ConfigureSecurity @Matrix = @SecurityMatrix, @DryRun = 1, @TargetDatabase = 'MyAppDB'; -- preview single DB | |
| Idempotent stored procedure to configure database security for SQL Server systems. | |
| The security matrix is passed in as a Table-Valued Parameter (@Matrix) of type | |
| dbo.SecurityMatrixType. Each row is one principal + one database + a flags string | |
| encoding its permissions for that combination. | |
| PARAMETERS: | |
| @Matrix SecurityMatrixType READONLY | |
| Table-valued parameter containing the security matrix. | |
| Must have at least one row or the SP will raise an error. | |
| Columns: | |
| PrincipalName SYSNAME -- login / group name | |
| PrincipalType VARCHAR(10) -- 'SQL', 'WINDOWS', or 'WIN' (shorthand) | |
| ServerPattern VARCHAR(50) -- LIKE pattern matched against @@SERVERNAME | |
| DatabaseName SYSNAME -- exact DB name, '*' for all user DBs, | |
| -- or '*,-DB1,-DB2' for all EXCEPT listed DBs | |
| DefaultSchema SYSNAME -- default schema (usually 'dbo') | |
| Flags VARCHAR(50) -- dash-delimited permission flags (see key below) | |
| ServerRole VARCHAR(50) -- e.g. 'bulkadmin' | |
| ServerGrant VARCHAR(100) -- e.g. 'VIEW SERVER STATE' | |
| CustomRole SYSNAME -- creates this DB role, adds principal as member | |
| ExtraGrants NVARCHAR(MAX) -- pipe-delimited GRANT statements (fully written, executed as-is) | |
| @DryRun BIT = 0 When 0 (default): applies all security changes normally. | |
| When 1: performs a read-only comparison of the matrix's | |
| desired state against the current server state and outputs | |
| a report showing what WOULD change, without making any | |
| modifications. Lines are prefixed with: | |
| [OK] - permission already matches desired state | |
| [ADD] - permission needs to be added | |
| [DROP] - user/role exists on server but not in matrix | |
| [REVOKE] - permission exists on server but not in matrix | |
| [MISS] - server-level login does not exist | |
| @Verbose INT = 0 Controls output detail level. | |
| 0 (default): In dry-run mode, shows only pending changes | |
| (ADD, DROP, REVOKE, MISS) and hides OK confirmations. | |
| In normal mode, shows all actions taken. | |
| Summary and warnings always shown. | |
| 1: Shows everything including dry-run OK confirmations. | |
| @TargetDatabase SYSNAME = NULL | |
| When NULL (default): processes all user databases. | |
| When set to a database name: processes only that single | |
| database. Server-level permissions (Phase 3) are always | |
| processed regardless of this setting. | |
| @EnforceMatrix BIT = 0 | |
| Controls whether extra permissions are removed. | |
| Applies to BOTH dry-run and normal execution modes. | |
| 0 (default, safe): Only ADD missing permissions. Never | |
| revoke or drop anything. Safe for production use. | |
| - Creates missing logins, users, role memberships, grants | |
| - Never removes extra roles, grants, or users | |
| - In dry-run mode, hides [DROP] and [REVOKE] items | |
| 1 (strict): Also REVOKE extra role memberships, REVOKE | |
| extra grants, and DROP users not in the matrix. | |
| - Same additive behavior as 0, plus: | |
| - Revokes standard role memberships not in the matrix | |
| - Revokes standard grants (EXECUTE, VIEW DEFINITION, | |
| SHOWPLAN) not in the matrix | |
| - Drops database users that are not in the matrix | |
| - In dry-run mode, shows [DROP] and [REVOKE] items | |
| FLAGS KEY (dash-delimited in the Flags column): | |
| R = db_datareader (adds principal to db_datareader role) | |
| W = db_datawriter (adds principal to db_datawriter role) | |
| A = db_ddladmin (adds principal to db_ddladmin role) | |
| O = db_owner (adds principal to db_owner role) | |
| Ex = GrantExecute (GRANT EXECUTE TO principal) | |
| VD = GrantViewDef (GRANT VIEW DEFINITION TO principal) | |
| SP = GrantShowplan (GRANT SHOWPLAN TO principal) | |
| Examples: 'R-W' = reader+writer | 'O' = owner only | 'R-W-A-Ex' = all four | |
| PrincipalType shorthand: | |
| 'WIN' is automatically expanded to 'WINDOWS' inside the SP. | |
| ServerPattern uses LIKE syntax to match @@SERVERNAME: | |
| 'SERVER-01' = one specific server | |
| 'SQL-DEV%' = all servers starting with SQL-DEV | |
| 'SQL-[DQ]%' = servers starting with SQL-D or SQL-Q (e.g., DEV and QA) | |
| '%' = all servers | |
| DatabaseName: | |
| Exact name = that specific database only | |
| '*' = ALL user databases (system/excluded DBs are skipped automatically) | |
| '*,-DB1,-DB2' = all user databases EXCEPT DB1 and DB2 (comma-separated, dash-prefixed) | |
| ServerRole: e.g. 'bulkadmin' -- adds principal to that server role | |
| ServerGrant: e.g. 'VIEW SERVER STATE' -- GRANT <perm> TO principal at server level | |
| CustomRole: e.g. 'AppDataRole' -- creates this DB role, adds the principal | |
| as a member, then grants the role the flagged standard roles | |
| ExtraGrants: pipe-delimited custom GRANT statements executed as-is in the database | |
| context. Write the full statement including the principal name, e.g.: | |
| 'GRANT CONTROL ON CERTIFICATE::cert_sk_apps TO [my-api]' | |
| -- ============================================================================= | |
| -- EXAMPLE USAGE: | |
| -- ============================================================================= | |
| -- | |
| -- DECLARE @SecurityMatrix SecurityMatrixType; | |
| -- INSERT INTO @SecurityMatrix | |
| -- (PrincipalName, PrincipalType, ServerPattern, DatabaseName, DefaultSchema, | |
| -- Flags, ServerRole, ServerGrant, CustomRole, ExtraGrants) | |
| -- VALUES | |
| -- -- A SQL login with read/write on a specific DB, plus server-level bulkadmin | |
| -- ('app-service-account', 'SQL', 'SQL-[DQ]%', 'MyAppDB', 'dbo', 'R-W', 'bulkadmin', NULL, NULL, NULL), | |
| -- -- A Windows group with db_owner on all user databases + VIEW SERVER STATE | |
| -- ('DOMAIN\DBA Team', 'WIN', '%', '*', 'dbo', 'O-VD-SP', NULL, 'VIEW SERVER STATE', NULL, NULL), | |
| -- -- A Windows group with read on all user databases EXCEPT P350 and MRA | |
| -- ('DOMAIN\Readers', 'WIN', '%', '*,-P350,-MRA','dbo', 'R', NULL, NULL, NULL, NULL), | |
| -- -- A SQL login via custom role pattern | |
| -- ('etl-service', 'SQL', 'SQL-[DQ]%', 'StagingDB', 'dbo', 'O', NULL, NULL, 'ETLRole', NULL), | |
| -- -- A login with extra grants (pipe-delimited) | |
| -- ('report-reader', 'SQL', '%', 'ReportsDB', 'dbo', 'R', NULL, NULL, NULL, | |
| -- 'GRANT VIEW DEFINITION ON SYMMETRIC KEY::sk_Reports TO [report-reader]'); | |
| -- | |
| -- EXEC dbo.usp_ConfigureSecurity @Matrix = @SecurityMatrix; -- apply all | |
| -- EXEC dbo.usp_ConfigureSecurity @Matrix = @SecurityMatrix, @DryRun = 1; -- preview changes only | |
| -- EXEC dbo.usp_ConfigureSecurity @Matrix = @SecurityMatrix, @DryRun = 1, @Verbose = 1; -- preview all (incl. OK) | |
| -- EXEC dbo.usp_ConfigureSecurity @Matrix = @SecurityMatrix, @TargetDatabase = 'MyAppDB'; -- single DB | |
| HISTORY: | |
| See source control for version history. | |
| *******************************************************************************/ | |
| AS | |
| BEGIN TRY | |
| SET NOCOUNT ON; | |
| DECLARE @MESSAGE VARCHAR(8000); | |
| DECLARE @ErrorMessage NVARCHAR(4000); | |
| DECLARE @ErrorSeverity INT; | |
| DECLARE @ErrorState INT; | |
| DECLARE @DynSQL NVARCHAR(MAX); | |
| DECLARE @CurrentDB SYSNAME; | |
| -- ======================================================================== | |
| -- P R O C E S S I N G L O G I C | |
| -- ======================================================================== | |
| DECLARE @ExcludedDBs TABLE (DbName SYSNAME NOT NULL); | |
| INSERT INTO @ExcludedDBs (DbName) VALUES ('master'),('model'),('msdb'),('tempdb'),('ssisdb'); | |
| -- Add any additional databases to exclude from processing: | |
| -- INSERT INTO @ExcludedDBs (DbName) VALUES ('AdminDB'),('MonitoringDB'); | |
| -- Validate @TargetDatabase if specified | |
| IF @TargetDatabase IS NOT NULL | |
| BEGIN | |
| IF NOT EXISTS (SELECT 1 FROM sys.databases WHERE name = @TargetDatabase) | |
| BEGIN | |
| RAISERROR('Error - Database [%s] does not exist on this server.', 16, 1, @TargetDatabase) WITH NOWAIT; | |
| RETURN; | |
| END | |
| IF @TargetDatabase IN (SELECT DbName FROM @ExcludedDBs) | |
| BEGIN | |
| RAISERROR('Error - Database [%s] is in the excluded list and cannot be targeted.', 16, 1, @TargetDatabase) WITH NOWAIT; | |
| RETURN; | |
| END | |
| SET @MESSAGE = 'Targeting single database: [' + @TargetDatabase + ']'; | |
| RAISERROR(@MESSAGE, 0, 42) WITH NOWAIT; | |
| END | |
| -- Validate that @Matrix is not empty | |
| IF NOT EXISTS (SELECT 1 FROM @Matrix) | |
| BEGIN | |
| RAISERROR('Error - @Matrix is empty. No security configuration to process.', 16, 1) WITH NOWAIT; | |
| RETURN; | |
| END | |
| -- ======================================================================== | |
| -- Parse the flags-string TVP into an internal table with individual BIT columns | |
| -- ======================================================================== | |
| DECLARE @Parsed TABLE ( | |
| PrincipalName SYSNAME NOT NULL, | |
| PrincipalType VARCHAR(10) NOT NULL, | |
| ServerPattern VARCHAR(50) NOT NULL, | |
| DatabaseName SYSNAME NOT NULL, | |
| DefaultSchema SYSNAME NOT NULL, | |
| db_datareader BIT NOT NULL, | |
| db_datawriter BIT NOT NULL, | |
| db_ddladmin BIT NOT NULL, | |
| db_owner BIT NOT NULL, | |
| GrantExecute BIT NOT NULL, | |
| GrantViewDef BIT NOT NULL, | |
| GrantShowplan BIT NOT NULL, | |
| ServerRole VARCHAR(50) NULL, | |
| ServerGrant VARCHAR(100) NULL, | |
| CustomRole SYSNAME NULL, | |
| ExtraGrants NVARCHAR(MAX) NULL | |
| ); | |
| INSERT INTO @Parsed | |
| SELECT | |
| PrincipalName, | |
| CASE WHEN PrincipalType = 'WIN' THEN 'WINDOWS' ELSE PrincipalType END, | |
| ServerPattern, DatabaseName, DefaultSchema, | |
| IIF(Flags LIKE '%R%' AND Flags NOT LIKE '%Re%', 1, 0), -- R = db_datareader | |
| IIF(Flags LIKE '%W%', 1, 0), -- W = db_datawriter | |
| IIF(Flags LIKE '%A%', 1, 0), -- A = db_ddladmin | |
| IIF(Flags LIKE '%O%', 1, 0), -- O = db_owner | |
| IIF(Flags LIKE '%Ex%', 1, 0), -- Ex = GrantExecute | |
| IIF(Flags LIKE '%VD%', 1, 0), -- VD = GrantViewDef | |
| IIF(Flags LIKE '%SP%', 1, 0), -- SP = GrantShowplan | |
| ServerRole, ServerGrant, CustomRole, ExtraGrants | |
| FROM @Matrix; | |
| DECLARE @Actions TABLE (ActionType VARCHAR(10) NOT NULL, ActionTarget NVARCHAR(500) NOT NULL); | |
| DECLARE @EnsuredLogins TABLE (LoginName SYSNAME NOT NULL); | |
| DECLARE @db_name SYSNAME, @prinName SYSNAME, @prinType VARCHAR(10), @defSchema SYSNAME; | |
| DECLARE @fReader BIT, @fWriter BIT, @fDDLAdmin BIT, @fOwner BIT; | |
| DECLARE @fExec BIT, @fViewDef BIT, @fShowplan BIT; | |
| DECLARE @actType VARCHAR(10), @actTarget NVARCHAR(500); | |
| DECLARE @extraGrants NVARCHAR(MAX), @grantStmt NVARCHAR(MAX), @delimPos INT; | |
| -- Dry-run counters and state variables (declared at top level for T-SQL scoping) | |
| DECLARE @srv_OK INT, @srv_ADD INT, @srv_MISS INT; | |
| DECLARE @db_OK INT, @db_ADD INT, @db_DROP INT, @db_MISS INT; | |
| DECLARE @exists BIT; | |
| DECLARE @srv_Principal SYSNAME, @srv_PrinType VARCHAR(10), @srv_PermType VARCHAR(10), @srv_PermValue VARCHAR(100); | |
| DECLARE @cr_RoleName SYSNAME, @cr_Member SYSNAME, @cr_PrinType VARCHAR(10), @cr_Schema SYSNAME; | |
| DECLARE @roleList VARCHAR(500); | |
| DECLARE @grantList VARCHAR(500); | |
| DECLARE @eg_check_nouser NVARCHAR(MAX); | |
| DECLARE @currentSchema SYSNAME; | |
| DECLARE @checkRoles TABLE (RoleName SYSNAME NOT NULL); | |
| DECLARE @checkRole SYSNAME; | |
| DECLARE @checkGrants TABLE (GrantName SYSNAME NOT NULL); | |
| DECLARE @checkGrant SYSNAME; | |
| DECLARE @eg_check NVARCHAR(MAX); | |
| DECLARE @eg_count INT; | |
| DECLARE @memberExists BIT; | |
| DECLARE @inCustomRole BIT; | |
| DECLARE @crCheckRoles TABLE (RoleName SYSNAME NOT NULL); | |
| DECLARE @crCheckRole SYSNAME; | |
| DECLARE @crRoleExists BIT; | |
| DECLARE @ExpectedPrincipals TABLE (PName SYSNAME NOT NULL); | |
| DECLARE @ExtraUsers TABLE (UserName SYSNAME NOT NULL); | |
| DECLARE @extraUser SYSNAME; | |
| DECLARE @totalChanges INT; | |
| DECLARE @userRoles TABLE (RoleName SYSNAME NOT NULL); | |
| DECLARE @userGrants TABLE (GrantName SYSNAME NOT NULL); | |
| DECLARE @extraRole SYSNAME; | |
| DECLARE @extraGrant SYSNAME; | |
| DECLARE @db_REVOKE INT; | |
| -- ======================================================================== | |
| -- UNIFIED CODE PATH: single pass for both dry-run and normal execution | |
| -- Dry-run (@DryRun=1): checks state and reports [OK]/[ADD]/[MISS]/[DROP]/[REVOKE] | |
| -- Normal (@DryRun=0): checks state, reports, and acts (idempotent) | |
| -- ======================================================================== | |
| -- Initialize dry-run counters (always -- cheap, and avoids NULL in summary) | |
| SET @srv_OK = 0; SET @srv_ADD = 0; SET @srv_MISS = 0; | |
| SET @db_OK = 0; SET @db_ADD = 0; SET @db_DROP = 0; SET @db_MISS = 0; SET @db_REVOKE = 0; | |
| -- Mode-specific banners | |
| IF @DryRun = 1 | |
| BEGIN | |
| RAISERROR('=== DRY RUN MODE: No changes will be made ===', 0, 42) WITH NOWAIT; | |
| RAISERROR('=== Comparing matrix desired state vs. current server state ===', 0, 42) WITH NOWAIT; | |
| IF @EnforceMatrix = 1 | |
| RAISERROR('*** @EnforceMatrix = 1: will also show extras to REVOKE/DROP ***', 0, 42) WITH NOWAIT; | |
| END | |
| ELSE | |
| BEGIN | |
| IF @EnforceMatrix = 1 | |
| RAISERROR('*** @EnforceMatrix = 1: extras will be revoked/dropped ***', 0, 42) WITH NOWAIT; | |
| END | |
| IF @Verbose = 0 | |
| RAISERROR('(Use @Verbose = 1 for full detail)', 0, 42) WITH NOWAIT; | |
| RAISERROR('', 0, 42) WITH NOWAIT; | |
| -- ============================================================== | |
| -- PHASE 3: Server-level permissions (unified) | |
| -- ============================================================== | |
| IF @DryRun = 1 | |
| RAISERROR('=== Server-Level Permissions ===', 0, 42) WITH NOWAIT; | |
| ELSE | |
| RAISERROR('=== PHASE 3: Applying server-level permissions ===', 0, 42) WITH NOWAIT; | |
| DECLARE cur_SrvPerms CURSOR LOCAL FAST_FORWARD FOR | |
| SELECT DISTINCT PrincipalName, PrincipalType, 'ROLE', ServerRole | |
| FROM @Parsed WHERE ServerRole IS NOT NULL AND ServerRole <> '' AND @@SERVERNAME LIKE ServerPattern | |
| UNION | |
| SELECT DISTINCT PrincipalName, PrincipalType, 'GRANT', ServerGrant | |
| FROM @Parsed WHERE ServerGrant IS NOT NULL AND ServerGrant <> '' AND @@SERVERNAME LIKE ServerPattern; | |
| OPEN cur_SrvPerms; FETCH NEXT FROM cur_SrvPerms INTO @srv_Principal, @srv_PrinType, @srv_PermType, @srv_PermValue; | |
| WHILE @@FETCH_STATUS = 0 | |
| BEGIN | |
| -- Ensure Windows login exists (normal mode only, deduplicated) | |
| IF @DryRun = 0 AND @srv_PrinType = 'WINDOWS' AND NOT EXISTS (SELECT 1 FROM @EnsuredLogins WHERE LoginName = @srv_Principal) | |
| BEGIN | |
| INSERT INTO @EnsuredLogins (LoginName) VALUES (@srv_Principal); | |
| IF NOT EXISTS (SELECT 1 FROM sys.server_principals WHERE name = @srv_Principal) | |
| BEGIN | |
| SET @DynSQL = N'CREATE LOGIN ' + QUOTENAME(@srv_Principal) + N' FROM WINDOWS WITH DEFAULT_DATABASE=[master]'; | |
| BEGIN TRY EXEC sp_executesql @DynSQL; | |
| SET @MESSAGE = 'Created login [' + @srv_Principal + ']'; RAISERROR(@MESSAGE, 0, 42) WITH NOWAIT; | |
| END TRY | |
| BEGIN CATCH SET @MESSAGE = 'WARNING: Could not create login [' + @srv_Principal + ']: ' + ERROR_MESSAGE(); RAISERROR(@MESSAGE, 0, 42) WITH NOWAIT; END CATCH | |
| END | |
| END | |
| -- Check if login exists at server level | |
| IF NOT EXISTS (SELECT 1 FROM sys.server_principals WHERE name = @srv_Principal) | |
| BEGIN | |
| -- Dry-run: report MISS; Normal mode: the CREATE LOGIN above already failed or login is SQL type | |
| IF @DryRun = 1 | |
| BEGIN | |
| IF @srv_PermType = 'ROLE' | |
| SET @MESSAGE = '[MISS] Server role [' + @srv_PermValue + '] needs member [' + @srv_Principal + '] but login does not exist'; | |
| ELSE | |
| SET @MESSAGE = '[MISS] Server grant [' + @srv_PermValue + '] needs [' + @srv_Principal + '] but login does not exist'; | |
| RAISERROR(@MESSAGE, 0, 42) WITH NOWAIT; | |
| SET @srv_MISS = @srv_MISS + 1; | |
| END | |
| END | |
| ELSE IF @srv_PermType = 'ROLE' | |
| BEGIN | |
| IF EXISTS ( | |
| SELECT 1 FROM sys.server_role_members m | |
| JOIN sys.server_principals p ON p.principal_id = m.member_principal_id | |
| JOIN sys.server_principals r ON r.principal_id = m.role_principal_id | |
| WHERE p.name = @srv_Principal AND r.name = @srv_PermValue) | |
| BEGIN | |
| SET @srv_OK = @srv_OK + 1; | |
| IF @DryRun = 1 AND @Verbose >= 1 | |
| BEGIN | |
| SET @MESSAGE = '[OK] Server role [' + @srv_PermValue + '] already has member [' + @srv_Principal + ']'; | |
| RAISERROR(@MESSAGE, 0, 42) WITH NOWAIT; | |
| END | |
| END | |
| ELSE | |
| BEGIN | |
| IF @DryRun = 1 | |
| BEGIN | |
| SET @MESSAGE = '[ADD] Server role [' + @srv_PermValue + '] needs member [' + @srv_Principal + ']'; | |
| RAISERROR(@MESSAGE, 0, 42) WITH NOWAIT; | |
| SET @srv_ADD = @srv_ADD + 1; | |
| END | |
| ELSE | |
| BEGIN | |
| SET @DynSQL = N'ALTER SERVER ROLE ' + QUOTENAME(@srv_PermValue) + N' ADD MEMBER ' + QUOTENAME(@srv_Principal); | |
| BEGIN TRY EXEC sp_executesql @DynSQL; | |
| SET @MESSAGE = 'Added [' + @srv_Principal + '] to server role [' + @srv_PermValue + ']'; RAISERROR(@MESSAGE, 0, 42) WITH NOWAIT; | |
| END TRY | |
| BEGIN CATCH SET @MESSAGE = 'WARNING: Could not add [' + @srv_Principal + '] to role [' + @srv_PermValue + ']: ' + ERROR_MESSAGE(); RAISERROR(@MESSAGE, 0, 42) WITH NOWAIT; END CATCH | |
| END | |
| END | |
| END | |
| ELSE -- GRANT | |
| BEGIN | |
| IF EXISTS ( | |
| SELECT 1 FROM sys.server_permissions sp | |
| JOIN sys.server_principals pr ON sp.grantee_principal_id = pr.principal_id | |
| WHERE sp.permission_name = @srv_PermValue AND pr.name = @srv_Principal) | |
| BEGIN | |
| SET @srv_OK = @srv_OK + 1; | |
| IF @DryRun = 1 AND @Verbose >= 1 | |
| BEGIN | |
| SET @MESSAGE = '[OK] Server grant [' + @srv_PermValue + '] already granted to [' + @srv_Principal + ']'; | |
| RAISERROR(@MESSAGE, 0, 42) WITH NOWAIT; | |
| END | |
| END | |
| ELSE | |
| BEGIN | |
| IF @DryRun = 1 | |
| BEGIN | |
| SET @MESSAGE = '[ADD] Server grant [' + @srv_PermValue + '] needs to be granted to [' + @srv_Principal + ']'; | |
| RAISERROR(@MESSAGE, 0, 42) WITH NOWAIT; | |
| SET @srv_ADD = @srv_ADD + 1; | |
| END | |
| ELSE | |
| BEGIN | |
| SET @DynSQL = N'GRANT ' + @srv_PermValue + N' TO ' + QUOTENAME(@srv_Principal); | |
| BEGIN TRY EXEC sp_executesql @DynSQL; | |
| SET @MESSAGE = 'Granted [' + @srv_PermValue + '] to [' + @srv_Principal + ']'; RAISERROR(@MESSAGE, 0, 42) WITH NOWAIT; | |
| END TRY | |
| BEGIN CATCH SET @MESSAGE = 'WARNING: Could not grant [' + @srv_PermValue + '] to [' + @srv_Principal + ']: ' + ERROR_MESSAGE(); RAISERROR(@MESSAGE, 0, 42) WITH NOWAIT; END CATCH | |
| END | |
| END | |
| END | |
| FETCH NEXT FROM cur_SrvPerms INTO @srv_Principal, @srv_PrinType, @srv_PermType, @srv_PermValue; | |
| END | |
| CLOSE cur_SrvPerms; DEALLOCATE cur_SrvPerms; | |
| -- ============================================================== | |
| -- PHASE 4: Database-level security (unified) | |
| -- ============================================================== | |
| RAISERROR('', 0, 42) WITH NOWAIT; | |
| IF @DryRun = 1 | |
| RAISERROR('=== Database-Level Permissions ===', 0, 42) WITH NOWAIT; | |
| ELSE | |
| RAISERROR('=== PHASE 4: Applying database-level security ===', 0, 42) WITH NOWAIT; | |
| DECLARE cur_Databases CURSOR LOCAL FAST_FORWARD FOR | |
| SELECT name FROM sys.databases | |
| WHERE name NOT IN (SELECT DbName FROM @ExcludedDBs) | |
| AND name NOT LIKE 'ReportServer%' AND name NOT LIKE '%SNAPSHOT%' | |
| AND state_desc = 'ONLINE' | |
| AND (@TargetDatabase IS NULL OR name = @TargetDatabase) | |
| ORDER BY name; | |
| OPEN cur_Databases; FETCH NEXT FROM cur_Databases INTO @db_name; | |
| WHILE @@FETCH_STATUS = 0 | |
| BEGIN | |
| IF @DryRun = 1 | |
| BEGIN SET @MESSAGE = '--- Database: [' + @db_name + '] ---'; RAISERROR(@MESSAGE, 0, 42) WITH NOWAIT; END | |
| ELSE | |
| BEGIN SET @MESSAGE = '--- Processing database: [' + @db_name + '] ---'; RAISERROR(@MESSAGE, 0, 42) WITH NOWAIT; END | |
| -- ---- Phase 4a: Standard principals (non-CustomRole rows) ---- | |
| DECLARE cur_Principals CURSOR LOCAL FAST_FORWARD FOR | |
| SELECT DISTINCT PrincipalName, PrincipalType FROM @Parsed | |
| WHERE @@SERVERNAME LIKE ServerPattern | |
| AND (DatabaseName = @db_name OR DatabaseName = '*' | |
| OR (DatabaseName LIKE '*,-%' AND CHARINDEX(',-' + @db_name + ',', DatabaseName + ',') = 0)) | |
| AND CustomRole IS NULL ORDER BY PrincipalName; | |
| OPEN cur_Principals; FETCH NEXT FROM cur_Principals INTO @prinName, @prinType; | |
| WHILE @@FETCH_STATUS = 0 | |
| BEGIN | |
| -- Resolve schema and aggregate flags (single query set for both modes) | |
| SET @defSchema = 'dbo'; | |
| SELECT TOP 1 @defSchema = DefaultSchema FROM @Parsed | |
| WHERE PrincipalName = @prinName AND @@SERVERNAME LIKE ServerPattern | |
| AND (DatabaseName = @db_name OR DatabaseName = '*' | |
| OR (DatabaseName LIKE '*,-%' AND CHARINDEX(',-' + @db_name + ',', DatabaseName + ',') = 0)) | |
| AND CustomRole IS NULL AND DefaultSchema <> 'dbo'; | |
| SELECT @fReader = MAX(CAST(db_datareader AS INT)), @fWriter = MAX(CAST(db_datawriter AS INT)), | |
| @fDDLAdmin = MAX(CAST(db_ddladmin AS INT)), @fOwner = MAX(CAST(db_owner AS INT)), | |
| @fExec = MAX(CAST(GrantExecute AS INT)), @fViewDef = MAX(CAST(GrantViewDef AS INT)), | |
| @fShowplan = MAX(CAST(GrantShowplan AS INT)) | |
| FROM @Parsed WHERE PrincipalName = @prinName AND @@SERVERNAME LIKE ServerPattern | |
| AND (DatabaseName = @db_name OR DatabaseName = '*' | |
| OR (DatabaseName LIKE '*,-%' AND CHARINDEX(',-' + @db_name + ',', DatabaseName + ',') = 0)) | |
| AND CustomRole IS NULL; | |
| -- Check if login exists at server level | |
| IF @prinType = 'SQL' AND NOT EXISTS (SELECT 1 FROM sys.server_principals WHERE name = @prinName) | |
| BEGIN | |
| IF @DryRun = 1 | |
| BEGIN | |
| SET @MESSAGE = '[MISS] User [' + @prinName + '] -- login does not exist on server'; | |
| RAISERROR(@MESSAGE, 0, 42) WITH NOWAIT; | |
| SET @db_MISS = @db_MISS + 1; | |
| END | |
| FETCH NEXT FROM cur_Principals INTO @prinName, @prinType; | |
| CONTINUE; | |
| END | |
| -- Ensure Windows login exists (normal mode only, deduplicated) | |
| IF @prinType = 'WINDOWS' AND NOT EXISTS (SELECT 1 FROM sys.server_principals WHERE name = @prinName) | |
| BEGIN | |
| IF @DryRun = 1 | |
| BEGIN | |
| SET @MESSAGE = '[ADD] Windows login [' + @prinName + '] will be created on server'; | |
| RAISERROR(@MESSAGE, 0, 42) WITH NOWAIT; | |
| SET @db_ADD = @db_ADD + 1; | |
| -- Don't CONTINUE here -- still check database-level state | |
| END | |
| ELSE IF NOT EXISTS (SELECT 1 FROM @EnsuredLogins WHERE LoginName = @prinName) | |
| BEGIN | |
| INSERT INTO @EnsuredLogins (LoginName) VALUES (@prinName); | |
| SET @DynSQL = N'CREATE LOGIN ' + QUOTENAME(@prinName) + N' FROM WINDOWS WITH DEFAULT_DATABASE=[master]'; | |
| BEGIN TRY EXEC sp_executesql @DynSQL; | |
| SET @MESSAGE = 'Created login [' + @prinName + ']'; RAISERROR(@MESSAGE, 0, 42) WITH NOWAIT; | |
| END TRY | |
| BEGIN CATCH SET @MESSAGE = 'WARNING: Could not create login [' + @prinName + ']: ' + ERROR_MESSAGE(); RAISERROR(@MESSAGE, 0, 42) WITH NOWAIT; END CATCH | |
| END | |
| END | |
| ELSE IF @DryRun = 0 AND @prinType = 'WINDOWS' AND NOT EXISTS (SELECT 1 FROM @EnsuredLogins WHERE LoginName = @prinName) | |
| BEGIN | |
| -- Login exists but not yet in dedup table -- just register it | |
| INSERT INTO @EnsuredLogins (LoginName) VALUES (@prinName); | |
| END | |
| -- Check if database user exists (dynamic SQL to query target database) | |
| SET @exists = 0; | |
| SET @DynSQL = N'USE ' + QUOTENAME(@db_name) + N'; | |
| SELECT @exists = CASE WHEN EXISTS ( | |
| SELECT 1 FROM sys.database_principals WHERE name = @userName AND type NOT IN (''R'') | |
| ) THEN 1 ELSE 0 END'; | |
| BEGIN TRY | |
| EXEC sp_executesql @DynSQL, N'@userName SYSNAME, @exists BIT OUTPUT', | |
| @userName = @prinName, @exists = @exists OUTPUT; | |
| END TRY | |
| BEGIN CATCH | |
| IF @DryRun = 1 | |
| SET @MESSAGE = '[WARN] Could not query [' + @db_name + '] for user [' + @prinName + ']: ' + ERROR_MESSAGE(); | |
| ELSE | |
| SET @MESSAGE = 'WARNING: Could not query [' + @db_name + '] for user [' + @prinName + ']: ' + ERROR_MESSAGE(); | |
| RAISERROR(@MESSAGE, 0, 42) WITH NOWAIT; | |
| FETCH NEXT FROM cur_Principals INTO @prinName, @prinType; | |
| CONTINUE; | |
| END CATCH | |
| IF @exists = 0 | |
| BEGIN | |
| IF @DryRun = 1 | |
| BEGIN | |
| -- User does not exist: report everything that needs to be added | |
| SET @roleList = ''; | |
| IF @fReader = 1 SET @roleList = @roleList + 'db_datareader, '; | |
| IF @fWriter = 1 SET @roleList = @roleList + 'db_datawriter, '; | |
| IF @fDDLAdmin = 1 SET @roleList = @roleList + 'db_ddladmin, '; | |
| IF @fOwner = 1 SET @roleList = @roleList + 'db_owner, '; | |
| IF LEN(@roleList) > 0 SET @roleList = LEFT(@roleList, LEN(@roleList) - 1); | |
| ELSE SET @roleList = '(none)'; | |
| SET @grantList = ''; | |
| IF @fExec = 1 SET @grantList = @grantList + 'EXECUTE, '; | |
| IF @fViewDef = 1 SET @grantList = @grantList + 'VIEW DEFINITION, '; | |
| IF @fShowplan = 1 SET @grantList = @grantList + 'SHOWPLAN, '; | |
| IF LEN(@grantList) > 0 SET @grantList = LEFT(@grantList, LEN(@grantList) - 1); | |
| SET @MESSAGE = '[ADD] User [' + @prinName + '] needs to be created (schema=' + @defSchema | |
| + ', roles: ' + @roleList | |
| + CASE WHEN LEN(@grantList) > 0 THEN ', grants: ' + @grantList ELSE '' END + ')'; | |
| RAISERROR(@MESSAGE, 0, 42) WITH NOWAIT; | |
| SET @db_ADD = @db_ADD + 1; | |
| -- Report ExtraGrants that would also be applied | |
| DECLARE cur_EGCheck CURSOR LOCAL FAST_FORWARD FOR | |
| SELECT ExtraGrants FROM @Parsed WHERE PrincipalName = @prinName AND @@SERVERNAME LIKE ServerPattern | |
| AND (DatabaseName = @db_name OR DatabaseName = '*' | |
| OR (DatabaseName LIKE '*,-%' AND CHARINDEX(',-' + @db_name + ',', DatabaseName + ',') = 0)) | |
| AND CustomRole IS NULL AND ExtraGrants IS NOT NULL; | |
| OPEN cur_EGCheck; FETCH NEXT FROM cur_EGCheck INTO @extraGrants; | |
| WHILE @@FETCH_STATUS = 0 | |
| BEGIN | |
| SET @extraGrants = @extraGrants + N'|'; | |
| WHILE LEN(@extraGrants) > 1 | |
| BEGIN | |
| SET @delimPos = CHARINDEX(N'|', @extraGrants); | |
| SET @grantStmt = LTRIM(RTRIM(LEFT(@extraGrants, @delimPos - 1))); | |
| SET @extraGrants = SUBSTRING(@extraGrants, @delimPos + 1, LEN(@extraGrants)); | |
| IF LEN(@grantStmt) > 0 | |
| BEGIN | |
| SET @MESSAGE = '[ADD] User [' + @prinName + '] will also need ExtraGrant: ' + @grantStmt; | |
| RAISERROR(@MESSAGE, 0, 42) WITH NOWAIT; | |
| SET @db_ADD = @db_ADD + 1; | |
| END | |
| END | |
| FETCH NEXT FROM cur_EGCheck INTO @extraGrants; | |
| END | |
| CLOSE cur_EGCheck; DEALLOCATE cur_EGCheck; | |
| END | |
| ELSE | |
| BEGIN | |
| -- Normal mode: create the user | |
| SET @DynSQL = N'USE ' + QUOTENAME(@db_name) + N'; CREATE USER ' + QUOTENAME(@prinName) + N' WITH DEFAULT_SCHEMA=' + QUOTENAME(@defSchema) + N';'; | |
| BEGIN TRY EXEC sp_executesql @DynSQL; | |
| SET @MESSAGE = 'Created user [' + @prinName + '] in [' + @db_name + '] (schema=' + @defSchema + ')'; RAISERROR(@MESSAGE, 0, 42) WITH NOWAIT; | |
| END TRY | |
| BEGIN CATCH | |
| SET @MESSAGE = 'WARNING: Could not create user [' + @prinName + '] in [' + @db_name + ']: ' + ERROR_MESSAGE(); RAISERROR(@MESSAGE, 0, 42) WITH NOWAIT; | |
| FETCH NEXT FROM cur_Principals INTO @prinName, @prinType; CONTINUE; | |
| END CATCH | |
| END | |
| END | |
| ELSE IF @DryRun = 1 | |
| BEGIN | |
| -- User exists in dry-run: check schema | |
| SET @currentSchema = ''; | |
| SET @DynSQL = N'USE ' + QUOTENAME(@db_name) + N'; | |
| SELECT @sch = dp.default_schema_name FROM sys.database_principals dp WHERE dp.name = @userName'; | |
| BEGIN TRY | |
| EXEC sp_executesql @DynSQL, N'@userName SYSNAME, @sch SYSNAME OUTPUT', | |
| @userName = @prinName, @sch = @currentSchema OUTPUT; | |
| END TRY | |
| BEGIN CATCH SET @currentSchema = '(unknown)'; END CATCH | |
| IF ISNULL(@currentSchema, 'dbo') = @defSchema | |
| BEGIN | |
| SET @db_OK = @db_OK + 1; | |
| IF @Verbose >= 1 | |
| BEGIN | |
| SET @MESSAGE = '[OK] User [' + @prinName + '] exists with correct schema [' + @defSchema + ']'; | |
| RAISERROR(@MESSAGE, 0, 42) WITH NOWAIT; | |
| END | |
| END | |
| ELSE | |
| BEGIN | |
| SET @MESSAGE = '[ADD] User [' + @prinName + '] exists but schema is [' + ISNULL(@currentSchema,'NULL') + '], expected [' + @defSchema + ']'; | |
| RAISERROR(@MESSAGE, 0, 42) WITH NOWAIT; | |
| SET @db_ADD = @db_ADD + 1; | |
| END | |
| END | |
| -- (Normal mode with user existing: no action needed for user creation, proceed to roles/grants) | |
| -- Skip role/grant processing for dry-run when user does not exist (already reported above) | |
| IF @DryRun = 1 AND @exists = 0 | |
| BEGIN | |
| FETCH NEXT FROM cur_Principals INTO @prinName, @prinType; | |
| CONTINUE; | |
| END | |
| -- ---- Role membership checks (unified) ---- | |
| DELETE FROM @checkRoles; | |
| IF @fReader = 1 INSERT INTO @checkRoles VALUES ('db_datareader'); | |
| IF @fWriter = 1 INSERT INTO @checkRoles VALUES ('db_datawriter'); | |
| IF @fDDLAdmin = 1 INSERT INTO @checkRoles VALUES ('db_ddladmin'); | |
| IF @fOwner = 1 INSERT INTO @checkRoles VALUES ('db_owner'); | |
| DECLARE cur_Roles CURSOR LOCAL FAST_FORWARD FOR SELECT RoleName FROM @checkRoles; | |
| OPEN cur_Roles; FETCH NEXT FROM cur_Roles INTO @checkRole; | |
| WHILE @@FETCH_STATUS = 0 | |
| BEGIN | |
| SET @exists = 0; | |
| SET @DynSQL = N'USE ' + QUOTENAME(@db_name) + N'; | |
| SELECT @exists = CASE WHEN EXISTS ( | |
| SELECT 1 FROM sys.database_role_members rm | |
| JOIN sys.database_principals r ON rm.role_principal_id = r.principal_id | |
| JOIN sys.database_principals m ON rm.member_principal_id = m.principal_id | |
| WHERE r.name = @roleName AND m.name = @userName | |
| ) THEN 1 ELSE 0 END'; | |
| BEGIN TRY | |
| EXEC sp_executesql @DynSQL, N'@roleName SYSNAME, @userName SYSNAME, @exists BIT OUTPUT', | |
| @roleName = @checkRole, @userName = @prinName, @exists = @exists OUTPUT; | |
| END TRY | |
| BEGIN CATCH SET @exists = 0; END CATCH | |
| IF @exists = 1 | |
| BEGIN | |
| SET @db_OK = @db_OK + 1; | |
| IF @DryRun = 1 AND @Verbose >= 1 | |
| BEGIN | |
| SET @MESSAGE = '[OK] User [' + @prinName + '] is member of [' + @checkRole + ']'; | |
| RAISERROR(@MESSAGE, 0, 42) WITH NOWAIT; | |
| END | |
| END | |
| ELSE | |
| BEGIN | |
| IF @DryRun = 1 | |
| BEGIN | |
| SET @MESSAGE = '[ADD] User [' + @prinName + '] needs [' + @checkRole + '] membership'; | |
| RAISERROR(@MESSAGE, 0, 42) WITH NOWAIT; | |
| SET @db_ADD = @db_ADD + 1; | |
| END | |
| ELSE | |
| BEGIN | |
| SET @DynSQL = N'USE ' + QUOTENAME(@db_name) + N'; ALTER ROLE ' + QUOTENAME(@checkRole) + N' ADD MEMBER ' + QUOTENAME(@prinName) + N';'; | |
| BEGIN TRY EXEC sp_executesql @DynSQL; | |
| SET @MESSAGE = ' Added [' + @prinName + '] to [' + @checkRole + ']'; RAISERROR(@MESSAGE, 0, 42) WITH NOWAIT; | |
| END TRY | |
| BEGIN CATCH SET @MESSAGE = ' WARNING: Could not add [' + @prinName + '] to [' + @checkRole + '] in [' + @db_name + ']: ' + ERROR_MESSAGE(); RAISERROR(@MESSAGE, 0, 42) WITH NOWAIT; END CATCH | |
| END | |
| END | |
| FETCH NEXT FROM cur_Roles INTO @checkRole; | |
| END | |
| CLOSE cur_Roles; DEALLOCATE cur_Roles; | |
| -- ---- Extra roles detection/revocation (unified, @EnforceMatrix only) ---- | |
| IF @EnforceMatrix = 1 | |
| BEGIN | |
| DELETE FROM @userRoles; | |
| SET @DynSQL = N'USE ' + QUOTENAME(@db_name) + N'; | |
| SELECT r.name | |
| FROM sys.database_role_members rm | |
| JOIN sys.database_principals r ON rm.role_principal_id = r.principal_id | |
| JOIN sys.database_principals m ON rm.member_principal_id = m.principal_id | |
| WHERE m.name = @userName | |
| AND r.name IN (''db_datareader'',''db_datawriter'',''db_ddladmin'',''db_owner'')'; | |
| BEGIN TRY | |
| INSERT INTO @userRoles EXEC sp_executesql @DynSQL, N'@userName SYSNAME', @userName = @prinName; | |
| END TRY | |
| BEGIN CATCH END CATCH | |
| DECLARE cur_ExtraRoles CURSOR LOCAL FAST_FORWARD FOR | |
| SELECT RoleName FROM @userRoles WHERE RoleName NOT IN (SELECT RoleName FROM @checkRoles); | |
| OPEN cur_ExtraRoles; FETCH NEXT FROM cur_ExtraRoles INTO @extraRole; | |
| WHILE @@FETCH_STATUS = 0 | |
| BEGIN | |
| IF @DryRun = 1 | |
| BEGIN | |
| SET @MESSAGE = '[REVOKE] User [' + @prinName + '] has [' + @extraRole + '] but matrix does not specify it'; | |
| RAISERROR(@MESSAGE, 0, 42) WITH NOWAIT; | |
| SET @db_REVOKE = @db_REVOKE + 1; | |
| END | |
| ELSE | |
| BEGIN | |
| SET @DynSQL = N'USE ' + QUOTENAME(@db_name) + N'; ALTER ROLE ' + QUOTENAME(@extraRole) + N' DROP MEMBER ' + QUOTENAME(@prinName) + N';'; | |
| BEGIN TRY EXEC sp_executesql @DynSQL; | |
| SET @MESSAGE = ' Revoked [' + @prinName + '] from [' + @extraRole + '] (not in matrix)'; RAISERROR(@MESSAGE, 0, 42) WITH NOWAIT; | |
| END TRY | |
| BEGIN CATCH SET @MESSAGE = ' WARNING: Could not revoke [' + @prinName + '] from [' + @extraRole + '] in [' + @db_name + ']: ' + ERROR_MESSAGE(); RAISERROR(@MESSAGE, 0, 42) WITH NOWAIT; END CATCH | |
| END | |
| FETCH NEXT FROM cur_ExtraRoles INTO @extraRole; | |
| END | |
| CLOSE cur_ExtraRoles; DEALLOCATE cur_ExtraRoles; | |
| END | |
| -- ---- Grant checks (EXECUTE, VIEW DEFINITION, SHOWPLAN) (unified) ---- | |
| DELETE FROM @checkGrants; | |
| IF @fExec = 1 INSERT INTO @checkGrants VALUES ('EXECUTE'); | |
| IF @fViewDef = 1 INSERT INTO @checkGrants VALUES ('VIEW DEFINITION'); | |
| IF @fShowplan = 1 INSERT INTO @checkGrants VALUES ('SHOWPLAN'); | |
| DECLARE cur_Grants CURSOR LOCAL FAST_FORWARD FOR SELECT GrantName FROM @checkGrants; | |
| OPEN cur_Grants; FETCH NEXT FROM cur_Grants INTO @checkGrant; | |
| WHILE @@FETCH_STATUS = 0 | |
| BEGIN | |
| SET @exists = 0; | |
| SET @DynSQL = N'USE ' + QUOTENAME(@db_name) + N'; | |
| SELECT @exists = CASE WHEN EXISTS ( | |
| SELECT 1 FROM sys.database_permissions dp | |
| JOIN sys.database_principals pr ON dp.grantee_principal_id = pr.principal_id | |
| WHERE dp.permission_name = @grantName AND pr.name = @userName AND dp.state IN (''G'',''W'') | |
| ) THEN 1 ELSE 0 END'; | |
| BEGIN TRY | |
| EXEC sp_executesql @DynSQL, N'@grantName SYSNAME, @userName SYSNAME, @exists BIT OUTPUT', | |
| @grantName = @checkGrant, @userName = @prinName, @exists = @exists OUTPUT; | |
| END TRY | |
| BEGIN CATCH SET @exists = 0; END CATCH | |
| IF @exists = 1 | |
| BEGIN | |
| SET @db_OK = @db_OK + 1; | |
| IF @DryRun = 1 AND @Verbose >= 1 | |
| BEGIN | |
| SET @MESSAGE = '[OK] User [' + @prinName + '] has grant [' + @checkGrant + ']'; | |
| RAISERROR(@MESSAGE, 0, 42) WITH NOWAIT; | |
| END | |
| END | |
| ELSE | |
| BEGIN | |
| IF @DryRun = 1 | |
| BEGIN | |
| SET @MESSAGE = '[ADD] User [' + @prinName + '] needs grant [' + @checkGrant + ']'; | |
| RAISERROR(@MESSAGE, 0, 42) WITH NOWAIT; | |
| SET @db_ADD = @db_ADD + 1; | |
| END | |
| ELSE | |
| BEGIN | |
| SET @DynSQL = N'USE ' + QUOTENAME(@db_name) + N'; GRANT ' + @checkGrant + N' TO ' + QUOTENAME(@prinName) + N';'; | |
| BEGIN TRY EXEC sp_executesql @DynSQL; | |
| SET @MESSAGE = ' Granted [' + @checkGrant + '] to [' + @prinName + ']'; RAISERROR(@MESSAGE, 0, 42) WITH NOWAIT; | |
| END TRY | |
| BEGIN CATCH SET @MESSAGE = ' WARNING: Could not grant [' + @checkGrant + '] to [' + @prinName + '] in [' + @db_name + ']: ' + ERROR_MESSAGE(); RAISERROR(@MESSAGE, 0, 42) WITH NOWAIT; END CATCH | |
| END | |
| END | |
| FETCH NEXT FROM cur_Grants INTO @checkGrant; | |
| END | |
| CLOSE cur_Grants; DEALLOCATE cur_Grants; | |
| -- ---- Extra grants detection/revocation (unified, @EnforceMatrix only) ---- | |
| IF @EnforceMatrix = 1 | |
| BEGIN | |
| DELETE FROM @userGrants; | |
| SET @DynSQL = N'USE ' + QUOTENAME(@db_name) + N'; | |
| SELECT dp.permission_name | |
| FROM sys.database_permissions dp | |
| JOIN sys.database_principals pr ON dp.grantee_principal_id = pr.principal_id | |
| WHERE pr.name = @userName | |
| AND dp.permission_name IN (''EXECUTE'',''VIEW DEFINITION'',''SHOWPLAN'') | |
| AND dp.state IN (''G'',''W'') | |
| AND dp.class = 0'; | |
| BEGIN TRY | |
| INSERT INTO @userGrants EXEC sp_executesql @DynSQL, N'@userName SYSNAME', @userName = @prinName; | |
| END TRY | |
| BEGIN CATCH END CATCH | |
| DECLARE cur_ExtraGrants CURSOR LOCAL FAST_FORWARD FOR | |
| SELECT GrantName FROM @userGrants WHERE GrantName NOT IN (SELECT GrantName FROM @checkGrants); | |
| OPEN cur_ExtraGrants; FETCH NEXT FROM cur_ExtraGrants INTO @extraGrant; | |
| WHILE @@FETCH_STATUS = 0 | |
| BEGIN | |
| IF @DryRun = 1 | |
| BEGIN | |
| SET @MESSAGE = '[REVOKE] User [' + @prinName + '] has grant [' + @extraGrant + '] but matrix does not specify it'; | |
| RAISERROR(@MESSAGE, 0, 42) WITH NOWAIT; | |
| SET @db_REVOKE = @db_REVOKE + 1; | |
| END | |
| ELSE | |
| BEGIN | |
| SET @DynSQL = N'USE ' + QUOTENAME(@db_name) + N'; REVOKE ' + @extraGrant + N' FROM ' + QUOTENAME(@prinName) + N';'; | |
| BEGIN TRY EXEC sp_executesql @DynSQL; | |
| SET @MESSAGE = ' Revoked grant [' + @extraGrant + '] from [' + @prinName + '] (not in matrix)'; RAISERROR(@MESSAGE, 0, 42) WITH NOWAIT; | |
| END TRY | |
| BEGIN CATCH SET @MESSAGE = ' WARNING: Could not revoke grant [' + @extraGrant + '] from [' + @prinName + '] in [' + @db_name + ']: ' + ERROR_MESSAGE(); RAISERROR(@MESSAGE, 0, 42) WITH NOWAIT; END CATCH | |
| END | |
| FETCH NEXT FROM cur_ExtraGrants INTO @extraGrant; | |
| END | |
| CLOSE cur_ExtraGrants; DEALLOCATE cur_ExtraGrants; | |
| END | |
| -- ---- ExtraGrants processing (unified) ---- | |
| DECLARE cur_EG CURSOR LOCAL FAST_FORWARD FOR | |
| SELECT ExtraGrants FROM @Parsed WHERE PrincipalName = @prinName AND @@SERVERNAME LIKE ServerPattern | |
| AND (DatabaseName = @db_name OR DatabaseName = '*' | |
| OR (DatabaseName LIKE '*,-%' AND CHARINDEX(',-' + @db_name + ',', DatabaseName + ',') = 0)) | |
| AND CustomRole IS NULL AND ExtraGrants IS NOT NULL; | |
| OPEN cur_EG; FETCH NEXT FROM cur_EG INTO @extraGrants; | |
| WHILE @@FETCH_STATUS = 0 | |
| BEGIN | |
| SET @extraGrants = @extraGrants + N'|'; | |
| WHILE LEN(@extraGrants) > 1 | |
| BEGIN | |
| SET @delimPos = CHARINDEX(N'|', @extraGrants); | |
| SET @grantStmt = LTRIM(RTRIM(LEFT(@extraGrants, @delimPos - 1))); | |
| SET @extraGrants = SUBSTRING(@extraGrants, @delimPos + 1, LEN(@extraGrants)); | |
| IF LEN(@grantStmt) > 0 | |
| BEGIN | |
| IF @DryRun = 1 | |
| BEGIN | |
| IF @Verbose >= 1 | |
| BEGIN | |
| SET @MESSAGE = '[INFO] User [' + @prinName + '] has ExtraGrant configured: ' + @grantStmt; | |
| RAISERROR(@MESSAGE, 0, 42) WITH NOWAIT; | |
| END | |
| END | |
| ELSE | |
| BEGIN | |
| SET @DynSQL = N'USE ' + QUOTENAME(@db_name) + N'; ' + @grantStmt; | |
| BEGIN TRY EXEC sp_executesql @DynSQL; | |
| IF @Verbose >= 1 | |
| BEGIN SET @MESSAGE = ' ExtraGrant: ' + @grantStmt; RAISERROR(@MESSAGE, 0, 42) WITH NOWAIT; END | |
| END TRY | |
| BEGIN CATCH SET @MESSAGE = ' WARNING: ExtraGrant failed in [' + @db_name + '] for [' + @prinName + ']: ' + ERROR_MESSAGE(); RAISERROR(@MESSAGE, 0, 42) WITH NOWAIT; END CATCH | |
| END | |
| END | |
| END | |
| FETCH NEXT FROM cur_EG INTO @extraGrants; | |
| END | |
| CLOSE cur_EG; DEALLOCATE cur_EG; | |
| FETCH NEXT FROM cur_Principals INTO @prinName, @prinType; | |
| END | |
| CLOSE cur_Principals; DEALLOCATE cur_Principals; | |
| -- ---- Phase 4b: Custom roles (unified) ---- | |
| DECLARE cur_CustomRoles CURSOR LOCAL FAST_FORWARD FOR | |
| SELECT DISTINCT CustomRole, PrincipalName, PrincipalType, DefaultSchema FROM @Parsed | |
| WHERE @@SERVERNAME LIKE ServerPattern AND DatabaseName = @db_name AND CustomRole IS NOT NULL; | |
| OPEN cur_CustomRoles; FETCH NEXT FROM cur_CustomRoles INTO @cr_RoleName, @cr_Member, @cr_PrinType, @cr_Schema; | |
| WHILE @@FETCH_STATUS = 0 | |
| BEGIN | |
| -- Check if SQL login exists at server level before proceeding | |
| IF @cr_PrinType = 'SQL' AND NOT EXISTS (SELECT 1 FROM sys.server_principals WHERE name = @cr_Member) | |
| BEGIN | |
| IF @DryRun = 1 | |
| BEGIN | |
| SET @MESSAGE = '[MISS] Custom role [' + @cr_RoleName + '] member [' + @cr_Member + '] -- login does not exist on server'; | |
| RAISERROR(@MESSAGE, 0, 42) WITH NOWAIT; | |
| SET @db_MISS = @db_MISS + 1; | |
| END | |
| FETCH NEXT FROM cur_CustomRoles INTO @cr_RoleName, @cr_Member, @cr_PrinType, @cr_Schema; | |
| CONTINUE; | |
| END | |
| -- Aggregate flags for this custom role + member | |
| SELECT @fReader = MAX(CAST(db_datareader AS INT)), @fWriter = MAX(CAST(db_datawriter AS INT)), | |
| @fDDLAdmin = MAX(CAST(db_ddladmin AS INT)), @fOwner = MAX(CAST(db_owner AS INT)) | |
| FROM @Parsed WHERE CustomRole = @cr_RoleName AND PrincipalName = @cr_Member | |
| AND @@SERVERNAME LIKE ServerPattern AND DatabaseName = @db_name; | |
| -- Ensure Windows login exists (normal mode only, deduplicated) | |
| IF @DryRun = 0 AND @cr_PrinType = 'WINDOWS' AND NOT EXISTS (SELECT 1 FROM @EnsuredLogins WHERE LoginName = @cr_Member) | |
| BEGIN | |
| INSERT INTO @EnsuredLogins (LoginName) VALUES (@cr_Member); | |
| IF NOT EXISTS (SELECT 1 FROM sys.server_principals WHERE name = @cr_Member) | |
| BEGIN | |
| SET @DynSQL = N'CREATE LOGIN ' + QUOTENAME(@cr_Member) + N' FROM WINDOWS WITH DEFAULT_DATABASE=[master]'; | |
| BEGIN TRY EXEC sp_executesql @DynSQL; | |
| SET @MESSAGE = ' Created login [' + @cr_Member + ']'; RAISERROR(@MESSAGE, 0, 42) WITH NOWAIT; | |
| END TRY | |
| BEGIN CATCH SET @MESSAGE = ' WARNING: Could not create login [' + @cr_Member + ']: ' + ERROR_MESSAGE(); RAISERROR(@MESSAGE, 0, 42) WITH NOWAIT; END CATCH | |
| END | |
| END | |
| -- Check if the custom role exists | |
| SET @exists = 0; | |
| SET @DynSQL = N'USE ' + QUOTENAME(@db_name) + N'; | |
| SELECT @exists = CASE WHEN EXISTS ( | |
| SELECT 1 FROM sys.database_principals WHERE name = @roleName AND type = ''R'' | |
| ) THEN 1 ELSE 0 END'; | |
| BEGIN TRY | |
| EXEC sp_executesql @DynSQL, N'@roleName SYSNAME, @exists BIT OUTPUT', | |
| @roleName = @cr_RoleName, @exists = @exists OUTPUT; | |
| END TRY | |
| BEGIN CATCH SET @exists = 0; END CATCH | |
| IF @exists = 0 | |
| BEGIN | |
| IF @DryRun = 1 | |
| BEGIN | |
| SET @MESSAGE = '[ADD] Custom role [' + @cr_RoleName + '] needs to be created in [' + @db_name + ']'; | |
| RAISERROR(@MESSAGE, 0, 42) WITH NOWAIT; | |
| SET @db_ADD = @db_ADD + 1; | |
| END | |
| ELSE | |
| BEGIN | |
| SET @DynSQL = N'USE ' + QUOTENAME(@db_name) + N'; CREATE ROLE ' + QUOTENAME(@cr_RoleName) + N' AUTHORIZATION [dbo];'; | |
| BEGIN TRY EXEC sp_executesql @DynSQL; | |
| SET @MESSAGE = ' Created custom role [' + @cr_RoleName + '] in [' + @db_name + ']'; RAISERROR(@MESSAGE, 0, 42) WITH NOWAIT; | |
| END TRY | |
| BEGIN CATCH SET @MESSAGE = ' WARNING: Could not create role [' + @cr_RoleName + '] in [' + @db_name + ']: ' + ERROR_MESSAGE(); RAISERROR(@MESSAGE, 0, 42) WITH NOWAIT; END CATCH | |
| END | |
| END | |
| ELSE | |
| BEGIN | |
| SET @db_OK = @db_OK + 1; | |
| IF @DryRun = 1 AND @Verbose >= 1 | |
| BEGIN | |
| SET @MESSAGE = '[OK] Custom role [' + @cr_RoleName + '] exists in [' + @db_name + ']'; | |
| RAISERROR(@MESSAGE, 0, 42) WITH NOWAIT; | |
| END | |
| END | |
| -- Check if the member user exists in the database | |
| SET @memberExists = 0; | |
| SET @DynSQL = N'USE ' + QUOTENAME(@db_name) + N'; | |
| SELECT @exists = CASE WHEN EXISTS ( | |
| SELECT 1 FROM sys.database_principals WHERE name = @userName AND type NOT IN (''R'') | |
| ) THEN 1 ELSE 0 END'; | |
| BEGIN TRY | |
| EXEC sp_executesql @DynSQL, N'@userName SYSNAME, @exists BIT OUTPUT', | |
| @userName = @cr_Member, @exists = @memberExists OUTPUT; | |
| END TRY | |
| BEGIN CATCH SET @memberExists = 0; END CATCH | |
| IF @memberExists = 0 | |
| BEGIN | |
| IF @DryRun = 1 | |
| BEGIN | |
| SET @MESSAGE = '[ADD] User [' + @cr_Member + '] needs to be created in [' + @db_name + '] (schema=' + @cr_Schema + ')'; | |
| RAISERROR(@MESSAGE, 0, 42) WITH NOWAIT; | |
| SET @db_ADD = @db_ADD + 1; | |
| END | |
| ELSE | |
| BEGIN | |
| SET @DynSQL = N'USE ' + QUOTENAME(@db_name) + N'; CREATE USER ' + QUOTENAME(@cr_Member) + N' WITH DEFAULT_SCHEMA=' + QUOTENAME(@cr_Schema) + N';'; | |
| BEGIN TRY EXEC sp_executesql @DynSQL; | |
| SET @MESSAGE = ' Created user [' + @cr_Member + '] in [' + @db_name + '] (schema=' + @cr_Schema + ')'; RAISERROR(@MESSAGE, 0, 42) WITH NOWAIT; | |
| END TRY | |
| BEGIN CATCH SET @MESSAGE = ' WARNING: Could not create user [' + @cr_Member + '] in [' + @db_name + ']: ' + ERROR_MESSAGE(); RAISERROR(@MESSAGE, 0, 42) WITH NOWAIT; END CATCH | |
| END | |
| END | |
| ELSE | |
| BEGIN | |
| SET @db_OK = @db_OK + 1; | |
| IF @DryRun = 1 AND @Verbose >= 1 | |
| BEGIN | |
| SET @MESSAGE = '[OK] User [' + @cr_Member + '] exists in [' + @db_name + ']'; | |
| RAISERROR(@MESSAGE, 0, 42) WITH NOWAIT; | |
| END | |
| END | |
| -- Check if member is in the custom role (only when both role and member exist or were just created) | |
| -- In normal mode, we just created them above, so re-check @exists after potential creation | |
| IF @DryRun = 0 | |
| BEGIN | |
| -- Re-query role existence after potential creation | |
| SET @DynSQL = N'USE ' + QUOTENAME(@db_name) + N'; | |
| SELECT @exists = CASE WHEN EXISTS ( | |
| SELECT 1 FROM sys.database_principals WHERE name = @roleName AND type = ''R'' | |
| ) THEN 1 ELSE 0 END'; | |
| BEGIN TRY | |
| EXEC sp_executesql @DynSQL, N'@roleName SYSNAME, @exists BIT OUTPUT', | |
| @roleName = @cr_RoleName, @exists = @exists OUTPUT; | |
| END TRY | |
| BEGIN CATCH SET @exists = 0; END CATCH | |
| SET @DynSQL = N'USE ' + QUOTENAME(@db_name) + N'; | |
| SELECT @exists = CASE WHEN EXISTS ( | |
| SELECT 1 FROM sys.database_principals WHERE name = @userName AND type NOT IN (''R'') | |
| ) THEN 1 ELSE 0 END'; | |
| BEGIN TRY | |
| EXEC sp_executesql @DynSQL, N'@userName SYSNAME, @exists BIT OUTPUT', | |
| @userName = @cr_Member, @exists = @memberExists OUTPUT; | |
| END TRY | |
| BEGIN CATCH SET @memberExists = 0; END CATCH | |
| END | |
| IF @exists = 1 AND @memberExists = 1 | |
| BEGIN | |
| SET @inCustomRole = 0; | |
| SET @DynSQL = N'USE ' + QUOTENAME(@db_name) + N'; | |
| SELECT @exists = CASE WHEN EXISTS ( | |
| SELECT 1 FROM sys.database_role_members rm | |
| JOIN sys.database_principals r ON rm.role_principal_id = r.principal_id | |
| JOIN sys.database_principals m ON rm.member_principal_id = m.principal_id | |
| WHERE r.name = @roleName AND m.name = @userName | |
| ) THEN 1 ELSE 0 END'; | |
| BEGIN TRY | |
| EXEC sp_executesql @DynSQL, N'@roleName SYSNAME, @userName SYSNAME, @exists BIT OUTPUT', | |
| @roleName = @cr_RoleName, @userName = @cr_Member, @exists = @inCustomRole OUTPUT; | |
| END TRY | |
| BEGIN CATCH SET @inCustomRole = 0; END CATCH | |
| IF @inCustomRole = 1 | |
| BEGIN | |
| SET @db_OK = @db_OK + 1; | |
| IF @DryRun = 1 AND @Verbose >= 1 | |
| BEGIN | |
| SET @MESSAGE = '[OK] User [' + @cr_Member + '] is member of custom role [' + @cr_RoleName + ']'; | |
| RAISERROR(@MESSAGE, 0, 42) WITH NOWAIT; | |
| END | |
| END | |
| ELSE | |
| BEGIN | |
| IF @DryRun = 1 | |
| BEGIN | |
| SET @MESSAGE = '[ADD] User [' + @cr_Member + '] needs membership in custom role [' + @cr_RoleName + ']'; | |
| RAISERROR(@MESSAGE, 0, 42) WITH NOWAIT; | |
| SET @db_ADD = @db_ADD + 1; | |
| END | |
| ELSE | |
| BEGIN | |
| SET @DynSQL = N'USE ' + QUOTENAME(@db_name) + N'; ALTER ROLE ' + QUOTENAME(@cr_RoleName) + N' ADD MEMBER ' + QUOTENAME(@cr_Member) + N';'; | |
| BEGIN TRY EXEC sp_executesql @DynSQL; | |
| SET @MESSAGE = ' Added [' + @cr_Member + '] to custom role [' + @cr_RoleName + ']'; RAISERROR(@MESSAGE, 0, 42) WITH NOWAIT; | |
| END TRY | |
| BEGIN CATCH SET @MESSAGE = ' WARNING: Could not add [' + @cr_Member + '] to [' + @cr_RoleName + '] in [' + @db_name + ']: ' + ERROR_MESSAGE(); RAISERROR(@MESSAGE, 0, 42) WITH NOWAIT; END CATCH | |
| END | |
| END | |
| END | |
| -- Check if the custom role has the right standard role memberships | |
| -- Re-query @exists for role after potential creation in normal mode | |
| IF @DryRun = 0 | |
| BEGIN | |
| SET @DynSQL = N'USE ' + QUOTENAME(@db_name) + N'; | |
| SELECT @exists = CASE WHEN EXISTS ( | |
| SELECT 1 FROM sys.database_principals WHERE name = @roleName AND type = ''R'' | |
| ) THEN 1 ELSE 0 END'; | |
| BEGIN TRY | |
| EXEC sp_executesql @DynSQL, N'@roleName SYSNAME, @exists BIT OUTPUT', | |
| @roleName = @cr_RoleName, @exists = @exists OUTPUT; | |
| END TRY | |
| BEGIN CATCH SET @exists = 0; END CATCH | |
| END | |
| IF @exists = 1 | |
| BEGIN | |
| DELETE FROM @crCheckRoles; | |
| IF @fReader = 1 INSERT INTO @crCheckRoles VALUES ('db_datareader'); | |
| IF @fWriter = 1 INSERT INTO @crCheckRoles VALUES ('db_datawriter'); | |
| IF @fDDLAdmin = 1 INSERT INTO @crCheckRoles VALUES ('db_ddladmin'); | |
| IF @fOwner = 1 INSERT INTO @crCheckRoles VALUES ('db_owner'); | |
| DECLARE cur_CRRoles CURSOR LOCAL FAST_FORWARD FOR SELECT RoleName FROM @crCheckRoles; | |
| OPEN cur_CRRoles; FETCH NEXT FROM cur_CRRoles INTO @crCheckRole; | |
| WHILE @@FETCH_STATUS = 0 | |
| BEGIN | |
| SET @crRoleExists = 0; | |
| SET @DynSQL = N'USE ' + QUOTENAME(@db_name) + N'; | |
| SELECT @exists = CASE WHEN EXISTS ( | |
| SELECT 1 FROM sys.database_role_members rm | |
| JOIN sys.database_principals r ON rm.role_principal_id = r.principal_id | |
| JOIN sys.database_principals m ON rm.member_principal_id = m.principal_id | |
| WHERE r.name = @stdRole AND m.name = @customRole | |
| ) THEN 1 ELSE 0 END'; | |
| BEGIN TRY | |
| EXEC sp_executesql @DynSQL, N'@stdRole SYSNAME, @customRole SYSNAME, @exists BIT OUTPUT', | |
| @stdRole = @crCheckRole, @customRole = @cr_RoleName, @exists = @crRoleExists OUTPUT; | |
| END TRY | |
| BEGIN CATCH SET @crRoleExists = 0; END CATCH | |
| IF @crRoleExists = 1 | |
| BEGIN | |
| SET @db_OK = @db_OK + 1; | |
| IF @DryRun = 1 AND @Verbose >= 1 | |
| BEGIN | |
| SET @MESSAGE = '[OK] Custom role [' + @cr_RoleName + '] is member of [' + @crCheckRole + ']'; | |
| RAISERROR(@MESSAGE, 0, 42) WITH NOWAIT; | |
| END | |
| END | |
| ELSE | |
| BEGIN | |
| IF @DryRun = 1 | |
| BEGIN | |
| SET @MESSAGE = '[ADD] Custom role [' + @cr_RoleName + '] needs [' + @crCheckRole + '] membership'; | |
| RAISERROR(@MESSAGE, 0, 42) WITH NOWAIT; | |
| SET @db_ADD = @db_ADD + 1; | |
| END | |
| ELSE | |
| BEGIN | |
| SET @DynSQL = N'USE ' + QUOTENAME(@db_name) + N'; ALTER ROLE ' + QUOTENAME(@crCheckRole) + N' ADD MEMBER ' + QUOTENAME(@cr_RoleName) + N';'; | |
| BEGIN TRY EXEC sp_executesql @DynSQL; | |
| SET @MESSAGE = ' Added role [' + @cr_RoleName + '] to [' + @crCheckRole + ']'; RAISERROR(@MESSAGE, 0, 42) WITH NOWAIT; | |
| END TRY | |
| BEGIN CATCH SET @MESSAGE = ' WARNING: Could not add [' + @cr_RoleName + '] to [' + @crCheckRole + '] in [' + @db_name + ']: ' + ERROR_MESSAGE(); RAISERROR(@MESSAGE, 0, 42) WITH NOWAIT; END CATCH | |
| END | |
| END | |
| FETCH NEXT FROM cur_CRRoles INTO @crCheckRole; | |
| END | |
| CLOSE cur_CRRoles; DEALLOCATE cur_CRRoles; | |
| END | |
| FETCH NEXT FROM cur_CustomRoles INTO @cr_RoleName, @cr_Member, @cr_PrinType, @cr_Schema; | |
| END | |
| CLOSE cur_CustomRoles; DEALLOCATE cur_CustomRoles; | |
| -- ---- Extra user detection/drop (unified, @EnforceMatrix only) ---- | |
| IF @EnforceMatrix = 1 | |
| BEGIN | |
| -- Build a list of all principals the matrix expects in this database | |
| DELETE FROM @ExpectedPrincipals; | |
| INSERT INTO @ExpectedPrincipals (PName) | |
| SELECT DISTINCT PrincipalName FROM @Parsed | |
| WHERE @@SERVERNAME LIKE ServerPattern | |
| AND (DatabaseName = @db_name OR DatabaseName = '*' | |
| OR (DatabaseName LIKE '*,-%' AND CHARINDEX(',-' + @db_name + ',', DatabaseName + ',') = 0)); | |
| -- Get actual non-system users from this database and compare | |
| DELETE FROM @ExtraUsers; | |
| SET @DynSQL = N'USE ' + QUOTENAME(@db_name) + N'; | |
| SELECT Name FROM sys.database_principals | |
| WHERE type NOT IN (''R'',''C'') AND sid IS NOT NULL AND principal_id > 4'; | |
| BEGIN TRY | |
| INSERT INTO @ExtraUsers EXEC sp_executesql @DynSQL; | |
| END TRY | |
| BEGIN CATCH | |
| IF @DryRun = 1 | |
| SET @MESSAGE = '[WARN] Could not query users in [' + @db_name + ']: ' + ERROR_MESSAGE(); | |
| ELSE | |
| SET @MESSAGE = 'WARNING: Could not query users in [' + @db_name + ']: ' + ERROR_MESSAGE(); | |
| RAISERROR(@MESSAGE, 0, 42) WITH NOWAIT; | |
| END CATCH | |
| DECLARE cur_ExtraUsers CURSOR LOCAL FAST_FORWARD FOR | |
| SELECT UserName FROM @ExtraUsers WHERE UserName NOT IN (SELECT PName FROM @ExpectedPrincipals) | |
| AND UserName NOT IN (SELECT ISNULL(CustomRole,'') FROM @Parsed | |
| WHERE @@SERVERNAME LIKE ServerPattern | |
| AND (DatabaseName = @db_name OR DatabaseName = '*' | |
| OR (DatabaseName LIKE '*,-%' AND CHARINDEX(',-' + @db_name + ',', DatabaseName + ',') = 0)) | |
| AND CustomRole IS NOT NULL) | |
| ORDER BY UserName; | |
| OPEN cur_ExtraUsers; FETCH NEXT FROM cur_ExtraUsers INTO @extraUser; | |
| WHILE @@FETCH_STATUS = 0 | |
| BEGIN | |
| IF @DryRun = 1 | |
| BEGIN | |
| SET @MESSAGE = '[DROP] User [' + @extraUser + '] exists in [' + @db_name + '] but is not in the matrix'; | |
| RAISERROR(@MESSAGE, 0, 42) WITH NOWAIT; | |
| SET @db_DROP = @db_DROP + 1; | |
| END | |
| ELSE | |
| BEGIN | |
| SET @DynSQL = N'USE ' + QUOTENAME(@db_name) + N'; DROP USER ' + QUOTENAME(@extraUser) + N';'; | |
| BEGIN TRY EXEC sp_executesql @DynSQL; | |
| SET @MESSAGE = ' Dropped extra user [' + @extraUser + '] from [' + @db_name + '] (not in matrix)'; RAISERROR(@MESSAGE, 0, 42) WITH NOWAIT; | |
| END TRY | |
| BEGIN CATCH SET @MESSAGE = ' WARNING: Could not drop user [' + @extraUser + '] from [' + @db_name + ']: ' + ERROR_MESSAGE(); RAISERROR(@MESSAGE, 0, 42) WITH NOWAIT; END CATCH | |
| END | |
| FETCH NEXT FROM cur_ExtraUsers INTO @extraUser; | |
| END | |
| CLOSE cur_ExtraUsers; DEALLOCATE cur_ExtraUsers; | |
| END | |
| FETCH NEXT FROM cur_Databases INTO @db_name; | |
| END | |
| CLOSE cur_Databases; DEALLOCATE cur_Databases; | |
| -- ============================================================== | |
| -- Final output | |
| -- ============================================================== | |
| IF @DryRun = 1 | |
| BEGIN | |
| -- Dry-run summary | |
| RAISERROR('', 0, 42) WITH NOWAIT; | |
| RAISERROR('=== DRY RUN SUMMARY ===', 0, 42) WITH NOWAIT; | |
| SET @MESSAGE = 'Server-level: ' + CAST(@srv_OK AS VARCHAR) + ' OK, ' | |
| + CAST(@srv_ADD AS VARCHAR) + ' to ADD' | |
| + CASE WHEN @srv_MISS > 0 THEN ', ' + CAST(@srv_MISS AS VARCHAR) + ' MISSING login' ELSE '' END; | |
| RAISERROR(@MESSAGE, 0, 42) WITH NOWAIT; | |
| SET @MESSAGE = 'Database-level: ' + CAST(@db_OK AS VARCHAR) + ' OK, ' | |
| + CAST(@db_ADD AS VARCHAR) + ' to ADD, ' | |
| + CAST(@db_DROP AS VARCHAR) + ' to DROP, ' | |
| + CAST(@db_REVOKE AS VARCHAR) + ' to REVOKE' | |
| + CASE WHEN @db_MISS > 0 THEN ', ' + CAST(@db_MISS AS VARCHAR) + ' MISSING login' ELSE '' END; | |
| RAISERROR(@MESSAGE, 0, 42) WITH NOWAIT; | |
| SET @totalChanges = @srv_ADD + @srv_MISS + @db_ADD + @db_DROP + @db_MISS + @db_REVOKE; | |
| SET @MESSAGE = 'Total changes needed: ' + CAST(@totalChanges AS VARCHAR); | |
| RAISERROR(@MESSAGE, 0, 42) WITH NOWAIT; | |
| IF @EnforceMatrix = 0 | |
| RAISERROR('(Note: @EnforceMatrix = 0 -- extras will not be revoked/dropped. Use @EnforceMatrix = 1 to include.)', 0, 42) WITH NOWAIT; | |
| END | |
| ELSE | |
| BEGIN | |
| RAISERROR('=== Security configuration complete ===', 0, 42) WITH NOWAIT; | |
| END | |
| END TRY | |
| BEGIN CATCH | |
| SELECT @ErrorMessage = ERROR_MESSAGE(), @ErrorSeverity = ERROR_SEVERITY(), @ErrorState = ERROR_STATE(); | |
| RAISERROR(@ErrorMessage, @ErrorSeverity, @ErrorState); | |
| END CATCH | |
| GO |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment