Determining members of local groups via C# - c#

I wondered whether anybody knows how to obtain membership of local groups on a remote server programmatically via C#. Would this require administrator permissions? And if so is there any way to confirm the currently logged in user's membership (or not) of these groups?

Howto: (Almost) Everything In Active Directory via C# is very helpfull and also includes instructions on how to iterate AD members in a group.
public ArrayList Groups(string userDn, bool recursive)
{
ArrayList groupMemberships = new ArrayList();
return AttributeValuesMultiString("memberOf", userDn,
groupMemberships, recursive);
}
You will also need this function:
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;
}
If you do now want to use this AD-method, you could use the info in this article, but it uses unmanaged code:
http://www.codeproject.com/KB/cs/groupandmembers.aspx
The sample application that they made:

It appears there is a new Assembly in .net 3.5 called System.DirectoryServices.AccountManagement which gives a cleaner implementation than System.DirectoryServices. Dominick Baier blogs about a couple of simple operations including checking membership of a group:-
public static bool IsUserInGroup(string username, string groupname, ContextType type)
{
PrincipalContext context = new PrincipalContext(type);
UserPrincipal user = UserPrincipal.FindByIdentity(
context,
IdentityType.SamAccountName,
username);
GroupPrincipal group = GroupPrincipal.FindByIdentity(
context, groupname);
return user.IsMemberOf(group);
}
I think I will use this approach, thanks for the suggestions though however! :-)

Perhaps this is something that can be done via WMI?

I asked a similar question, and ended up writing an answer which used WMI to enum the group members. I had real problems with authentication in the system.directoryservices.accountmanagement stuff. YMMV, of course.

I'd be curious if the System.DirectoryServices.AccountManagement is fully managed. I've used System.DirectoryServices.ActiveDirectory which is a wrapper for COM Interop which has led to many headaches...

This may possibly help. I had to develop an app where we want to authenticate against active directory, and also examine the groups strings that the user is in.
For a couple of reasons we don't want to use windows authentication, but rather have our own forms based authentication. I developed the routine below to firstly authenticate the user, and secondly examine all the groups that the user belongs to. Perhaps it may help. The routine uses LogonUser to authenticate, and then gets the list of numerical guid-like group ids (SIDs) for that user, and translates each one to a human readable form.
Hope this helps, I had to synthesise this approach from a variety of different google searches.
private int validateUserActiveDirectory()
{
IntPtr token = IntPtr.Zero;
int DBgroupLevel = 0;
// make sure you're yourself -- recommended at msdn http://support.microsoft.com/kb/248187
RevertToSelf();
if (LogonUser(txtUserName.Value, propDomain, txtUserPass.Text, LOGON32_LOGON_NETWORK, LOGON32_PROVIDER_DEFAULT, token) != 0) {
// ImpersonateLoggedOnUser not required for us -- we are not doing impersonated stuff, but leave it here for completeness.
//ImpersonateLoggedOnUser(token);
// do impersonated stuff
// end impersonated stuff
// ensure that we are the original user
CloseHandle(token);
RevertToSelf();
System.Security.Principal.IdentityReferenceCollection groups = Context.Request.LogonUserIdentity.Groups;
IdentityReference translatedGroup = default(IdentityReference);
foreach (IdentityReference g in groups) {
translatedGroup = g.Translate(typeof(NTAccount));
if (translatedGroup.Value.ToLower().Contains("desired group")) {
inDBGroup = true;
return 1;
}
}
}
else {
return 0;
}
}

Related

PrincipalSearchResult and System.OutOfMemoryException

