Getting last Logon Time on Computers in Active Directory - c#

How can I get a list of users from active directory?
Please see the page above. It answered most of my questions, but I get a problem when I try to get last logon time for a computer. Sorry if there was some way to comment on that page instead of making a whole new question because I didn't find such an option.
using (var context = new PrincipalContext(ContextType.Domain, "cat.pcsb.org"))
{
using (var searcher = new PrincipalSearcher(new ComputerPrincipal(context)))
{
foreach (var result in searcher.FindAll())
{
DirectoryEntry de = result.GetUnderlyingObject() as DirectoryEntry;
Console.WriteLine("Name: " + de.Properties["name"].Value);
Console.WriteLine("Last Logon Time: " + de.Properties["lastLogon"].Value);
Console.WriteLine();
}
}
}
Console.ReadLine();
I replaced UserPrincipal with ComputerPrincipal. Name and some other properties work fine, but logon doesn't. I've tried doing different things like casting it to DateTime (the cast failed) but nothing worked. The above just results in System.__ComObject. So what can I do to get it to get last logon time correctly?

Why aren't you just using the the LastLogon property returned by ComputerPrincipal? (ComputerPrincipal is a AuthenicatablePrincipal)
using (var context = new PrincipalContext(ContextType.Domain, "cat.pcsb.org"))
{
using (var searcher = new PrincipalSearcher(new ComputerPrincipal(context)))
{
foreach (var result in searcher.FindAll())
{
var auth = result as AuthenticablePrincipal;
if(auth != null)
{
Console.WriteLine("Name: " + auth.Name);
Console.WriteLine("Last Logon Time: " + auth.LastLogon);
Console.WriteLine();
}
}
}
}
Console.ReadLine();
Note that LastLogon is not a replicated property, so if you have more than one domain controller you need to query each controller and find out who gives the most recent result.

You need to iterate through all domain controllers and find the lastest logon time.
The below code finds last logon time for a user.
public DateTime findlastlogon(string userName)
{
DirectoryContext context = new DirectoryContext(DirectoryContextType.Domain, "domainName");
DateTime latestLogon = DateTime.MinValue;
DomainControllerCollection dcc = DomainController.FindAll(context);
Parallel.ForEach(dcc.Cast<object>(), dc1 =>
{
DirectorySearcher ds;
DomainController dc = (DomainController)dc1;
using (ds = dc.GetDirectorySearcher())
{
try
{
ds.Filter = String.Format(
"(sAMAccountName={0})",
userName
);
ds.PropertiesToLoad.Add("lastLogon");
ds.SizeLimit = 1;
SearchResult sr = ds.FindOne();
if (sr != null)
{
DateTime lastLogon = DateTime.MinValue;
if (sr.Properties.Contains("lastLogon"))
{
lastLogon = DateTime.FromFileTime(
(long)sr.Properties["lastLogon"][0]
);
}
if (DateTime.Compare(lastLogon, latestLogon) > 0)
{
latestLogon = lastLogon;
//servername = dc1.Name;
}
}
}
catch (Exception)
{
}
}
});
return latestLogon;
}
To get last logon time for a computer replace sAMAccountName to Name.
ds.Filter = String.Format(
"(Name={0})",
userName
);

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;
}

How to get the Windows user's login time?

