A SQL Server Permission Report in PowerBI

Table of Contents

What it is and how to get it

When working with SQL Instances that have lots of databases, sensitive data and equally lots of users/applications that accesses those, it can get pretty troublesome to keep track of every user, for every database, down to the single column/action.
That’s why I developed this report, in orde to be able to quickly determine the security status of each object or user in the system.

The granting information can be filtered for each Database, Object, Object Column, Type of object, User, Type of Permission and Permission State, and it returns information on everything that may interest you in auditing this kind of information, including the user roles (if it has any).

The neat part of this report is that is completely self-contained, there is no need for any external procedure or component to live on the server, you can just publish the Report to the cloud and have it refreshed through the PowerBI gateway (and a user that has access to the security DMV, of course).
If you don’t have permissions to fetch this kind of information from all the databases, those will be skipped without blocking the output for the others.

You can get the report from my GitHub here

Technical information ahead

There are basically 4 queries in the PowerBI Model, two of them trivial; You can reuse the code below as a way to extract all the database permissions (and user roles) for each database in your instance:

Database Permissions

It’s the core of the report, basically loops via Dynamic SQL through all the databases, skipping those where the user running the report doesn’t have the right privileges.

SET NOCOUNT ON;

DECLARE @code nvarchar(max) = 'SET XACT_ABORT OFF;';
SET @code += CHAR(10);

IF (OBJECT_ID('tempdb..##database_principals') IS NOT NULL)
    DROP TABLE  ##database_principals

IF (OBJECT_ID('tempdb..##database_permissions') IS NOT NULL)
    DROP TABLE  ##database_permissions

IF (OBJECT_ID('tempdb..##database_roles') IS NOT NULL)
    DROP TABLE  ##database_roles


CREATE TABLE ##database_principals
(
 [DBname]                   SYSname NULL,
 [name]                     SYSname NULL,
 [principal_id]             INT NULL,
 [type]                     CHAR(1) NULL,
 [type_desc]                NVARCHAR(60) NULL,
 [default_schema_name]      SYSname NULL,
 [create_date]              DATETIME NULL,
 [modify_date]              DATETIME NULL,
 [owning_principal_id]      INT NULL,
 [sid]                      VARBINARY(85) NULL,
 [is_fixed_role]            BIT NULL,
 [authentication_type]      INT NULL,
 [authentication_type_desc] NVARCHAR(60) NULL,
 [default_language_name]    SYSname NULL,
 [default_language_lcid]    INT NULL
);


INSERT INTO ##database_principals
       SELECT 'Master' AS [DBname],
              [name],
              [principal_id],
              [type],
              [type_desc],
              [default_schema_name],
              [create_date],
              [modify_date],
              [owning_principal_id],
              [sid],
              [is_fixed_role],
              [authentication_type],
              [authentication_type_desc],
              [default_language_name],
              [default_language_lcid]
       FROM   master.sys.database_principals;

SELECT @code += 'BEGIN TRY' + CHAR(10) +
       'INSERT INTO ##database_principals' + CHAR(10) +
       'SELECT ''' + name + ''' as DBname, 
              [name],
              [principal_id],
              [type],
              [type_desc],
              [default_schema_name],
              [create_date],
              [modify_date],
              [owning_principal_id],
              [sid],
              [is_fixed_role],
              [authentication_type],
              [authentication_type_desc],
              [default_language_name],
              [default_language_lcid]' + CHAR(10)+
       'FROM ' + QUOTEname(name) + '.sys.database_principals;' + CHAR(10) +
       'END TRY' + CHAR(10) +
       'BEGIN CATCH PRINT ''Cannot Read database_principals information from ' + name + '''; END CATCH' + CHAR(10)
FROM sys.databases
WHERE [name] <> 'Master'

EXEC sp_executesql @code


CREATE TABLE ##database_permissions
([DBname]          SYSname NULL,
 [class_desc]      NVARCHAR(60) NULL,
 [Object name]     SYSname NULL,
 [Grantee name]    SYSname NULL,
 [Grantor name]    SYSname NULL,
 [permission_name] NVARCHAR(128) NULL,
 [state_desc]      NVARCHAR(60) NULL,
 [Column]          SYSname NULL,
 [IsSystem]        CHAR(3)
);

