Fully Script out a MSSQL Database Role

A New Hope

SQL Server Management Studio is a phenomenal tool, especially when you want to script out complex tasks.  Do whatever you want in the GUI and click the handy little script button instead of OK and you’ve got a set of raw SQL that you can modify/reuse/etc.  This process works for a good majority of the operations you will find yourself doing as a DBA, but there’s one gaping hole that MS has yet to fill: scripting out Database Roles.  If you manage any level of database security, chances are you are using Database Roles.  If you were lucky enough to create them yourself, it’s not difficult to keep a script library to see what role allows for what permission(s).  However, the moment you inherit a database that contains custom Database Roles, you enter a world of hurt.  Traditionally, you can either painstakingly struggle with the GUI trying to associate various records and checkboxes with actual commands or you can throw in the towel and start from scratch.  Thankfully, there is a new way, as I’ve scripted out a process that will generate the role definition for you.

Tada!

Here’s the code; copy/paste and modify the role name, and wah-lah  you are now in the possession of the role definition.  If you find any bugs, please let me know with a comment.  Thanks!

/********************************************************************
 *                                                                  *
 * Author: John Eisbrener                                           *
 * Script Purpose: Script out Database Role Definition              *
 * Notes: Please report any bugs to http://www.dbaeyes.com/         *
 *                                                                  *
 * Update: 2014-03-03 - Adjusted output to accommodate Role         *
 *                      definitions that are longer than 8000 chars *
 * Update: 2013-09-03 - Added user output per Joe Spivey's comment  *
 *                    - Modified formatting for oddly named objects *
 *                    - Included support for Grants on DMVs         *
 ********************************************************************/
DECLARE @roleName VARCHAR(255)
SET @roleName = 'DatabaseRoleName'

-- Script out the Role
DECLARE @roleDesc VARCHAR(MAX), @crlf VARCHAR(2)
SET @crlf = CHAR(13) + CHAR(10)
SET @roleDesc = 'CREATE ROLE [' + @roleName + ']' + @crlf + 'GO' + @crlf + @crlf

