web-dev-qa-db-ja.com

C#を使用して再帰的なグループメンバーシップ(Active Directory)を検索する

ユーザーがActive Directoryでメンバーになっているすべてのグループのリストを取得しようとしています。これは、memberOfプロパティリストに明示的にリストされているか、ネストされたグループメンバーシップによって暗黙的にリストされています。たとえば、UserAを調べ、UserAがGroupAとGroupBの一部である場合、GroupBがGroupCのメンバーである場合は、GroupCも一覧表示します。

私のアプリケーションについてもう少し洞察を与えるために、私はこれを限定的に行います。基本的に、これらの追加のメンバーシップをリストするセキュリティチェックをときどき必要としています。私は2つを区別したいと思いますが、それは難しいことではありません。

私の問題は、このクエリを機能させる効率的な方法が見つからないことです。 Active Directoryの標準テキスト( このCodeProject記事 )は、これを行う方法を示しています。これは基本的に再帰的なルックアップです。それはひどく非効率的です。私の小さなドメインでも、ユーザーには30以上のグループメンバーシップがある可能性があります。つまり、1人のユーザーがActive Directoryを30回以上呼び出すことになります。

すべてのmemberOfエントリを一度に取得するために、次のLDAPコードを調べました。

(memberOf:1.2.840.113556.1.4.1941:={0})

ここで、{0}は私のLDAPパスです(例:CN = UserA、OU = Users、DC = foo、DC = org)。ただし、レコードは返されません。この方法の欠点は、たとえそれが機能したとしても、どのグループが明示的で、どのグループが暗黙的であるかがわからないことです。

それが今のところです。 CodeProjectの記事よりも優れた方法があるかどうか、ある場合はそれをどのようにして実現できるかを知りたい(実際のコードはすばらしいでしょう)。 .NET 4.0とC#を使用しています。私のActive DirectoryはWindows 2008の機能レベルです(まだR2ではありません)。

32
IAmTimCorey

これは興味深い質問です。

次に、ちょうど訂正して、あなたは言う:

すべてのmemberOfエントリを一度に取得するために、次のLDAPコードを調べました。

(memberOf:1.2.840.113556.1.4.1941:={0})

あなたはそれを機能させません。その存在について知ったときに私はそれを動作させることを覚えていますが、それはLDIFDE.EXEフィルターにありました。だから私はそれをC#のADSIに適用し、それはまだ機能しています。 Microsoftから取得したサンプルには括弧が多すぎましたが、機能していました( AD検索フィルター構文のソース )。

ユーザーが明示的にグループに属しているかどうかがわからないというあなたの発言によれば、もう1つリクエストを追加します。私はこれがあまり良くないことを知っていますが、私ができる限り最高です。

static void Main(string[] args)
{
  /* Connection to Active Directory
   */
  DirectoryEntry deBase = new DirectoryEntry("LDAP://WM2008R2ENT:389/dc=dom,dc=fr");


  /* To find all the groups that "user1" is a member of :
   * Set the base to the groups container DN; for example root DN (dc=dom,dc=fr) 
   * Set the scope to subtree
   * Use the following filter :
   * (member:1.2.840.113556.1.4.1941:=cn=user1,cn=users,DC=x)
   */
  DirectorySearcher dsLookFor = new DirectorySearcher(deBase);
  dsLookFor.Filter = "(member:1.2.840.113556.1.4.1941:=CN=user1 Users,OU=MonOu,DC=dom,DC=fr)";
  dsLookFor.SearchScope = SearchScope.Subtree;
  dsLookFor.PropertiesToLoad.Add("cn");

  SearchResultCollection srcGroups = dsLookFor.FindAll();

  /* Just to know if user is explicitly in group
   */
  foreach (SearchResult srcGroup in srcGroups)
  {
    Console.WriteLine("{0}", srcGroup.Path);

    foreach (string property in srcGroup.Properties.PropertyNames)
    {
      Console.WriteLine("\t{0} : {1} ", property, srcGroup.Properties[property][0]);
    }

    DirectoryEntry aGroup = new DirectoryEntry(srcGroup.Path);
    DirectorySearcher dsLookForAMermber = new DirectorySearcher(aGroup);
    dsLookForAMermber.Filter = "(member=CN=user1 Users,OU=MonOu,DC=dom,DC=fr)";
    dsLookForAMermber.SearchScope = SearchScope.Base;
    dsLookForAMermber.PropertiesToLoad.Add("cn");

    SearchResultCollection memberInGroup = dsLookForAMermber.FindAll();
    Console.WriteLine("Find the user {0}", memberInGroup.Count);

  }

  Console.ReadLine();
}