INSERT INTO ##database_permissions
SELECT 'Master' as DBname,
       [PERM].[class_desc],
       OBJECT_name([PERM].[major_id]) AS [Object name],
       [GRANTEE].[name] AS [Grantee name],
       [GRANTOR].[name] AS [Grantor name],
       [PERM].[permission_name],
       [state_desc],
       [COL].[name] AS [Column],
       CASE
           WHEN [PERM].[major_id] < 0 THEN 'Yes'
           ELSE 'No'
       END AS [IsSystem]
FROM   sys.database_permissions PERM
JOIN ##database_principals GRANTEE ON GRANTEE.DBname = 'Master' AND GRANTEE.principal_id = grantee_principal_id
JOIN ##database_principals GRANTOR ON GRANTOR.DBname = 'Master' AND GRANTOR.principal_id = grantor_principal_id
LEFT JOIN sys.columns COL ON COL.object_id = PERM.major_id AND COL.column_id = PERM.minor_id;

SET @code = ''

SELECT @code +=  'BEGIN TRY' + CHAR(10) +
       'INSERT INTO ##database_permissions
        SELECT ''' + name + ''' as DBname,
               [PERM].[class_desc],
               OBJECT_name([PERM].[major_id],' + CONVERT(nvarchar,database_id) + ') AS [Object name],
               [GRANTEE].[name] AS [Grantee name],
               [GRANTOR].[name] AS [Grantor name],
               [PERM].[permission_name],
               [state_desc],
               [COL].[name] AS [Column],
               CASE
                   WHEN [PERM].[major_id] < 0 THEN ''Yes''
                   ELSE ''No''
               END AS [IsSystem]
        FROM ' + QUOTEname(name) + '.sys.database_permissions PERM
        JOIN ##database_principals GRANTEE ON GRANTEE.DBname = ''' + name + ''' AND GRANTEE.principal_id = grantee_principal_id
        JOIN ##database_principals GRANTOR ON GRANTOR.DBname = ''' + name + ''' AND GRANTOR.principal_id = grantor_principal_id
        LEFT JOIN ' + QUOTEname(name) + '.sys.columns COL ON COL.object_id = PERM.major_id AND COL.column_id = PERM.minor_id;' + CHAR(10) +
        'END TRY' + CHAR(10) +
       'BEGIN CATCH PRINT ''Cannot Read ##database_permissions information from ' + name + '''; END CATCH' + CHAR(10)
FROM sys.databases
WHERE [name] <> 'Master'

EXEC sp_executesql @code

SELECT 'Master' as DBname,
       [Roles].[Role_ID],
       [Roles].[Role_name],
       [Roles].[Member_ID],
       [Principals].[name] AS [Member_name]
INTO ##database_roles
FROM
(
    SELECT [Principals].[name] AS [Role_name],
           [Roles].[role_principal_id] AS [Role_ID],
           [Roles].[member_principal_id] AS [Member_ID]
    FROM   sys.database_principals Principals
    INNER JOIN sys.database_role_members Roles
    ON Principals.principal_id = Roles.role_principal_id
) Roles
INNER JOIN sys.database_principals Principals
ON Roles.Member_ID = Principals.principal_id
ORDER BY [Member_name];

SET @code = ''

SELECT @code +=    'BEGIN TRY' + CHAR(10) +
                   'INSERT INTO ##database_roles
                   SELECT ''' + name +''' as DBname,
                   [Roles].[Role_ID],
                   [Roles].[Role_name],
                   [Roles].[Member_ID],
                   [Principals].[name] AS [Member_name]
            FROM
            (
                SELECT [Principals].[name] AS [Role_name],
                       [Roles].[role_principal_id] AS [Role_ID],
                       [Roles].[member_principal_id] AS [Member_ID]
                FROM   ' + QUOTEname(name) + '.sys.database_principals Principals
                INNER JOIN ' + QUOTEname(name) + '.sys.database_role_members Roles
                ON Principals.Principal_ID = Roles.role_principal_id
            ) Roles
            INNER JOIN ' + QUOTEname(name) + '.sys.database_principals Principals
            ON Roles.Member_ID = Principals.Principal_ID
            ORDER BY [Member_name];' + CHAR(10) +
                   'END TRY' + CHAR(10) +
                   'BEGIN CATCH PRINT ''Cannot Read database_roles information from ' + name + '''; END CATCH' + CHAR(10) + CHAR(10)
