DirectoryEntry call to only dns configured dc servers under a domain - c#

We are querying Active Directory using the DirectoryEntry method by passing a domain name.
Under this domain, there are 40 DC's, in that 20 of them are DNS configured, and the rest of them are non-DNS configured, which are not maintained well and not reliable(connecting to these non-DNS configured DC's will usually timeout or thread being aborted).
Now, while making an AD call with directoryEntry method, is there a way to query only the DC's which has the DNS configured?
Currently, the code picks the Non-DNS configured DC.
I know picking the DC in a domain is a domain server task, based on the geographical location and other factors. Is there any way we can modify the code to instruct the DirectoryEntry to pick only the DNS configured DC's when we pass the DomainName.
Sample code in c# .net:
DirectoryEntry obEntry = new DirectoryEntry(#"LDAP://" + DomainName + "/<SID=" + new SecurityIdentifier(groupSid, 0).Value + ">", serviceAccountUser, serviceAccountPassword);
if (obEntry.Guid != null)
{
string distinguishedNameObtained = Convert.ToString(obEntry.Properties["distinguishedName"].Value);
}

You can't tell DirectoryEntry to pick a subset of DCs, but you can tell it to use one specific DC. In your code, you would set your DomainName variable to the name of the DC:
var DomainName = "dc1.example.com";
That's the easiest way, but now you've hard-coded one single DC, and if that one goes down, you have to change your code, which isn't ideal.
If you want to chose from the available DCs, you could try using Domain.GetCurrentDomain() or Domain.GetDomain() (if the computer you're running this from is not on the same domain you're connecting to) and then examining the DomainControllers collection. I don't know what you mean by the DCs not being configured for DNS, so I'm not sure if that's something you can determine from the DomainController class. Take a look at the documentation for DomainController and see if there is something you can use. There is a SiteName property if you want to choose a DC from a specific site.
If you are able to do that, then you can use the Name property of the DomainController in your LDAP string.

Related

Manipulate Windows "Users" group without access to domain?

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.

Active Directory groups not immediately available after creation

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"

Connecting to LDAP Server from .NET

I've been recommended to use System.DirectoryServices.Protocols to be able to support connecting to LDAP servers other than Active Directoy here.
Unfortunately, I have not been able to search the directory properly. I'd like to be able to get a certain attribute for a user (e.g. mail). This is easily done in System.DirectoryServices namespace by using DirectorySearcher class. How can I achieve the same in System.DirectoryServices.Protocols namespace. Here's what I have so far:
var domainParts = domain.Split('.');
string targetOu = string.Format("cn=builtin,dc={0},dc={1}", domainParts[0], domainParts[1]);
string ldapSearchFilter = string.Format("(&(ObjectClass={0})(sAMAccountName={1}))", "person", username);
// establish a connection to the directory
LdapConnection connection = new LdapConnection(
new LdapDirectoryIdentifier(domain),
new NetworkCredential() { UserName = username,
Password = "MyPassword" });
SearchRequest searchRequest = new SearchRequest(
targetOu, ldapSearchFilter, SearchScope.OneLevel, new[] {"mail"});
This code raises exception of type DirectoryOperationException with message The object does not exist.
I suspect there's something wrong with my targetOu and ldapSearchFilter variables.
Thanks.
I suspect the main problem might be: samAccountName is a strictly Windows-only attribute that other LDAP servers won't know about.
So if you're going against a non-Active Directory LDAP, you should use something else for searching - e.g. sn (for surname or last name), givenName (first name), possibly displayName.
Another interesting option might be to use ANR (ambiguous name resolution) searches - see this page on SelfADSI roughly in the middle, where ANR is explained.
With ANR, you would write your query like this:
string ldapSearchFilter =
string.Format("(&(ObjectCategory={0})(anr={1}))", "person", username);
I also changed ObjectClass to ObjectCategory for two reasons:
ObjectCategory is single-valued, e.g. only contains a single value (ObjectClass is multi-valued)
ObjectCategory is typically indexed, and thus searches are typically a lot faster using ObjectCategory
Does this return the results you're looking for?

How can I specify alternate credentials in code?

I want to run this function, or at least the bit that deletes the machine account from AD with different credentials:
public static void DeleteMachineAccount(String MachineName)
{
String MachineLdapPath = LdapPath(MachineName);
String OuLdapPath = MachineLdapPath.Replace("CN=" + MachineName + ",", "");
Console.WriteLine(MachineLdapPath);
Console.WriteLine(OuLdapPath);
if (DirectoryEntry.Exists(MachineLdapPath))
{
try
{
DirectoryEntry MachineOu = new DirectoryEntry(OuLdapPath);
DirectoryEntry MachineToDelete = new DirectoryEntry(MachineLdapPath);
MachineOu.Children.Remove(MachineToDelete);
MachineToDelete.CommitChanges();
}
catch (Exception e)
{
Console.WriteLine(e.Message.ToString());
}
}
}
(The LdapPath function just returns an LDAP path for the machine name specified.)
How / where do I specify some different credentials to allow this to run? At the moment I get access denied, as the account I am using will not have permission to do this.
Thanks,
Ben
You can use the overload of the DirectoryEntry class that provides authentication. This will cause your LDAP query to be run from the DirectoryServices with this particular user's permission. A word of caution, in order to do this you'll need to pass credentials (which would need to be stored or entered by the user), so be careful in how you handle them. Storing them in plain text may cause system security problems.
New DirectoryEntry(ldapRoot, _activeDirectoryUsername, _activeDirectoryPassword);
You need to use impersonation. The easiest way to do this is to actually "borrow" the permission of whoever called this method. e.g., if this is invoked from a named pipe or WCF call, there are built-in ways to impersonate the caller and do this on their behalf.

How do I detect if my program runs in an Active Directory environment?

How do I detect if my program runs in an Active Directory environment?
I'm using C# and .Net 2.0
Try getting Environment.UserDomainName and comparing it to Environment.MachineName. If the two are the same then it's likely that the user does not have a domain. If they are not the same then the user is logged into a domain which must have a directory server.
This code will check if the Computer itself is a member of a domain
using System.DirectoryServices.ActiveDirectory;
bool isDomain = false;
try
{
Domain.GetComputerDomain();
isDomain = true;
}
catch (ActiveDirectoryObjectNotFoundException)
{
}
However the computer can be in a domain, but the currently logged in user may be a local user account. If you want to check for this use the Domain.GetCurrentDomain() function
One way might be to query the LOGONSERVER environmental variable. That'll give the server name of your AD controller... Which, as far as I know, will be blank (or match current workstation? Not sure) if it isn't currently logged into a domain.
Example Usage:
string ADServer = Environment.GetEnvironmentVariable("LOGONSERVER");
I found something that works:
using System.Net.NetworkInformation;
IPGlobalProperties.GetIPGlobalProperties().DomainName;
Works with a local user and a domain user.
From http://msdn.microsoft.com/en-us/library/system.directoryservices.directoryentry.path.aspx
To bind to the current domain using LDAP, use the path "LDAP://RootDSE", then get the default naming context and rebind the entry.
So without a domain the binding to "LDAP://RootDSE" should either fail or return nothing. I didn't try it for myself.
use System.DirectoryServices; // add reference to system.directoryservices.dll
...
DirectoryEntry ent = new DirectoryEntry("LDAP://RootDSE");
String str = ent.Properties["defaultNamingContext"][0];
DirectoryEntry domain = new DirectoryEntry("LDAP://" + str);
This is definitely a cleaner way of checking for an Active Directory than relying on an environment variable (which the user could delete or add to spoof the program).

Categories