ひょんなことからADSIを使用するはめになったので、勉強した結果を掲載したいと思います。今回は、System.DirectoryServices 名前空間のクラスを使用してアクティブディレクトリのディレクトリオブジェクトへのバインドと簡単な操作をしてみます。

動作確認環境

  • 確認環境:Windows Server 2003
  • 作成環境:Visual Studio 2008 Professional

前提として、ActiveDirectory に実行するマシンが参加している事とします。

1. ディレクトリオブジェクトへのバインド

1.1 接続パスを指定して接続する

DirectoryEntryクラスを使用して、ディレクトリオブジェクトへバインドします。ドメインのルートの識別名が DC=crm1,DC=local とした場合、現在ログイン中のユーザの資格情報を使用して、 DirectoryEntry を使用する場合は次のようにします。

DirectoryEntry("LDAP://DC=crm1,DC=local", null, null, AuthenticationTypes.Secure);

別のユーザを指定する場合は、2番目の引数にユーザ名,3番目の引数にパスワードを指定します。

特定のLDAPサーバに接続する場合は、完全名(DC=crm1,DC=local)の前に、接続するサーバ名とポート名(既定値は389)を指定します。LDAP サーバがserver1でサービスのポート番号が5555の場合は次のようになります。389ポートの場合はポートは省略可能です。

DirectoryEntry("LDAP://server1:5555/DC=crm1,DC=local", null, null, AuthenticationTypes.Secure);

接続スキームとしてLDAPを指定していますが、フォレスト内の別のドメインのディレクトリオブジェクトにバインドする場合は、GCをスキームとして指定します。GCの場合、ポート番号は389ではなく、3268になります。SSLを使用する場合は、LDAPの接続ポートは636, GCの場合は3269となります。

アクティブディレクトリ用の接続文字として使用できるのはたとえば次のようなものがあります。

現在ログインしているドメインのルートディレクトオブジェクトを使用する場合、LDAP//RootDSEを接続文字列として使用します。RootDSE はドメインとドメイン コントローラの情報を提供することができるディレクトリオブジェクトです。RootDSEのプロパティ defaultNamingContext にディレクトリのルートエントリのパス(例:DC=crm1,DC=local)を取得することができます。

たとえば次のようになります。

// System.DirectoryServices.dllへの参照が必要です
static void Main(string[] args)
{
    string dn = string.Empty;
    using (DirectoryEntry entry = GetDirectoryEntry("RootDSE"))
    {
        dn = entry.Properties["defaultNamingContext"].Value as string;
    }
    if (!string.IsNullOrEmpty(dn))
    {
        using (DirectoryEntry entry = GetDirectoryEntry(dn))
        {
            object nativeObject = entry.NativeObject;
            Console.WriteLine(entry.Properties["distinguishedName"].Value);
            Console.WriteLine(entry.Guid);
            byte[] objectGUID = (byte[])entry.Properties["objectGUID"].Value;
            Console.WriteLine(new Guid(objectGUID)); } } Console.ReadLine(); } /// <summary> /// 現在ログインしているアクティブディレクトリにpath /// で指定されたディレクトリオブジェクトをあらわす /// DirectoryEntryのインスタンスを作成する. /// 接続には、プログラムの実行ユーザの視覚情報を使用して /// Kerberos認証を行います。 /// </summary> /// <param name="path">ディレクトリオブジェクトのパス</param> /// <returns></returns> private static DirectoryEntry GetDirectoryEntry(string path) { path = "LDAP://" + path; return new DirectoryEntry(path, null, null, AuthenticationTypes.Secure); }

entry.NativeObject というのは、作成したDirectoryEntryをアクティブディレクトリに強制的にバインディングをさせるために行っています。DirectoryEntry は作成してすぎにバインドされるのではなく、プロパティの読み込みや更新処理を行ったとき初めてバインド処理が行われます。この動作は知ってさえいればそして回避策をしっていればとくに問題はないと思います。強制バインドの方法としてDirectoryEntry.RefreshCache() メソッドを使用する方法もあります。

まとめとして接続文字列のサンプルをいくつか掲載します。

 LDAP://domain.com/OU=Users,DC=domain,DC=com  サーバ名にドメイン名のみを指定してディレクトリオブジェクトにバインドする
LDAP://dc1.domain.com/DC=domain,DC=com ドメインコントローラのDNS名まで指定してディレクトリオブジェクトにバインドする
LDAP://dc1/<GUID=38d27af5-0a83-43e2-9668-dc73e1d2f741> サーバ名のみを指定して接続する。パスにディレクトリオブジェクトのGUIDを指定する。
LDAP://RootDSE ドメインのRootDSEドメインオブジェクトにバインドする
GC://dc1/OU=crmUser,DC=domain,DC=com グローバルカタログサーバにバインドする

