Automation: Granting Read Perms for Developers

on August 16, 2010

Yeah, you heard me.

“But Kendra, why would we want to grant developers read  permissions? And why would we automate  it? And at what point did you lose your mind?”

Well, Virginia, there may or may not be a Santa Claus, but there are a lot of developers and good reasons to give them read access on many SQL Server instances. In my world, it’s every instance in the pre-production environment, which is a couple  hundred and growing. And it ain’t just read they’re needing, they should be empowered with all of:

  • In master: View server state– so they can look at all those pesky spids they can block, and which may be causing problems they’re investigating.
  • In msdb:
    • db_datareader – so they can query things not easily seen through the GUI
    • SQLAgentReaderRole – so they can look at currently executing jobs and history through the GUI
  • In each user database:
    • db_datareader – for troubleshooting, troublemaking, and general read-i-ness
    • view definition – so they can see what they’re dealing with
    • showplan – so there ain’t no excuse if they’re causing issues, and also to help further with the troubleshooting

And of course a user needs to be created in msdb and the user databases for this all to work out. Things to look out for: SQL Server will let you create a login and apply database level permissions with T-SQL without the user being created, but then you’ll find it doesn’t work.

When you’re dealing with a large pre-production environment and a large group  of developers (> 75 total) who periodically change roles, this permission set can be a bit complicated to manage.

How I Rigged this Up

In my implementation, this script is in a job on an MSX master server, and our server build steps for pre-production servers include configuring  the instance as a TSX subscriber and adding the job. The job is scheduled to run daily, so if new databases are created, restored, or dropped and recreated, permissions will automatically be re-applied. I like using the MSX  job for the implementation because it’s easy for our DBA team to see the job on the instance and see exactly what it does, and if there’s any reason to exempt an instance then that’s very simple to do.

So with this solution, I don’t have to worry about the mechanics of applying read permissions. I just have to worry about making sure the right people are in the active directory group, and I can pass that task on to their managers.

Important_:_ Because of how our active directory and firewalls are configured, I do not have to worry about  this job being accidentally deployed to a production server and granting read where it should not. Something to think  about! Don’t automate yourself into security violations. This was another reason I chose the relatively “transparent” solution of an MSX subscriber job rather  than other solutions.

The Script

Disclaimer: This script was tossed together quickly. If you read the story above, you know I wasn’t even working on it for production. It supports windows groups only  in its current form. (Please don’t use sql authentication to grant read. Domain groups are so much more auditable and controllable!) It grants permissions directly to a login and does not create a role in each database. This is for a good reason in my world, but the best practice is to create a role, so keep that in mind.

As always, test the stuffing out of this and edit as needed if you’d like to make use  of it.

SET nocount ON ;

DECLARE @login_name SYSNAME = 'WAGGERTAIL\IWantToRead';
DECLARE @perms NVARCHAR(MAX)
DECLARE @debug bit
DECLARE @error_message NVARCHAR(4000);

SET @debug=0

/*********************
* Master perms
*********************/
USE [master] ;

--Create Login if needed
IF ( SELECT COUNT(*)
 FROM   sys.server_principals
 WHERE  name = @login_name
 ) = 0
 BEGIN
 PRINT '--Creating Login ' + QUOTENAME(@login_name) + ' on ' + @@SERVERNAME
 SET @perms = 'CREATE LOGIN ' + QUOTENAME(@login_name) + ' FROM WINDOWS WITH DEFAULT_DATABASE=[master], DEFAULT_LANGUAGE=[us_english] ;'

 IF @debug=1 PRINT @PERMS
 ELSE
 begin try
 exec sp_executesql @perms ;
 end try
 begin catch
 SELECT @error_message = ERROR_MESSAGE();
 RAISERROR (@error_message, 16, 2 );
 end catch
 END
ELSE
 PRINT '--Login already exists on ' + @@SERVERNAME

--If the login still doesn't exist, just return, something's wrong...
IF ( SELECT COUNT(*)
 FROM   sys.server_principals
 WHERE  name = @login_name
 ) = 0
