Listing Local Administrators on a Remote Machine with WMI - c#

I am able to get a list of members of the 'Administrators' group on a remote machine with this function:
public IEnumerable<string> GroupMembers(string groupname)
{
var result = new HashSet<string>(StringComparer.CurrentCultureIgnoreCase);
try
{
var qs = new StringBuilder();
qs.Append("SELECT PartComponent FROM Win32_GroupUser WHERE GroupComponent = \"Win32_Group.Domain='");
qs.Append(_machineName);
qs.Append("',Name='");
qs.Append(groupname);
qs.AppendLine("'\"");
var query = new ObjectQuery(qs.ToString()); // SELECT PartComponent FROM Win32_GroupUser WHERE GroupComponent = "Win32_Group.Domain='MYMACHINENAME',Name='Administrators'"
var searcher = new ManagementObjectSearcher(_scope, query);
var queryCollection = searcher.Get();
foreach (var m in queryCollection)
{
var path = new ManagementPath(m["PartComponent"].ToString());
{
var names = path.RelativePath.Split(',');
result.Add(names[0].Substring(names[0].IndexOf("=") + 1).Replace("\"", " ").Trim() + "\\" + // Win32_UserAccount.Domain=xxxxxx
names[1].Substring(names[1].IndexOf("=") + 1).Replace("\"", " ").Trim()); // Name=xxxxx
}
}
return result; // MYMACHINENAME\Administrator,MYDOMAIN\Domain Admins,MYDOMAIN\Dev Service Accounts
}
catch (Exception e)
{
_logger.Error("Message: " + e.Message);
throw;
}
}
However, this does not expand the groups returned to list all the individual members. This is effectively the same as net localgroup Administrators.
In my example, I'm looking to see if one of the members of MYDOMAIN\Dev Service Accounts is an administrator.
How can I adjust my WMI query to expand all the groups to a flat list of usernames?

Related

Get email addresses from distribution list using c#

