SQLServerCentral Article

Enumerate Windows Group Members

,

I think the best practice is that, whenever possible, you create an Active Directory group and grant that group permissions in your SQL Server. If you have twenty developers, I like seeing one group , i.e. [mydomain\it-developers] instead of twenty individual logins.

Over time however, things sometimes get a little disjointed, and you've added six, seven or more different Windows groups, and are never really sure who is in what group. The problem with this strategy is that someone that is not the DBA can add someone to an Active Directory group that gives him more access that you intended, since the AD user account can be in multiple groups. It's up to you, the DBA, to identify any AD Users that exist in multiple AD Groups, that might get inadvertently higher permissions.

My Real Life Scenario

So one day i'm just looking at the results of Adam Mechanic's sp_whoisactive and i see a specific business user's login running a query on the production database. But wait a minute, i know the AD group he is in, [mydomain\BusinessReporting], and I KNOW that group definitely doesn't have access to that database.

Since SQL Server doesn't change peoples permissions behind my back, right away, I know someone must have added him to one of the other groups. But which one? Did he get added to mydomain\SQL-DBA's?

The Quick Solution

I put together the script below quickly and then bullet proofed it a little more. The built in extended stored procedure, xp_logininfo, is the core.

That procedure has the ability to enumerate ADgroup details, or to enumerate ADLogin Details, depending on whether you pass in the two parameters. These are the typical examples I always use:

EXEC master..xp_logininfo @acctname = 'mydomain\lizaguirre',@option = 'all' -- Show all paths a user gets his auth from
EXEC master..xp_logininfo @acctname = 'mydomain\authenticatedusers',@option = 'members'     -- show group members

Simple and straight forward,  and I can manually enumerate any groups I want with this.

However, I'm not in the habit of running a line of code, modifying it, and running it again. I love filling the gaps with metadata instead. Everything I code needs to be written with automation in mind. And the code needs to work in all situations, so I have to bug-proof the code for any errors I encounter during testing.

The Grunt Work Details

It's time to break out the Legos! By Legos, I mean all the snippets and tricks I've learned over the years and put them all together. If I'm going to scan one to many AD groups, I'm going to need a cursor to create a loop to call the procedure multiple times. if I call a stored procedure multiple times, I'll have multiple data sets; but I want all the results in ONE dataset. To do that I need a temp table to capture the results.

So I put those pieces in place and discover there are some Windows groups cannot be enumerated. For various reasons, an error like this happens: 

Msg 15404, Level 16, State 5

Could not obtain information about Windows NT group/user 'NT SERVICE\MSSQLSERVER', error code 0x8ac.

This error stops my simple cursor, so I need to add a try-catch to handle that. I added print statements, so I can review the errors, and find out more!

I get five groups that I cannot enumerate, and your mileage may vary, obviously.

  • NT SERVICE\MSSQLSERVER
  • NT SERVICE\ClusSvc
  • NT SERVICE\SQLSERVERAGENT
  • MYDOMAIN\FTL-Accounting
  • MYDOMAIN\SCP-SQL-Admins

Since I went to the trouble of grouping my results, I really want to group my errors too, since print statements are not really "in your face" enough for me to really be aware of. So I added a table variable, which gets it's results in the catch.

Finally, I added a few comments so that when I read this code again, six months from now, I know what I was thinking.

Give this a spin, and let me know what you think! I've attached the identical code as an attachment as well.

--###############################################################################################
-- Quick script to enumerate Active directory users who get permissions from An Active Directory Group
--###############################################################################################
--a table variable capturing any errors in the try...catch below
DECLARE @ErrorRecap TABLE
  (
     ID           INT IDENTITY(1, 1) NOT NULL PRIMARY KEY,
     AccountName  NVARCHAR(256),
     ErrorMessage NVARCHAR(256)
  ) 
IF OBJECT_ID('tempdb.dbo.#tmp') IS NOT NULL
  DROP TABLE #tmp
--table for capturing valid resutls form xp_logininfo
CREATE TABLE [dbo].[#TMP] ( 
[ACCOUNT NAME]       NVARCHAR(256)                        NULL ,
[TYPE]               VARCHAR(8)                           NULL ,
[PRIVILEGE]          VARCHAR(8)                           NULL ,
[MAPPED LOGIN NAME]  NVARCHAR(256)                        NULL ,
[PERMISSION PATH]    NVARCHAR(256)                        NULL )
 DECLARE @groupname NVARCHAR(256)
 DECLARE c1 cursor LOCAL FORWARD_ONLY STATIC READ_ONLY for
  --###############################################################################################
  --cursor definition
  --###############################################################################################
   SELECT name 
   FROM master.sys.server_principals 
   WHERE type_desc =  'WINDOWS_GROUP' 
   --###############################################################################################
  OPEN c1
  FETCH NEXT FROM c1 INTO @groupname
  WHILE @@FETCH_STATUS <> -1
    BEGIN
      BEGIN TRY
        INSERT INTO #tmp([ACCOUNT NAME],[TYPE],[PRIVILEGE],[MAPPED LOGIN NAME],[PERMISSION PATH])
          EXEC master..xp_logininfo @acctname = @groupname,@option = 'members'     -- show group members
      END TRY
      BEGIN CATCH
        --capture the error details
        DECLARE @ErrorSeverity INT, 
                @ErrorNumber INT, 
                @ErrorMessage NVARCHAR(4000), 
                @ErrorState INT
        SET @ErrorSeverity = ERROR_SEVERITY()
        SET @ErrorNumber = ERROR_NUMBER()
        SET @ErrorMessage = ERROR_MESSAGE()
        SET @ErrorState = ERROR_STATE()
       --put all the errors in a table together
        INSERT INTO @ErrorRecap(AccountName,ErrorMessage)
          SELECT @groupname,@ErrorMessage
         --echo out the supressed error, the try catch allows us to continue processing, isntead of stopping on the first error
        PRINT 'Msg ' + convert(varchar,@ErrorNumber) + ' Level ' + convert(varchar,@ErrorSeverity) + ' State ' + Convert(varchar,@ErrorState)
        PRINT @ErrorMessage
    END CATCH
    FETCH NEXT FROM c1 INTO @groupname
    END
  CLOSE c1
  DEALLOCATE c1
--display both results and errors
SELECT * FROM #tmp
SELECT * FROM @ErrorRecap

Resources

Rate

4.6 (30)

You rated this post out of 5. Change rating

Share

Share

Rate

4.6 (30)

You rated this post out of 5. Change rating