SELECT    @roleDesc = @roleDesc +
        CASE dp.state
            WHEN 'D' THEN 'DENY '
            WHEN 'G' THEN 'GRANT '
            WHEN 'R' THEN 'REVOKE '
            WHEN 'W' THEN 'GRANT '
        END + 
        dp.permission_name + ' ' +
        CASE dp.class
            WHEN 0 THEN ''
            WHEN 1 THEN --table or column subset on the table
                CASE WHEN dp.major_id < 0 THEN
                    + 'ON [sys].[' + OBJECT_NAME(dp.major_id) + '] '
                ELSE
                    + 'ON [' +
                    (SELECT SCHEMA_NAME(schema_id) + '].[' + name FROM sys.objects WHERE object_id = dp.major_id)
                        + -- optionally concatenate column names
                    CASE WHEN MAX(dp.minor_id) > 0 
                         THEN '] ([' + REPLACE(
                                        (SELECT name + '], [' 
                                         FROM sys.columns 
                                         WHERE object_id = dp.major_id 
                                            AND column_id IN (SELECT minor_id 
                                                              FROM sys.database_permissions 
                                                              WHERE major_id = dp.major_id
                                                                AND USER_NAME(grantee_principal_id) IN (@roleName)
                                                             )
                                         FOR XML PATH('')
                                        ) --replace final square bracket pair
                                    + '])', ', []', '')
                         ELSE ']'
                    END + ' '
                END
            WHEN 3 THEN 'ON SCHEMA::[' + SCHEMA_NAME(dp.major_id) + '] '
            WHEN 4 THEN 'ON ' + (SELECT RIGHT(type_desc, 4) + '::[' + name FROM sys.database_principals WHERE principal_id = dp.major_id) + '] '
            WHEN 5 THEN 'ON ASSEMBLY::[' + (SELECT name FROM sys.assemblies WHERE assembly_id = dp.major_id) + '] '
            WHEN 6 THEN 'ON TYPE::[' + (SELECT name FROM sys.types WHERE user_type_id = dp.major_id) + '] '
            WHEN 10 THEN 'ON XML SCHEMA COLLECTION::[' + (SELECT SCHEMA_NAME(schema_id) + '.' + name FROM sys.xml_schema_collections WHERE xml_collection_id = dp.major_id) + '] '
            WHEN 15 THEN 'ON MESSAGE TYPE::[' + (SELECT name FROM sys.service_message_types WHERE message_type_id = dp.major_id) + '] '
            WHEN 16 THEN 'ON CONTRACT::[' + (SELECT name FROM sys.service_contracts WHERE service_contract_id = dp.major_id) + '] '
            WHEN 17 THEN 'ON SERVICE::[' + (SELECT name FROM sys.services WHERE service_id = dp.major_id) + '] '
            WHEN 18 THEN 'ON REMOTE SERVICE BINDING::[' + (SELECT name FROM sys.remote_service_bindings WHERE remote_service_binding_id = dp.major_id) + '] '
            WHEN 19 THEN 'ON ROUTE::[' + (SELECT name FROM sys.routes WHERE route_id = dp.major_id) + '] '
            WHEN 23 THEN 'ON FULLTEXT CATALOG::[' + (SELECT name FROM sys.fulltext_catalogs WHERE fulltext_catalog_id = dp.major_id) + '] '
            WHEN 24 THEN 'ON SYMMETRIC KEY::[' + (SELECT name FROM sys.symmetric_keys WHERE symmetric_key_id = dp.major_id) + '] '
            WHEN 25 THEN 'ON CERTIFICATE::[' + (SELECT name FROM sys.certificates WHERE certificate_id = dp.major_id) + '] '
            WHEN 26 THEN 'ON ASYMMETRIC KEY::[' + (SELECT name FROM sys.asymmetric_keys WHERE asymmetric_key_id = dp.major_id) + '] '
         END COLLATE SQL_Latin1_General_CP1_CI_AS
         + 'TO [' + @roleName + ']' + 
         CASE dp.state WHEN 'W' THEN ' WITH GRANT OPTION' ELSE '' END + @crlf
FROM    sys.database_permissions dp
WHERE    USER_NAME(dp.grantee_principal_id) IN (@roleName)
GROUP BY dp.state, dp.major_id, dp.permission_name, dp.class

SELECT @roleDesc = @roleDesc + 'GO' + @crlf + @crlf