Update:
For me, LDAP way only worked for finding email addresses inside AD groups, for eg: named ITSolutionDeliveryDevelopers group. NOT inside Exchange Distribution Lists, for eg: named abc#domainname.com.
// I was able to figure out entry as suggested by #Gabriel Luci and
// all of the following possible formats worked for me:
// ngroupnet.com is my company domain name.
var entry = new DirectoryEntry();
var entry = new DirectoryEntry("LDAP://ngroupnet.com");
var entry = new DirectoryEntry("LDAP://ngroupnet.com", "MyAccountUsername", "MyAccountPassword");
var entry = new DirectoryEntry("LDAP://ngroupnet.com", "MyName#mycompany.com", "MyEmailAccountPassword");
For my complete answer, take a look below: https://stackoverflow.com/a/71518937/8644294
Original Question:
What is the best way to get all the individual email addresses comprising an exchange distribution list?
For eg: I have this distribution list called abc#domainname.com that has email addresses:
a#domainname.com
b#domainname.com
c#domainname.com
Now I need to get these addresses using C# code.
I found solution using LDAP but I felt it'd be a hassle to figure out LDAP path to my Active Directory.
// How do I get this LDAP Path, username and password?
// Is the username and password service account credentials of the app?
// And do they need to be registered in AD?
var entry = new DirectoryEntry("LDAP Path");//, username, password);
LDAP Way:
public static List<string> GetDistributionListMembers(string dlName = "abc#domainname.com")
{
var result = new List<string>();
try
{
// How do I get this LDAP Path?
var entry = new DirectoryEntry("LDAP Path");//, username, password);
var search = new DirectorySearcher(entry);
search.Filter = $"CN={dlName}";
int i = search.Filter.Length;
string str = "", str1 = "";
foreach (SearchResult AdObj in search.FindAll())
{
foreach (String objName in AdObj.GetDirectoryEntry().Properties["member"])
{
str += Convert.ToString(objName) + "<Br>";
int selIndex = objName.IndexOf("CN=") + 3;
int selEnd = objName.IndexOf(",OU") - 3;
str1 += objName.Substring(selIndex, selEnd).Replace("\\", "");
DirectorySearcher dsSearch = new DirectorySearcher(entry);
dsSearch.Filter = "CN=" + objName.Substring(selIndex, selEnd).Replace("\\", "");
foreach (SearchResult rs in dsSearch.FindAll())
{
//str1 += "<p align='right'><font face='calibri' color='#2266aa' size=2>" + Convert.ToString(rs.GetDirectoryEntry().Properties["mail"].Value) + "|" + Convert.ToString(rs.GetDirectoryEntry().Properties["displayName"].Value) + "|" + Convert.ToString(rs.GetDirectoryEntry().Properties["sAMAccountName"].Value) + "|" + Convert.ToString(rs.GetDirectoryEntry().Properties["department"].Value) + "|" + Convert.ToString(rs.GetDirectoryEntry().Properties["memberOf"].Value) + "</font></p>";
str1 = Convert.ToString(rs.GetDirectoryEntry().Properties["mail"].Value);
result.Add(str1);
}
}
}
return result;
}
catch (Exception ex)
{
//Do some logging or what have you.
throw;
}
}
So I just went with the EWS route.
EWS Way:
public static static List<string> GetDistributionListMembers(string dlName = "abc#domainname.com")
{
try
{
var service = new ExchangeService();
var cred = new WebCredentials("sharedmailbox#domain.com", "some_password");
service.Credentials = cred;
service.Url = new Uri("https://outlook.office365.com/ews/exchange.asmx");
service.TraceEnabled = true;
service.TraceFlags = TraceFlags.All;
var expandedEmailAddresses = new List<string>();
ExpandGroupResults myGroupMembers = service.ExpandGroup(dlName);
foreach (EmailAddress address in myGroupMembers.Members)
{
expandedEmailAddresses.Add(address.Address);
}
return expandedEmailAddresses;
}
catch (Exception ex)
{
// The DL doesn't have any members. Handle it how you want.
// Handle/ Log other errors.
}
}
Is EWS approach a good way?
If Yes, then I'm good. If not, I'll have to figure out that LDAP path.
Or if there's even a better way, please let me know.
If the computer you run this from is joined to the same domain as the group you're looking for, then you don't need to figure out the LDAP path. You can just do:
var search = new DirectorySearcher();
If your computer is not joined to the same domain, then you just use the domain name:
var entry = new DirectoryEntry("LDAP://domainname.com");
This requires that there is no firewall blocking port 389 between your computer and the domain controller(s). If you need to pass credentials, then do that:
var entry = new DirectoryEntry("LDAP://domainname.com", username, password);
The credentials can be any user on the domain.
That said, there are a lot of inefficiencies in your code that will make it run much slower than needed. I wrote an article about this that can help you update your code: Active Directory: Better Performance
Is EWS approach a good way?
If it works, it works. I'm not an expert on EWS (although I have used it), but I'm fairly certain that's using Basic Authentication, which is going to be disabled in October.
If all the Mailboxes are on Office365 then i would suggest you use the Graph API instead eg https://learn.microsoft.com/en-us/graph/api/group-list-members?view=graph-rest-1.0&tabs=http . There are several advantage in terms of security eg you could use Application permissions and all you need is access to the directory while if you did the same thing in EWS it would require full access to at least one mailbox.
LDAP will be best performing of the 3 if ultimate speed in the only thing that is important.
My complete solution for future reference. :)
EWS Way - For expanding Exchange Distribution Lists
public class SomeHelper
{
private static ExchangeService _exchangeService = null;
public static async Task<HashSet<string>> GetExchangeDistributionListMembersAsync(IEnumerable<string> dlNames)
{
var allEmailAddresses = new HashSet<string>();
foreach (var dlName in dlNames)
{
if (!SomeCache.TryGetCachedItem(dlName, out var dlMembers))
{
var groupEmailAddresses = new List<string>();
var exchangeService = await GetExchangeServiceAsync();
try
{
var myGroupMembers = exchangeService.ExpandGroup(dlName);
// Add the group members.
foreach (var address in myGroupMembers.Members)
{
groupEmailAddresses.Add(address.Address);
}
}
catch (Exception ex)
{
//If it can't expand the dlName, just return it.
groupEmailAddresses.Add(dlName);
//groupEmailAddresses.Add($"Attempting to expand '{dlName}' resulted in error message: '{ex.Message}'.");
}
// Cache the groupEmailAddresses for 7 days.
// Because Distribution Lists rarely change and expanding DL is an expensive operation.- AshishK Notes
SomeCache.AddItemToCache(dlName, groupEmailAddresses, 10080);
allEmailAddresses.UnionWith(groupEmailAddresses);
}
else
{
allEmailAddresses.UnionWith((List<string>)dlMembers);
}
}
return allEmailAddresses;
}
private static async Task<ExchangeService> GetExchangeServiceAsync()
{
if (_exchangeService == null)
{
_exchangeService = new ExchangeService();
var exchangeUrl = "https://outlook.office365.com/ews/exchange.asmx";
var cred = new WebCredentials("sharedmailbox#domain.com", "some_password");
_exchangeService.Credentials = cred;
//_exchangeService.AutodiscoverUrl("sharedmailbox#domain.com");
_exchangeService.Url = new Uri(exchangeUrl);
_exchangeService.TraceEnabled = true;
_exchangeService.TraceFlags = TraceFlags.All;
return _exchangeService;
}
else
{
return _exchangeService;
}
}
}
public class SomeCache
{
private static readonly ObjectCache _cache = MemoryCache.Default;
public static void AddItemToCache(string key, object itemToAdd, int cacheDurationMinutes)
{
var _policy = new CacheItemPolicy
{
Priority = CacheItemPriority.Default,
AbsoluteExpiration = DateTimeOffset.Now.AddMinutes(cacheDurationMinutes)
};
_cache.Set(key, itemToAdd, _policy);
}
public static bool TryGetCachedItem(string key, out object cachedObject)
{
try
{
cachedObject = _cache[key] as object;
}
catch (Exception ex)
{
cachedObject = null;
}
return !(cachedObject == null);
}
}
LDAP Way - For expanding Active Directory Groups
public static List<string> GetADGroupDistributionListMembers(string adGroupName)
{
var returnResult = new List<string>();
var entry = GetDirectoryEntry();
DirectorySearcher groupSearch = new DirectorySearcher(entry)
{
Filter = "(SAMAccountName=" + adGroupName + ")"
};
groupSearch.PropertiesToLoad.Add("member");
SearchResult groupResult = groupSearch.FindOne(); // getting members who belong to the adGroupName
if (groupResult != null)
{
for (int iSearchLoop = 0; iSearchLoop < groupResult.Properties["member"].Count; iSearchLoop++)
{
string userName = groupResult.Properties["member"][iSearchLoop].ToString();
int index = userName.IndexOf(',');
userName = userName.Substring(0, index).Replace("CN=", "").ToString(); // the name of the user will be fetched.
DirectorySearcher search = new DirectorySearcher(entry)
{
Filter = "(name=" + userName + ")"
};
search.PropertiesToLoad.Add("mail");
SearchResult result = search.FindOne(); //finding the mail id
if (result != null)
{
returnResult.Add(result.Properties["mail"][0].ToString());
}
}
}
return returnResult;
}
public static DirectoryEntry GetDirectoryEntry()
{
DirectoryEntry entryRoot = new DirectoryEntry("LDAP://RootDSE");
string Domain = (string)entryRoot.Properties["defaultNamingContext"][0];
DirectoryEntry de = new DirectoryEntry
{
Path = "LDAP://" + Domain,
AuthenticationType = AuthenticationTypes.Secure
};
return de;
}

