By: K. Brian Kelley | Comments (5) | Related: 1 | 2 | 3 | > Security
Problem
I have read part 1 and part 2 of your tip series and the tips are great. I have a large environment with numerous SQL Server databases where I need to clone logins. How can I automate this code to save time and ensure accuracy?
Solution
If you have a lot of databases, running the queries (from the second tip) manually against each one and then copying the output into a query window and executing that query will take forever. Let's automate some of this.
First, let's wrap the permissions query into a stored procedure where the generated T-SQL code gets executed for us. We will break up the long SELECT statement into the individual parts and insert the commands into a temporary table. Note that I'm building this stored procedure in a database called DBAWork. Because of that, if I want the permissions in another database, I have to use the three part naming convention (database.schema.object) in order to get to the correct catalog views. Finally, when granting permissions, I have to ensure I'm in the database where the permissions are needed. That's why we'll prepend every T-SQL statement with a USE command.
If we look at Jimmy in TestA before we execute anything, we see he has no permissions:
When we execute the stored procedure, we'll see what permissions are being assigned:
And now when we check Jimmy, we'll see the new permissions. Well, you may not see the database level permissions I've highlighted below. In order to see it, click on the Search button, select the Specific Objects option, and click on Database and specify the TestA Database. It should then appear as a securable in the dialog window like you see below:
Now let's also build stored procedures to create the user and to handle the role memberships.
First, creating the user:
USE DBAWork; GO CREATE PROC dbo.CreateUserInDB @NewLogin sysname, @LoginToClone sysname, @DBName sysname AS BEGIN SET NOCOUNT ON; DECLARE @TSQL nvarchar(MAX); DECLARE @Return int; BEGIN TRAN; SET @TSQL = 'USE [' + @DBName + ']; IF EXISTS(SELECT name FROM sys.database_principals WHERE name = ''' + @LoginToClone + ''') BEGIN CREATE USER [' + @NewLogin + '] FROM LOGIN [' + @NewLogin + ']; END;'; EXEC @Return = sp_executesql @TSQL; IF (@Return <> 0) BEGIN ROLLBACK TRAN; RAISERROR('Error creating user', 16, 1); RETURN(1); END; COMMIT TRAN; END;
Then, assigning the user to roles:
USE DBAWork; GO CREATE PROC dbo.GrantUserRoleMembership @NewLogin sysname, @LoginToClone sysname, @DBName sysname AS BEGIN SET NOCOUNT ON; DECLARE @TSQL nvarchar(MAX); DECLARE @Return int; BEGIN TRAN; CREATE TABLE #RoleMembershipSQL ( RoleMembersTSQL nvarchar(MAX) ); SET @TSQL = 'INSERT INTO #RoleMembershipSQL (RoleMembersTSQL) SELECT ''EXEC sp_addrolemember @rolename = '''''' + r.name + '''''', @membername = ''''' + @NewLogin + ''''';'' FROM [' + @DBName + '].sys.database_principals AS U JOIN [' + @DBName + '].sys.database_role_members AS RM ON U.principal_id = RM.member_principal_id JOIN [' + @DBName + '].sys.database_principals AS R ON RM.role_principal_id = R.principal_id WHERE U.name = ''' + @LoginToClone + ''';'; EXEC @Return = sp_executesql @TSQL; IF (@Return <> 0) BEGIN ROLLBACK TRAN; RAISERROR('Could not retrieve role memberships.', 16, 1); RETURN(1) END; DECLARE cursDBRoleMembersSQL CURSOR FAST_FORWARD FOR SELECT RoleMembersTSQL FROM #RoleMembershipSQL; OPEN cursDBRoleMembersSQL; FETCH FROM cursDBRoleMembersSQL INTO @TSQL; WHILE (@@FETCH_STATUS = 0) BEGIN SET @TSQL = 'USE [' + @DBName + ']; ' + @TSQL; EXECUTE @Return = sp_executesql @TSQL; IF (@RETURN <> 0) BEGIN ROLLBACK TRAN; RAISERROR('Error encountered assigning DB role memberships.', 16, 1); CLOSE cursDBRoleMembersSQL; DEALLOCATE cursDBRoleMembersSQL; END; FETCH NEXT FROM cursDBRoleMembersSQL INTO @TSQL; END; CLOSE cursDBRoleMembersSQL; DEALLOCATE cursDBRoleMembersSQL; DROP TABLE #RoleMembershipSQL; COMMIT TRAN; END;
The Master Script:
Now that we have all of our stored procedures, let's wrap everything up into one stored procedure to create the login, grant server level permissions, and then handle each database in turn:
USE DBAWork; GO CREATE PROC dbo.CloneLoginAndAllDBPerms @NewLogin sysname, @NewLoginPwd NVARCHAR(MAX), @WindowsLogin CHAR(1), @LoginToClone sysname AS BEGIN SET NOCOUNT ON; DECLARE @Return int; BEGIN TRAN; EXEC @Return = DBAWork.dbo.CloneLogin @NewLogin = @NewLogin, @NewLoginPwd = @NewLoginPwd, @WindowsLogin = @WindowsLogin, @LoginToClone = @LoginToClone; IF (@Return <> 0) BEGIN ROLLBACK TRAN; RAISERROR('Exiting because login could not be created', 16, 1); RETURN(1); END DECLARE @DBName sysname; DECLARE @SQL nvarchar(MAX); DECLARE cursDBs CURSOR FAST_FORWARD FOR SELECT name FROM sys.databases WHERE state_desc = 'ONLINE'; OPEN cursDBs; FETCH FROM cursDBs INTO @DBName; WHILE (@@FETCH_STATUS = 0) BEGIN EXEC @Return = DBAWork.dbo.CreateUserInDB @NewLogin = @NewLogin, @LoginToClone = @LoginToClone, @DBName = @DBName; IF (@Return <> 0) BEGIN ROLLBACK TRAN; RAISERROR('Exiting because user could not be created.', 16, 1); RETURN(1); END; EXEC @Return = DBAWork.dbo.GrantUserRoleMembership @NewLogin = @NewLogin, @LoginToClone = @LoginToClone, @DBName = @DBName; IF (@Return <> 0) BEGIN ROLLBACK TRAN; RAISERROR('Exiting because role meberships could not be granted.', 16, 1); RETURN(1); END; EXEC @Return = DBAWork.dbo.CloneDBPerms @NewLogin = @NewLogin, @LoginToClone = @LoginToClone, @DBName = @DBName; IF (@Return <> 0) BEGIN ROLLBACK TRAN; RAISERROR('Exiting because user could not be created.', 16, 1); RETURN(1); END; FETCH NEXT FROM cursDBs INTO @DBName; END; CLOSE cursDBs; DEALLOCATE cursDBs; COMMIT TRAN; END; GO
In order to test this, we'll need to get rid of Jimmy. Here's a quick clean up script that will do this:
USE TestA; GO IF EXISTS(SELECT name FROM sys.database_principals WHERE name = 'Jimmy') DROP USER Jimmy; GO USE TestB; GO IF EXISTS(SELECT name FROM sys.database_principals WHERE name = 'Jimmy') DROP USER Jimmy; GO USE master; GO IF EXISTS (SELECT name FROM sys.server_principals WHERE name = 'Jimmy') DROP LOGIN Jimmy; GO
And once Jimmy is cleaned up, let's re-add him with our one cloning stored procedure:
EXEC DBAWork.dbo.CloneLoginAndAllDBPerms @NewLogin = 'Jimmy', @NewLoginPwd = 'SomeOtherPassw0rd!', @LoginToClone = 'Bobby', @WindowsLogin = 'F';
Now, if we check with a few test queries, we see that Jimmy has been handled as a login:
Has been granted the appropriate memberships in permissions in the TestA database:
And is set up in TestB database as well:
Next Steps
- Review the first tip in the series on cloning SQL Server logins.
- Review the second tip on the scripts for cloning SQL Server logins.
- Learn why you should default to Windows groups when possible as a best practice.
- Find out what tables and views to use to audit logins.
About the author
This author pledges the content of this article is based on professional experience and not AI generated.
View all my tips