BEGIN
 SELECT @error_message = 'We didn''t create our login... what''s going wrong??'
 RAISERROR (@error_message, 16, 2 );
 RETURN;
END

--Grant view server state if needed
IF ( SELECT COUNT(*)
 FROM   sys.server_principals users
 JOIN sys.server_permissions prm ON users.principal_id = prm.grantee_principal_id
 WHERE  users.name = @login_name
 AND permission_name = 'VIEW SERVER STATE'
 ) = 0
 BEGIN
 PRINT '--Granting Server State on ' + @@SERVERNAME + ' TO ' + QUOTENAME(@login_name);
 SET @perms = 'GRANT VIEW SERVER STATE TO ' + QUOTENAME(@login_name);

 IF @debug=1 PRINT @PERMS
 ELSE
 begin try
 exec sp_executesql @perms ;
 end try
 begin catch
 SELECT @error_message = ERROR_MESSAGE();
 RAISERROR (@error_message, 16, 2 );
 end catch
 END
ELSE
 PRINT '--View Server State already granted on ' + @@SERVERNAME

/*********************
* MSDB perms
*********************/
USE [msdb] ;

-- Create user if needed
IF ( SELECT COUNT(*)
 FROM   sys.database_principals users
 WHERE  users.name = @login_name
 AND users.type = 'G' -- Windows Group
 ) = 0
 BEGIN
 PRINT CHAR(10) + '--Working on msdb...'

 PRINT '--Creating user...'
 SET @perms = 'CREATE USER ' + QUOTENAME(@login_name) + ' FOR LOGIN ' + QUOTENAME(@login_name);

 IF @debug=1 PRINT @PERMS
 ELSE
 begin try
 exec sp_executesql @perms ;
 end try
 begin catch
 SELECT @error_message = ERROR_MESSAGE();
 RAISERROR (@error_message, 16, 2 );
 end catch
 END
ELSE
 PRINT '--User ' + QUOTENAME(@login_name) + ' already created in MSDB'

IF ( SELECT COUNT(*)
 FROM   sys.database_principals dbrole
 JOIN sys.database_role_members rel ON rel.role_principal_id = dbrole.principal_id
 JOIN sys.database_principals mem ON rel.member_principal_id = mem.principal_id
 AND mem.name = @login_name
 WHERE  dbrole.name = 'db_datareader'
 ) = 0
 BEGIN

 PRINT '--Granting datareader...'
 IF @debug=1 PRINT 'EXEC sp_addrolemember N''db_datareader'',' +  @login_name
 ELSE
 EXEC sp_addrolemember N'db_datareader', @login_name
 END
ELSE
 PRINT '--Datareader for ' + @login_name + ' already granted in MSDB'

IF ( SELECT COUNT(*)
 FROM   sys.database_principals dbrole
 JOIN sys.database_role_members rel ON rel.role_principal_id = dbrole.principal_id
 JOIN sys.database_principals mem ON rel.member_principal_id = mem.principal_id
 AND mem.name = @login_name
 WHERE  dbrole.name = 'SQLAgentReaderRole'
 ) = 0
 BEGIN
 PRINT '--Granting SQLAgentReaderRole...'

 IF @debug=1 PRINT 'EXEC sp_addrolemember N''SQLAgentReaderRole'',' + @login_name
 ELSE
 EXEC sp_addrolemember N'SQLAgentReaderRole', @login_name
 END
ELSE
 PRINT '--SQLAgentReaderRole for ' + @login_name + ' already granted in MSDB'

/******************************************
* Loop through user dbs and set perms...
******************************************/

DECLARE @dbs TABLE ( dbname SYSNAME )

DECLARE @dbname SYSNAME ;

INSERT  @dbs
 SELECT  name
 FROM    sys.databases
 WHERE   database_id > 4