私のテストツリーではこれが与える:

LDAP://WM2008R2ENT:389/CN=MonGrpSec,OU=MonOu,DC=dom,DC=fr
adspath : LDAP://WM2008R2ENT:389/CN=MonGrpSec,OU=MonOu,DC=dom,DC=fr
cn : MonGrpSec
Find the user 1

LDAP://WM2008R2ENT:389/CN=MonGrpDis,OU=ForUser1,DC=dom,DC=fr
adspath : LDAP://WM2008R2ENT:389/CN=MonGrpDis,OU=ForUser1,DC=dom,DC=fr
cn : MonGrpDis
Find the user 1

LDAP://WM2008R2ENT:389/CN=MonGrpPlusSec,OU=ForUser1,DC=dom,DC=fr
adspath : LDAP://WM2008R2ENT:389/CN=MonGrpPlusSec,OU=ForUser1,DC=dom,DC=fr
cn : MonGrpPlusSec
Find the user 0

LDAP://WM2008R2ENT:389/CN=MonGrpPlusSecUniv,OU=ForUser1,DC=dom,DC=fr
adspath : LDAP://WM2008R2ENT:389/CN=MonGrpPlusSecUniv,OU=ForUser1,DC=dom,DC=fr
cn : MonGrpPlusSecUniv
Find the user 0

(編集)「1.2.840.113556.1.4.1941」はW2K3 SP1では機能せず、SP2で機能し始めます。 W2K3 R2でも同じだと思います。 W2K8で動作するはずです。ここではW2K8R2でテストします。私はすぐにこれをW2K8でテストできるようになります。

24
JPBlanc

再帰呼び出し以外の方法がない場合(そして、私はそうは思わない)、少なくともフレームワークにあなたに代わって仕事をさせることができます: serPrincipal.GetAuthorizationGroups methodSystem.DirectoryServices.AccountManagement名前空間、.Net 3.5で導入)

このメソッドは、すべてのグループを再帰的に検索し、ユーザーがメンバーであるグループを返します。返されるセットには、システムがユーザーを承認目的でメンバーと見なす追加のグループが含まれる場合もあります。

GetGroups( "現在のプリンシパルがメンバーであるグループを指定するグループオブジェクトのコレクションを返す"の結果と比較します。)メンバーシップが明示的か暗黙的かを確認します。

7
stuartd

exchangeサーバー上にいる場合は、tokenGroupsプロパティとtokenGroupsGlobalAndUniversalプロパティを利用できます。 tokenGroupsは、ネストされたグループやドメインユーザー、ユーザーなどを含む、このユーザーが属するすべてのセキュリティグループを提供しますtokenGroupsGlobalAndUniversalは、tokenGroupsおよび配布グループからのすべてを含みます

private void DoWorkWithUserGroups(string domain, string user)
    {
        var groupType = "tokenGroupsGlobalAndUniversal"; // use tokenGroups for only security groups

        using (var userContext = new PrincipalContext(ContextType.Domain, domain))
        {
            using (var identity = UserPrincipal.FindByIdentity(userContext, IdentityType.SamAccountName, user))
            {
                if (identity == null)
                    return;

                var userEntry = identity.GetUnderlyingObject() as DirectoryEntry;
                userEntry.RefreshCache(new[] { groupType });
                var sids = from byte[] sid in userEntry.Properties[groupType]
                           select new SecurityIdentifier(sid, 0);

                foreach (var sid in sids)
                {
                    using(var groupIdentity = GroupPrincipal.FindByIdentity(userContext, IdentityType.Sid, sid.ToString()))
                    {
                        if(groupIdentity == null)
                            continue; // this group is not in the domain, probably from sidhistory

                        // extract the info you want from the group
                    }
                }
            }
        }
    }