I'm using Domain PrincipalContext to find users groups. And I got it. But when I'm trying to work with group collection I get System.OutOfMemoryException. All Principal objects are disposable. And there is using section in my code. I had tried to use Dispose() method and GC.Collect() But it does not help.
Here is code:
using (var ctx = new PrincipalContext(ContextType.Domain, _domain, _user, _password))
{
using (UserPrincipal user = UserPrincipal.FindByIdentity(ctx,IdentityType.SamAccountName, sAMAccountName))
{
PrincipalSearchResult<Principal> userGroups = user.GetGroups();
using (userGroups)
{
foreach (Principal p in userGroups)
{
using (p)
{
result.Add(p.Guid == null ? Guid.Empty : (Guid)p.Guid);
}
}
}
}
}
foreach loop return exception. Even foreach is empty loop.
I find that the System.DirectoryServices.AccountManagement namespace (UserPrincipal, etc.) does waste a lot of memory. For example, every time you create a UserPrincipal or GroupPrincipal, it asks AD for every attribute that has a value - even if you only ever use one of them.
If the user is a member of many, many groups, that could be the cause, although I am still surprised. Maybe your computer just doesn't have the available memory to load all that.
You can do the same thing and use less memory (and probably less time) by using the System.DirectoryServices namespace directly (which is what the AccountManagement namespace uses in the background anyway).
Here is an example that will look at the memberOf attribute of the user to find the groups and pull the Guid. This does have some limitations, which I describe an article I wrote: Finding all of a user’s groups (this example is modified from one in that article). However, in most cases (especially if you only have one domain in your environment and no trusted domains) it'll be fine.
public static IEnumerable<Guid> GetUserMemberOf(DirectoryEntry de) {
var groups = new List<Guid>();
//retrieve only the memberOf attribute from the user
de.RefreshCache(new[] {"memberOf"});
while (true) {
var memberOf = de.Properties["memberOf"];
foreach (string group in memberOf) {
using (var groupDe = new DirectoryEntry($"LDAP://{group.Replace("/", "\\/")}") {
groupDe.RefreshCache(new[] {"objectGUID"});
groups.Add(new Guid((byte[]) groupDe.Properties["objectGUID"].Value));
}
}
//AD only gives us 1000 or 1500 at a time (depending on the server version)
//so if we've hit that, go see if there are more
if (memberOf.Count != 1500 && memberOf.Count != 1000) break;
try {
de.RefreshCache(new[] {$"memberOf;range={groups.Count}-*"});
} catch (COMException e) {
if (e.ErrorCode == unchecked((int) 0x80072020)) break; //no more results
throw;
}
}
return groups;
}
You need to feed it a DirectoryEntry object of the user. If you know the distinguishedName beforehand, you can use that (e.g. new DirectoryEntry($"LDAP://{distinguishedName}")). But if not, you can search for it:
var ds = new DirectorySearcher(
new DirectoryEntry($"LDAP://{_domain}"),
$"(&(objectClass=user)(sAMAccountName={sAMAccountName}))");
ds.PropertiesToLoad.Add("distinguishedName"); //add at least one attribute so it doesn't return everything
var result = ds.FindOne();
var userDe = result.GetDirectoryEntry();
I notice you are also passing the username and password to PrincipalContext. If that's needed here, the constructor for DirectoryEntry does accept a username and password, so you can update this code to include that every time you create a new DirectoryEntry.

how to check if current user is in admin group c#

I have read the relevant Stack Overflow questions and tried out the following code:
WindowsIdentity identity = WindowsIdentity.GetCurrent();
if (null != identity)
{
WindowsPrincipal principal = new WindowsPrincipal(identity);
return principal.IsInRole(WindowsBuiltInRole.Administrator);
}
return false;
It does not return true even though I have manually confirmed that the current user is a member of the local built-in Administrators group.
What am I missing ?
Thanks.
Just found other way to check if user is admin, not running application as admin:
private static bool IsAdmin()
{
WindowsIdentity identity = WindowsIdentity.GetCurrent();
if (identity != null)
{
WindowsPrincipal principal = new WindowsPrincipal(identity);
List<Claim> list = new List<Claim>(principal.UserClaims);
Claim c = list.Find(p => p.Value.Contains("S-1-5-32-544"));
if (c != null)
return true;
}
return false;
}
Credit to this answer, but code is corrected a bit.
The code you have above seemed to only work if running as an administrator, however you can query to see if the user belongs to the local administrators group (without running as an administrator) by doing something like the code below. Note, however, that the group name is hard-coded, so I guess you would have some localization work to do if you want to run it on operating systems of different languages.
using (var pc = new PrincipalContext(ContextType.Domain, Environment.UserDomainName))
{
using (var up = UserPrincipal.FindByIdentity(pc, WindowsIdentity.GetCurrent().Name))
{
return up.GetAuthorizationGroups().Any(group => group.Name == "Administrators");
}
}
Note that you can also get a list of ALL the groups the user is a member of by doing this inside the second using block:
var allGroups = up.GetAuthorizationGroups();
But this will be much slower depending on how many groups they're a member of. For example, I'm in 638 groups and it takes 15 seconds when I run it.

GroupPrincipal.GetMembers and cross-domain members error

I have 2 domains, A and B. The Domain A has the group GroupA which contains users from Domain B.
My code:
using (var context = new PrincipalContext(ContextType.Domain, DomainName, User, Password))
{
using (var groupPrincipal = GroupPrincipal.FindByIdentity(context, IdentityType.SamAccountName,
groupName))
{
if (groupPrincipal == null) return null;
using (var principalSearchResult = groupPrincipal.GetMembers(true))
{
var changedUsersFromGroup =
principalSearchResult
.Where(member => member is UserPrincipal)
.Where(member => IsModifiedUser(member, usnChanged))
.Cast<UserPrincipal>()
.Select(adsUser => new AdsUser(adsUser)).Cast<IAdsUser>()
.ToArray();
return changedUsersFromGroup;
}
}
}
System.DirectoryServices.AccountManagement.PrincipalOperationException:
While trying to resolve a cross-store reference, the target principal
could not be found in the domain indicated by the principal's SID.
But if I add user from here
new PrincipalContext(ContextType.Domain, DomainName, User, Password)
to domain B, it works correctly.
How can I fix it?
At least in .NET 4.7, you can work around it by manually managing the enumerator. This has been tested and has been able to successfully get past the errors.
static System.Guid[] GetGroupMemberGuids(System.DirectoryServices.AccountManagement.GroupPrincipal group)
{
System.Collections.Generic.List<System.Guid> result = new List<Guid>();
if (group == null) return null;
System.DirectoryServices.AccountManagement.PrincipalCollection px = group.Members;
System.Collections.IEnumerator en = px.GetEnumerator();
bool hasMore = true;
int consecFaults = 0;
while (hasMore && consecFaults < 10)
{
System.DirectoryServices.AccountManagement.Principal csr = null;
try
{
hasMore = en.MoveNext();
if (!hasMore) break;
csr = (System.DirectoryServices.AccountManagement.Principal)en.Current;
consecFaults = 0;
}
catch (System.DirectoryServices.AccountManagement.PrincipalOperationException e)
{
Console.Error.WriteLine(" Unable to enumerate a member due to the following error: {0}", e.Message);
consecFaults++;
csr = null;
}
if (csr is System.DirectoryServices.AccountManagement.UserPrincipal)
result.Add(csr.Guid.Value);
}
if (consecFaults >= 10) throw new InvalidOperationException("Too many consecutive errors on retrieval.");
return result.ToArray();
}
Check if it behaves differently when not casting to UserPrincipal, e.g.
var changedUsersFromGroup = principalSearchResult.ToArray();
As per other threads it might be some issue there. Also as per MSDN, using GetMembers(true) returned principal collection that does not contain group objects, only leaf nodes are returned, and so maybe you don't need that casting at all. Next, is to check how many results such search would return. If your AD has many users/nested groups, it might be better to try not to use GetMembers(true) to ensure it works on small groups of users.
It seems you are defining a PrincipalContext of domain A (so you can get the group), but because the users inside the group are defined in domain B the context cannot access them (as it's a domain A context).
You might need to define a second 'PricipalContext` for domain B and run the query against it and filter the objects using maybe the list of SIDs of the users located in the domain A group (you'll need to get the list of SIDs without causing the underlying code to try and resolve them).
Hope it helps!
Unfortunately I currently cannot test it, but maybe you can try this
var contextB = new PrincipalContext(ContextType.Domain, DomainName_B, User_B, Password_B)
[..]
var changedUsersFromGroup =
principalSearchResult
.Where(member => member is UserPrincipal)
.Where(member => IsModifiedUser(member, usnChanged))
.Select(principal => Principal.FindByIdentity(contextB, principal.SamAccountName))
.Cast<UserPrincipal>()
.Select(adsUser => new AdsUser(adsUser)).Cast<IAdsUser>()
.ToArray();
This could work, but only, if all members are in Domain B of course. If they are mixed in different domains you may have to filter it before that and iterate through your domains.
Alternatively you could run your application with a domain account? Then don't pass user/pass and give the required access rights to this account to avoid the error.
Explanation: The domain context will switch for your retrieved principals (you can view this in debugging mode if you comment out the new AdsUser / IAdsUser Cast part). This is the one, that seems to cause the exception. Though exactly this is the part I cannot test, I think the creation of AdsUser creates a new ldap Bind to the target Domain. This fails with the "original" Credentials. Is AdsUser Part of ActiveDS or 3rd Party? I did not find any hint if passing credentials uses basic authentication, but I think it should. Using application credentials uses negotiate and "handling this over" to the new AdsUser(..) should fix the issue as well.
problem found and reported to MS as bug. currently impossible to do it with .net :( but it works via native c++ api via queries

How to provide DirectoryEntry.Exists with credentials?

This morning I discovered a nice method (DirectoryEntry.Exists), that should be able to check whether an Active Directory object exists on the server. So I tried with a simple:
if (DirectoryEntry.Exists(path)) {}
Of course it lacks any overloads to provide credentials with it. Because, if credentials are not provided I get this Exception:
Logon failure: unknown user name or
bad password.
(System.DirectoryServices.DirectoryServicesCOMException)
Is there any other option that gives me the possibility to authenticate my code at the AD server? Or to check the existence of an object?
In this case you can't use the static method Exists as you said :
DirectoryEntry directoryEntry = new DirectoryEntry(path);
directoryEntry.Username = "username";
directoryEntry.Password = "password";
bool exists = false;
// Validate with Guid
try
{
var tmp = directoryEntry.Guid;
exists = true;
}
catch (COMException)
{
exists = false;
}
I know this is an old question, but the source code is now available so you can just Steal and Modify™ to make a version that accepts credentials:
public static bool Exists(string path, string username, string password)
{
DirectoryEntry entry = new DirectoryEntry(path, username, password);
try
{
_ = entry.NativeObject; // throws exceptions (possibly can break applications)
return true;
}
catch (System.Runtime.InteropServices.COMException e)
{
if (e.ErrorCode == unchecked((int)0x80072030) ||
e.ErrorCode == unchecked((int)0x80070003) || // ERROR_DS_NO_SUCH_OBJECT and path not found (not found in strict sense)
e.ErrorCode == unchecked((int)0x800708AC)) // Group name could not be found
return false;
throw;
}
finally
{
entry.Dispose();
}
}
The one change you must make is changing the use of Bind, since that's an internal method and can't be used by mere mortals like us. Instead, I just get the NativeObject property, which calls Bind() for us.
You can use that like this:
var ouExists = Exists("LDAP://hadoop.com/OU=Students,DC=hadoop,DC=com", "username", "password");
There is no way to do this and I have written a connect issue to hopefully resolve it.
DirectoryEntry.Exists Does Not Accept Credentials
Here you can read about impersonation in C#:
http://www.codeproject.com/KB/cs/zetaimpersonator.aspx
http://www.codeproject.com/KB/system/everythingInAD.aspx
So answer to the question: impossible.
Finally write an own method to get the DirectoryEntry by distinguised name, with credentials specified. In both cases of existence/inexistence I got an instance of DirectoryEntry. To check whether it's a valid object returned I do a simple try...catch to see if it results in an Exception. If so, it's invalid.
Nasty check, but it works. Too bad the default .net method DirectoryEntry.Exists doesn't provide an overload to provide credentials just like the DirectoryEntry constructor...
If the user who ran the process doesn't have permissions to call DirectoryEntry.Exists, then you can use impersonation.
This may be helpful (discusses impersonation in an AD context): http://www.codeproject.com/KB/system/everythingInAD.aspx
Btw, if you already have credentials of a user who has access to everything you need, why not just the process with that user (e.g. /runas)?

Strange Error When Using System.DirectoryServices.AccountManagement.PrincipalContext.ValidateCredentials with SAM

I am hosting a WCF Web Service with IIS 6.0. My application pool is running under a local Administrator account, and I have other local users defined for accessing the Web Service. I've written the following code to validate users:
//Any public static (Shared in Visual Basic) members of this type are thread safe
public static PrincipalContext vpc;
//static initializer
static UserManagement()
{
vpc = new PrincipalContext(ContextType.Machine);
}
//Determines whether the given credentials are for a valid Administrator
public static bool validateAdminCredentials(string username, string password)
{
using (PrincipalContext principalContext = new PrincipalContext(ContextType.Machine))
{
if (vpc.ValidateCredentials(username, password))
{
using (UserPrincipal user = UserPrincipal.FindByIdentity(principalContext, IdentityType.SamAccountName, username))
{
foreach (GroupPrincipal gp in user.GetGroups())
{
try
{
if (gp.Name.Equals("Administrators"))
{
return true;
}
}
finally
{
gp.Dispose();
}
}
}
}
return false;
} //end using PrincipalContext
}
...and in another class:
//verify the user's password
if (!UserManagement.vpc.ValidateCredentials(username, password))
{
string errorMsg = string.Format("Invalid credentials received for user '{0}'", username);
throw new Exception(errorMsg);
}
(Note: I am using the public static PrincipalContext (vpc) solely for calling the ValidateCredentials method; I create a different, temporary PrincipalContext for creating, deleting, and finding users and groups, as I got various COM-related errors when I tried using the global PrincipalContext for everything).
So, most of the time, this code works wonderfully. However, intermittently, I get the following error:
Multiple connections to a server or shared resource by the same user, using more than one user name, are not allowed. Disconnect all previous connections to the server or shared resource and try again. (Exception from HRESULT: 0x800704C3)
Application:
System.DirectoryServices.AccountManagement
Stack Trace:
at System.DirectoryServices.AccountManagement.CredentialValidator.BindSam(String target, String userName, String password)
at System.DirectoryServices.AccountManagement.CredentialValidator.Validate(String userName, String password)
at System.DirectoryServices.AccountManagement.PrincipalContext.ValidateCredentials(String userName, String password)
at MyNamespace.User..ctor(String username, String password)
Once the error occurs, I continue to get it until I restart my entire server (not just IIS). I've tried restarting my application pool and/or IIS, but the error does not go away until I restart the machine. I've also tried instantiating (via a using block) a new PrincipalContext for every call to ValidateCredentials (which I shouldn't have to do), but I still eventually get the same error. From what I've read on System.DirectoryServices.AccountManagement (msdn docs, articles), I believe I'm using it correctly, but this error is crippling my application! I need (and should be able) to validate local user credentials from web service requests coming from multiple clients. Am I doing something wrong? Any help on solving this issue would be much appreciated...
"Any public static (Shared in Visual Basic) members of this type are thread safe"
This boilerplate text confuses a lot of people. It means that any static members exposed by the type are thread-safe. It doesn't mean that any instance of the type stored in a static member will be thread-safe.
Your validateAdminCredentials method creates a new PrincipalContext object, but then proceeds to use the static vpc instance to validate the credentials. Since you have no locks around the access to the static instance, and the instance methods are not thread-safe, you'll eventually get two threads trying to access the same instance at the same time, which will not work.
Try removing the vpc field and using the principalContext instance to validate the credentials. You'll need to create and dispose of the PrincipalContext on every call.
Also, rather than manually iterating the user's groups, you can use the IsMemberOf method to test for membership in a group.
public static bool ValidateCredentials(string username, string password)
{
using (PrincipalContext principalContext = new PrincipalContext(ContextType.Machine))
{
return principalContext.ValidateCredentials(username, password);
}
}
public static bool validateAdminCredentials(string username, string password)
{
using (PrincipalContext principalContext = new PrincipalContext(ContextType.Machine))
{
if (principalContext.ValidateCredentials(username, password))
{
using (UserPrincipal user = UserPrincipal.FindByIdentity(principalContext, IdentityType.SamAccountName, username))
using (GroupPrincipal group = GroupPrincipal.FindByIdentity(principalContext, IdentityType.SamAccountName, "Administrators"))
{
if (null != group && user.IsMemberOf(group))
{
return true;
}
}
}
return false;
}
}
My suggestion would be to not use ValidateCredentials with the SAM provider. I believe it should work, but I am also not surprised that it runs into problems. I think you'll do better by doing a p/invoke into LogonUser using Network logon type for programmatic credential validation.
I'd like to see if Microsoft has a known issue and solution to this problem but in the meantime I'd suggest just coding around it.
If you need a solution the works even with Windows 2000 and w/o running code as system you could also use this:
public static bool ValidateUser(
string userName,
string domain,
string password)
{
var tcpListener = new TcpListener(IPAddress.Loopback, 0);
tcpListener.Start();
var isLoggedOn = false;
tcpListener.BeginAcceptTcpClient(
delegate(IAsyncResult asyncResult)
{
using (var serverSide =
new NegotiateStream(
tcpListener.EndAcceptTcpClient(
asyncResult).GetStream()))
{
try
{
serverSide.AuthenticateAsServer(
CredentialCache.DefaultNetworkCredentials,
ProtectionLevel.None,
TokenImpersonationLevel.Impersonation);
var id = (WindowsIdentity)serverSide.RemoteIdentity;
isLoggedOn = id != null;
}
catch (InvalidCredentialException) { }
}
}, null);
var ipEndpoint = (IPEndPoint) tcpListener.LocalEndpoint;
using (var clientSide =
new NegotiateStream(
new TcpClient(
ipEndpoint.Address.ToString(),
ipEndpoint.Port).GetStream()))
{
try
{
clientSide.AuthenticateAsClient(
new NetworkCredential(
userName,
password,
domain),
"",
ProtectionLevel.None,
TokenImpersonationLevel.Impersonation);
}
catch(InvalidCredentialException){}
}
tcpListener.Stop();
return isLoggedOn;
}
I resolved this same problem by using the domain context. It is possible to use the machine context to delegate to the domain, but it appears that you will be forced to run into this problem eventually.
Note that you should create the domain context fresh for each call, because it must be created using the domain of the user you want to authenticate.
My application needed to support machine users as well. I found that whenever a user was a machine user, the domain context would throw an exception. If I catch the exception, I work against the machine context.

Categories