WHILE ( SELECT  COUNT(*)
 FROM    @dbs
 ) > 0
 BEGIN
 SELECT TOP 1
 @dbname = dbname
 FROM    @dbs

 PRINT CHAR(10) + '--Working on ' + QUOTENAME(@dbName) +  '...'

 SELECT  @perms = '
 use ' + QUOTENAME(@dbName) +  '
 if (select count(*)
 from sys.database_principals users
 where users.name=' + QUOTENAME(@login_name,'''') + '
 and users.type=''G'' -- Windows Group
 ) = 0
 BEGIN
 print ''--Creating user ''' + QUOTENAME(@login_name,'''') + ''' on '' + DB_NAME()
 CREATE USER ' + QUOTENAME(@login_name) + ' FOR LOGIN ' + QUOTENAME(@login_name) + '
 END
 ELSE
 print ''--User ' + QUOTENAME(@login_name) + ' already created in '' + @@SERVERNAME + ''.'' + DB_NAME()
 '
 if @debug=1 PRINT @PERMS
 else
 begin try
 exec sp_executesql @perms ;
 end try
 begin catch
 SELECT @error_message = ERROR_MESSAGE();
 RAISERROR (@error_message, 16, 2 );
 end catch

 SELECT  @perms = '
 use ' + QUOTENAME(@dbName) +  '
 if (select count(*)
 from sys.database_principals dbrole
 join sys.database_role_members rel on
 rel.role_principal_id=dbrole.principal_id
 join sys.database_principals mem on
 rel.member_principal_id=mem.principal_id
 and mem.name=' + QUOTENAME(@login_name,'''') + '
 where dbrole.name = ''db_datareader''
 ) = 0
 begin
 print ''--Granting db_datareader ''' + QUOTENAME(@login_name,'''') + ''' on '' + DB_NAME()
 exec sp_addrolemember @rolename=''db_datareader'', @membername=' + QUOTENAME(@login_name,'''') + '
 END
 ELSE
 print ''--VIEW DEFINITION already granted to ''' + QUOTENAME(@login_name,'''') + ''' on '' + DB_NAME()
 '
 if @debug=1 PRINT @PERMS
 else
 begin try
 exec sp_executesql @perms ;
 end try
 begin catch
 SELECT @error_message = ERROR_MESSAGE();
 RAISERROR (@error_message, 16, 2 );
 end catch

 SELECT  @perms = '
 use ' + QUOTENAME(@dbName) +  '
 if (select count(*)
 from sys.database_principals users
 join sys.database_permissions prm on
 users.principal_id =prm.grantee_principal_id
 where users.name=' + QUOTENAME(@login_name,'''') + '
 and permission_name=''VIEW DEFINITION''
 ) = 0
 begin
 print ''--Granting VIEW DEFINITION ''' + QUOTENAME(@login_name,'''') + ''' on '' + DB_NAME()
 GRANT VIEW DEFINITION TO ' + QUOTENAME(@login_name) + '
 end
 ELSE
 print ''--VIEW DEFINITION already granted to ''' + QUOTENAME(@login_name,'''') + ''' on '' + DB_NAME()
 '
 if @debug=1 PRINT @PERMS
 else
 begin try
 exec sp_executesql @perms ;
 end try
 begin catch
 SELECT @error_message = ERROR_MESSAGE();
 RAISERROR (@error_message, 16, 2 );
 end catch

 SELECT  @perms = '
 use ' + QUOTENAME(@dbName) +  '
 if (select count(*)
 from sys.database_principals users
 join sys.database_permissions prm on
 users.principal_id =prm.grantee_principal_id
 where users.name=' + QUOTENAME(@login_name,'''') + '
 and permission_name=''SHOWPLAN''
 ) = 0
 begin
 print ''--Granting SHOWPLAN ''' + QUOTENAME(@login_name,'''') + ''' on '' + DB_NAME()
 GRANT SHOWPLAN TO ' + QUOTENAME(@login_name) + '
 end
 ELSE
 print ''--SHOWPLAN already granted to ''' + QUOTENAME(@login_name,'''') + ''' on '' + DB_NAME()
 '
 if @debug=1 PRINT @PERMS
 else
 begin try
 exec sp_executesql @perms ;
 end try
 begin catch
 SELECT @error_message = ERROR_MESSAGE();
 RAISERROR (@error_message, 16, 2 );
 end catch

 -- Move on to the next DB
 DELETE  FROM @dbs
 WHERE   dbname = @dbname

 END