I have a C#/WPF application that is used to manipulate local users and groups on a system. We only care about local users and groups, regardless of whether the machine is joined to a domain or not. When we create a user in our application, I want to add the user to the "Users" group. Normally this works fine, but if the machine is domain-joined and NOT connected to the network (e.g. a laptop out of the office), I get "the network path is not found" errors when trying to add a local user to the "Users" group.
I think the reason is because the "Users" group contains domain users, as shown in this screenshot.
And this is essentially my code:
public static void AddUserToGroup(UserPrincipal oUserPrincipal, string groupName)
{
using (PrincipalContext pc = new PrincipalContext(ContextType.Machine))
{
GroupPrincipal group = GroupPrincipal.FindByIdentity(pc, groupName);
if (group == null)
{
group = CreateLocalWindowsGroup(groupName);
}
if (!group.Members.Contains(oUserPrincipal)) // this line throws "network path not found" exception if the machine is domain joined, but can't contact the domain controller
group.Members.Add(oUserPrincipal);
group.Save();
}
}
I can't figure out how to approach this with the API, but it seems like it should be possible because I can add the exact same user to the same group manually with the "Local Users and Groups" tool with no issues, regardless of network connectivity. How can I get around this issue?
This is one reason I don't like using the AccountManagement namespace.
The GroupPrincipal.Members property returns a PrincipalCollection, which is just a collection of Principal objects. The actual type will be UserPrincipal or GroupPrincipal depending on what the actual member is.
But, those Principal classes, when they're created, load all the details for that object. So just the act of creating a UserPrincipal for a domain user triggers it to go out to the domain and get all the details for the user.
You're better off using DirectoryEntry directly, which is what the AccountManagement namespace uses in the background anyway. It gives you more control over what's actually happening.
var usersGroup = new DirectoryEntry($"WinNT://{Environment.MachineName}/{groupName}");
usersGroup.Invoke("Add", new object[] { $"WinNT://{Environment.MachineName}/{userName}" });
This assumes a userName variable with the name of the local user. If the user is already in the group, it will throw an exception, so you may want to catch that.
Besides actually working in this case, this will run faster since you're not wasting time collecting details for all the existing members when you have no intention of using any of that data.
Update: To read all the members of a local group, use .Invoke("Members"). Then you have to create a new DirectoryEntry with each member in the collection. For example:
foreach (var member in (IEnumerable) usersGroup.Invoke("Members")) {
using (var memberDe = new DirectoryEntry(member)) {
Console.WriteLine(memberDe.Name);
}
}
The DirectoryEntry class is really a wrapper around the Windows native ADSI Interfaces. For a group, the underlying object will really be IADsGroup. When you call .Invoke on a DirectoryEntry object, that lets you call the IADsGroup methods (you'll see the Members method listed in the documentation there). All of the object-specific classes like IADsGroup and IADsUser all inherit from IADs, so the methods from that are usable too.
This only applies to local groups. With Active Directory groups, you don't have to resort to using the IADs methods.
Related
In a Web API controller I needed to determine the role membership using an AD group that contained members from multiple domains in another forest.
this.RequestContext.Principal.IsInRole(roleName)
returned false and no indication of an error could be found. The code above did work with other AD groups, though. I then modified the code to loop through the group in question and received an exception.
GroupPrincipal group = GroupPrincipal.FindByIdentity(ctx, roleName);
if (group != null)
{
foreach (Principal p in group.GetMembers())
{
if (p != null && currentUserPrincipal.UserPrincipalName == p.UserPrincipalName)
{
roles.Add(roleName);
break;
}
}
}
The specified directory service attribute or value does not exist.
I determined it was the exception was being thrown on a group member from a specific domain. I removed said individual and code executed normally. I added another account form the same domain as the first and the error returned.
Searching for the given error message I found the following SO question and answer. The top answer states.
When omitting the LDAP container property as described in PrincipalContext Class, the user running the code must have read permissions to both the default User Container (i.e. CN=Users,DC=yourDomain,DC=COM) and the Computers Container (i.e. CN=Computers,DC=yourDomain,DC=COM).
Using Active Directory Users and Computers I browsed the AD of the problem domain and could not see any Computers container. I contacted IS and informed them of this and they restored the directory to a good state. At that point this.RequestContext.Principal.IsInRole(roleName) worked as expected and I was able to evaluate role membership.
Edit: OMG! This also fixed an issue with the SharePoint user profile service not syncing user details from members in the same domain. I have been trying for two years to track down the cause of the the user profile error with no success.
I am creating Active Directory groups in my app. I make security and distribution groups. The groups will get created just fine, but it takes about 10-15 minutes to show up in the Active Directory Users and Computers.
Is there some kind of forced sync I can perform in C# to make this happen sooner? Or maybe some setting I can change in my directory to change this behavior?
Example code
DirectoryEntry ou1= topLevel.Children.Find("OU=ou1");
DirectoryEntry secGroups = ou1.Children.Find("OU=Security Groups");
DirectoryEntry newGroup = secGroups.Children.Add("CN=" + name + "", "group");
newGroup.CommitChanges();
GroupPrincipal createdGroup = GroupPrincipal.FindByIdentity(this._context, name);
createdGroup.SamAccountName = name;
createdGroup.DisplayName = name;
createdGroup.GroupScope = GroupScope.Universal;
createdGroup.Save();
if (members.Any())
{
foreach (var item in members)
{
createdGroup.Members.Add(this._context, IdentityType.SamAccountName, item);
}
createdGroup.Save();
}
Using ASP.NET MVC, C#, System.DirectoryServices.AccountManagement, System.DirectoryServices.ActiveDirectory.
The most likely answer is that it takes time to propagate to all domain controllers on your network. You may be connected to a different DC via ADUC from the one your application updated.
Something that might help in this situation where you have multiple domain controllers replicating is to target a specific DC for each call you make to the AD server.
So instead of "LDAP://mydomain.com" it becomes something like "LDAP://myDC.mydomain.com"
I am working on a web application, ASP.NET, C#. Users are required to log in using an account local to the machine the app is running on, which I'll call "cyclops" for this example. I want the app to be able to query the local directory of users and groups to determine what groups the user is in. The code looks something like this:
DirectoryEntry entry = new DirectoryEntry("WinNT://cyclops/Users", "SomeServiceAccount",
"SvcAcctP#$$word", AuthenticationTypes.Secure);
entry.RefreshCache();
// Etc.
My two problems are:
That's pretty clearly not the correct path to use, but my research
and experimentation hasn't found the right answer. This MSDN
article talks about local paths, but doesn't fill in the blanks.
Do I use "LDAP://cyclops/Users", "WinNT://localhost/Users",
"WinNT://cyclops/cn=Users"?
As you can see, I'm providing the
credentials of a local service account. That account needs
permission to access the local directory, but I have no idea where
to set those permissions. Is it a specific file somewhere? Does
the account need to be a member of a particular group?
My experimentation has produced many errors: "The group name could not be found.", "The provider does not support searching...", "The server is not operational.", "Unknown error (0x80005004)", etc.
Thank you for your time...
-JW
WinNT requires the following format
WinNT://<domain/server>/<object name>,<object class>
To get groups of a given user, use
using (DirectoryEntry user = new DirectoryEntry("WinNT://./UserAccount,user"))
{
foreach(object group in (IEnumerable)user.Invoke("Groups",null))
{
using(DirectoryEntry g = new DirectoryEntry(group))
{
Response.Write(g.Name);
}
}
}
where
UserAccount is a name of required user.
dot stands for current machine (you can replace it with cyclops or use Environment.MachineName)
user credentials ("SomeServiceAccount", "SvcAcctP#$$word") might be required, depends on setup
To get users in a particular group, use
using (DirectoryEntry entry = new DirectoryEntry("WinNT://./Users,group"))
{
foreach (object member in (IEnumerable)entry.Invoke("Members"))
{
using(DirectoryEntry m = new DirectoryEntry(member))
{
Response.Write(m.Name);
}
}
}
where
Users is a name of group
Problem:
We've upgraded the AD server from 2003 to 2008 and due to some "bad code", where developer has coded in such a way that, he directly casts "badPwdCount" property value to INT and it blows up because of NULL value conversion - NULL reference exception - NULL cannot be converted to INT.
Bigger problem:
We cannot do a deployment at this point because there are over 100 individual apps that depended on this change and we're looking for a least involved way of dealing with it for now.
Background:
Now the way this "badPwdCount" property works is, that when user logs on to the domain, it will get set to zero, otherwise it's NULL. The problem is that none of these users are ever going to log on interactively because they're external and we authenticate them via API and they cannot log in using the API either..
Question:
Does anyone know if this value is in the registry or somewhere, where I can get to it and set it to zero? Was also thinking of initiating a log in per user via a script, but wanted to gather other ideas too...
MSDN page for badPwdCount:
http://msdn.microsoft.com/en-us/library/windows/desktop/ms675244(v=vs.85).aspx
Normally this would be easy, all you would need to do is update all the users in active directory and set the value to 0 if it is null. There are various ways you could do this, for example a script or code, or a bulk update tool.
In this case, badPwdCount is a special property that is not replicated (i.e. it is different for each domain controller) and so far as I can tell, there is no way to update it manually or by script, however, I think I have a solution for you.
You should be able to easily trigger a single failed login for every user in active directory against each domain controller, causing the value to be incremented.
Since you tagged your post with C#, here is some C# code that will do the trick for you:
using System.DirectoryServices.AccountManagement;
using System.DirectoryServices.ActiveDirectory;
...
using (Domain domain = Domain.GetComputerDomain())
{
foreach (DomainController domainController in domain.DomainControllers)
{
using (PrincipalContext context = new PrincipalContext(ContextType.Domain, domainController.Name))
using (UserPrincipal userPrincipal = new UserPrincipal(context))
using (PrincipalSearcher searcher = new PrincipalSearcher(userPrincipal))
using (PrincipalSearchResult<Principal> results = searcher.FindAll())
{
foreach (UserPrincipal user in results.OfType<UserPrincipal>())
{
context.ValidateCredentials(user.SamAccountName, "THEREISNOWAYTHISISTHECORRECTPASSWORD");
}
}
}
}
PS. If this screws up your AD I take no responsibility for it!
In my Sharepoint code I display a list of all defined users via:
foreach (SPUser user in SPContext.Current.Web.AllUsers)
{
...
}
The great part is, I can add a domain security group to a Sharepoint group (like Visitors) thus adding many users at once (simpler administration). But my code doesn't see those users at least not until they log-in for the first time (if they have sufficient rights). In this case I can only see the domain security group SPUser object instance with its IsDomainGroup set to true.
Is it possible to get domain group members by means of Sharepoint without resorting to Active Directory querying (which is something I would rather avoid because you probably need sufficient rights to do such operations = more administration: Sharepoint rights + AD rights).
You can use the method SPUtility.GetPrincipalsInGroup (MSDN).
All parameters are self-explaining except string input, which is the NT account name of the security group:
bool reachedMaxCount;
SPWeb web = SPContext.Current.Web;
int limit = 100;
string group = "Domain\\SecurityGroup";
SPPrincipalInfo[] users = SPUtility.GetPrincipalsInGroup(web, group, limit, out reachedMaxCount);
Please note that this method does not resolve nested security groups. Further the executing user is required to have browse user info permission (SPBasePermissions.BrowseUserInfo) on the current web.
Update:
private void ResolveGroup(SPWeb w, string name, List<string> users)
{
foreach (SPPrincipalInfo i in SPUtility.GetPrincipalsInGroup(w, name, 100, out b))
{
if (i.PrincipalType == SPPrincipalType.SecurityGroup)
{
ResolveGroup(w, i.LoginName, users);
}
else
{
users.Add(i.LoginName);
}
}
}
List<string> users = new List<string>();
foreach (SPUser user in SPContext.Current.Web.AllUsers)
{
if (user.IsDomainGroup)
{
ResolveGroup(SPContext.Current.Web, user.LoginName, users);
}
else
{
users.Add(user.LoginName);
}
}
Edit:
[...] resorting to Active Directory querying (which is something I would rather avoid because you probably need sufficient rights to do such operations [...]
That's true, of course, but SharePoint has to lookup the AD as well. That's why a application pool service account is required to have read access to the AD.
In other words, you should be safe executing queries against the AD if you run your code reverted to the process account.
I would suggest you just query Active Directory directly. You are spending a lot of effort to try to get SharePoint to make this call to AD for you. Every account that has Domain User access should be able to query the AD groups you have nested in SharePoint. I would just go to the source.
This way you don't have to worry about Browse User Permissions or anything else. In my opinion trying to proxy this through SharePoint is just making your life more difficult.