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.
Related
Relevant Code: -- This is a work in progress, and I'm still in the discovery stage, so not all exception paths are complete. No snark about re-throwing exceptions -- I'll get to it.
I also have to say that I have minimal experience with AD. It's never really come up for me before.
Most of the code came from Microsoft Examples.
public static bool AddUser(string firstName, string lastName, string userLogonName,
string employeeID, string emailAddress, string telephone, string address,
string Password, DateTime expiry)
{
PrincipalContext principalContext = GetContext();
// Check if user object already exists in the store
if (UserExists(userLogonName))
{
throw new Exception(userLogonName + " already exists. Please use a different User Logon Name.");
}
// Create the new UserPrincipal object
UserPrincipal userPrincipal = new UserPrincipal(principalContext);
if (lastName != null && lastName.Length > 0)
{
userPrincipal.Surname = lastName;
}
if (firstName != null && firstName.Length > 0)
{
userPrincipal.GivenName = firstName;
}
if (employeeID != null && employeeID.Length > 0)
{
userPrincipal.EmployeeId = employeeID;
}
if (emailAddress != null && emailAddress.Length > 0)
{
userPrincipal.EmailAddress = emailAddress;
}
if (telephone != null && telephone.Length > 0)
{
userPrincipal.VoiceTelephoneNumber = telephone;
}
if (userLogonName != null && userLogonName.Length > 0)
{
userPrincipal.SamAccountName = userLogonName;
}
userPrincipal.AccountExpirationDate = expiry;
userPrincipal.SetPassword(Password);
userPrincipal.Enabled = true;
userPrincipal.PasswordNeverExpires = true;
try
{
userPrincipal.Save();
}
catch (Exception e)
{
throw new Exception("Exception saving user object. ", e);
}
return true;
}
AND
public static void AddUserToGroup(string userLogonName, string groupName)
{
try
{
using (PrincipalContext principalContext = GetContext())
{
GroupPrincipal group = GroupPrincipal.FindByIdentity(principalContext, groupName);
group.Members.Add(FindUser(userLogonName));
group.Save();
}
}
catch (System.DirectoryServices.DirectoryServicesCOMException e)
{
throw e;
}
}
When I run my test, (add user) and check Active Directory, the user is there. So far so good.
Then I ran my add to group test, and the user shows as MemberOf group in AD. Again, everything is as expected.
Now I navigate to the Sharepoint site and try and login as the newly created user. I get the "Sorry, this site hasn't been shared with you."
... Interlude: much poking around groups and permissions ensues to no avail ...
Next, I created a user manually in AD, and then ran the Add To Group test. Everything looks good in AD, and I can successfully login to the Sharepoint site.
So, I suspect that there is something wrong with the AddUser method, but I can't figure out what. I see no difference between the user created programatically and the user created manually.
As mentioned in our comments, just wait longer for your changes to replicate to all the domain controllers before you test.
Depending on how your GetContext() method is written, you could even have replication issues creating the account. If it's creating a new PrincipalContext object each time, it could theoretically connect to a different DC the the second time, where the new account doesn't exist yet. (although it tries to connect you to the closest one, so it will likely always be the same one)
To avoid any chance of getting a different DC, you can either reuse the same PrincipalContext object, or can you read the ConnectedServer property of the PrincipalContext, which will tell you which DC it ended up using. You could then use that in later to make sure you're making all your changes on the same DC.
The constructor for PrincipalContext will let you pass a specific DC as the domain name if you want to target a specific DC:
var context = new PrincipalContext(ContextType.Domain, "dc1.domain.com");
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.
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
I need to have the code so that it takes a username and outputs a list of groups that the user is a part of on my domain. I am currently just running this on a console application but I will need it to run on a ASP .NET Web Application once the code is complete.
I currently have this as my starting code.
public List<GroupPrincipal> GetGroups(string userName)
{
List<GroupPrincipal> result = new List<GroupPrincipal>();
// establish domain context
PrincipalContext yourDomain = new PrincipalContext(ContextType.Domain);
// find your user
UserPrincipal user = UserPrincipal.FindByIdentity(yourDomain, userName);
// if found - grab its groups
if (user != null)
{
PrincipalSearchResult<Principal> groups = user.GetAuthorizationGroups();
// iterate over all groups
foreach (Principal p in groups)
{
// make sure to add only group principals
if (p is GroupPrincipal)
{
result.Add((GroupPrincipal)p);
}
}
}
return result;
}
I have the following code that takes a username and a password and then creates the user on the machine and adds them to two specific groups. When I get to the group part it is really slow and I have no idea why. My last run according to my logs file says that adding the user to the Users group took 7 minutes, but the IIS_IUSRS was super fast.
below is my initial code that calls the methods that do the real work. I have tried using a task to help speed up the process of checking for groups, but it still runs super slow.
public void Apply(Section.User.User user, Action<string> status)
{
#region Sanity Checks
if (user == null)
{
throw new ArgumentNullException("user");
}
if (status == null)
{
throw new ArgumentNullException("status");
}
#endregion
_logger.Debug(string.Format("Starting to apply the user with name {0}", user.UserName));
status(string.Format("Applying User {0} to the system.", user.UserName));
using (PrincipalContext pc = new PrincipalContext(ContextType.Machine))
{
UserPrincipal userPrincipal = UserPrincipal.FindByIdentity(pc, user.UserName);
try
{
_logger.Debug("Checking if user already exists");
if (userPrincipal == null)
{
userPrincipal = CreateNewUser(user, pc);
}
_logger.Debug("Setting user password and applying to the system.");
userPrincipal.SetPassword(user.UserPassword);
userPrincipal.Save();
Task<PrincipalSearchResult<Principal>> groups =
Task<PrincipalSearchResult<Principal>>.Factory.StartNew(userPrincipal.GetGroups);
_logger.Debug("Adding user to the groups.");
AddUserToGroups(pc, userPrincipal, groups, user.UserType.Equals(UserType.WorkerProcess.ToString()) ? "Administrators" : "Users", "IIS_IUSRS");
AddCurrentUser(user);
}
finally
{
if (userPrincipal != null)
{
userPrincipal.Dispose();
}
}
}
}
This is my private method I use to create the user if it doesn't exist.
private UserPrincipal CreateNewUser(Section.User.User user, PrincipalContext principal)
{
_logger.Debug("User did not exist creating now.");
UserPrincipal newUser = new UserPrincipal(principal)
{
Name = user.UserName,
Description = user.UserDescription,
UserCannotChangePassword = false,
PasswordNeverExpires = true,
PasswordNotRequired = false
};
_logger.Debug("User created.");
return newUser;
}
Below is the logic for the groups. I have made a comment above the offending code that I get hung on whenever I walk through with the debugger. Also the debug log entry is always the last one I get before the hang as well.
private void AddUserToGroups(PrincipalContext principal, UserPrincipal user, Task<PrincipalSearchResult<Principal>> userGroups, params string[] groups)
{
groups.AsParallel().ForAll(s =>
{
using (GroupPrincipal gp = GroupPrincipal.FindByIdentity(principal, s))
{
_logger.Debug(string.Format("Checking if user is alread in the group."));
if (gp != null && !userGroups.Result.Contains(gp))
{
_logger.Debug(string.Format("The user was not a member of {0} adding them now.", gp.Name));
//This is the point that the 7 minute hang starts
gp.Members.Add(user);
gp.Save();
_logger.Debug(string.Format("User added to {0}.", gp.Name));
}
}
});
}
Any help with this would be greatly appreciated as this project is expected to release in October, but I can't release with a 7 minute hang when creating a user.
Had the same problem. It seems that
gp.Members.Add( user );
is slow because it first enumerates groups (to get Members) and only then it adds to the collection (which adds another slowdown).
The solution was to have it like:
UserPrincipal user = this is your user;
GroupPrincipal group = this is your group;
// this is fast
using ( DirectoryEntry groupEntry = group.GetUnderlyingObject() as DirectoryEntry )
using ( DirectoryEntry userEntry = user.GetUnderlyingObject() as DirectoryEntry )
{
groupEntry.Invoke( "Add", new object[] { userEntry.Path } );
}
//group.Members.Add(user); // and this is slow!
//group.Save();
Just a tip - creating passwords with SetPassword was also terribly slow for us. The solution was to follow the approach from "The .NET Developer's Guide to Directory Services Programming" where they use a low level password setting using LdapConnection from System.DirectoryServices.Protocols.
The last bottleneck we've discovered was caused by the User.GetGroups() method.
Anyway, drop a note if the code for adding users to groups makes a difference for you. Also note that you don't really need to perform this in parallel - I understand that this was your approach to speed up the code but you don't really need this.