SQL Server User Permissions and Role Memberships
A single catalog query can list every grant, deny, and role membership in a database, and the result is the starting point for an access review. The script below returns one flat result set that covers permissions granted directly to a user, permissions a user holds through a database role, and permissions granted to the public role.
Purpose and Overview
Who can read the SensitiveTable object, and do they get that access through a direct grant, a role, or public? Answering that from SSMS means opening the properties of every user and every role. The catalog answers it in one pass, because database permissions live in sys.database_permissions, principals live in sys.database_principals, and role membership lives in sys.database_role_members.
Permissions in SQL Server come from more than one place. A database user can be granted a permission directly. A user can be a member of a database role that holds the permission. And every database user belongs to the public database role: when a user has not been granted or denied a permission on a securable, the user inherits the permissions granted to public on that securable. The script reads all three paths and labels each row, so a reviewer can see why a user has an access, not only that they have it.
The output has one row per principal, permission, and object or column. The reviewer looks for grants made directly to users that should have gone through a role, DENY entries that explain a permission error, and public grants that open objects to everyone. The columns are described at the end of the Code Breakdown section.
Code Breakdown
Run the script in the context of the database you want to audit. The catalog views it reads are scoped to the current database. The script lists direct database user permissions, role-based permissions, and public permissions as three SELECT statements joined by UNION.
1-- Direct grants to SQL users, Windows users and Windows groups
2SELECT
3 [DatabaseUserName] = princ.[name],
4 [UserType] = princ.[type_desc],
5 [LoginName] = sp.[name] COLLATE DATABASE_DEFAULT,
6 [Role] = CAST(NULL AS sysname),
7 [PermissionClass] = perm.[class_desc],
8 [PermissionType] = perm.[permission_name],
9 [PermissionState] = perm.[state_desc],
10 [ObjectType] = obj.[type_desc],
11 [ObjectName] = CASE perm.[class]
12 WHEN 1 THEN OBJECT_NAME(perm.[major_id])
13 WHEN 3 THEN SCHEMA_NAME(perm.[major_id])
14 END,
15 [ColumnName] = col.[name]
16FROM sys.database_principals AS princ
17LEFT JOIN sys.server_principals AS sp
18 ON princ.[sid] = sp.[sid]
19LEFT JOIN sys.database_permissions AS perm
20 ON perm.[grantee_principal_id] = princ.[principal_id]
21LEFT JOIN sys.objects AS obj
22 ON perm.[class] = 1
23 AND perm.[major_id] = obj.[object_id]
24LEFT JOIN sys.columns AS col
25 ON perm.[class] = 1
26 AND col.[object_id] = perm.[major_id]
27 AND col.[column_id] = perm.[minor_id]
28WHERE princ.[type] IN ('S', 'U', 'G')
29
30UNION
31
32-- Grants a member holds through a database role or application role
33SELECT
34 [DatabaseUserName] = memberprinc.[name],
35 [UserType] = memberprinc.[type_desc],
36 [LoginName] = sp.[name] COLLATE DATABASE_DEFAULT,
37 [Role] = roleprinc.[name],
38 [PermissionClass] = perm.[class_desc],
39 [PermissionType] = perm.[permission_name],
40 [PermissionState] = perm.[state_desc],
41 [ObjectType] = obj.[type_desc],
42 [ObjectName] = CASE perm.[class]
43 WHEN 1 THEN OBJECT_NAME(perm.[major_id])
44 WHEN 3 THEN SCHEMA_NAME(perm.[major_id])
45 END,
46 [ColumnName] = col.[name]
47FROM sys.database_role_members AS members
48JOIN sys.database_principals AS roleprinc
49 ON roleprinc.[principal_id] = members.[role_principal_id]
50JOIN sys.database_principals AS memberprinc
51 ON memberprinc.[principal_id] = members.[member_principal_id]
52LEFT JOIN sys.server_principals AS sp
53 ON memberprinc.[sid] = sp.[sid]
54LEFT JOIN sys.database_permissions AS perm
55 ON perm.[grantee_principal_id] = roleprinc.[principal_id]
56LEFT JOIN sys.objects AS obj
57 ON perm.[class] = 1
58 AND perm.[major_id] = obj.[object_id]
59LEFT JOIN sys.columns AS col
60 ON perm.[class] = 1
61 AND col.[object_id] = perm.[major_id]
62 AND col.[column_id] = perm.[minor_id]
63
64UNION
65
66-- Grants made to the public role, which every database user belongs to
67SELECT
68 [DatabaseUserName] = '{All Users}',
69 [UserType] = '{All Users}',
70 [LoginName] = '{All Users}',
71 [Role] = roleprinc.[name],
72 [PermissionClass] = perm.[class_desc],
73 [PermissionType] = perm.[permission_name],
74 [PermissionState] = perm.[state_desc],
75 [ObjectType] = obj.[type_desc],
76 [ObjectName] = OBJECT_NAME(perm.[major_id]),
77 [ColumnName] = col.[name]
78FROM sys.database_principals AS roleprinc
79JOIN sys.database_permissions AS perm
80 ON perm.[grantee_principal_id] = roleprinc.[principal_id]
81JOIN sys.objects AS obj
82 ON perm.[class] = 1
83 AND obj.[object_id] = perm.[major_id]
84LEFT JOIN sys.columns AS col
85 ON col.[object_id] = perm.[major_id]
86 AND col.[column_id] = perm.[minor_id]
87WHERE roleprinc.[type] = 'R'
88 AND roleprinc.[name] = 'public'
89 AND obj.[is_ms_shipped] = 0
90ORDER BY
91 [DatabaseUserName],
92 [ObjectName],
93 [ColumnName],
94 [PermissionType],
95 [PermissionState],
96 [ObjectType];
First SELECT: permissions granted directly to users
The first SELECT starts from sys.database_principals, which returns one row per security principal in the database. The filter type IN ('S', 'U', 'G') keeps SQL users (S), Windows users (U), and Windows groups (G). sys.database_principals defines further types, such as certificate-mapped users, external Microsoft Entra users, and database roles; add their letters to the filter if your database has them.
It then LEFT JOINs sys.database_permissions on grantee_principal_id = principal_id. A LEFT JOIN keeps principals that hold no explicit permission, so they still appear in the report with empty permission columns. The Role column is NULL because nothing here passes through a role.
sys.database_permissions returns a row for every permission or column-exception permission in the database. The class column says what kind of thing the permission is on, and what major_id means depends on that class: for object or column permissions (class 1) it is the object ID, for schema permissions (class 3) it is the schema ID. The script therefore resolves major_id with OBJECT_NAME only for class 1 and with SCHEMA_NAME for class 3, and it joins sys.objects and sys.columns only for class 1. Without that guard, a schema ID or a database-level major_id of 0 would be looked up as an object ID and could show an unrelated object name. For a column permission, minor_id holds the column ID; it is 0 for an object-level grant, which matches no row in sys.columns.
Resolving the login with sys.server_principals
The LoginName column joins sys.database_principals to sys.server_principals on sid. A database user's sid is the security identifier of the principal, and for a Windows principal the server principal's SID matches the Windows SID. A database user with no matching server principal gets a NULL in LoginName.
The COLLATE DATABASE_DEFAULT clause is there because of a collation rule: identifiers of instance-level objects, such as logins, take the default collation of the instance, while identifiers of objects inside a database take the database collation. The clause converts the login name to the database collation, so the column lines up with the other database-level text columns in the UNION.
sys.login_token is the wrong view for this job. It returns one row for every server principal that is part of the login token of the session running the query, so it identifies the caller, not every user in the database; joined to other users, it finds nothing.
Second SELECT: permissions held through a role
The second SELECT starts from sys.database_role_members, which returns one row for each member of each database role. Database users, application roles, and other database roles can all be members. The query joins sys.database_principals twice, once as roleprinc for the role and once as memberprinc for the member, then joins sys.database_permissions on the role's principal ID. The permission columns describe what the role holds, and the member inherits it by membership.
Role membership is not expanded recursively. If a member is itself a role, its row appears with UserType of DATABASE_ROLE, and the users inside that nested role do not appear under the parent role. Adding user-defined roles as members of fixed roles is discouraged because it can enable unintended privilege escalation, so a nested-role row is worth a second look.
Where permissions are granted through roles, a report that only reads direct grants to users misses that access.
Third SELECT: the public role
Every database user belongs to the public database role, so the third SELECT reports grants made to it, using {All Users} as a label. It uses an inner join to sys.objects and keeps is_ms_shipped = 0 so the long list of default grants on system objects stays out. Many permissions are assigned to the public roles by default, and most of them are needed for routine operations. What this branch shows you is what was added on top for your own objects.
Output columns
| Column | Description |
|---|---|
| DatabaseUserName | The database principal name |
| UserType | The principal's type_desc, such as SQL_USER, WINDOWS_USER, WINDOWS_GROUP, or DATABASE_ROLE |
| LoginName | The server principal with the same SID; NULL if none |
| Role | The role through which the permission is held; NULL for direct grants |
| PermissionClass | What the permission is on, such as DATABASE, OBJECT_OR_COLUMN, or SCHEMA |
| PermissionType | The permission name, such as SELECT, EXECUTE, ALTER, or CONTROL |
| PermissionState | DENY, REVOKE, GRANT, or GRANT_WITH_GRANT_OPTION |
| ObjectType | The type_desc from sys.objects, such as USER_TABLE |
| ObjectName | The object or schema name |
| ColumnName | The column for column-level permissions; NULL otherwise |
Key Benefits and Use Cases
- Three access paths in one result: direct grants, role-based grants, and
publicgrants come back together, so a reviewer does not have to merge three reports. - Column-level permissions are visible: column-level permissions override object-level permissions on the same entity, and the
ColumnNamecolumn shows them. - All permission states are shown:
DENYentries appear next toGRANTentries.DENYalways supersedesGRANT, which makes theDENYrows the first place to look when a user reports an unexpected permission error. - Column exceptions are labelled: a
GRANTon a table followed by aREVOKEon one column shows up as oneGRANTrow and oneREVOKErow, so the exception is not hidden. - Public grants stand out: any object where
publicholds a permission is listed under{All Users}. - Easy export: the flat output copies into a spreadsheet for an access review or compliance sign-off.
Performance Considerations
- Current database only: the catalog views are scoped to the database you are connected to. To audit several databases, run the script in each one.
- Fixed database roles are incomplete here: the permissions of fixed database roles do not appear in
sys.database_permissions. Fordb_datareader,db_owner, and the other fixed roles, the role-based branch shows the membership but the permission columns areNULL. Their permissions are fixed by the role definition: for example,db_datareadermembers can read all data from all user tables and views. - Metadata visibility limits the result: any user can see their own permissions, but seeing other users' permissions needs
VIEW DEFINITION,ALTER ANY USER, or a permission on the user, andVIEW SECURITY DEFINITIONon the database applies to SQL Server 2022 and later. Catalog view visibility is also limited to securables a user owns or has some permission on. A low-privilege account can see a shorter result than an administrator. - Server-level access is separate: this script covers database permissions only. Server-level permissions are held at the server level; server-level permissions cannot be granted to database roles, and logins and server roles cannot be added to database roles.
- Output size: a database with many users and objects produces a large result. Add a
WHEREclause onprinc.[name]in the first branch, or on the object name, to narrow it.
Practical Tips
- Review
DENYrows first: filter onPermissionState = 'DENY'when someone reports a permission error that their grants should have prevented. - Look past GRANT and DENY:
REVOKErows mark column exceptions, andGRANT_WITH_GRANT_OPTIONrows mark grants that carry the grant option. - Filter after the union: to see one user or one table, put the three
SELECTstatements in a derived table without theORDER BY, then filter and sort in the outer query:
1SELECT *
2FROM (
3 -- the three SELECT statements joined by UNION, without ORDER BY
4) AS access_report
5WHERE [DatabaseUserName] = N'AppUser'
6ORDER BY [ObjectName], [PermissionType];
- Keep a baseline: write the output to an audit table on a schedule, such as a weekly SQL Server Agent job, and compare each week with the last to find grants nobody planned.
- Include more principal types when needed: add
'E','X','C', or'K'to the first branch's type filter if the database uses Microsoft Entra users, certificate-mapped users, or key-mapped users.
Conclusion
The three-branch UNION gives a database-level access report from the system catalog alone: direct grants, role-based grants, and public grants, each labelled with its state and the object or column it applies to. Run it per database, remember that fixed-role permissions are defined outside sys.database_permissions, and compare each run with the last one so changes to access are visible when they happen.
References
- sys.database_permissions (Transact-SQL) — Microsoft Learn — Permission classes, states,
major_idandminor_id, column exceptions, and visibility rules. - sys.database_principals (Transact-SQL) — Microsoft Learn — Principal types, SIDs, and fixed-role flags for database users and roles.
- sys.database_role_members (Transact-SQL) — Microsoft Learn — Role-to-member mapping and the permissions needed to read it.
- sys.server_principals (Transact-SQL) — Microsoft Learn — Server-level principals and their SIDs, used to resolve the login name.
- sys.login_token (Transact-SQL) — Microsoft Learn — Why this view describes only the current session's token.
- Database-level roles — Microsoft Learn — Fixed database roles and what each one can do.
- Principals (Database Engine) — Microsoft Learn — Membership in the
publicdatabase role and how its permissions are inherited. - COLLATE (Transact-SQL) — Microsoft Learn — Collation of instance-level and database-level identifiers.