2
Terry Tsay

Ldapフィルターを再帰的に使用しますが、各クエリの後に返されるすべてのグループをクエリして、ラウンドトリップの数を減らします。

例:

  1. ユーザーがメンバーであるすべてのグループを取得する
  2. ステップ1グループがメンバーであるすべてのグループを取得する
  3. ステップ2グループがメンバーであるすべてのグループを取得する
  4. ...

私の経験ではめったに5を超えることはありませんが、間違いなく30をはるかに下回る必要があります。

また:

  • 必要なプロパティのみをプルするようにしてください。
  • 結果をキャッシュするとパフォーマンスが大幅に向上しますが、コードがはるかに複雑になりました。
  • 必ず接続プールを利用してください。
  • プライマリグループは個別に処理する必要があります
2
BellBat

.NET 3.5以降を使用している場合は、System.DirectoryServices.AccountManagement名前空間は、これを本当に簡単にします。

ここで関連する回答を参照してください: Active Directoryのネストされたグループ

1
dave
    static List<SearchResult> ad_find_all_members(string a_sSearchRoot, string a_sGroupDN, string[] a_asPropsToLoad)
    {
        using (DirectoryEntry de = new DirectoryEntry(a_sSearchRoot))
            return ad_find_all_members(de, a_sGroupDN, a_asPropsToLoad);
    }

    static List<SearchResult> ad_find_all_members(DirectoryEntry a_SearchRoot, string a_sGroupDN, string[] a_asPropsToLoad)
    {
        string sDN = "distinguishedName";
        string sOC = "objectClass";
        string sOC_GROUP = "group";
        string[] asPropsToLoad = a_asPropsToLoad;
        Array.Sort<string>(asPropsToLoad);
        if (Array.BinarySearch<string>(asPropsToLoad, sDN) < 0)
        {
            Array.Resize<string>(ref asPropsToLoad, asPropsToLoad.Length+1);
            asPropsToLoad[asPropsToLoad.Length-1] = sDN;
        }
        if (Array.BinarySearch<string>(asPropsToLoad, sOC) < 0)
        {
            Array.Resize<string>(ref asPropsToLoad, asPropsToLoad.Length+1);
            asPropsToLoad[asPropsToLoad.Length-1] = sOC;
        }

        List<SearchResult> lsr = new List<SearchResult>();

        using (DirectorySearcher ds = new DirectorySearcher(a_SearchRoot))
        {
            ds.Filter = "(&(|(objectClass=group)(objectClass=user))(memberOf=" + a_sGroupDN + "))";
            ds.PropertiesToLoad.Clear();
            ds.PropertiesToLoad.AddRange(asPropsToLoad);
            ds.PageSize = 1000;
            ds.SizeLimit = 0;
            foreach (SearchResult sr in ds.FindAll())
                lsr.Add(sr);
        }

        for(int i=0;i<lsr.Count;i++)
            if (lsr[i].Properties.Contains(sOC) && lsr[i].Properties[sOC].Contains(sOC_GROUP))
                lsr.AddRange(ad_find_all_members(a_SearchRoot, (string)lsr[i].Properties[sDN][0], asPropsToLoad));

        return lsr;
    }

    static void Main(string[] args)
    {
    foreach (var sr in ad_find_all_members("LDAP://DC=your-domain,DC=com", "CN=your-group-name,OU=your-group-ou,DC=your-domain,DC=com", new string[] { "sAMAccountName" }))
        Console.WriteLine((string)sr.Properties["distinguishedName"][0] + " : " + (string)sr.Properties["sAMAccountName"][0]);
    }
0