FROM sys.databases
WHERE [name] <> 'Master'

EXEC sp_executesql @code

SELECT DP.*, LEFT(DR.[Roles],LEN(DR.[Roles])-1) as [Roles]
FROM ##database_permissions DP
OUTER APPLY 
(
    SELECT R.Role_name + '; '
    FROM ##database_roles R
    WHERE R.DBname = DP.DBname COLLATE DATABASE_DEFAULT
    AND R.Member_name = DP.[Grantee name] COLLATE DATABASE_DEFAULT
    FOR XML PATH ('')
) DR ([Roles]);


DROP TABLE ##database_principals
DROP TABLE ##database_permissions
DROP TABLE ##database_roles

Database Roles

Similar to the above code, it loops in all the databases to get the role of each user

SET NOCOUNT ON;

DECLARE @code nvarchar(max) = ''

IF (OBJECT_ID('tempdb..##database_rolesB') IS NOT NULL)
    DROP TABLE  ##database_rolesB

SELECT 'Master' as DBName,
       [Roles].[Role_ID],
       [Roles].[Role_Name],
       [Roles].[Member_ID],
       [Principals].[name] AS [Member_Name]
INTO ##database_rolesB
FROM
(
    SELECT [Principals].[name] AS [Role_Name],
           [Roles].role_principal_id AS [Role_ID],
           [Roles].member_principal_id AS [Member_ID]
    FROM   sys.database_principals Principals
    INNER JOIN sys.database_role_members Roles
    ON Principals.principal_id = Roles.role_principal_id
) Roles
INNER JOIN sys.database_principals Principals
ON Roles.Member_ID = Principals.principal_id
ORDER BY [Member_Name];


SELECT @code +=    'BEGIN TRY' + CHAR(10) +
                   'INSERT INTO ##database_rolesB
                   SELECT ''' + name +''' as DBName,
                   [Roles].[Role_ID],
                   [Roles].[Role_Name],
                   [Roles].[Member_ID],
                   [Principals].[Name] AS [Member_Name]
            FROM
            (
                SELECT [Principals].[Name] AS [Role_Name],
                       [Roles].[role_principal_id] AS [Role_ID],
                       [Roles].[member_principal_id] AS [Member_ID]
                FROM   ' + QUOTENAME(name) + '.sys.database_principals Principals
                INNER JOIN ' + QUOTENAME(name) + '.sys.database_role_members Roles
                ON Principals.principal_id = Roles.role_principal_id
            ) Roles
            INNER JOIN ' + QUOTENAME(name) + '.sys.database_principals Principals
            ON Roles.Member_ID = Principals.principal_id
            ORDER BY [Member_Name];' + CHAR(10) +
                   'END TRY' + CHAR(10) +
                   'BEGIN CATCH PRINT ''Cannot Read database_roles information from ' + name + '''; END CATCH' + CHAR(10) + CHAR(10)
FROM sys.databases
WHERE [name] <> 'Master'

PRINT @code

EXEC sp_executesql @code

SELECT * FROM ##database_rolesB

DROP TABLE ##database_rolesB
comments powered by Disqus

Related Posts

SQL Server Diagnostic Notebook Updated

Good news everyone! As always, I’ve updated the SQL Server Diagnostic Notebook to include the latest updates to the source scripts

Read More
How to build an Application with modern Technology

How to build an Application with modern Technology

Nemo vel ad consectetur namut rutrum ex, venenatis sollicitudin urna. Aliquam erat volutpat.

Read More

Can a single query deadlock itself? Apparently, yes: A curious case of Intra-query Parallelism

Imagine that you’re in a SQL data warehouse in the middle of the night, a single stored procedure is running, is nothing else, and it’s simply doing inserts and updates, one statement at the time, but then.

Read More