「VBScript で ADSI を使用し Active Directory 上のユーザの所属するグループの一覧の取得を行う方法。ただし 5000 件以上かつプライマリ グループも取りたい!」

東京は何だか急に肌寒くなってきて、まるで秋のようだと感じています。自転車で保育園の送り迎えをするときも、汗でじっとりすることもなく快適です。皆さんいかがお過ごしでしょうか。

私は最近駅近くの自転車駐輪場を利用することになったのですが、ここの管理人おじさんが、朝自転車を置きに行くたびに

へいらっしゃい!」

とまるで八百屋さんのようなイキのいい、ステキなご挨拶をくださるのに何だか違和感を感じてしまう今日この頃です。

さてそれはともかくとして、今日の御題を行って見ましょう。

<< 今日のお題 : VBScript で ADSI を使用し Active Directory 上のユーザの所属するグループの一覧の取得を行う方法。ただし 5000 件以上かつプライマリ グループも取りたい! >>

さて、管理者の皆様は激しく高速にまるっと取ってきたいと熱望されるのではないでしょうか。
やり方としては、Windows Management Instrumentation (WMI) を使ったり、range を使ってぐるぐると激しく回したりとかありますが、WMI は便利だけど、遅いです。はっきり言って遅いと思います。

こうした場合に高速化させつつ激しく全てのデータを取ってくる方法をご紹介させていただきたいと思います。

<< ポイント >>
"プライマリ グループが" というのが今回のキモだったりします。

ActiveDirectory オブジェクト(User/Group) について、グループ情報は以下属性に保持されています。

・memberOf : … プライマリ グループ以外の所属グループ情報を保持します。
・primaryGroupID : … プライマリ グループのID値(RID)を保持します。

memberOf 属性は distinguishedName 値(文字列)を保持しており、この値から Group オブジェクトを参照することが可能です。

primaryGroupID は RID値(数値)であり、この数値から Group オブジェクトを参照するためにはスクリプト実装が必要となります。
つまり、ユーザが所属するグループを全部とってくる場合は、非プライマリ グループ以外のグループ取得後プライマリ グループ専用の取得処理を行う必要があるということです。

ただし、Windows Server 2003 環境では、Range を使用しない場合 ADSI のグループ メンバーを一括取得する際は 1500 件という制限が存在します。
1500 件以上の場合、range 処理を分割して対処することで結果的にすべてのデータを取得する必要があります。

<< コード例 1 : range 指定>>
このコード自体は下記の MSDN ライブラリに記載があるものを
拡張したものとなります。

 Example Code for Using Ranging to Retrieve Members of a Group
 https://msdn.microsoft.com/en-us/library/aa705933.aspx

値などについては環境に合わせて適宜変更ください。

'<-------------------- ここから
strUserDN = "CN=user5k,OU=Test5000,OU=Sales,DC=domA,DC=Test"
strUsername = "doma\administrator"
strPassword = "Password1"

Set oConn = CreateObject("ADODB.Connection")
Set oComm = CreateObject("ADODB.Command")

oConn.Provider = "ADsDSOObject"
oConn.Properties("ADSI Flag") = 1

If strUsername <> "" Then
oConn.Properties("User ID") = strUsername
oConn.Properties("Password") = strPassword
End If

oConn.Open
oComm.ActiveConnection = oConn

' For compatibility with all operating systems, the number of objects
' retrieved by each query should not exceed 999.
rangeStep = 999

lastLoop = False
lowRange = 0
highRange = lowRange + rangeStep

commandPrefix = "<LDAP://" & strUserDN & ">;(&(objectClass=user)(objectcategory=person));memberof;range="
commandSuffix = ";base"

Do
If lastLoop Then
' Perform this query with the "range=<lowRange>-*" range.
oComm.CommandText = commandPrefix & lowRange & "-*" & commandSuffix
Else
' Perform this query with the "range=<lowRange>-<highRange>" range.
oComm.CommandText = commandPrefix & lowRange & "-" & highRange & commandSuffix
End If
wscript.echo "Current search command: " & oComm.CommandText