Is there a way in C# to get the time when the current user logged in?
A command called quser in command prompt will list some basic information about current users, including LOGON TIME.
Is there a System property or something I can access in c# which I can get the user's login time from?
I am getting username by Environment.UserName property. Need the login time.
I've tried this:
using System.DirectoryServices;
using System.DirectoryServices.AccountManagement;
Console.WriteLine("Login Time: {0}",GetLastLoginToMachine(Environment .MachineName , Environment.UserName));
public static DateTime? GetLastLoginToMachine(string machineName, string userName)
{
PrincipalContext c = new PrincipalContext(ContextType.Machine, machineName);
UserPrincipal uc = UserPrincipal.FindByIdentity(c, userName);
return uc.LastLogon;
}
Got the following errors:
You can get the LastUserLogon time from the following namespace.
using System.DirectoryServices.AccountManagement;
Try
DateTime? CurrentUserLoggedInTime = UserPrincipal.Current.LastLogon;
You can get the account information as well :
string userName = WindowsIdentity.GetCurrent().Name.Split('\\')[1];
string machineName = WindowsIdentity.GetCurrent().Name.Split('\\')[0];
Make sure you include the reference to System.DirectoryServices.AccountManagement:
Then you can do this to get the last logon time:
using System.DirectoryServices.AccountManagement;
public static DateTime? GetLastLoginToMachine(string machineName, string userName)
{
PrincipalContext c = new PrincipalContext(ContextType.Machine, machineName);
UserPrincipal uc = UserPrincipal.FindByIdentity(c, userName);
return uc.LastLogon;
}
You can query WMI:
// using System.Management;
private static Dictionary<string, DateTime> getMachineLogonName(string machine)
{
var loggedOnUsers = new Dictionary<string, DateTime>();
ManagementScope scope = new ManagementScope(String.Format(#"\\{0}\root\cimv2", machine));
SelectQuery sessionQuery = new SelectQuery("Win32_LogonSession");
using (ManagementObjectSearcher sessionSearcher = new ManagementObjectSearcher(scope, sessionQuery))
using (ManagementObjectCollection sessionMOs = sessionSearcher.Get())
{
foreach (var sessionMO in sessionMOs)
{
// Interactive sessions
if ((UInt32)sessionMO.Properties["LogonType"].Value == 2)
{
var logonId = (string)sessionMO.Properties["LogonId"].Value;
var startTimeString = (string)sessionMO.Properties["StartTime"].Value;
var startTime = DateTime.ParseExact(startTimeString.Substring(0, 21), "yyyyMMddHHmmss.ffffff", System.Globalization.CultureInfo.InvariantCulture);
WqlObjectQuery userQuery = new WqlObjectQuery(#"ASSOCIATORS OF {Win32_LogonSession.LogonId='" + logonId + #"'} WHERE AssocClass=Win32_LoggedOnUser");
using (var userSearcher = new ManagementObjectSearcher(scope, userQuery))
using (var userMOs = userSearcher.Get())
{
var username = userMOs.OfType<ManagementObject>().Select(u => (string)u.Properties["Name"].Value).FirstOrDefault();
if (!loggedOnUsers.ContainsKey(username))
{
loggedOnUsers.Add(username, startTime);
}
else if(loggedOnUsers[username]> startTime)
{
loggedOnUsers[username] = startTime;
}
}
}
}
}
return loggedOnUsers;
}
Then just call the method with target machine name:
var logins = getMachineLogonName(".");

C# Search Active Directory error

I am pretty new to using C# and this is my second time using it with active directory. I keep getting the error: Object reference not set to an instance of an object. Below is my code. I know that my null reference is in the line var result = searcher.FindOne(); I am unsure of what I need to do to fix this.
static void Main(string[] args)
{
List<string> userList = new List<string>();
try
{
string[] newUsers = { List of users is here ex: jsmith#xyz.com, bsmith#xyz.com, ... };
PrincipalContext AD = new PrincipalContext(ContextType.Domain, "xyz.com");
UserPrincipal u = new UserPrincipal(AD);
PrincipalSearcher search = new PrincipalSearcher(u);
DirectorySearcher searcher = new DirectorySearcher();
foreach (string x in newUsers)
{
searcher.Filter = string.Format("(&(objectCategory=person)(anr={0}))", x);
var result = searcher.FindOne();
userList.Add(string.Format("{0} {1}", result.Properties["DisplayName"][0].ToString(), result.Properties["Company"][0].ToString()));
search.Dispose();
}
foreach(string y in userList)
{
Console.WriteLine(y);
}
Console.ReadLine();
}
catch (Exception e)
{
Console.WriteLine("Error: " + e.Message);
}
File.WriteAllLines(file location, userList);
}
Your problem is that you're declaring PrincipalSearcher and DirectorySearcher, yet you're only populating the PrincipalSearcher with UserPrincipal object.
...
UserPrincipal u = new UserPrincipal(AD);
PrincipalSearcher search = new PrincipalSearcher(u);
...
However, your DirectorySearcher object searcher is empty.
DirectorySearcher searcher = new DirectorySearcher();
In foreach loop you're searching for one user using DirectorySearcher object not PrincipalSearcher:
var result = searcher.FindOne();
The above line will always return null. You need to populate DirectorySearcher.
DirectorySearcher searcher = new DirectorySearcher(/*need a DirectoryEntry*/);
I would suggest you take full advantage of UserPrincipal class. It seems that you want to search for users in Active Directory and you know their UserPrincipal names.
using (PrincipalContext ctx = new PrincipalContext(ContextType.Domain, "yourdomain.com"))
{
string [] newUsers; //need to declare list of new users
foreach (string user in newUsers)
{
using (UserPrincipal newUser = UserPrincipal.FindByIdentity(ctx, IdentityType.UserPrincipalName, user))
{
if (newUser != null)
{
//do what you need to do
//newUser will contain all info on a particular user
}
}
}
}
As several commenters have noted, your code is not handling the situation where no user is found by DirectorySearcher.FindOne - and, as noted in the MSDN documentation, FindOne returns null if no user is found:
If more than one entry is found during the search, only the first entry is returned. If no entries are found to match the search criteria, a null reference (Nothing in Visual Basic) is returned.
So you'll need to handle the case where the user you're looking for isn't there:
foreach (string x in newUsers)
{
Console.WriteLine("looking for user {0}", x);
searcher.Filter = string.Format("(&(objectCategory=person)(anr={0}))", x);
var result = searcher.FindOne();
if (result == null)
{
userList.Add(String.Format("user {0} not found!", x));
}
else
{
userList.Add(string.Format("{0} {1}", result.Properties["DisplayName"][0].ToString(), result.Properties["Company"][0].ToString()));
}
search.Dispose();
}

Searching for lastLogon attribute of user in multiple domain servers

First of all, please forgive me if I'm not using the correct terminologies. Correct me wherever I'm using the wrong terminology.
The objective is to programmatically retrieve the lastLogon date of a given username.
We have what I believe is a forest; two AD servers like - adserver01.aa.mycompany.com and adserver02.aa.mycompany.com
I connected to these servers from a third machine using Microsoft's ADExplorer to inspect the objects. There I see some users having lastLogon date available in adserver01, but not in adserver02. For example, the value for lastLogon is 0x0 in adserver02 whereas it is a valid date in adserver01 for some users.
The code I've developed so far as a Windows Forms application, works fine if only one AD Server is involved. How do I check both servers and return the non-zero value if available, in either for lastLogon date attribute?
private static string GetLastActivityDate(string UserName)
{
string domainAndUserName = String.Format(#"LDAP://aa.mycompany.com/CN={0},OU=CLIENT_PROD,OU=clients.mycompany.com,DC=aa,DC=mycompany,DC=com", UserName);
string OUAdminUserName = "abc";
string OUAdminPassword = "xyz";
AuthenticationTypes at = AuthenticationTypes.Secure;
DateTime lastActivityDate;
string returnvalue;
long lastLogonDateAsLong;
using (DirectoryEntry entryUser = new DirectoryEntry(domainAndUserName, OUAdminUserName, OUAdminPassword, at))
using (DirectorySearcher mysearcher = new DirectorySearcher(entryUser))
try
{
using (SearchResultCollection results = mysearcher.FindAll())
{
if (results.Count >= 1)
{
DirectoryEntry de = results[0].GetDirectoryEntry();
lastLogonDateAsLong = GetInt64(de, "lastLogon");
try
{
if (lastLogonDateAsLong != -1)
{
lastActivityDate = DateTime.FromFileTime(lastLogonDateAsLong);
returnvalue = lastActivityDate.ToString();
}
else
{
returnvalue = "-Not available-";
}
}
catch (System.ArgumentOutOfRangeException aore)
{
returnvalue = "Not available";
}
}
else
{
returnvalue = string.Empty;
}
}
}
catch (System.DirectoryServices.DirectoryServicesCOMException dsce)
{
returnvalue = "- Not available -";
}
return returnvalue;
}
Thank you.
EDIT:
private static Int64 GetInt64(DirectoryEntry entry, string attr)
{
DirectorySearcher ds = new DirectorySearcher(
entry,
String.Format("({0}=*)", attr),
new string[] { attr },
SearchScope.Base
);
SearchResult sr = ds.FindOne();
if (sr != null)
{
if (sr.Properties.Contains(attr))
{
return (Int64)sr.Properties[attr][0];
}
}
return -1;
}
Forgot to mention, the AD schema, structure etc, looks exactly alike in the two servers.
Check this post
http://www.codeproject.com/Articles/19181/Find-LastLogon-Across-All-Windows-Domain-Controlle
I had the same issue but but only for one domain,
I solved it by using the following code however i'm checking the lastLogin of all users
public static Dictionary<string, DateTime> UsersLastLogOnDate()
{
var lastLogins = new Dictionary<string, DateTime>();
DomainControllerCollection domains = Domain.GetCurrentDomain().DomainControllers;
foreach (DomainController controller in domains)
{
try
{
using (var directoryEntry = new DirectoryEntry(string.Format("LDAP://{0}", controller.Name)))
{
using (var searcher = new DirectorySearcher(directoryEntry))
{
searcher.PageSize = 1000;
searcher.Filter = "(&(objectClass=user)(!objectClass=computer))";
searcher.PropertiesToLoad.AddRange(new[] { "distinguishedName", "lastLogon" });
foreach (SearchResult searchResult in searcher.FindAll())
{
if (searchResult.Properties.Contains("lastLogon"))
{
var lastLogOn = DateTime.FromFileTime((long)searchResult.Properties["lastLogon"][0]);
var username = Parser.ParseLdapAttrValue(searchResult.Properties["distinguishedName"][0].ToString());
if (lastLogins.ContainsKey(username))
{
if (DateTime.Compare(lastLogOn, lastLogins[username]) > 0)
{
lastLogins[username] = lastLogOn;
}
}
else
{
lastLogins.Add(username, lastLogOn);
}
}
}
}
}
}
catch (System.Runtime.InteropServices.COMException comException)
{
// Domain controller is down or not responding
Log.DebugFormat("Domain controller {0} is not responding.",controller.Name);
Log.Error("Error in one of the domain controllers.", comException);
continue;
}
}
return lastLogins;
}
On top of the code you can use the following to get all domains in a forest.
Forest currentForest = Forest.GetCurrentForest();
DomainCollection domains = currentForest.Domains;
foreach(Domain domain in domains)
{
// check code above
}
There may be a simpler approach? There is actually another attribute lastLogonTimestamp, added with 2003 domain level I think, that tries to keep a single, consistent value across the domain for the last login. Alas, it has a bizarre replication time pattern, and could be up to two weeks out of date.

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