サーバー名を指定するのではなく、ドメイン名のみを指定するほうが、接続処理が柔軟だと思います。

ディレクトリオブジェクトのキーは objectGUID 属性値です。上記のサンプルでもわかるとおり、DirectoryEntry.Guidで取得するか、objectGUIDをPropertiesの配列に指定して取得することができます。識別名(DN)が変化しても、objectGUIDは変わることはありません。objectGUIDを使用したバインド例は上記表の3番目に記載しています。

1.2 既知のGUIDを使用する

そのほかのバインド方法として、既知のGUID(well-known guid)を使用する方法があります。接続パスは次のようになります。

DirectoryEntry("<LDAP://<WKGUID=aa312825768811d1aded00c04fd8d5cd,DC=crm1,DC=local>")

WKGUIDで設定しているGUIDはコンピュータグループの既知のGUIDです。このGUIDはobjectGUIDで取得するGUIDとは異なることに注意してください。GUID値はPlatformSDK の ntdsapi.h に記載されています。

コンテナ GUID識別子
Users GUID_USERS_CONTAINER_A
Computers GUID_COMPUTRS_CONTAINER_A
Systems GUID_SYSTEMS_CONTAINER_A
Domain Controllers

GUID_DOMAIN_CONTROLLERS_CONTAINER_A

Infrastructure

GUID_INFRASTRUCTURE_CONTAINER_A

Deleted Objects

GUID_DELETED_OBJECTS_CONTAINER_A

Lost and Found

GUID_LOSTANDFOUND_CONTAINER_A

資格情報に使用するユーザ名について 

DirectoryNameのバインド時に別ユーザの資格情報を使用する場合、ユーザ名は次の形式が使用できます。

  • 完全識別名(Full DN) CN=username,OU=Users,DC=domain,DC=loca). ディレクトリオブジェクトの distinguishedName 属性値
  • NTアカウント名  domain\username
  • ユーザープリンシパル名 username@domain.local ディレクトリオブジェクトの userPrincipalName属性値
  • アカウント名 username ディレクトリオブジェクトの sAMAccountName の値

ただし、接続のセキュリティオプションによっては使用できない形式があるので注意。アカウント名はAuthentcationTypes.Secureの場合のみ使用でき、完全識別名はAuthenticationTypes.Secureではない場合に使用できます。また、ADAMを使用している場合は、ユーザ名は完全識別名かユーザプリンシパル名を使用します。

ユーザプリンシパルが重複する可能性がある
ユーザプリンシパルはフォレスト内でユニークでなければならないのにもかかわらず、重複する可能性があります。以下のリンク参照
ユーザー プリンシパル名が重複する可能性がある
http://support.microsoft.com/kb/251359/ja

2. ディレクトリオブジェクトのCRUD

System.DirectoryServices.DirectoryEntry のCRUDを簡単に記載します。ディレクトリオブジェクトの検索は DirectorySearcher クラスを使用します。ディレクトリオブジェクトを作成するには、作成する親の DicretoryEntry のインスタンスを作成し、DirectoryEntry.Children.Add メソッドを使用し、プロパティを設定した後に、DirectoryEntry.CommitChanges() メソッドを使用します。MSDNを参照してもらえればサンプルが掲載されていると思います。更新処理は、同様にCommitChangesメソッドを使用します。削除は、ひとつのディレクトリオブジェクト,子要素が空のディレクトリオブジェクトを削除するには、 DirectoryEntries.Remove メソッドを使用し、自身と子ディレクトリオブジェクトをすべて削除する場合は、DirectoryEntry.DeleteTree() メソッドを使用します。 パスを変更する場合は、DirectoryEntry.MoveTo メソッドを使用し、相対名(RDN)のみを変更する場合は、DirectoryEntry.Rename メソッドを使用します。

3. ディレクトリオブジェクトのすべての属性値を表示するサンプル

ディレクトリオブジェクトにバインドして、そのすべての属性値を標準出力に表示するサンプルを掲載します。下記プログラムでは、constructedな属性値を取得することはできません。その場合は、DirectoryEntry.RefreshCache()を使用して、属性値を取得します。

public  void DisplayAllProperty()
{
    using (DirectoryEntry user = GetDirectoryEntry("CN=crmuser02,OU=CrmUsers,DC=crm1,DC=local"))
    {
        foreach (PropertyValueCollection values in user.Properties)
        {
            Console.WriteLine("PropName:" + values.PropertyName);
            foreach (Object val in values)
            {
                if (val is byte[])
                {
                     byte[] v = (byte[])val;
                     Console.Write("\t" + val);
                     Console.WriteLine(":" + BitConverter.ToString(v));
                }
                else
                {
                    Console.WriteLine("\t" + val);
                }
            }
        }
    }
} 

説明は以上です。間違い等があれば、ご連絡ください。