.NET で、ドメインサーバーにある情報を検索するには、3 つのクラスを駆使します。
- DirectoryEntry クラス: エントリそのもの
- DirectorySearcher クラス: LDAP クエリで検索
- SearchResult クラス: DirectorySearcher で検索した結果
あとは、DirectoryEntry オブジェクトの Properties コレクションを使えば、なんとかなるのですが…結構、これが慣れるまでが大変なので、メモがてら公開しておきます。
# 事情があって、コードは VB で。
■ドメイン構成と問題
ドメイン構成は、下記のようになっています。
訳あって、ドメインサーバーが2つあります。通常、ログインするほうは、plan.local ドメインなのですが、グループの設定やらなにやらがあるのは、moonmile.local ドメインのほうなのです。まぁ、通常業務のセキュリティ(文書閲覧とか)は plan.local ドメインで行っていて、アプリケーション絡みのややこしいセキュリティ関係は moonmile.local に閉じ込めた、と考えてください。
ここで、tomoaki@plan.local のユーザーがログインしているときに、GRP001 などのグループに属しているか?をチェックする、ことになります。
普通ならば、plan.local のほうにグループを作ればいいのですが…そこは業務的な制限です。
■ユーザーとグループの設定
実験的に、windows server 2008 R2 を使って、設定しています。
tomoaki@plan.local ユーザーを、どのようにして moonmile.local のほうに潜り込ませるかというと、tomoaki@plan.local の SID を使ったユーザーを moonmile.local に作成します。
これを moonmile.local ドメイン内で検索して、グループに入っているかどうかをチェックしようという仕組みです。
ForeignSecurityPrincipals のほうに入れているのは、SID を公開しているか、一応、ってことですね。本来ならば、moonmile.local と plan.local の SID を同じものにすれば話は簡単なのですが、作り方が分からない(苦笑)ので、別々の SID になります。
SID 自体をユーザー名にしていまいます。windows server 2008 R2 だと、SID の長さのままだと後ろのほうが切れてしまうので、実際に検索するのは表示名(displayName)になります。
■実験開始
少しずつ作っている/作ったので、ボタンが4つあります。
- AD 検索(全検索): ひとまず、全検索してみる。
- ForeignSecurityPrincipals: CN を指定して、絞ってみる。
- ad-sv 問合せ: tomoaki ユーザーがログインするほうで、SID を取得します(実際は、ドメインのユーザー で、WindowsIdentity.GetCurrent().User のように SID が取得できます。
- win2008-sv 問合せ: グループ名まで検索します。
■AD 全検索
単純に、AD の情報を取得します。
Private Sub Button1_Click(sender As System.Object, e As System.EventArgs) Handles Button1.Click Dim root As New DirectoryEntry("LDAP://win2008-sv/DC=moonmile,DC=local", "masuda", "password") Dim se As New DirectorySearcher(root) ListBox1.Items.Clear() For Each res As SearchResult In se.FindAll Dim de As DirectoryEntry = res.GetDirectoryEntry Debug.Print(de.Path) ListBox1.Items.Add(de.Path) Next End Sub
LDAP クエリを指定して、DirectoryEntry オブジェクトを作成します。ここでは、ドメインに入っていない状態なので、AD を検索可能なユーザー名とパスワードを指定していますが、既にドメインに入っている場合は、
Dim root As New DirectoryEntry("LDAP://win2008-sv/DC=moonmile,DC=local")
のように指定しても OK です。また ドメインサーバーのフォワードがきちんと設定されていれば、
Dim root As New DirectoryEntry("LDAP://DC=moonmile,DC=local")
のように、サーバー名が無くても動作します。
全検索して、プログラム内で for/if しても良いのですが、ドメインサーバーに負担を掛けそうなので、もうちょっと工夫が必要です。
■CN などで検索を絞る
外部に公開している場合「CN=ForeignSecurityPrincipals」を付ければ少しは負担が軽くなります。
Private Sub Button2_Click(sender As System.Object, e As System.EventArgs) Handles Button2.Click Dim root As New DirectoryEntry("LDAP://win2008-sv/CN=ForeignSecurityPrincipals,DC=moonmile,DC=local", "masuda", "password") Dim obj = root.NativeObject Dim se As New DirectorySearcher(root) Dim de2 As DirectoryEntry = Nothing ListBox1.Items.Clear() For Each res As SearchResult In se.FindAll Dim de As DirectoryEntry = res.GetDirectoryEntry Debug.Print(de.Path) ListBox1.Items.Add(de.Path + " " + de.Properties("displayName").Value) If de.Path.IndexOf("増田 トニー") >= 0 Then de2 = de End If Next For Each nm In de2.Properties.PropertyNames Debug.Print(nm) Next End Sub
表示名を調べるときは、de.Properties(“displayName”).Value のように、Properties プロパティを使えば OK です。ただし、目的のユーザーが指定できる場合は、DirectorySearcher で new するときにフィルターを指定したほうが良さそうです。
ちなみに、ここでデバッグ出力されるプロパティは、以下のものです。
objectClass cn sn givenName distinguishedName instanceType whenCreated whenChanged displayName uSNCreated memberOf uSNChanged20111221 name objectGUID userAccountControl badPwdCount codePage countryCode badPasswordTime lastLogoff lastLogon pwdLastSet primaryGroupID objectSid accountExpires logonCount sAMAccountName sAMAccountType userPrincipalName objectCategory dSCorePropagationData msDS-SupportedEncryptionTypes nTSecurityDescriptor
ここでは、表示名(displayName)とSID(objectSid)を使います。あと、ユーザーが属しているグループを memberOf を使うと取得できます。
■フィルターを使ってみる
DirectorySearcher クラスで指定するフィルター(LDAPクエリ)を使って、カテゴリ(objectCategory)と名前で検索データを絞れます。
Private Sub Button3_Click(sender As System.Object, e As System.EventArgs) Handles Button3.Click Dim root As New DirectoryEntry("LDAP://ad-sv/DC=plan,DC=local", "tomoaki", "password") Dim obj = root.NativeObject Dim filter As String = "(&(objectCategory=User)(name=tomoaki))" Dim se As New DirectorySearcher(root, filter) Dim de As DirectoryEntry = se.FindOne.GetDirectoryEntry ListBox1.Items.Clear() For Each nm In de.Properties.PropertyNames Debug.Print(nm) Dim s As String = String.Format("{0}={1}", nm, de.Properties(nm).Value) ListBox1.Items.Add(s) Next Dim sid As String = SidToStringSid( CType(de.Properties("objectSid").Value, Byte())) Debug.Print(sid) End Sub Declare Auto Function ConvertSidToStringSid Lib "advapi32.dll" (ByVal pSID() As Byte, _ ByRef ptrSid As IntPtr) As Boolean Private Function SidToStringSid(ByRef bytes As Byte()) As String Dim psid As IntPtr = Nothing Dim sid As String = "" ConvertSidToStringSid(bytes, psid) sid = System.Runtime.InteropServices.Marshal.PtrToStringAuto(psid) Return sid End Function
あと、おまけですが、objectSid で取得するデータは byte 型の配列なのでちょっと扱いづらいのです。「S-…」のような文字列で使っていきたいので、変換関数を作ります。
ちなみに、CType(de.Properties(“objectSid”).Value, Byte()) のところが非常に遅いのですよね…CType を使って Byte 配列にするところが遅いらしい。DirectCast を使ってみたのですが、スピードはさほど変わらないので、妙なことになっているのかもしれません。このあたりは、後で調べる…ハズ。
■属しているグループの検索
ドメインにログオンしているユーザーの SID は WindowsIdentity.GetCurrent.User で取得できるので、最初の「ad-sv で “tomoaki” を検索」部分は不要になります。
Private Sub Button4_Click(sender As System.Object, e As System.EventArgs) Handles Button4.Click ' ad-sv で "tomoaki" を検索 Dim root As New DirectoryEntry("LDAP://ad-sv/DC=plan,DC=local", "tomoaki", "password") Dim filter As String = "(&(objectCategory=User)(name=tomoaki))" Dim se As New DirectorySearcher(root, filter) Dim de As DirectoryEntry = se.FindOne.GetDirectoryEntry Dim bytes As Byte() = CType(de.Properties("objectSID").Value, Byte()) Dim sid As String = SidToStringSid(bytes) ' win2008-sv で sid で検索 root = New DirectoryEntry("LDAP://win2008-sv/CN=ForeignSecurityPrincipals,DC=moonmile,DC=local", "masuda", "password") filter = String.Format("(&(objectCategory=User)(displayName={0}))", sid) se = New DirectorySearcher(root, filter) de = se.FindOne.GetDirectoryEntry Dim sid2 As String = SidToStringSid(CType(de.Properties("objectSID").Value, Byte())) ListBox1.Items.Add("SID1:" + sid) ListBox1.Items.Add("SID2:" + sid2) ' 属しているグループを取得 root = New DirectoryEntry("LDAP://win2008-sv/DC=moonmile,DC=local", "masuda", "password") Dim groups As List(Of DirectoryEntry) = GetGroups(root, de) For Each ent As DirectoryEntry In groups ListBox1.Items.Add(ent.Properties("name").Value) Next End Sub Private Function GetGroups(root As DirectoryEntry, de As DirectoryEntry) As List(Of DirectoryEntry) Dim lst As New List(Of DirectoryEntry) If de.Properties("memberOf").Value IsNot Nothing Then If de.Properties("memberOf").Value.GetType Is GetType(String) Then Dim grp As String = de.Properties("memberOf").Value Dim se As New DirectorySearcher(root, String.Format("(&(objectCategory=Group)(distinguishedName={0}))", grp)) Dim ent As DirectoryEntry = se.FindOne.GetDirectoryEntry lst.Add(ent) lst.AddRange(GetGroups(root, ent)) Else Dim groups As Object() = CType(de.Properties("memberOf").Value, Object()) For Each grp As String In groups Dim se As New DirectorySearcher(root, String.Format("(&(objectCategory=Group)(distinguishedName={0}))", grp)) Dim ent As DirectoryEntry = se.FindOne.GetDirectoryEntry lst.Add(ent) lst.AddRange(GetGroups(root, ent)) Next End If End If Return lst End Function
LDAP クエリを使って、表示名(displayName)の SID の一致を検索するわけです。その時の DirectoryEntry オブジェクトが、それぞれのグループに属しているので、memberOf を使って調べていきます。取得した DirectoryEntry の SID を表示させていますが、実はこれも不要です。
属しているグループは、再帰的に検索させています。これは、GRP001 が GRP001PA に属している場合、ユーザーが属しているグループとしては「GRP001,GRP001PA」のように、両方とも取得させたいためです。memberOf プロパティで取得するデータは、ややこしいことに、String 単体と object 配列の 2 種類が存在します。属しているグループが1つの場合は String 単体で、2 つ以上属している場合は、String 配列が返されるという….変な仕様のため、GetType でクラスを比較させています。
ここまで来ると、属しているグループの一覧が取得できるので、グループのエントリから name プロパティなどを使えば、どのグループに属しているかどうかは簡単に調べられます。
■パフォーマンスの問題
これを試しに実行すると結構待たされます。多分、LDAP クエリの作り方がまずいような気がするのですが、object 配列から byte 配列へのキャスト(ctype)も結構重いのです。
最後の例だと、
se = New DirectorySearcher(root, filter) ★1 de = se.FindOne.GetDirectoryEntry Dim sid2 As String = SidToStringSid(CType(de.Properties("objectSID").Value, Byte())) ★2
のように、★1 の検索と、★2 の byte 配列への変換で遅くなります。
ここでは、SID を表示させているだけなので、ここは削ってしまうと早くなります。
LDAP クエリの検索部分は、キャッシュを使うようにすれば早くなるんですかね…問合せなので多少は掛かってもよいのでしょうが、もうちょっとレスポンスが良いほうがいいなぁと。
— 補足 2011/12/22
byte 配列のところ、以下のように分解すると、
Dim obj As Object = de.Properties("objectSid").Value ★ここで遅くなっている Dim bytes As Byte() = CType(obj, Byte()) Dim sid As String = SidToStringSid(bytes)
どうやら、Properties にアクセスして値を拾ってくるところが重たいようです。byte 配列は関係ないですね。
キャッシュを有効にするとかで、スピードがあがる?
突然ですみません。DirectoryEntryで、部署コードがとれるプロパティはないでしょうか?
マゴさん、こんにちは。
AD の環境が手元にないので確認ができないのですが、
「部署」自体は、GROUP で作ってあると思うので、fillter のところで、
Dim filter As String = “(&(objectClass=organizationalUnit)”
あるいは
Dim filter As String = “(&(objectCategory=Group)”
のようにすると「OU」が取れるかもしれません。
このあたりのフィルターの値は、下記のような LDAP, Active Directory の設定を参考にすると良いです。
知られざるActive Directory技術の「舞台裏」:第3回 LDAPを使ってActive Directoryを制御しよう[その1:ldpとcsvde]|gihyo.jp … 技術評論社
http://gihyo.jp/admin/serial/01/ad2010/0003?page=2
↓にグループの検索などがあります。
ユーザやグループなどの情報の検索と取得
http://blogs.wankuma.com/mitchin/archive/2011/07/14/201050.aspx
早々の回答ありがとうございます。
ご提示して頂いたサイトを参考にさせて頂きます。
ご丁寧にありがとうございました。