Retrieve LoggedOnUsers on Remote Machine

I'm building a C# application to monitor server and workstation workloads with WMI and WQL queries. I'm using WMI because it seems to be faster in comparison to powershell queries. My hardship starts when I try to retrieve logged on users on a remote machine. I figured I need to use the Win32_LoggedOnUser class. I have tried the following queries:
#"SELECT * FROM Win32_LoggedOnUser"
#"SELECT Antecedent FROM Win32_LoggedOnUser"
What I'm used to is to retrieve the desired value like this:
var cims = connection.getCimInstances(this, queryUser);
if (cims != null)
{
foreach (CimInstance cim in cims)
{
Komponenten.User user = new Komponenten.User();
user.Name = Convert.ToString(cim.CimInstanceProperties["Name"].Value);
users.Add(user);
}
}
where queryUser is one of the strings from above.
In both cases, I get a Win32_Account object in return, which seems to suggest - and the debugger seems to confirm - that I should use CimInstanceProperties["Name"].Value on the returned Win32_Account class again. But that's not working at all. Any ideas on how to get access to the CimInstanceProperties of a Win32_Account stored in a CimInstanceProperity ? I can't find anything on the respective Windows reference page (https://learn.microsoft.com/en-us/windows/win32/cimwin32prov/win32-loggedonuser) nor during my extensive google-search.
Thanks!
I ended up using the ManagementObject-Class and Regex to find the usernames after converting the Antecedent - Object to a string:
var users = new List<Komponenten.User>();
var searcher = this.connection.makeQuery(this, "SELECT * FROM Win32_LoggedOnUser");
if (searcher != null)
{
foreach (ManagementObject queryObj in searcher.Get())
{
Komponenten.User user = new Komponenten.User();
var win32_account = queryObj["Antecedent"].ToString();
string stripped = Regex.Replace(win32_account, "[^a-zA-Z=]+", "", RegexOptions.Compiled);
int end = stripped.LastIndexOf("=");
user.Name = stripped.Substring(end+1);
users.Add(user);
}
this.users = users;
An alternative which takes the LogonSession into account is:
var users = new List<Komponenten.User>();
var searcher = this.connection.makeQuery(this, "SELECT LogonId FROM Win32_LogonSession Where LogonType=2");
var Scope = this.connection.getScope(this, this.connection.getConnection());
if (searcher != null)
{
foreach (ManagementObject queryObj in searcher.Get())
{
ObjectQuery LQuery = new ObjectQuery("Associators of {Win32_LogonSession.LogonId=" + queryObj["LogonId"] + "} Where AssocClass=Win32_LoggedOnUser Role=Dependent");
ManagementObjectSearcher LSearcher = new ManagementObjectSearcher(Scope, LQuery);
foreach (ManagementObject LWmiObject in LSearcher.Get())
{
Komponenten.User user = new Komponenten.User();
user.Name = Convert.ToString(LWmiObject["Name"]);
users.Add(user);
}
}
this.users = users;
}
where this.connection.getConnection is a ConnectionsOption object depending on your respective domain and account data

Obtaining HDD Serial Number via Drive Letter using WMI query in C#

I have the following code which works well in obtaining HDD serial numbers via the Model string value in Win32_DiskDrive. But I need to obtain the serial number via the drive letter assigned. Is this possible? If so how?
public string GetDriveSerial(string model)
{
string serial = string.Empty;
string query = "SELECT * FROM Win32_DiskDrive WHERE Model = '" + model + "'";
try
{
using (var results = new ManagementObjectSearcher("root\\CIMV2", query))
{
foreach (ManagementObject result in results.Get())
{
serial = result["SerialNumber"].ToString();
}
}
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
}
return serial;
}
You can try this:
public static string GetSerialFromDrive(string driveLetter)
{
try
{
using (var partitions = new ManagementObjectSearcher("ASSOCIATORS OF {Win32_LogicalDisk.DeviceID='" + driveLetter +
"'} WHERE ResultClass=Win32_DiskPartition"))
{
foreach (var partition in partitions.Get())
{
using (var drives = new ManagementObjectSearcher("ASSOCIATORS OF {Win32_DiskPartition.DeviceID='" +
partition["DeviceID"] +
"'} WHERE ResultClass=Win32_DiskDrive"))
{
foreach (var drive in drives.Get())
{
return (string)drive["SerialNumber"];
}
}
}
}
}
catch
{
return "<unknown>";
}
// Not Found
return "<unknown>";
}
This expects the drive letter like this: "C:", "D:" ...
The original code can be found here.

Check is Group is already present in an Active Directory

I have the following piece of code wherein i am trying to check if a group is already present in an active directory. If not present, then go ahead & create the group. But my group check function is not working correct.
Can you please give some suggestions.
Below is the portion of the code which checks if the group is already present or not.
using (var domainContext = new PrincipalContext(ContextType.Domain, "xyz"))
{
var foundGrp = GroupPrincipal.FindByIdentity(domainContext, IdentityType.SamAccountName, obj);
bool UserExists = (foundGrp != null);
if (!UserExists)
{
//DirectoryEntry group = entry.Children.Add("CN=" + obj, "group");
group.CommitChanges();
Console.WriteLine("AD Group " + obj + " has been created");
Console.WriteLine("=====================================");
}
}
And here is my entire code. I am just trying a couple of features. In the longer run i will modularize my code into separate functions. Thanks.
static void Main(string[] args)
{
try
{
/*Read the input XML File*/
XmlDocument xml = new XmlDocument();
xml.Load(#"xyz.xml");
var exportNode = xml.SelectSingleNode("//Export");
for (int i = 0; i < exportNode.ChildNodes.Count; i++)
{
/*Node 1 reads block 1 of the XML*/
XmlNode dataNode = exportNode.ChildNodes[i];
var environmentNode = dataNode.SelectSingleNode("env");
var tech_domainNode = dataNode.SelectSingleNode("Tech");
string env = environmentNode.InnerText;
string tech_domain = tech_domainNode.InnerText;
var output_valueNode = dataNode.SelectSingleNode("Output_Value");
string obj = output_valueNode.InnerText;
Console.WriteLine("obj is " + obj);
if (tech_domain == "AD Group")
{
string path = "LDAP://OU=Security,OU=Groups,DC=xyz";
using (DirectoryEntry entry = new DirectoryEntry(path))
{
DirectoryEntry group = entry.Children.Add("CN=" + obj, "group");
group.Properties["sAmAccountName"].Value = obj;
using (var domainContext = new PrincipalContext(ContextType.Domain, "xyz"))
{
var foundGrp = GroupPrincipal.FindByIdentity(domainContext, IdentityType.SamAccountName, obj);
bool UserExists = (foundGrp != null);
if (!UserExists)
{
//DirectoryEntry group = entry.Children.Add("CN=" + obj, "group");
group.CommitChanges();
Console.WriteLine("AD Group " + obj + " has been created");
Console.WriteLine("=====================================");
}
}
}
}
else
{
Console.WriteLine("Technology Domain for " + obj + " is not an AD Group");
Console.WriteLine("====================================================");
}
}
Console.ReadLine();
}
catch (Exception e)
{
Console.WriteLine(e.Message.ToString());
Console.WriteLine("===============================");
}
}
}
}
I think you are going a bout this in a very round-about way. Please check out the Everything in Active Directory via C#.NET. There are three series of posts that describe using the .NET System.DirectoryServices.AccountManagement namespace to manage AD and perform CRUD operations on any given principals.

How to get all the AD groups for a particular user?

I checked this post already. But it doesn't answer my question. I want to get all the active directory groups in which a particular user is a member.
I've written the following code. But I'm not able to proceed further as I don't know how to give the filter and how to access the properties.
class Program
{
static void Main(string[] args)
{
DirectoryEntry de = new DirectoryEntry("LDAP://mydomain.com");
DirectorySearcher searcher = new DirectorySearcher(de);
searcher.Filter = "(&(ObjectClass=group))";
searcher.PropertiesToLoad.Add("distinguishedName");
searcher.PropertiesToLoad.Add("sAMAccountName");
searcher.PropertiesToLoad.Add("name");
searcher.PropertiesToLoad.Add("objectSid");
SearchResultCollection results = searcher.FindAll();
int i = 1;
foreach (SearchResult res in results)
{
Console.WriteLine("Result" + Convert.ToString(i++));
DisplayProperties("distinguishedName", res);
DisplayProperties("sAMAccouontName", res);
DisplayProperties("name", res);
DisplayProperties("objectSid", res);
Console.WriteLine();
}
Console.ReadKey();
}
private static void DisplayProperties(string property, SearchResult res)
{
Console.WriteLine("\t" + property);
ResultPropertyValueCollection col = res.Properties[property];
foreach (object o in col)
{
Console.WriteLine("\t\t" + o.ToString());
}
}
}
Any ideas?
You should use System.DirectoryServices.AccountManagement. It's much easier. Here is a nice code project article giving you an overview on all the classes in this DLL.
As you pointed out, your current approach doesn't find out the primary group. Actually, it's much worse than you thought. There are some more cases that it doesn't work, like the domain local group from another domain. You can check here for details. Here is how the code looks like if you switch to use System.DirectoryServices.AccountManagement. The following code can find the immediate groups this user assigned to, which includes the primary group.
UserPrincipal user = UserPrincipal.FindByIdentity(new PrincipalContext (ContextType.Domain, "mydomain.com"), IdentityType.SamAccountName, "username");
foreach (GroupPrincipal group in user.GetGroups())
{
Console.Out.WriteLine(group);
}
Use tokenGroups:
DirectorySearcher ds = new DirectorySearcher();
ds.Filter = String.Format("(&(objectClass=user)(sAMAccountName={0}))", username);
SearchResult sr = ds.FindOne();
DirectoryEntry user = sr.GetDirectoryEntry();
user.RefreshCache(new string[] { "tokenGroups" });
for (int i = 0; i < user.Properties["tokenGroups"].Count; i++) {
SecurityIdentifier sid = new SecurityIdentifier((byte[]) user.Properties["tokenGroups"][i], 0);
NTAccount nt = (NTAccount)sid.Translate(typeof(NTAccount));
//do something with the SID or name (nt.Value)
}
Note: this only gets security groups
Just query the "memberOf" property and iterate though the return, example:
search.PropertiesToLoad.Add("memberOf");
StringBuilder groupNames = new StringBuilder(); //stuff them in | delimited
SearchResult result = search.FindOne();
int propertyCount = result.Properties["memberOf"].Count;
String dn;
int equalsIndex, commaIndex;
for (int propertyCounter = 0; propertyCounter < propertyCount;
propertyCounter++)
{
dn = (String)result.Properties["memberOf"][propertyCounter];
equalsIndex = dn.IndexOf("=", 1);
commaIndex = dn.IndexOf(",", 1);
if (-1 == equalsIndex)
{
return null;
}
groupNames.Append(dn.Substring((equalsIndex + 1),
(commaIndex - equalsIndex) - 1));
groupNames.Append("|");
}
return groupNames.ToString();
This just stuffs the group names into the groupNames string, pipe delimited, but when you spin through you can do whatever you want with them
This code works even faster (two 1.5 faster than my previous version):
public List<String> GetUserGroups(WindowsIdentity identity)
{
List<String> groups = new List<String>();
String userName = identity.Name;
int pos = userName.IndexOf(#"\");
if (pos > 0) userName = userName.Substring(pos + 1);
PrincipalContext domain = new PrincipalContext(ContextType.Domain, "riomc.com");
UserPrincipal user = UserPrincipal.FindByIdentity(domain, IdentityType.SamAccountName, userName); // NGeodakov
DirectoryEntry de = new DirectoryEntry("LDAP://RIOMC.com");
DirectorySearcher search = new DirectorySearcher(de);
search.Filter = "(&(objectClass=group)(member=" + user.DistinguishedName + "))";
search.PropertiesToLoad.Add("cn");
search.PropertiesToLoad.Add("samaccountname");
search.PropertiesToLoad.Add("memberOf");
SearchResultCollection results = search.FindAll();
foreach (SearchResult sr in results)
{
GetUserGroupsRecursive(groups, sr, de);
}
return groups;
}
public void GetUserGroupsRecursive(List<String> groups, SearchResult sr, DirectoryEntry de)
{
if (sr == null) return;
String group = (String)sr.Properties["cn"][0];
if (String.IsNullOrEmpty(group))
{
group = (String)sr.Properties["samaccountname"][0];
}
if (!groups.Contains(group))
{
groups.Add(group);
}
DirectorySearcher search;
SearchResult sr1;
String name;
int equalsIndex, commaIndex;
foreach (String dn in sr.Properties["memberof"])
{
equalsIndex = dn.IndexOf("=", 1);
if (equalsIndex > 0)
{
commaIndex = dn.IndexOf(",", equalsIndex + 1);
name = dn.Substring(equalsIndex + 1, commaIndex - equalsIndex - 1);
search = new DirectorySearcher(de);
search.Filter = "(&(objectClass=group)(|(cn=" + name + ")(samaccountname=" + name + ")))";
search.PropertiesToLoad.Add("cn");
search.PropertiesToLoad.Add("samaccountname");
search.PropertiesToLoad.Add("memberOf");
sr1 = search.FindOne();
GetUserGroupsRecursive(groups, sr1, de);
}
}
}
This is how I list all the groups (direct and indirect) for a specific Distinguished Name:
The string 1.2.840.113556.1.4.1941 specifies LDAP_MATCHING_RULE_IN_CHAIN.
This rule is limited to filters that apply to the DN. This is a special "extended" match operator that walks the chain of ancestry in objects all the way to the root until it finds a match.
This method is 25 times faster than the UserPrincipal.GetGroups() method in my testing.
Note: The primary group (typically Domain Users) is not returned by this or GetGroups() method. To get the primary group name too, I've confirmed this method works.
Additionally, I found this list of LDAP filters extremely useful.
private IEnumerable<string> GetGroupsForDistinguishedName(DirectoryEntry domainDirectoryEntry, string distinguishedName)
{
var groups = new List<string>();
if (!string.IsNullOrEmpty(distinguishedName))
{
var getGroupsFilterForDn = $"(&(objectCategory=group)(member:1.2.840.113556.1.4.1941:={distinguishedName}))";
using (DirectorySearcher dirSearch = new DirectorySearcher(domainDirectoryEntry))
{
dirSearch.Filter = getGroupsFilterForDn;
dirSearch.PropertiesToLoad.Add("name");
using (var results = dirSearch.FindAll())
{
foreach (SearchResult result in results)
{
if (result.Properties.Contains("name"))
groups.Add((string)result.Properties["name"][0]);
}
}
}
}
return groups;
}
The following example is from the Code Project article, (Almost) Everything In Active Directory via C#:
// userDn is a Distinguished Name such as:
// "LDAP://CN=Joe Smith,OU=Sales,OU=domain,OU=com"
public ArrayList Groups(string userDn, bool recursive)
{
ArrayList groupMemberships = new ArrayList();
return AttributeValuesMultiString("memberOf", userDn,
groupMemberships, recursive);
}
public ArrayList AttributeValuesMultiString(string attributeName,
string objectDn, ArrayList valuesCollection, bool recursive)
{
DirectoryEntry ent = new DirectoryEntry(objectDn);
PropertyValueCollection ValueCollection = ent.Properties[attributeName];
IEnumerator en = ValueCollection.GetEnumerator();
while (en.MoveNext())
{
if (en.Current != null)
{
if (!valuesCollection.Contains(en.Current.ToString()))
{
valuesCollection.Add(en.Current.ToString());
if (recursive)
{
AttributeValuesMultiString(attributeName, "LDAP://" +
en.Current.ToString(), valuesCollection, true);
}
}
}
}
ent.Close();
ent.Dispose();
return valuesCollection;
}
Just call the Groups method with the Distinguished Name for the user, and pass in the bool flag to indicate if you want to include nested / child groups memberships in your resulting ArrayList:
ArrayList groups = Groups("LDAP://CN=Joe Smith,OU=Sales,OU=domain,OU=com", true);
foreach (string groupName in groups)
{
Console.WriteLine(groupName);
}
If you need to do any serious level of Active Directory programming in .NET I highly recommend bookmarking & reviewing the Code Project article I mentioned above.
Here is the code that worked for me:
public ArrayList GetBBGroups(WindowsIdentity identity)
{
ArrayList groups = new ArrayList();
try
{
String userName = identity.Name;
int pos = userName.IndexOf(#"\");
if (pos > 0) userName = userName.Substring(pos + 1);
PrincipalContext domain = new PrincipalContext(ContextType.Domain, "riomc.com");
UserPrincipal user = UserPrincipal.FindByIdentity(domain, IdentityType.SamAccountName, userName);
DirectoryEntry de = new DirectoryEntry("LDAP://RIOMC.com");
DirectorySearcher search = new DirectorySearcher(de);
search.Filter = "(&(objectClass=group)(member=" + user.DistinguishedName + "))";
search.PropertiesToLoad.Add("samaccountname");
search.PropertiesToLoad.Add("cn");
String name;
SearchResultCollection results = search.FindAll();
foreach (SearchResult result in results)
{
name = (String)result.Properties["samaccountname"][0];
if (String.IsNullOrEmpty(name))
{
name = (String)result.Properties["cn"][0];
}
GetGroupsRecursive(groups, de, name);
}
}
catch
{
// return an empty list...
}
return groups;
}
public void GetGroupsRecursive(ArrayList groups, DirectoryEntry de, String dn)
{
DirectorySearcher search = new DirectorySearcher(de);
search.Filter = "(&(objectClass=group)(|(samaccountname=" + dn + ")(cn=" + dn + ")))";
search.PropertiesToLoad.Add("memberof");
String group, name;
SearchResult result = search.FindOne();
if (result == null) return;
group = #"RIOMC\" + dn;
if (!groups.Contains(group))
{
groups.Add(group);
}
if (result.Properties["memberof"].Count == 0) return;
int equalsIndex, commaIndex;
foreach (String dn1 in result.Properties["memberof"])
{
equalsIndex = dn1.IndexOf("=", 1);
if (equalsIndex > 0)
{
commaIndex = dn1.IndexOf(",", equalsIndex + 1);
name = dn1.Substring(equalsIndex + 1, commaIndex - equalsIndex - 1);
GetGroupsRecursive(groups, de, name);
}
}
}
I measured it's performance in a loop of 200 runs against the code that uses the AttributeValuesMultiString recursive method; and it worked 1.3 times faster.
It might be so because of our AD settings. Both snippets gave the same result though.
I would like to say that Microsoft LDAP has some special ways to search recursively for all of memberships of a user.
The Matching Rule you can specify for the "member" attribute. In particular, using the Microsoft Exclusive LDAP_MATCHING_RULE_IN_CHAIN rule for "member" attribute allows recursive/nested membership searching. The rule is used when you add it after the member attribute. Ex. (member:1.2.840.113556.1.4.1941:= XXXXX )
For the same Domain as the Account, The filter can use <SID=S-1-5-21-XXXXXXXXXXXXXXXXXXXXXXX> instead of an Accounts DistinguishedName attribute which is very handy to use cross domain if needed. HOWEVER it appears you need to use the ForeignSecurityPrincipal <GUID=YYYY> as it will not resolve your SID as it appears the <SID=> tag does not consider ForeignSecurityPrincipal object type. You can use the ForeignSecurityPrincipal DistinguishedName as well.
Using this knowledge, you can LDAP query those hard to get memberships, such as the "Domain Local" groups an Account is a member of but unless you looked at the members of the group, you wouldn't know if user was a member.
//Get Direct+Indirect Memberships of User (where SID is XXXXXX)
string str = "(& (objectCategory=group)(member:1.2.840.113556.1.4.1941:=<SID=XXXXXX>) )";
//Get Direct+Indirect **Domain Local** Memberships of User (where SID is XXXXXX)
string str2 = "(& (objectCategory=group)(|(groupType=-2147483644)(groupType=4))(member:1.2.840.113556.1.4.1941:=<SID=XXXXXX>) )";
//TAA DAA
Feel free to try these LDAP queries after substituting the SID of a user you want to retrieve all group memberships of. I figure this is similiar if not the same query as what the PowerShell Command Get-ADPrincipalGroupMembership uses behind the scenes. The command states "If you want to search for local groups in another domain, use the ResourceContextServer parameter to specify the alternate server in the other domain."
If you are familiar enough with C# and Active Directory, you should know how to perform an LDAP search using the LDAP queries provided.
Additional Documentation:
<SID> Binding String
<GUID> Binding String
If you have a LDAP connection with a username and password to connect to Active Directory, here is the code I used to connect properly:
using System.DirectoryServices.AccountManagement;
// ...
// Connection information
var connectionString = "LDAP://domain.com/DC=domain,DC=com";
var connectionUsername = "your_ad_username";
var connectionPassword = "your_ad_password";
// Get groups for this user
var username = "myusername";
// Split the LDAP Uri
var uri = new Uri(connectionString);
var host = uri.Host;
var container = uri.Segments.Count() >=1 ? uri.Segments[1] : "";
// Create context to connect to AD
var princContext = new PrincipalContext(ContextType.Domain, host, container, connectionUsername, connectionPassword);
// Get User
UserPrincipal user = UserPrincipal.FindByIdentity(princContext, IdentityType.SamAccountName, username);
// Browse user's groups
foreach (GroupPrincipal group in user.GetGroups())
{
Console.Out.WriteLine(group.Name);
}
there is a helpers class based in curtisk response:
public static class ActiveDirectoryHelpers
{
private static readonly Regex keyValuePair = new Regex($"(?<key>[^=]+)=(?<value>[^,]+),?");
public enum X500DirectorySpecification
{
/// <summary>Common Name</summary>
CN,
/// <summary>Organizational Unit</summary>
OU,
/// <summary>Domain Component</summary>
DC
}
public static IEnumerable<string> GetUserMemberOfNodeValue(this PrincipalContext principalContext, string userName, X500DirectorySpecification node)
{
return principalContext.GetUserMemberOf(userName)
.SelectMany(memberOf =>
GetUserMemberOfKeyValues(memberOf).Where(item => item.Key == node.ToString()).Select(item => item.Value));
}
private static IEnumerable<string> GetUserMemberOf(this PrincipalContext principalContext, string userName)
{
using var user = UserPrincipal.FindByIdentity(principalContext, userName);
IEnumerable<string> result = null;
if (user != null)
{
var directoryEntry = (DirectoryEntry)user.GetUnderlyingObject();
var directorySearcher = new DirectorySearcher(directoryEntry);
directorySearcher.PropertiesToLoad.Add("memberOf");
result = directorySearcher.FindOne().Properties["memberOf"].Cast<string>();
}
return result ?? Enumerable.Empty<string>();
}
private static IEnumerable<KeyValuePair<string, string>> GetUserMemberOfKeyValues(string memberOfValue)
{
return keyValuePair.Matches(memberOfValue).OfType<Match>()
.Select(item => new KeyValuePair<string, string>(item.Groups["key"].Value.Trim(), item.Groups["value"].Value));
}
}
PrincipalContext pc1 = new PrincipalContext(ContextType.Domain, "DomainName", UserAccountOU, UserName, Password);
UserPrincipal UserPrincipalID = UserPrincipal.FindByIdentity(pc1, IdentityType.SamAccountName, UserID);
searcher.Filter = "(&(ObjectClass=group)(member = " + UserPrincipalID.DistinguishedName + "));

Categories

Resources