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
Related
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 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.
I have a piece of functionality that creates a voice message and if authenticated through Twitter, will post a message to their Twitter account. I want the ability for the user to turn off the message posting to Twitter if they desire, so I was curious if there was a way to clear the credentials. I followed an example from the LinqToTwitter documentation:
IOAuthCredentials credentials = new SessionStateCredentials();
if (credentials.ConsumerKey == null || credentials.ConsumerSecret == null)
{
credentials.ConsumerKey = ConfigurationManager.AppSettings["twitterConsumerKey"];
credentials.ConsumerSecret = ConfigurationManager.AppSettings["twitterConsumerSecret"];
}
auth = new WebAuthorizer
{
Credentials = credentials,
PerformRedirect = authUrl => Response.Redirect(authUrl)
};
if (!Page.IsPostBack && Request.QueryString["oauth_token"] != null)
{
auth.CompleteAuthorization(Request.Url);
}
if (auth.IsAuthorized)
{
twitterCtx = new TwitterContext(auth);
Session["TwitterContext"] = twitterCtx;
twLoginButton.Text = "Logout of Twitter";
}
I've tried the following code and variations:
credentials = null;
or
SessionStateCredentials credentials = Dispose();
But it shows errors for each of these. I was hoping someone could guide me in clearing out
IOAuthCredentials credentials = new SessionStateCredentials();
which is what I think needs to happen. Any advice would be appreciated.
The SessionStateCredentials type has properties that use Session state as their backing store. Here are a few options, with pros and cons of each:
Set the properties to null. e.g.
credentials.ConsumerKey = null;
credentials.ConsumerSecret = null;
// etc ...
This is a little ugly, though you could write a method to encapsulate the statements.
Clear out the individual Session state items. e.g.
Session.Remove("ConsumerKey");
Session.Remove("ConsumerSecret");
// etc ...
This is more explicit. However, it breaks the existing encapsulation and forces you to obtain a reference to the current session.
Derive a new class from SessionStateCredentials with a Clear method that performs the steps from one of the previous methods. This might be the cleanest option.
Here's a link to the SessionStateCredentials class so you can see the internal implementation:
http://linqtotwitter.codeplex.com/SourceControl/latest#LinqToTwitter/OAuth/SessionStateCredentials.cs
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.
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;
}
}