-- Display users within Role.  Code stubbed by Joe Spivey
SELECT	@roleDesc = @roleDesc + 'EXECUTE sp_AddRoleMember ''' + roles.name + ''', ''' + users.name + '''' + @crlf
FROM	sys.database_principals users
		INNER JOIN sys.database_role_members link 
			ON link.member_principal_id = users.principal_id
		INNER JOIN sys.database_principals roles 
			ON roles.principal_id = link.role_principal_id
WHERE	roles.name = @roleName

-- PRINT out in blocks of up to 8000 based on last \r\n
DECLARE @printCur INT
SET @printCur = 8000

WHILE LEN(@roleDesc) > 8000
BEGIN
    -- Reverse first 8000 characters and look for first lf cr (reversed crlf) as delimiter
    SET @printCur = 8000 - CHARINDEX(CHAR(10) + CHAR(13), REVERSE(SUBSTRING(@roleDesc, 0, 8000)))

    PRINT LEFT(@roleDesc, @printCur)
    SELECT @roleDesc = RIGHT(@roleDesc, LEN(@roleDesc) - @printCur)
END

PRINT @roleDesc + 'GO'

The Not-So-Devilish Details

If you’ve made it past the actual script above, I will assume you are interested in the whys and hows.  Effectively, all database role definitions are stored in the catalog view sys.database_permissions.  Each role has it’s own ID and based on some identifiers and lookup values, specific joins to tertiary tables are required.  Digging through the script will show you what tables relate to what keys.  Obviously it’s not entirely straight-forward otherwise this functionality would be more common knowledge.  As you dig through the script though, you will see some idiosyncrasies which I will explain.

The first had to do with table permissions.  You will see a GROUP BY clause which is a sneaky way to filter out duplicate records.  Permissions that relate explicitly to Table Columns are represented by the minor_id column in the database_permissions table.  Because of this you get many records for one table permission operation, and since the same command doesn’t need to be executed multiple times, it’s best to eliminate the duplicate records.  This is also the reason for the MAX aggregate function around the minor_id column.

The second item had to do with how elaborate the GRANT clause can be in general.  You can do a LOT with a GRANT covering a wide-range of objects as is evident by the script above.  To be honest, I’ve ever only had to grant permissions to three of these types (e.g. tables, database-level, and database roles), but thankfully with a little digging it’s relatively easy to discern the syntax for those I’ve not directly worked with.

The final quirk had to do with database roles and users.  Both are stored in the database_principals table, and the syntax is different for each.  Thankfully ROLE and USER are both 4-letter words, hence the RIGHT function.

In closing, I hope this comes in handy for others as I’ve already experienced a good deal of use out of it myself.  Maybe a Connect Submission is in order to have MS add this as the call for the Database Role script -> CREATE AS option?  Time will tell.

Subscribe to the post in case more updates find there way here.  Thanks!

John

UPDATES

UPDATE (2013-09-03): Thanks to a comment left by Joe Spivey, I’ve updated the code to also output the users currently in the role.  I’ve also taken the opportunity to update it to qualify table and column names within square brakets.  The final adjustment has been a slight alteration to accommodate explicit GRANTS on DMVs.  DMVs have negative object IDs and were nulling out the entirety of the script.  This has since been resolved, and hopefully the script is in it’s final state.

UPDATE (2014-03-03): If you find that you have a role which contains a large number of explicit grants/deny’s you’ll quickly find out that this script will cut off the definition at 8000 characters.  This is a limitation of the PRINT command and not the actual routine itself.  To correct this flaw, I’ve updated the final statement to iterate through the final output utilizing CRLFs as the cutoff point for the output so that it looks clean.

NOTE (2014-03-03): If you are applying a role definition to a recently refreshed database (e.g. you copied a PROD backup down to your QA environment), you may run into a situation where the sp_AddRoleMember system sp adds users to the DB that weren’t previously there.  In this case, even though they are added to the db they are NOT granted the CONNECT permission, and any connection attempt made to the recently refreshed db by said user or group will generate a user login error.  To rectify this issue, you need to execute the following per user/group:

USE [DatabaseName]
GO
GRANT CONNECT TO [Login/GroupName]
GO
References:
http://msdn.microsoft.com/en-us/library/ms189121.aspx

23 responses to “Fully Script out a MSSQL Database Role

  1. Pingback: Script Database Role –Very good | pradydba

  2. Thanks for taking the time to post this handy script and to discuss the implementation. I looked at several variations and found yours to be the most complete and useful. I have added on a little extra code, and while trivial, I wanted to share back with you.
    Best Regards, Joe Spivey – Houston

    /********************************************************************
    * *
    * Author: John Eisbrener *
    * Script Purpose: Script out Database Role Definition *
    * Notes: Please report any bugs to http://www.dbaeyes.com/ *
    * Note: Extended by Joe Spivey to include user grants *
    * *
    ********************************************************************/

    DECLARE @roleName VARCHAR(255)
    SET @roleName = ‘db_view_definition_role’

    — Script out the Role
    DECLARE @roleDesc VARCHAR(MAX), @crlf VARCHAR(2)
    SET @crlf = CHAR(13) + CHAR(10)
    SET @roleDesc = ‘CREATE ROLE [‘ + @roleName + ‘]’ + @crlf + ‘GO’ + @crlf + @crlf

    SELECT @roleDesc = @roleDesc +
    CASE dp.state
    WHEN ‘D’ THEN ‘DENY ‘
    WHEN ‘G’ THEN ‘GRANT ‘
    WHEN ‘R’ THEN ‘REVOKE ‘
    WHEN ‘W’ THEN ‘GRANT ‘
    END +
    dp.permission_name + ‘ ‘ +
    CASE dp.class
    WHEN 0 THEN ”
    WHEN 1 THEN –table or column subset on the table
    + ‘ON ‘ +
    (SELECT SCHEMA_NAME(schema_id) + ‘.’ + name FROM sys.objects WHERE object_id = dp.major_id)
    + — optionally concatenate column names
    CASE WHEN MAX(dp.minor_id) > 0
    THEN ‘ (‘ + REPLACE(
    (SELECT name + ‘, ‘
    FROM sys.columns
    WHERE object_id = dp.major_id
    AND column_id IN (SELECT minor_id
    FROM sys.database_permissions
    WHERE major_id = dp.major_id
    AND USER_NAME(grantee_principal_id) IN (@roleName)
    )
    FOR XML PATH(”)
    ) –remove final comma
    + ‘)’, ‘, )’, ‘)’)
    ELSE ”
    END + ‘ ‘
    WHEN 3 THEN ‘ON SCHEMA::[‘ + SCHEMA_NAME(dp.major_id) + ‘] ‘
    WHEN 4 THEN ‘ON ‘ + (SELECT RIGHT(type_desc, 4) + ‘::[‘ + name FROM sys.database_principals WHERE principal_id = dp.major_id) + ‘] ‘
    WHEN 5 THEN ‘ON ASSEMBLY::[‘ + (SELECT name FROM sys.assemblies WHERE assembly_id = dp.major_id) + ‘] ‘
    WHEN 6 THEN ‘ON TYPE::[‘ + (SELECT name FROM sys.types WHERE user_type_id = dp.major_id) + ‘] ‘
    WHEN 10 THEN ‘ON XML SCHEMA COLLECTION::[‘ + (SELECT SCHEMA_NAME(schema_id) + ‘.’ + name FROM sys.xml_schema_collections WHERE xml_collection_id = dp.major_id) + ‘] ‘
    WHEN 15 THEN ‘ON MESSAGE TYPE::[‘ + (SELECT name FROM sys.service_message_types WHERE message_type_id = dp.major_id) + ‘] ‘
    WHEN 16 THEN ‘ON CONTRACT::[‘ + (SELECT name FROM sys.service_contracts WHERE service_contract_id = dp.major_id) + ‘] ‘
    WHEN 17 THEN ‘ON SERVICE::[‘ + (SELECT name FROM sys.services WHERE service_id = dp.major_id) + ‘] ‘
    WHEN 18 THEN ‘ON REMOTE SERVICE BINDING::[‘ + (SELECT name FROM sys.remote_service_bindings WHERE remote_service_binding_id = dp.major_id) + ‘] ‘
    WHEN 19 THEN ‘ON ROUTE::[‘ + (SELECT name FROM sys.routes WHERE route_id = dp.major_id) + ‘] ‘
    WHEN 23 THEN ‘ON FULLTEXT CATALOG::[‘ + (SELECT name FROM sys.fulltext_catalogs WHERE fulltext_catalog_id = dp.major_id) + ‘] ‘
    WHEN 24 THEN ‘ON SYMMETRIC KEY::[‘ + (SELECT name FROM sys.symmetric_keys WHERE symmetric_key_id = dp.major_id) + ‘] ‘
    WHEN 25 THEN ‘ON CERTIFICATE::[‘ + (SELECT name FROM sys.certificates WHERE certificate_id = dp.major_id) + ‘] ‘
    WHEN 26 THEN ‘ON ASYMMETRIC KEY::[‘ + (SELECT name FROM sys.asymmetric_keys WHERE asymmetric_key_id = dp.major_id) + ‘] ‘
    END COLLATE SQL_Latin1_General_CP1_CI_AS
    + ‘TO [‘ + @roleName + ‘]’ +
    CASE dp.state WHEN ‘W’ THEN ‘ WITH GRANT OPTION’ ELSE ” END + @crlf
    FROM sys.database_permissions dp
    WHERE USER_NAME(dp.grantee_principal_id) IN (@roleName)
    GROUP BY dp.state, dp.major_id, dp.permission_name, dp.class

    PRINT @roleDesc
    PRINT ‘GO’

    — Joe Spivey – 26-Aug-2013 Also provide the user grant information

    DECLARE @memberDesc VARCHAR(MAX)
    DECLARE memberList CURSOR FOR
    SELECT ‘EXECUTE sp_AddRoleMember ”’ + roles.name + ”’, ”’ + users.name + ”” as member
    from sys.database_principals users
    inner join sys.database_role_members link on link.member_principal_id = users.principal_id
    inner join sys.database_principals roles on roles.principal_id = link.role_principal_id
    where roles.name = @roleName

    open memberList
    while (1 = 1)
    begin
    fetch next from memberList into @memberDesc
    if @@FETCH_STATUS = 0
    begin
    print @memberDesc
    end
    else
    break;
    end;
    close memberList
    deallocate memberList

    PRINT ‘GO’

    • Hey Joe,

      Thanks for the input. I was meaning to circle back round to print out the users, but you beat me too it. I’ve incorporated your code above, but modified it to remove the cursor so it more closely ties into the output statement. I also took the opportunity to incorporate some additional formatting to accommodate oddly named tables/columns and DMVs with negative object IDs.

      Thanks again for the feedback, and glad you got some use out of the script!

      John

  3. thanks for sharing this great script. this is exactly what i needed, and instead of reinventing the wheel i was just able to use your elegant creation!

  4. Thank you so much for taking the time to share this. Just saved me a headache.

  5. Much appreciated, thank you!

  6. Nice Script, thank you !!!!

  7. Here here. Saved my day.

  8. Great script! Makes my life easier.

  9. You made my day.Thank you so much

  10. Thanks for this script! It was a huge help. One small issue that 99% of people will never experience- the casing of the @RoleDesc variable is a bit inconsistent, which will cause issues if you’re in a case sensitive collation. I did a find/replace on it to standardize it throughout and it worked like a charm. I was also getting weird results when I didn’t capitalize the role name properly, but by that point I really should have known better.

    Again, thank you for the help!

  11. Thanks John, this has saved me a great deal of effort

  12. Awesome script man you are a genius!

  13. I added this around line 110. It identified a schema owned by the role. (This wasn’t in my environment at the time, I forced it to see if it would show up)

    /*EP 1/3/2019*/
    SELECT @roleDesc = @roleDesc + ISNULL(‘ALTER AUTHORIZATION ON SCHEMA::[‘+s.[name]+’] TO [‘+@roleName+’]’+ @crlf,”)
    from sys.schemas s
    join sys.sysusers u on s.principal_id = u.[uid]
    where u.[name] in (@roleName)

  14. Hi John

    Would you be Ok if this script was used as basis of a command in the dbatools powershell module (dbatools.io).

    The code comments would include attribution and a link back to this article.

    Regards

    Patrick Flynn

  15. Hi John

    New command has been added.
    Documentation is at:
    https://docs.dbatools.io/#Export-DbaDbRole

    Code can be seen at
    https://github.com/sqlcollaborative/dbatools/blob/master/functions/Export-DbaDbRole.ps1

    A similar command
    https://docs.dbatools.io/#Export-DbaServerRole

    was also created using your code as inspiration

    Regards

    Patrick Flynn

  16. Fabulous! Really helped me out today. Thank You!

  17. Pingback: MSSQL database roles get all securables – Full script out the Database Roles

  18. Thanks, this was just what I was after. Trying to make heads or tails of several roles in a 2000+ table database was doing my head in, I can now sleep again 🙂

  19. Pingback: How to script SQL server database role?

  20. Thanks a lot for the script!
    It’s 2023 and scripting a role from SSMS still gives only
    USE [TheDB]
    GO
    CREATE ROLE [TheROLE]
    GO
    In constrast your script does exactly what is needed.

Leave a comment