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