I need to run a foreach loop in DirectorySearcher.FindAll() and get the displayname property. It seems like there are memory issues with that (referred link: Memory Leak when using DirectorySearcher.FindAll()).
My code is as follows:
List<string> usersList = new List<string>();
string displayName = string.Empty;
try
{
using (DirectoryEntry directoryEntry = new DirectoryEntry(ldap, userName, password))
{
DirectorySearcher directorySearcher = new DirectorySearcher(directoryEntry);
directorySearcher.PageSize = 500; // ADD THIS LINE HERE !
string strFilter = "(&(objectCategory=User))";
directorySearcher.PropertiesToLoad.Add("displayname");//first name
directorySearcher.Filter = strFilter;
directorySearcher.CacheResults = false;
SearchResult result;
var resultOne = directorySearcher.FindOne();
using (var resultCol = directorySearcher.FindAll())
{
for (int counter = 0; counter < resultCol.Count; counter++)
{
result = resultCol[counter];
if (result.Properties.Contains("displayname"))
{
displayName = (String)result.Properties["displayname"][0];
usersList.Add(displayName);
}
}
}
}
}
Is there any possible way to looping. I have also tried calling Dispose() method but it doesn't work. Any help is really appreciated.
Here is a solution that I came up with for a project I'm working on that I think would work for you. My solution loops through the search results and returns the display name in a list of strings up to 20.
using (DirectorySearcher ds = new DirectorySearcher())
{
//My original filter
//ds.Filter = string.Format("(|(&(objectClass=group)(|(samaccountname=*{0}*)(displayname=*{0}*)))(&(objectCategory=person)(objectClass=user)(|(samaccountname=*{0}*)(displayname=*{0}*))))", name);
//Your Modified filter
ds.filter = "(objectCategory=User)"
ds.PropertiesToLoad.Add("displayname");
ds.SizeLimit = 20;
SearchResultCollection result = ds.FindAll();
List<string> names = new List<string>();
foreach (SearchResult r in result)
{
var n = r.Properties["displayname"][0].ToString();
if (!names.Contains(n))
names.Add(n);
}
return Json(names, JsonRequestBehavior.AllowGet);
}
If it's not valid solution in managed code and your code depend hard on not-managed things such Active Directory i think it's good idea to move logic of directory searching to isolate such logic from your main application domain. It's way to not run against wind.
For me - better is to use outer console process, send parameters to argument string and catch StandardOutput.
It will be slower, but memory will be not leaked because it will be freed by system after console process die.
Additionally in this case u can not to make self-made ad_searcher.ex, but use standard dsquery and so on. For example:
[https://serverfault.com/questions/49405/command-line-to-list-users-in-a-windows-active-directory-group]
Another approach - usage of managed sub-domains, BUT i think it's harder and i'm not sure that all unmanaged resources will be freed.
Related
PLEASE HELP!!! I have a code to get data from AD. This used to work in SQL2014/Visual Studio2013. Now, we are migrating to SQL2016. I tested the code in a Console App and it worked just fine. It just does not work when I create the same code into a SQL CLR Stored Proc using Visual Studio 2017.
This is the code in the Console App:
static void Main(string[] args)
{
DirectoryEntry ldapConnection;
DirectorySearcher search;
SearchResult result;
DirectoryEntry ldapconnection = null;
string szJDEID = "";
string szErrorMessage = "";
string szNetworkID = "xyz";
//--- Create and return new LDAP connection with desired settings
ldapConnection = new DirectoryEntry("LDAP://DC.company.com:389", "userid", "password");
ldapConnection.Path = "LDAP://DC=company,DC=com";
ldapConnection.AuthenticationType = AuthenticationTypes.Secure;
//--- create search object which operates on ldap connection object and set search object to only find the user specified
search = new DirectorySearcher(ldapconnection);
search.Filter = "(&(samaccountname=" + szNetworkID.Trim() + ")(memberOf=CN=JDEdwards Users,OU=Mail Enabled Manual,OU=Groups,OU=Global,DC=company,DC=com))";
result = search.FindOne();
if (result != null)
{
//--- create an array of properties that we would like and add them to the search object
string[] requiredproperties = new string[] { "extensionattribute13" };
foreach (string property in requiredproperties)
search.PropertiesToLoad.Add(property);
result = search.FindOne();
if (result != null)
{
foreach (string property in requiredproperties)
foreach (object mycollection in result.Properties[property])
szJDEID = mycollection.ToString();
}
}
else
{
szErrorMessage = "ERROR: This user does not belong to the JDEdwards Users AD Group. Please check with the IT Helpdesk team.";
}
}
I get the value of szJDEID as stored in Extension Attribute13. When I put the same code in a SQL CLR Stored Proc, the Logic always returns the szErrorMessage value.
What am I missing?
Thanks in advance.
Finally. As you had rightly pointed earlier, the bindDn was incorrect. The issue is we moved from one DC to another. I used lip.exe to find out the principal - userid#domain.com. Also, the ldapConnection.Path was not needed anymore as it was incorrect with the new DC. So, finally, it is working. Thanks Clint.
I am trying to scrape all the groups a user is a member of in Active Directory (including groups that they are indirect members of), but I am running into memory exception errors.
Latest error message passed was:
Insufficient memory to continue the execution of the program
The code I run is started in a backgroundworker on a schedule.
I have looked through ALL the SO active directory group scraping posts, and tried as many of the iterations of this process as I could get to work. This one will (usually) work if i don't run it on a schedule. Sometimes it will crash the program though.
Our Active Directory database is quite considerably large.
Any help you could offer with this would be great. I don't mind it taking a bit longer, speed is not an overly great concern, as long as I can get it to perform well, and correctly.
Here's what I have:
DataTable resultsTable = new DataTable();
resultsTable.Columns.Add("EmailID");
resultsTable.Columns.Add("UserID");
resultsTable.Columns.Add("memberOf");
resultsTable.Columns.Add("groupType");
resultsTable.Columns.Add("record_date");
try
{
string RecordDate = DateTime.Now.ToString(format: "dd/MM/yyyy", provider: CultureInfo.CreateSpecificCulture("en-GB"));
string ou = "OU=Groups,OU=nonya,DC=my,DC=domain,DC=net";
using (PrincipalContext context = new PrincipalContext(ContextType.Domain, "my.domain.net", ou))
{
using (GroupPrincipal GroupPrin = new GroupPrincipal(context))
{
using (var searcher = new PrincipalSearcher(GroupPrin))
{
using (var results = searcher.FindAll())
{
foreach (var result in results)
{
DirectoryEntry de = result.GetUnderlyingObject() as DirectoryEntry;
string GUID = de.Guid.ToString();
string testname1 = de.Name;
string testParentName1 = de.Parent.Name;
string MasterGroupname = testname1.Substring(3, (testname1.Length - 3));
string type = testParentName1.Substring(3, (testParentName1.Length - 3));
using (var group = GroupPrincipal.FindByIdentity(context, IdentityType.Guid, GUID))
{
using (var users = group.GetMembers(true))
{
foreach (Principal user in users)
{
DataRow dr1 = resultsTable.NewRow();
dr1["EmailID"] = user.UserPrincipalName;
dr1["UserID"] = user.SamAccountName;
dr1["memberOf"] = MasterGroupname;
dr1["groupType"] = type;
dr1["record_date"] = RecordDate;
resultsTable.Rows.Add(dr1);
user.Dispose();
}
}
}
}
}
}
}
}
//Table is uploaded to SQL database here, unless it crashes before reaching this point
}
catch (Exception E)
{
logger.Error($"ADGroupScrape error. System says: {E.Message}");
return false;
}
finally
{
resultsTable.Dispose();
}
Disposing each object at the end of the loop will probably solve your problem (result.Dispose()). I've had memory problems with long loops when working with AD. Assuming you're working with thousands of results, and you're allocating memory for each, and there's no break between processing each result, the garbage collector doesn't have an opportunity to clean up for you.
But you're also unnecessarily going back out to AD to search for the group you already found (GroupPrincipal.FindByIdentity). That would also increase memory consumption, but also slow down the whole operation. Instead, just cast result to a GroupPrincipal so you can call .GetMembers() on it.
Unrelated, but helpful: When you have multiple using blocks nested like that, you can combine them all into one block. It saves you indenting so much.
This is what your code would look like with all those suggestions:
using (PrincipalContext context = new PrincipalContext(ContextType.Domain, "my.domain.net", ou))
using (GroupPrincipal GroupPrin = new GroupPrincipal(context))
using (var searcher = new PrincipalSearcher(GroupPrin))
using (var results = searcher.FindAll())
{
foreach (GroupPrincipal result in results)
{
DirectoryEntry de = result.GetUnderlyingObject() as DirectoryEntry;
string GUID = de.Guid.ToString();
string testname1 = de.Name;
string testParentName1 = de.Parent.Name;
string MasterGroupname = testname1.Substring(3, (testname1.Length - 3));
string type = testParentName1.Substring(3, (testParentName1.Length - 3));
using (var users = result.GetMembers(true))
{
foreach (Principal user in users)
{
DataRow dr1 = resultsTable.NewRow();
dr1["EmailID"] = user.UserPrincipalName;
dr1["UserID"] = user.SamAccountName;
dr1["memberOf"] = MasterGroupname;
dr1["groupType"] = type;
dr1["record_date"] = RecordDate;
user.Dispose();
}
}
result.Dispose();
}
}
This will probably work, but could probably still be much faster. Using Principal objects (and the whole AccountManagement namespace) is a wrapper around DirectoryEntry/DirectorySearcher which makes things somewhat easier for you, but at the cost of performance. Using DirectoryEntry/DirectorySearcher directly is always faster.
If you want to experiment with that, I wrote a couple articles that will help:
Active Directory: Better performance
Active Directory: Find all the members of a group
So the issue didn't appear to be with my code, but thank you all for the suggestions in cleaning it up.
I changed the Target Platform in Configuration Manager (Visual Studio) to X64, as it was originally set to "Any CPU", and my code now runs successfully every time.
Couple of months down the line and i started getting out of memory exceptions again.
added pagination (which i didnt actually know you could do) and it seems to have resolved the issue. The line i added was ((DirectorySearcher)searcher.GetUnderlyingSearcher()).PageSize = 500; in between the searcher declaration and the searcher.FindAll().
using (var searcher = new PrincipalSearcher(GroupPrin))
{
((DirectorySearcher)searcher.GetUnderlyingSearcher()).PageSize = 500;
using (var results = searcher.FindAll())
{
I currently have a method that queries for Canonical Name and the CN of a specified machine. Although, when validating the data within the method, nothing is outputted. I'm not sure if I've formatted something incorrectly or didn't filter properly. I've tried a number of things (changing the format of search.Filter), etc., to no avail. Below is the latest code I have. I'm hoping someone will see something that I am missing.
public class ComputersCheck
{
public string OU { get; set; }
public string ComputerName { get; set; }
public override string ToString()
{
return OU + " " + " " + ComputerName;
}
}
public List<ComputersCheck> GetOU(string PCName)
{
List<ComputersCheck> Computers = new List<ComputersCheck>().ToList();
string DomainPath = "LDAP://DC=DOMAIN,DC=COM";
DirectoryEntry searchRoot = new DirectoryEntry(DomainPath);
DirectorySearcher search = new DirectorySearcher(searchRoot);
search.Filter = "(&(objectClass=Computer)(computerName=MACHINE_NAME))";
search.PropertiesToLoad.Add("CN");
search.PropertiesToLoad.Add("Canonical Name");
SearchResult result;
SearchResultCollection resultCol = search.FindAll();
if (resultCol != null)
{
for (int counter = 0; counter < resultCol.Count; counter++)
{
string ComputerNameOUString = string.Empty;
result = resultCol[counter];
if(result.Properties.Contains("CN") &&
result.Properties.Contains("Canonical Name"))
{
ComputersCheck objSurveyComputers = new ComputersCheck();
objSurveyComputers.ComputerName = (String)result.Properties["CN"][0];
objSurveyComputers.OU = (String)result.Properties["Canonical Name"][0];
Computers.Add(objSurveyComputers);
}
}
}
foreach (ComputersCheck computer in Computers)
{
Console.WriteLine(computer);
}
search.Dispose();
searchRoot.Dispose();
return Computers;
}
You do have a few things that need to be corrected:
There is no computerName attribute. The attribute you're after is called sAMAccountName, which is the same attribute used for the username for user accounts. However, for computers, it is the computer name followed by the $ character. So your search should look like this:
(&(objectClass=computer)(sAMAccountName=MACHINE_NAME$))
Attribute names never have spaces. You want the canonicalName attribute. I see you are assigning it to a property called OU. Just be aware that the canonicalName includes the name of the object itself at the end, not just the OU.
search.PropertiesToLoad.Add("canonicalName");
...
objSurveyComputers.OU = (String)result.Properties["canonicalName"][0];
You are calling .Dispose() on a couple objects, which is fine (although not absolutely needed). But the most important one to dispose is resultCol, because the documentation for SearchResultCollection says:
Due to implementation restrictions, the SearchResultCollection class cannot release all of its unmanaged resources when it is garbage collected. To prevent a memory leak, you must call the Dispose method when the SearchResultCollection object is no longer needed.
You can also put it in a using statement instead of calling .Dispose() manually, which would have the same effect.
I have an issue using c# on .Net 4 in a MVC web application, where when I query Active Directory, I frequently get an error: Attempted to access an unloaded appdomain. (Exception from HRESULT: 0x80131014).
The strange thing is, that it will work flawlessly for a time, and then it will just start happening, and then just disappear again.
I have made a few modifications to the function to get it to work , but they all seem to fail. I am wondering if I am doing something wrong, or if there is a better way to do it.
Here is my current function, that will accept a loginId, and a PrincipalContext. The loginId can either be the user DisplayName i.e "John Smith", or DOMAINNAME\josmi. The default is to use the first 2 letters of their firstname, and then the first 3 letters of their surname. There is a check in there if this is not the case. This part if fine.
public List<ADGroup> GetMemberGroups(string loginId, PrincipalContext principalContext, int tries = 0)
{
var result = new List<ADGroup>();
try
{
var samAccountName = "";
if (loginId.Contains(" "))
{
var fName = loginId.Split(Char.Parse(" "))[0].ToLower();
var sName = loginId.Split(Char.Parse(" "))[1].ToLower();
if (sName.Trim().Length == 2)
samAccountName = string.Format("{0}{1}", fName.StartsWith(".") ? fName.Substring(0, 4) : fName.Substring(0, 3), sName.Substring(0, 2));
else
samAccountName = string.Format("{0}{1}", fName.StartsWith(".") ? fName.Substring(0, 3) : fName.Substring(0, 2), sName.Substring(0, 3));
}
else
samAccountName = loginId.Substring(loginId.IndexOf(#"\") + 1);
var authPrincipal = UserPrincipal.FindByIdentity(principalContext, IdentityType.SamAccountName, samAccountName);
if (authPrincipal == null)
throw new Exception(string.Format("authPrincipal is null for loginId - {0}", loginId));
var firstLevelGroups = authPrincipal.GetGroups();
AddGroups(firstLevelGroups, ref result);
}
catch
{
if (tries > 5)
throw;
tries += 1;
System.Threading.Thread.Sleep(1000);
GetMemberGroups(loginId, principalContext, tries);
}
return result;
}
private void AddGroups(PrincipalSearchResult<Principal> principal, ref List<ADGroup> returnList)
{
foreach (var item in principal)
{
if (item.GetGroups().Count() > 0)
AddGroups(item.GetGroups(), ref returnList);
returnList.Add(new ADGroup(item.SamAccountName, item.Sid.Value));
}
}
This function is called like this:
MembershipGroups = ad.GetMemberGroups(user.SamAccountName, new PrincipalContext(ContextType.Domain));
The the error that I SOMETIMES get is:
System.AppDomainUnloadedException:
Attempted to access an unloaded
appdomain. (Exception from HRESULT:
0x80131014) at
System.StubHelpers.StubHelpers.InternalGetCOMHRExceptionObject(Int32
hr, IntPtr pCPCMD, Object pThis) at
System.StubHelpers.StubHelpers.GetCOMHRExceptionObject(Int32
hr, IntPtr pCPCMD, Object pThis) at
System.DirectoryServices.AccountManagement.UnsafeNativeMethods.IADsPathname.Retrieve(Int32
lnFormatType) at
System.DirectoryServices.AccountManagement.ADStoreCtx.LoadDomainInfo()
at
System.DirectoryServices.AccountManagement.ADStoreCtx.get_UserSuppliedServerName()
at
System.DirectoryServices.AccountManagement.ADDNLinkedAttrSet.BuildPathFromDN(String
dn) at
System.DirectoryServices.AccountManagement.ADDNLinkedAttrSet.MoveNextPrimaryGroupDN()
at
System.DirectoryServices.AccountManagement.ADDNLinkedAttrSet.MoveNext()
at
System.DirectoryServices.AccountManagement.FindResultEnumerator1.MoveNext()
at
System.DirectoryServices.AccountManagement.FindResultEnumerator1.System.Collections.IEnumerator.MoveNext()
looking though reflector at System.DirectoryServices.AccountManagement the internal class "UnsafeNativeMethods" is implemented in native code, so UserSuppliedServerName one level up is all I can go on without looking at the CLR VM, (frankly im not sure even how to do that) Seems that a node is failing to return its primary group, so perhaps consider other implementations, after a bit of googling ive come across these that may help
Active Directory and nested groups this one may be promising heres the code sample..
public IList<string> FindUserGroupsLdap(string username)
{
// setup credentials and connection
var credentials = new NetworkCredential("username", "password", "domain");
var ldapidentifier = new LdapDirectoryIdentifier("server", 389, true, false);
var ldapConn = new LdapConnection(ldapidentifier, credentials);
// retrieving the rootDomainNamingContext, this will make sure we query the absolute root
var getRootRequest = new SearchRequest(string.Empty, "objectClass=*", SearchScope.Base, "rootDomainNamingContext");
var rootResponse = (SearchResponse)ldapConn.SendRequest(getRootRequest);
var rootContext = rootResponse.Entries[0].Attributes["rootDomainNamingContext"][0].ToString();
// retrieve the user
string ldapFilter = string.Format("(&(objectCategory=person)(sAMAccountName={0}))", username);
var getUserRequest = new SearchRequest(rootContext, ldapFilter, SearchScope.Subtree, null);
var userResponse = (SearchResponse)ldapConn.SendRequest(getUserRequest);
// send a new request to retrieve the tokenGroups attribute, we can not do this with our previous request since
// tokenGroups needs SearchScope.Base (dont know why...)
var tokenRequest = new SearchRequest(userResponse.Entries[0].DistinguishedName, "(&(objectCategory=person))", SearchScope.Base, "tokenGroups");
var tokenResponse = (SearchResponse)ldapConn.SendRequest(tokenRequest);
var tokengroups = tokenResponse.Entries[0].Attributes["tokenGroups"].GetValues(typeof(byte[]));
// build query string this query will then look like (|(objectSid=sid)(objectSid=sid2)(objectSid=sid3))
// we need to convert the given bytes to a hexadecimal representation because thats the way they
// sit in ActiveDirectory
var sb = new StringBuilder();
sb.Append("(|");
for (int i = 0; i < tokengroups.Length; i++)
{
var arr = (byte[])tokengroups[i];
sb.AppendFormat("(objectSid={0})", BuildHexString(arr));
}
sb.Append(")");
// send the request with our build query. This will retrieve all groups with the given objectSid
var groupsRequest = new SearchRequest(rootContext, sb.ToString(), SearchScope.Subtree, "sAMAccountName");
var groupsResponse = (SearchResponse)ldapConn.SendRequest(groupsRequest);
// loop trough and get the sAMAccountName (normal, readable name)
var userMemberOfGroups = new List<string>();
foreach (SearchResultEntry entry in groupsResponse.Entries)
userMemberOfGroups.Add(entry.Attributes["sAMAccountName"][0].ToString());
return userMemberOfGroups;
}
private string BuildHexString(byte[] bytes)
{
var sb = new StringBuilder();
for (int i = 0; i < bytes.Length; i++)
sb.AppendFormat("\\{0}", bytes[i].ToString("X2"));
return sb.ToString();
}
These are more for info purposes
How to use the PrimaryGroupID attribute to find the primary group for a user
Determining User Group Membership in Active Directory and ADAM
I don't know how PrincipalContext is being passed in, here, but one thing I noticed in my own code and research when I had this error, I had:
PrincipalContext oPrincipalContext = new PrincipalContext(ContextType.Domain);
UserPrincipal oUserPrincipal = UserPrincipal.FindByIdentity(oPrincipalContext , strUserName);
Where strUserName was some user, i.e. DOMAIN\johndoe
I was calling that code (which was in a separate function) and returning the UserPrincipal object as up and passing it to:
using (PrincipalSearchResult<Principal> result = up.GetGroups())
{
// do something with result, here
}
result wouldn't be null, but after I checked for that condition, I checked if result.Count() > 0, and that's when it would fail (sometimes - though I could re-create the conditions when it would happen by clicking on a particular tab in my app that called this code - even though the same code was called onload of my app and had no issues). The Message property in result was Attempted to access an unloaded appdomain. (Exception from HRESULT: 0x80131014).
I found in a similar post to this one that all I had to do was specify the domain in my PrincipalContext. Since I could not hard code mine in, as we move our code between Dev, Test, and Production environments where they have different domains for each of these, I was able to specify it as Environment.UserDomainName:
PrincipalContext oPrincipalContext = new PrincipalContext(ContextType.Domain, Environment.UserDomainName);
This got rid of the error, for me.
This issue is the same as Determine if user is in AD group for .NET 4.0 application
It appears to be a bug in ADSI that was resolved with a hotfix. Windows 7 SP1 and Windows Server 2008 R2 SP1 don't include the fix, so it will need to be manually deployed on your development machines and server environments.
http://support.microsoft.com/kb/2683913
You could put in some logging to narrow down the problem. That Thread.Sleep does not look like something one would want in a web application :)
If you are getting exceptions maybe you could handle them differently.
I reckon your AppDomain is being recycled while AD is doing its voodoo. Adding logging to the Application_End could also provide some clues.
try
public List<ADGroup> GetMemberGroups(string loginId, PrincipalContext principalContext, int tries = 0)
{
var result = new List<ADGroup>();
bool Done = false;
try
{
var samAccountName = "";
if (loginId.Contains(" "))
{
var fName = loginId.Split(Char.Parse(" "))[0].ToLower();
var sName = loginId.Split(Char.Parse(" "))[1].ToLower();
if (sName.Trim().Length == 2)
samAccountName = string.Format("{0}{1}", fName.StartsWith(".") ? fName.Substring(0, 4) : fName.Substring(0, 3), sName.Substring(0, 2));
else
samAccountName = string.Format("{0}{1}", fName.StartsWith(".") ? fName.Substring(0, 3) : fName.Substring(0, 2), sName.Substring(0, 3));
}
else
samAccountName = loginId.Substring(loginId.IndexOf(#"\") + 1);
var authPrincipal = UserPrincipal.FindByIdentity(principalContext, IdentityType.SamAccountName, samAccountName);
if (authPrincipal == null)
throw new Exception(string.Format("authPrincipal is null for loginId - {0}", loginId));
var firstLevelGroups = authPrincipal.GetGroups();
AddGroups(firstLevelGroups, ref result);
Done = true;
}
catch
{
if (tries > 5)
throw;
tries += 1;
}
if ( ( !Done) && (tries < 6) )
{
System.Threading.Thread.Sleep(1000);
result = GetMemberGroups(loginId, principalContext, tries);
}
return result;
}
private void AddGroups(PrincipalSearchResult<Principal> principal, ref List<ADGroup> returnList)
{
if ( principal == null )
return;
foreach (var item in principal)
{
if (item.GetGroups().Count() > 0)
AddGroups(item.GetGroups(), ref returnList);
returnList.Add(new ADGroup(item.SamAccountName, item.Sid.Value));
}
}
When an exception happens you called the function again from the catch-block (depending on the value of tries) but discarded its return value - so even if the second/third... call worked you returned an empty result to the original caller.
I changed that so the result won't be discarded anymore...
In the second function you never checked the principal param for null before starting the foreach... I changed that too...
And I removed the recursion from within the catch block catch (although I am really not sure whether this change has any real effect).
How do I go about iterating over available and/or set settings in a given GPO (using name or GUID) in an AD domain? Without having to export to XML/HTML using powershell, etc.
I'm using C# (.NET 4.0).
That question got me hyped so I went to research it. So a +1
Some solutions I found from the top being the best to bottom being the worst
A good explanation with an example and example script!
This one, tells you to go through the registry but you gotta figure out who to access the AD
Pinvoke: Queries for a group policy override for specified power settings.
I had a similar problem, and didn't want to download and install the Microsoft GPO library (Microsoft.GroupPolicy.Management). I wanted to do it all with System.DirectoryServices. It took a little digging, but it can be done.
First retrieve your container using DirectorySearcher. You'll need to have already opened a directory entry to pass into the searcher. The filter you want is:
string filter = "(&" + "(objectClass=organizationalUnit)" + "(OU=" + container + "))";
and the property you're interested in is named "gPLink", so create an array with that property in it:
string[] requestProperties = { "gPLink" };
Now retrieve the results, and pull out the gPLink, if available.
using (var searcher = new DirectorySearcher(directory, filter, properties, SearchScope.Subtree))
{
SearchResultCollection results = searcher.FindAll();
DirectoryEntry entry = results[0].GetDirectoryEntry();
string gpLink = entry.Properties["gPLink"].Value;
If gpLink is null, there is no GPO associated with the container (OU).
Otherwise, gpLink will contain a string such as this:
"[LDAP://cn={31B2F340-016D-11D2-945F-00C04FB984F9},cn=policies,cn=system,DC=Test,DC=Domain;0]"
In the text above, you can see a CN for the GPO. All we need to do now is retrieve the GPO from the DC.
For that, we use a filter that looks like this:
string filter = "(&" +
"(objectClass=groupPolicyContainer)" +
"(cn={31B2F340-016D-11D2-945F-00C04FB984F9}))";
You'll want to create a Properties array that include the following:
Properties = { "objectClass", "cn", "distinguishedName", "instanceType", "whenCreated",
"whenChanged", "displayName", "uSNCreated", "uSNChanged", "showInAdvancedViewOnly",
"name", "objectGUID", "flags", "versionNumber", "systemFlags", "objectCategory",
"isCriticalSystemObject", "gPCFunctionalityVersion", "gPCFileSysPath",
"gPCMachineExtensionNames", "dSCorePropagationData", "nTSecurityDescriptor" };
Now use DirectorySearcher to retrieve the GPO. You'll get back a DirectoryEntry in the results that contains all of the above fields in the Properties collection. Some are COM objects, so you'll have to handle those appropriately.
Here is a better and more complete example then above.
class Program
{
static void Main(string[] args)
{
DirectoryEntry rootDse = new DirectoryEntry("LDAP://rootDSE");
DirectoryEntry root = new DirectoryEntry("GC://" + rootDse.Properties["defaultNamingContext"].Value.ToString());
DirectorySearcher searcher = new DirectorySearcher(root);
searcher.Filter = "(objectClass=groupPolicyContainer)";
foreach (SearchResult gpo in searcher.FindAll())
{
var gpoDesc = gpo.GetDirectoryEntry().Properties["distinguishedName"].Value.ToString();
Console.WriteLine($"GPO: {gpoDesc}");
DirectoryEntry gpoObject = new DirectoryEntry($"LDAP://{gpoDesc}");
try
{
Console.WriteLine($"DisplayName: {gpoObject.Properties["displayName"].Value.ToString()}");
}
catch
{
}
try
{
Console.WriteLine($"PCFileSysPath: {gpoObject.Properties["gPCFileSysPath"].Value.ToString()}");
}
catch
{
}
try
{
Console.WriteLine($"VersionNumber: {gpoObject.Properties["versionNumber"].Value.ToString()}");
}
catch
{
}
try
{
Console.WriteLine($"UserExtensionNames: {gpoObject.Properties["gPCUserExtensionNames"].Value.ToString()}");
}
catch
{
}
try
{
Console.WriteLine($"MachineExtensionNames: {gpoObject.Properties["gPCMachineExtensionNames"].Value.ToString()}");
}
catch
{
}
try
{
Console.WriteLine($"PCFunctionality: {gpoObject.Properties["gPCFunctionalityVersion"].Value.ToString()}");
}
catch
{
}
}
Console.ReadKey();
}
}
UPDATED: Working copy. You can now use c# to get read and parse a given GPO without having to use Powershell or write anything to disk.
using Microsoft.GroupPolicy;
var guid = new Guid("A7DE85DE-1234-F34D-99AD-5AFEDF7D7B4A");
var gpo = new GPDomain("Centoso.local");
var gpoData = gpo.GetGpo(guid);
var gpoXmlReport = gpoData.GenerateReport(ReportType.Xml).ToString();
using (XmlReader reader = XmlReader.Create(new StringReader(gpoXmlReport)))
{
string field;
while (reader.MoveToNextAttribute())
{
foreach (string attr in attributes)
{
// do something
}
}
}
This uses the Group Policy Management Console (GPMC) tools:
https://msdn.microsoft.com/en-us/library/windows/desktop/aa814316(v=vs.85).aspx
Microsoft.GroupPolicy Namespace
https://msdn.microsoft.com/en-us/library/windows/desktop/microsoft.grouppolicy(v=vs.85).aspx