One of my favorite subjects in SQL server is security, and one of my favorite security topics is permissions. This post will cover a script I wrote to recreate a user’s permissions within a database. I call it my User Clone Script, and use it quite heavily.
Use Cases
- Troubleshooting Permissions
- Cloning accounts
- Switching service accounts
- Copying permissions between development/test/production environments
- Refactoring permissions to use a SQL Server Role instead of explicit user permissions
- Refactoring permissions to use an AD group instead of explicitly named users
Highlights of the Script
This script has support for the following:
- Most object/column permissions (includes stored procedures, views, tables, and functions)
- Object ownerships
- Type permissions
- Schema permissions
- Schema ownerships
- Database permissions
- Role memberships
Implementation
In a few separate queries, I dump meta-data into a temp table from the following catalog views:
- Database permissions ( sys.database_permissions )
- Schema ownerships ( sys.schemas )
- Object Ownership ( sys.objects )
- Role Memberships ( sys.database_role_members )
Then— and this is the key to unraveling role memberships— I use a Recursive CTE to generate the final result set.
A recursive CTE takes the following form:
;with RecursiveCTE as(
--1) Base/Anchor Member
SELECT
Columns
FROM #SomeTable
WHERE [ID]=@BaseMemberID
--2) Always a UNION/UNION ALL
UNION ALL
--3) Recursive Member
Select
Columns
FROM #SomeTable T
INNER JOIN RecursiveCTE RCTE ON T.ParentID=RCTE.ID
)
SELECT * FROM RecursiveCTE;
More information about Recursive CTEs can be found here on TechNet.
Output Sample
The following screenshot shows the output of the script, including commands to copy the permissions of User1 to User2.
A couple of key points:
- If the @NewUser variable is NULL, the script will generate the commands used to grant [User1] the permissions they currently have (to copy permissions to another database, for example).
- Otherwise, it will replace [User1] with [User2].
- In this example:
- [User1] is an immediate and direct member of three roles: [CoolUsers], [Level2] and [Level2a].
- [CoolUsers] is a member of the [db_owner] fixed role.
- The [Level2] role is a member of the [Level1] role.
- The commands output by the script will create the roles, if they do not exist, and assign them the same permissions they had before (for the case that you’re trying to copy the permissions to a different database, for example)
- If a schema or object ownership is detected, the script will generate the command to transfer ownership to the new user, but it will be commented-out. This is done as a precautionary measure. As only one user can own any given schema/object, running the commands generated would possibly break existing rights for [User1]. If this is your intention, simply uncomment the ALTER AUTHORIZATION commands before running the scripts.
The Script
The only thing you’ll need to change before running the script are the two variables (@OldUser and possibly @NewUser) in the <setable> section.
/*
* Author: Ryan Snee
* Version: 1.0
* Date 20130120
* URL: http://sqlsnee.com/r/userclone
*
* License: This work is licensed under a Creative Commons Attribution-ShareAlike 3.0 Unported License.
* License Details: http://creativecommons.org/licenses/by-sa/3.0/deed.en_US
*
* LIMITATIONS
* Does not currently support a number of database-level securables (see list below)
* Does not unroll AD group memberships
*
* Does handle the following permissions:
*
* --From BOL for SQL 2012... class/class_desc columns
* --"X" indicates that the securable is supported by this script
*
* [X] 0 = Database
* [X] 1 = Object or Column
* [X] 3 = Schema
* [ ] 4 = Database Principal
* [ ] 5 = Assembly
* [X] 6 = Type
* [ ] 10 = XML Schema Collection
* [ ] 15 = Message Type
* [ ] 16 = Service Contract
* [ ] 17 = Service
* [ ] 18 = Remote Service Binding
* [ ] 19 = Route
* [ ] 23 = Full-Text Catalog
* [ ] 24 = Symmetric Key
* [ ] 25 = Certificate
* [ ] 26 = Asymmetric Key
*
* Also handles:
* Schema/Object Ownership
* Role Membership
*
*/
declare @sqlvers varchar(5);
--Determine which version of SQL we are running
declare @v numeric(4,2)
select @v = cast(left(cast(serverproperty('productversion') as varchar), 4) AS numeric(4,2))
select @sqlvers =CASE
WHEN @V >= 12 THEN '120'
WHEN @V >= 11 THEN '110'
WHEN @V >= 10 THEN '100'
WHEN @V >= 9 THEN '90'
WHEN @V >= 8 THEN '80'
ELSE 'No Version Found'
END
DECLARE
@OldUser sysname=NULL,
@NewUser sysname=NULL;
--<setable>
SET @OldUser = 'JohnDoe'
--SET @NewUser = 'AdminAssistant'
--</setable>
IF OBJECT_ID('tempdb..#Perms') IS NOT NULL
BEGIN
DROP TABLE #Perms
END
create table #Perms(
Class sysname
,Principal nvarchar(300)
,[NewPrincipal] nvarchar(300)
,[Role] sysname NULL
,type_desc nvarchar(60) NULL
,is_fixed_role bit NULL
,[State] nvarchar(60) NULL
,[Permission] nvarchar(128) NULL
,[Type] sysname NULL
,[Schema] sysname NULL
,[Object] sysname NULL
,[Column] sysname NULL
,[Cmd] nvarchar(max)
,[SortOrder] tinyint
)
--Base Permissions
INSERT INTO #Perms
select
dp.class_desc as [Class]
,princ.name as [Principal]
,case when princ.name=@OldUser
THEN Coalesce(@NewUser,princ.name)
ELSE princ.name
end as [NewPrincipal]
,NULL as [Role]
,princ.type_desc
,princ.is_fixed_role
,dp.state_desc as [State]
,dp.permission_name as [Permission]
,CASE
WHEN dp.class=0
THEN 'DATABASE'
WHEN dp.class=1
THEN o.type_desc
WHEN dp.class=3
THEN 'SCHEMA'
WHEN dp.class=6
THEN 'TYPE_TABLE'
ELSE ''
END as [Type]
,CASE
WHEN dp.class=3
THEN s3.name
ELSE s.name
END AS [Schema]
,o.name as [Object]
,c.name as [Column]
,CASE
WHEN dp.class=1 --OBJECT_OR_COLUMN level permission
THEN
case
when dp.state='W'
THEN 'GRANT'
ELSE state_desc
END
+' '+cast(permission_name As varchar(max)) COLLATE database_default + ' ON '
+quotename(s.name,']')+'.' +quotename(o.name,']')
+CASE
WHEN dp.minor_id=0 --Object Permission
THEN ' '
WHEN dp.minor_id > 0 --Column Permission
THEN ' '+quotename(quotename(c.name,']'),')')+' '
ELSE NULL
END
WHEN dp.class=0 --Database level permission
THEN
case
when dp.state='W'
THEN 'GRANT'
ELSE state_desc
END
+' '+cast(permission_name As varchar(max)) COLLATE database_default
WHEN dp.class=6 --Types
THEN
case
when dp.state='W'
THEN 'GRANT'
ELSE state_desc
END
+' '+cast(permission_name As varchar(max)) COLLATE database_default
+ ' ON TYPE::'
+quotename(s2.name,']')+'.' +quotename(t.name,']')
WHEN dp.class=3 --Schemas
THEN
case
when dp.state='W'
THEN 'GRANT'
ELSE state_desc
END
+' '+cast(permission_name As varchar(max)) COLLATE database_default
+ ' ON SCHEMA::'
+quotename(s3.name,']')
ELSE '--Not yet supported by this script'
END
+CASE
WHEN dp.class in(0,1,6,3)
THEN
' TO '
+case
when princ.name=@OldUser
THEN quotename(Coalesce(@NewUser,princ.name))
ELSE quotename(princ.name)
end
+CASE
WHEN dp.state='W'
THEN ' WITH GRANT OPTION;'
ELSE ';'
END
ELSE ''
END
AS [Cmd]
,1 as [SortOrder]
from sys.database_permissions dp
left join sys.all_objects o
on dp.major_id=o.object_id
and class=1
left join sys.all_columns c
on dp.major_id=c.object_id
and dp.minor_id=c.column_id
left join sys.database_principals princ
on dp.grantee_principal_id=princ.principal_id
left join sys.schemas s
on o.schema_id=s.schema_id
left join sys.extended_properties ep
on ep.major_id = o.object_id and
ep.minor_id = 0 and
ep.class = 1 and
ep.name = N'microsoft_database_tools_support'
left join sys.types t
on dp.major_id=t.user_type_id
left join sys.schemas s2
on t.schema_id=s2.schema_id
left join sys.schemas s3
on dp.major_id=s3.schema_id
and dp.class=3
where 1=1
--AND (dp.class<>1 OR o.is_ms_shipped=0) --If it's an object, can't be MS_Shipped
AND (dp.class<>6 OR t.is_user_defined=1) --If it's a type, only check if it's a user-defined type
and ep.value is null --Not a Microsoft Database Tool support object
--Schema Ownerships
INSERT INTO #Perms
select
'Schema OWNERSHIP' as [Class]
,dp.name as [Principal]
,case when dp.name=@OldUser
THEN Coalesce(@NewUser,dp.name)
ELSE dp.name
end as [NewPrincipal]
,NULL as [Role]
,dp.type_desc
,dp.is_fixed_role
,NULL as [State]
,NULL as [Permission]
,'SCHEMA' as [Type]
,s.name AS [Schema]
,NULL as [Object]
,NULL as [Column]
,'--ALTER AUTHORIZATION ON SCHEMA::['+s.name+'] TO '
+case when dp.name=@OldUser
THEN quotename(Coalesce(@NewUser,dp.name))
ELSE quotename(dp.name)
end
+'; --Warning, a schema can have only 1 owner' as [Cmd]
,2 as [SortOrder]
from sys.schemas s
inner join sys.database_principals dp
on s.principal_id=dp.principal_id
--Object Ownership
INSERT INTO #Perms
select
'Object OWNERSHIP' as [Class]
,dp.name as [Principal]
,case when dp.name=@OldUser
THEN Coalesce(@NewUser,dp.name)
ELSE dp.name
end as [NewPrincipal]
,NULL as [Role]
,dp.type_desc
,dp.is_fixed_role
,NULL as [State]
,NULL as [Permission]
,'OBJECT' as [Type]
,s.name AS [Schema]
,o.name as [Object]
,NULL as [Column]
,'--ALTER AUTHORIZATION ON OBJECT::'+quotename(s.name)+'.'+quotename(o.name)+' TO '
+case when dp.name=@OldUser
THEN quotename(Coalesce(@NewUser,dp.name))
ELSE quotename(dp.name)
end
+'; --Warning, an object can have only 1 owner' as [Cmd]
,3 as [SortOrder]
from sys.objects o
inner join sys.database_principals dp
on o.principal_id=dp.principal_id
inner join sys.schemas s
on o.schema_id=s.schema_id
WHERE o.type IN('U','TT','FN','IF','P','TF','V')
--Role Memberships
INSERT INTO #Perms
select
'ROLE MEMBERSHIP' as Class
,p2.name as Principal
,case when p2.name=@OldUser
THEN Coalesce(@NewUser,p2.name)
ELSE p2.name
end as [NewPrincipal]
,p1.name as [Role]
,p2.type_desc
,p2.[is_fixed_role]
,NULL as [State]
,NULL as [Permission]
,'ROLE' as [Type]
,NULL as [Schema]
,NULL as [Object]
,NULL as [Column]
,CASE when p1.is_fixed_role=1
THEN ''
ELSE
'IF NOT EXISTS (select 1 from sys.database_principals where type=''R'' and name='''+p1.name+''') CREATE ROLE '+quotename(p1.name)+';'+char(13)+char(10)
END
+
CASE
WHEN @sqlvers >=110
THEN 'ALTER ROLE '+quotename(p1.name,']')+' ADD MEMBER '+
case when p2.name=@OldUser
THEN quotename(Coalesce(@NewUser,p2.name))
ELSE quotename(p2.name)
end
+';'
ELSE 'EXEC sp_addrolemember N'''+p1.name+''', N'''+
case when p2.name=@OldUser
THEN Coalesce(@NewUser,p2.name)
ELSE p2.name
end
+''';'
END as [Cmd]
,4 as [SortOrder]
from sys.database_role_members rm
left join sys.database_principals p1
on rm.role_principal_id=p1.principal_id
left join sys.database_principals p2
on rm.member_principal_id=p2.principal_id
;with RecursiveCTE as(
--Base Member
SELECT
[Class]
,[Principal]
,[NewPrincipal]
,[Role]
,[State]
,[Permission]
,[Type]
,[Schema]
,[Object]
,[Column]
,[Cmd]
,[SortOrder]
,[is_fixed_role]
,0 as [Level]
from #Perms where [Principal]=@OldUser
UNION ALL
Select
[Perms].[Class]
,[Perms].[Principal]
,[Perms].[NewPrincipal]
,[Perms].[Role]
,[Perms].[State]
,[Perms].[Permission]
,[Perms].[Type]
,[Perms].[Schema]
,[Perms].[Object]
,[Perms].[Column]
,[Perms].[Cmd]
,[Perms].[SortOrder]
,[Perms].[is_fixed_role]
,[Level]+1
FROM #Perms Perms
INNER JOIN RecursiveCTE RCTE on Perms.Principal=RCTE.[Role]
)
SELECT
[Level]
,[Class]
,[Principal]
,[NewPrincipal]
,[Role]
,[State]
,[Permission]
,[Schema]
,[Object]
,[Column]
,[Cmd]
--,[Type]
--,[SortOrder]
--,is_fixed_role
FROM RecursiveCTE
WHERE is_fixed_role=0
ORDER BY [Level], [Principal], [SortOrder], [Class], [Type], [State] DESC, [Permission], [Schema], [Object], [Column], [Role]
IF OBJECT_ID('tempdb..#Perms') IS NOT NULL
BEGIN
DROP TABLE #Perms
END
Remember, like many of my scripts, this script doesn’t change anything; rather it generates TSQL commands you can choose to run.
Other Considerations
While this script does not generate a user’s effective permissions, it can help you to reverse-engineer them. It works on the level of the database user. This works fine if you have a named database user. If you are inheriting your rights through a single AD group membership, you can still ascertain this information by supplying the database user for that AD group as the @OldUser. What happens if you are a named user in the database, but are also granted certain privileges through a number of different AD groups? Well, that’s where it gets more difficult. One trick I like to employ is to use two catalog views, sys.login_token and sys.user_token. Both views are similar, but the first works at the server login level, whereas the second works at the database user level.
These two views can help you to unravel your AD group memberships. For the current user, they display the role memberships and the Windows group memberships of the user. In the case of a domain environment, they connect to AD and actually enumerate all your domain security groups. If a particular group actually matches a server login or database user, this view will also show you the principal_id you can use to join it back to sys.server_principals (in the case of sys.login_token) or sys.database_principals (in the case of sys.user_token).
I hope you’re beginning to see how powerful this is 🙂 .
This works great if you were to want to troubleshoot your own permissions. How though, as a DBA, would you be able to use this to troubleshoot a different account’s permissions? You can have a user run these queries, as themselves, and return the output to you… or, as a DBA, you can use the power of IMPERSONATE. If you are the DBA (or otherwise have impersonation rights on another account), you can run any command as that user. Be aware that this may violate security policies at your company, and could be ill-advised in certain security-sensitive environments. Also, if you have any type of auditing enabled, chances are good that you will leave footprints all over your audit logs that you are running commands on behalf of another user. This may or may not be a problem for you. That being said, if you determine that it’s acceptable to do, you can run a command such as:
use Adventureworks;
GO
execute as login='DOMAIN\User'
select * from sys.login_token where principal_id <> 0
select * from sys.user_token where principal_id <> 0
revert
This will show you [DOMAIN\User]’s login token for the server and user token for the Adventureworks database. Using this technique in conjunction with the User Clone script can go a long way to managing and troubleshooting database permissions.
The full script can be found here: UserClone.txt.
Please let me know if you found this script useful or if you have any suggestions to improve it.
LLAP,
Ryan
As always, please note the waive of liability published on the About page.
The text of this entire blog site (including all scripts) is under the copyright of Ryan Snee. You are free to reproduce it, as outlined by the Creative Commons Attribution-ShareAlike 3.0 Unported License.