' Execute the query.
Set oRS = oComm.Execute

' Reset the retrieved members counter.
nRetrieved = 0

' Enumerate the retrieved members.
While Not oRS.EOF
For Each oField In oRS.Fields
If VarType(oField) = (vbArray + vbVariant) Then
For Each oValue In oField.Value
wscript.echo vbTab & oValue
nRetrieved = nRetrieved + 1
Next
End If
Next
oRS.MoveNext
Wend

' If the last query was performed, exit the loop.
If lastLoop = True Then
Exit Do
End If

If nRetrieved = 0 Then
' No objects were retrieved by the last query; perform one last query
' with the "range=<lowRange>-*" range.
lastLoop = True
Else
' Increment the high and low ranges to query for the next block of objects.
lowRange = highRange + 1
highRange = lowRange + rangeStep
End If
Loop While True

'<-------------------- ここまで

<< コード例 2 : プライマリ グループ>>
RID 値から グループの SID を解決する参照方法としては、以下の二つの方法があります。
 
 1. WMI(Windows Management Instrumentation)
 2. 数値/文字演算

以下、上記 1. および 2. の両方を含むコード例です。range 指定の上記処理に追加して対応可能です。
 
'<-------------------- ここから
Set objUser = GetObject("LDAP://cn=User01,ou=TestOU,dc=testsvr30,dc=local")
Wscript.Echo GetPrimaryGroup1(objUser)
Wscript.Echo GetPrimaryGroup2(objUser)
 
'ADSI + WMI Version
Function GetPrimaryGroup1( objADs )
strGroupRID = objADs.primaryGroupID
strAccount = objADs.sAMAccountName
strDomain = objADs.userPrincipalName
strDomain = mid(strDomain, InStr(strDomain, "@")+1)
strDomain = left(strDomain, InStr(strDomain, ".")-1)
 
'WMI により 該当ドメインの SID 部分を取得 - Method 1. WMI
Set objWMI = GetObject("winmgmts:{impersonationlevel=impersonate}!" _
& "/root/cimv2:Win32_UserAccount.Domain='" & strDomain & "'" _
& ",Name='" & strAccount & "'")
 
'PrimaryGroupID(RID値) から SID を構成
strSID = mid(objWMI.SID, 1, InStrREV(objWMI.SID,"-")) & strGroupRID
 
'SID を元に PrimaryGroup の DN を取得
Set objGroup = GetObject("LDAP://<SID=" & strSID & ">")
GetPrimaryGroup1 = objGroup.distinguishedName
 
End Function
 
'ADSI Only - Method 2. 数値/文字演算
Function GetPrimaryGroup2( objADs )
 
'該当オブジェクトドメインの SID 部分を取得
objVal = objADs.ObjectSid
strSID = ""
For i = lBound(objVal) to uBound(objVal) - 4
hByte = ascb(midb(objVal, i+1, 1))
lByte = hByte mod 16
hByte = hByte \ 16
strSID = strSID & hex(hByte) & hex(lByte)
Next
 
'PrimaryGroupID(RID値) から SID を構成
strRid = right("0000000" & hex(objADs.primaryGroupID), 8)
hRid = right(left(strRid,4) ,2 ) & left(left(strRid,4) ,2 )
lRid = right(right(strRid,4) ,2 ) & left(right(strRid,4) ,2 )
strSID = strSID & lRid & hRid
 
'SID を元に PrimaryGroup の DN を取得
Set objGroup = GetObject("LDAP://<SID=" & strSID & ">")
GetPrimaryGroup2 = objGroup.distinguishedName
 
End Function
'<-------------------- ここまで

いかがでしたか?
皆さんのお役に立てれば幸いです。

寒暖の差が激しいこのごろですが、ご自愛くださいませ!

(2008/09/14 追記) この件についての関連案件です。

https://blogs.technet.com/jpilmblg/archive/2008/09/01/1015-teched-qa.aspx

-- お母さんより --