Найти рекурсивное членство в группе (Active Directory) с помощью C#
Я ищу, чтобы получить список всех групп, членом которых является пользователь в Active Directory, как явно перечисленных в списке свойств memberOf, так и неявно через членство во вложенных группах. Например, если я проверяю UserA и UserA является частью GroupA и GroupB, я также хочу перечислить GroupC, если GroupB является членом GroupC.
Чтобы дать вам более подробное представление о моем заявлении, я буду делать это на ограниченной основе. По сути, я хочу время от времени проверять безопасность, в которой будут перечислены эти дополнительные членства. Я хочу различать два, но это не должно быть трудно.
Моя проблема в том, что я не нашел эффективного способа заставить этот запрос работать. Стандартный текст в Active Directory ( эта статья о CodeProject) показывает способ сделать это, по сути, рекурсивным поиском. Это кажется ужасно неэффективным. Даже в моем небольшом домене у пользователя может быть более 30 групп. Это означает более 30 звонков в Active Directory для одного пользователя.
Я изучил следующий код LDAP, чтобы получить все записи memberOf одновременно:
(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).
6 ответов
Жажда за это интересный вопрос.
Далее, просто исправление, вы говорите:
Я изучил следующий код LDAP, чтобы получить все записи memberOf одновременно:
(memberOf:1.2.840.113556.1.4.1941:={0})
Ты не заставляешь это работать. Я помню, я заставил это работать, когда я узнал о его существовании, но это было в фильтре LDIFDE.EXE. Поэтому я применяю его к ADSI в C#, и он все еще работает. В образце, который я взял у Microsoft, было слишком много скобок, но он работал ( источник в синтаксисе фильтра поиска AD).
В соответствии с вашим замечанием о том, что мы не знаем, принадлежит ли пользователь к группе явно, я добавляю еще один запрос. Я знаю, что это не очень хорошо, но это лучшее, что я могу сделать.
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.
Если нет другого способа, кроме рекурсивных вызовов (и я не верю, что это так), то, по крайней мере, вы можете позволить платформе сделать всю работу за вас: см. Метод UserPrincipal.GetAuthorizationGroups (в System.DirectoryServices.AccountManagement
пространство имен и введено в.Net 3.5)
Этот метод рекурсивно ищет все группы и возвращает группы, в которые входит пользователь. Возвращенный набор может также включать дополнительные группы, в которых система будет считать пользователя участником для целей авторизации.
Сравните с результатами GetGroups ("Возвращает коллекцию групповых объектов, которые указывают группы, членом которых является текущий участник"), чтобы увидеть, является ли членство явным или неявным.
Вы можете использовать свойства tokenGroups и tokenGroupsGlobalAndUniversal, если вы находитесь на сервере Exchange. 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
}
}
}
}
}
Используйте фильтр ldap рекурсивно, но запрашивайте все группы, возвращаемые после каждого запроса, чтобы уменьшить количество циклов.
Пример:
- Получить все группы, где пользователь является членом
- Получить все группы, членами которых являются группы Step 1
- Получить все группы, в которых участвуют группы Step 2
- ...
По моему опыту, редко бывает больше 5, но определенно должно быть намного меньше 30.
Также:
- Обязательно возвращайте только те свойства, которые вам понадобятся.
- Результаты кэширования могут значительно повысить производительность, но значительно усложнили мой код.
- Обязательно используйте пул соединений.
- Первичная группа должна обрабатываться отдельно
Если вы используете.NET 3.5 или выше, вы можете использовать System.DirectoryServices.AccountManagement
пространство имен, которое действительно делает это легко.
См. Связанный ответ здесь: Вложенные группы Active Directory
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]);
}