Determine current domain controller programmatically - c#

I need to query current domain controller, probably primary to change user password.
(P)DC name should be fully qualified, i.e. DC=pdc,DC=example,DC=com (how to properly name such notation?)
How can it be done using C#?

To retrieve the information when the DomainController exists in a Domain in which your machine doesn't belong, you need something more.
DirectoryContext domainContext = new DirectoryContext(DirectoryContextType.Domain, "targetDomainName", "validUserInDomain", "validUserPassword");
var domain = System.DirectoryServices.ActiveDirectory.Domain.GetDomain(domainContext);
var controller = domain.FindDomainController();

We are using something like this for our internal applications.
Should return something like DC=d,DC=r,DC=ABC,DC=com
public static string RetrieveRootDseDefaultNamingContext()
{
String RootDsePath = "LDAP://RootDSE";
const string DefaultNamingContextPropertyName = "defaultNamingContext";
DirectoryEntry rootDse = new DirectoryEntry(RootDsePath)
{
AuthenticationType = AuthenticationTypes.Secure;
};
object propertyValue = rootDse.Properties[DefaultNamingContextPropertyName].Value;
return propertyValue != null ? propertyValue.ToString() : null;
}

(requires System.DirectoryServices.AccountManagement.dll):
using (var context = new System.DirectoryServices.AccountManagement.PrincipalContext(ContextType.Domain))
{
string server = context.ConnectedServer; // "pdc.examle.com"
string[] splitted = server.Split('.'); // { "pdc", "example", "com" }
IEnumerable<string> formatted = splitted.Select(s => String.Format("DC={0}", s));// { "DC=pdc", "DC=example", "DC=com" }
string joined = String.Join(",", formatted); // "DC=pdc,DC=example,DC=com"
// or just in one string
string pdc = String.Join(",", context.ConnectedServer.Split('.').Select(s => String.Format("DC={0}", s)));
}

If you are looking to interact the Active Directory, you shouldn't have to know where the FSMO roles are for the most part. If you want to change the AD topology from your program (I wouldn't), look at the DomainController class.
If you want to change a user password, you can invoke those actions on the User object, and Active Directory will make sure that the changes are properly replicated.
copied from http://www.rootsilver.com/2007/08/how-to-change-a-user-password
public static void ChangePassword(string userName, string oldPassword, string newPassword)
{
string path = "LDAP://CN=" + userName + ",CN=Users,DC=demo,DC=domain,DC=com";
//Instantiate a new DirectoryEntry using an administrator uid/pwd
//In real life, you'd store the admin uid/pwd elsewhere
DirectoryEntry directoryEntry = new DirectoryEntry(path, "administrator", "password");
try
{
directoryEntry.Invoke("ChangePassword", new object[]{oldPassword, newPassword});
}
catch (Exception ex) //TODO: catch a specific exception ! :)
{
Console.WriteLine(ex.Message);
}
Console.WriteLine("success");
}

Related

C# Active Directory Search

I have this powershell function and i want to make it as a C# function.
How can i put it into C#?
Get-ADComputer -filter {Name -Like 'myComp'} -property * | select DistinguishedName
You should be able to do this quite easily. Add a reference to System.DirectoryServices.AccountManagement and then use this code:
using (PrincipalContext ctx = new PrincipalContext(ContextType.Domain, 'YourDomain'))
{
ComputerPrincipal computer = ComputerPrincipal.FindByIdentity (ctx, "name");
if (computer != null)
{
// do whatever you need to do with your computer principal
string distinguishedName = computer.DistinguishedName;
}
}
Update: if you don't know your domain ........ - you can also use:
using (PrincipalContext ctx = new PrincipalContext(ContextType.Domain))
in which case the principal context is created for the current domain you're located in.
You can use C# in the following manner
Connect to the Domain controller and get the DomainContext
Use that to look up the computer objects based on a name.
using (PrincipalContext ctx = new PrincipalContext(ContextType.Domain, Environment.UserDomainName)
{
using (PrincipalSearcher srch = new PrincipalSearcher(new ComputerPrincipal(ctx) { Name = "ServerName"}))
{
return srch.FindAll().Cast<ComputerPrincipal>().ToList().Select(x => x.DistinguishedName);
}
}
Above returns a list of DistinguishedNames that matches the Server Name.
All the other answers suggest using the System.DirectoryServices.AccountManagement namespace. While that will work, it is really just a wrapper around the System.DirectoryServices namespace to make things a bit easier to use. It does make things easier (sometimes) but it does so at the cost of performance.
For example, in all the examples you've been given, your code will retrieve every attribute with a value from the computer object in AD, even though you only want one attribute.
If you use DirectorySearcher, you can make the search and retrieve only that one attribute that you want:
public string GetComputerDn(string computerName) {
var searcher = new DirectorySearcher {
Filter = $"(&(objectClass=computer)(sAMAccountName={computerName}$))",
PropertiesToLoad = { "distinguishedName" } //return only the distinguishedName attribute
};
var result = searcher.FindOne();
if (result == null) return null;
return (string) result.Properties["distinguishedName"][0];
}
Note that in AD, the sAMAccountName of computer objects is what you would normally refer to as the "computer name", followed by $, which is why the filter is what it is.
Please try this:
Add reference to Active Directory Services (%programfiles%\Reference Assemblies\Microsoft\Framework.NETFramework\\System.DirectoryServices.AccountManagement.dll)
public string GetComputerName(string computerName)
{
using (var context = new PrincipalContext(ContextType.Domain, "your domain name goes here"))
{
using (var group = GroupPrincipal.FindByIdentity(context, "Active Directory Group Name goes here"))
{
var computers = #group.GetMembers(true);
return computers.FirstOrDefault(c => c.Name == computerName).DistinguishedName;
}
}
return null; // or return "Not Found"
}

How can I get a custom field from Active Directory in C#?

I've read plenty of similar StackOverflow questions but none seem to address the issue I'm seeing. If I query for a user using userprincipalname I get back a search result with exactly 34 properties. None of the custom properties are returned. If I query again using a custom property like employeeNumber I get back a result with 71 properties. All custom properties are included.
My issue is that I don't have the employeeNumber at run time, just the userprincipalname. I need to get back all of the custom properties all of the time. Hopefully that makes sense. Here's my practice code:
string sid = "";
using (PrincipalContext context = new PrincipalContext(ContextType.Domain))
{
UserPrincipal user = UserPrincipal.Current;
//sid = user.SamAccountName;
sid = user.UserPrincipalName;
//sid = user.Sid.ToString();
DirectoryEntry entry = user.GetUnderlyingObject() as DirectoryEntry;
if (entry.Properties.Contains("employeeNumber"))
{
//this doesn't work
}
}
DirectoryEntry ldapConnection = new DirectoryEntry("companyname.com");
ldapConnection.Path = "LDAP://DC=companyname,DC=com";
ldapConnection.AuthenticationType = AuthenticationTypes.Secure;
DirectorySearcher search = new DirectorySearcher(ldapConnection);
search.Filter = string.Format("(&(ObjectClass=user)(userprincipalname={0}))", sid); // <-- this doesn't get custom properties
//search.Filter = string.Format("(employeeNumber={0})", "11663"); <-- this works
var result = search.FindOne(); // FindOne();
if (result.Properties.Contains("employeeNumber"))
{
//this never happens either :(
}
The above never returns the employeeNumber field, but if I uncomment the second search.Filter line and manually search by employeeNumber I find a result and it contains all of the fields that I need.
EDIT: I found a very good MSDN article Here which describes how to extend the UserPrincipal object to get custom attributes. The only issue is that it's giving me an empty string every time I access it even though I have verified that the property IS set in AD! Any help is appreciated.
EDIT 2: Here's the code for the custom principal extension:
[DirectoryRdnPrefix("CN")]
[DirectoryObjectClass("Person")]
public class UserPrincipalExtension : UserPrincipal
{
public UserPrincipalExtension(PrincipalContext context)
: base(context)
{
}
public UserPrincipalExtension(PrincipalContext context, string samAccountName, string password, bool enabled)
: base(context, samAccountName, password, enabled)
{
}
public static new UserPrincipalExtension FindByIdentity(PrincipalContext context, IdentityType type, string identityValue)
{
return (UserPrincipalExtension)FindByIdentityWithType(context, typeof(UserPrincipalExtension), type, identityValue);
}
PersonSearchFilter searchFilter;
new public PersonSearchFilter AdvancedSearchFilter
{
get
{
if (searchFilter == null)
searchFilter = new PersonSearchFilter(this);
return searchFilter;
}
}
[DirectoryProperty("employeeNumber")]
public string EmployeeNumber
{
get
{
if (ExtensionGet("employeeNumber").Length != 1)
return string.Empty;
return (string)ExtensionGet("employeeNumber")[0];
}
set
{
ExtensionSet("employeeNumber", value);
}
}
}
And the custom search filter:
public class PersonSearchFilter : AdvancedFilters
{
public PersonSearchFilter(Principal p)
: base(p)
{
}
public void SAMAccountName(string value, MatchType type)
{
this.AdvancedFilterSet("sAMAccountName", value, typeof(string), type);
}
}
Usage:
UserPrincipalExtension filter = new UserPrincipalExtension(context);
filter.AdvancedSearchFilter.SAMAccountName(UserPrincipal.Current.SamAccountName, MatchType.Equals);
PrincipalSearcher search = new PrincipalSearcher(filter);
foreach (var result in search.FindAll())
{
var q = (UserPrincipalExtension)result;
var m = q.EmployeeNumber;
}
var m is always an empty string even though ALL AD entries have an employeeNumber.
EDIT: From active directory:
I am more confidant in how to fix your second method so I will answer that first. You need to specify the property in DirectorySearcher.PropertiesToLoad when you do your search for the property to show up.
DirectorySearcher search = new DirectorySearcher(ldapConnection);
search.Filter = string.Format("(&(ObjectClass=user)(userprincipalname={0}))", sid);
search.PropertiesToLoad.Add("employeeNumber");
var result = search.FindOne(); // FindOne();
if (result.Properties.Contains("employeeNumber"))
{
//This should now work.
}
The reason it worked for string.Format("(employeeNumber={0})", "11663"); is because any search clause you add automatically gets put in to the PropertiesToLoad collection.
For your first method, I think you need to call DirectoryEntry.RefreshCache and pass in the property to make it show up.
string sid = "";
using (PrincipalContext context = new PrincipalContext(ContextType.Domain))
{
UserPrincipal user = UserPrincipal.Current;
//sid = user.SamAccountName;
sid = user.UserPrincipalName;
//sid = user.Sid.ToString();
DirectoryEntry entry = user.GetUnderlyingObject() as DirectoryEntry;
entry.RefreshCache(new[] {"employeeNumber"});
if (entry.Properties.Contains("employeeNumber"))
{
}
}
But I am not 100% sure if that will work or not.
For your custom user principal, I am not sure what is wrong. The only difference I see between what you are doing and what I have in a project that does it that I know works is you use [DirectoryObjectClass("Person")] but I have [DirectoryObjectClass("user")]. Perhaps that is the issue
[DirectoryRdnPrefix("CN")]
[DirectoryObjectClass("user")] //Maybe this will fix it???
public class UserPrincipalExtension : UserPrincipal
{

C# Domain change broke custom GetUserEmail method

The 'Users' on the network were moved to another domain recently. This caused a local method that gets the user's email address to break for a few of our web applications. This problem would likely fix itself after the web server is moved to the same domain, but would request help in fixing the code to allow this method to work in this split domain network (ex. Domain1.ad.com, Domain2.ad.com).
// Method below gets users email address.
public static string GetUserEmailAddress()
{
// Grab users network name. It will be in the form domain\username.
System.Security.Principal.WindowsPrincipal p = System.Threading.Thread.CurrentPrincipal as System.Security.Principal.WindowsPrincipal;
string NetworkID = p.Identity.Name;
// Remove the domain.
string[] networkIDparts = NetworkID.Split('\\');
string UserID = networkIDparts[1];
DirectorySearcher objsearch = new DirectorySearcher();
objsearch.Filter = "(& (cn="+ UserID.Trim() + ")(objectClass=user))";
objsearch.SearchScope = System.DirectoryServices.SearchScope.Subtree;
objsearch.PropertiesToLoad.Add("mail");
objsearch.PropertyNamesOnly = true ;
objsearch.Sort.Direction = System.DirectoryServices.SortDirection.Ascending;
SearchResultCollection colresults = objsearch.FindAll() ;
ArrayList arrGal = new ArrayList();
foreach(SearchResult objresult in colresults)
{
arrGal.Add(objresult.GetDirectoryEntry().Properties["mail"].Value);
}
objsearch.Dispose();
String[] myArray = (String[]) arrGal.ToArray(typeof(string));
if (myArray.Length == 0)
{ return "NoAddressAssigned#domain2.ad.com";}
else
{ return myArray[0] ;}
}
I'm also open to any ideas that would improve the functionality of this method.

How to get AD groups a user is in specifying username and domain

I have a function that returns a list of AD groups a user is in.
public static List<string> GetGroupNames(string userName)
{
using (var context = new PrincipalContext(ContextType.Domain, Environment.UserDomainName))
{
using (var userPrincipal = UserPrincipal.FindByIdentity(context, userName))
{
var groupSearch = userPrincipal.GetGroups(context);
var result = new List<string>();
groupSearch.ToList().ForEach(sr => result.Add(sr.SamAccountName));
return result;
}
}
}
This is working as I would expect. I would like to update this function so I can pass it an LDAP path to specify the domain I want to query.
I have searched for hours and can find any pointers (even though I am sure the answer is out there somewhere!) I would really appreciate any help here.
You can just add a new parameter, let's say string domainName and pass it to new PrincipalContext() instead of Environment.UserDomainName.

How to overcome the "The member's SID could not be resolved" error when checking if user is a member of group?

We have a process that requires to check whether a particular user is a member of local Administrators group.
The code that checks that looks like the following:
using (PrincipalContext context = new PrincipalContext(ContextType.Machine, null))
{
UserPrincipal user = UserPrincipal.FindByIdentity(context, IdentityType.SamAccountName, sUserName);
if (user != null)
{
SecurityIdentifier adminsGroupSID = new SecurityIdentifier(WellKnownSidType.BuiltinAdministratorsSid, null);
GroupPrincipal group = GroupPrincipal.FindByIdentity(context, IdentityType.Sid, adminsGroupSID.Value);
if (group != null)
{
if (user.IsMemberOf(group))
return 0;
}
}
}
When the group has accounts (e.g. domain accounts) that were removed, we're getting a PrincipalOperationException and a message "An error (1332) occurred while enumerating the group membership. The member's SID could not be resolved."
Is there any way to overcome this without:
a) Removing manually the orphaned SIDs from the group
b) Not ignoring it?
Thanks
This is based heavily on my finding at http://www.seirer.net/blog/2013/9/12/how-to-deal-with-localized-or-renamed-administrators-in-net written by Michael Seirer. He was attempting to get the SID of the local Admin account, while all we need are the names in that group. The reason for the error "The member's SID could not be resolved." is because there are accounts that are no longer recognized in Active Directory - likely relics that point to deleted user accounts. You can either do what Microsoft says and just delete them and hope your app never crashes again (though it will, the next time an account is deleted that is sitting in that Administrators group), or solve it permanently with this code I slightly modified from Mike.
using System.DirectoryServices;
using System.Collections;
using System.Runtime.InteropServices;
[DllImport("advapi32", CharSet = CharSet.Auto, SetLastError = true)]
static extern bool ConvertSidToStringSid(IntPtr pSid, out string strSid);
private static string GetTextualSID(DirectoryEntry objGroup)
{
string sSID = string.Empty;
byte[] SID = objGroup.Properties["objectSID"].Value as byte[];
IntPtr sidPtr = Marshal.AllocHGlobal(SID.Length);
sSID = "";
System.Runtime.InteropServices.Marshal.Copy(SID, 0, sidPtr, SID.Length);
ConvertSidToStringSid((IntPtr)sidPtr, out sSID);
System.Runtime.InteropServices.Marshal.FreeHGlobal(sidPtr);
return sSID;
}
public static List<string> GetLocalAdministratorsNames()
{
List<string> admins = new List<string>();
DirectoryEntry localMachine = new DirectoryEntry("WinNT://" + Environment.MachineName);
string adminsSID = new SecurityIdentifier(WellKnownSidType.BuiltinAdministratorsSid, null).ToString();
string localizedAdmin = new System.Security.Principal.SecurityIdentifier(adminsSID).Translate(typeof(System.Security.Principal.NTAccount)).ToString();
localizedAdmin = localizedAdmin.Replace(#"BUILTIN\", "");
DirectoryEntry admGroup = localMachine.Children.Find(localizedAdmin, "group");
object adminmembers = admGroup.Invoke("members", null);
DirectoryEntry userGroup = localMachine.Children.Find("users", "group");
object usermembers = userGroup.Invoke("members", null);
//Retrieve each user name.
foreach (object groupMember in (IEnumerable)adminmembers)
{
DirectoryEntry member = new DirectoryEntry(groupMember);
string sidAsText = GetTextualSID(member);
admins.Add(member.Name);
}
return admins;
}
It will return a List<string> of members of the local Administrators group on the local machine. You can even alter Environment.MachineName to be any computer name in your domain, if you don't want the local machine.
Then you can iterate the list to see if they're in it:
private static bool isAdmin(string user)
{
//string user = #"DOMAIN\doej";
user = user.Split(#'\')[1];
List<string> admins = GetLocalAdministratorsNames();
foreach (string s in admins)
{
if (s == user)
return true; // admin found
}
return false; // not an admin
}
Almost a decade later and even in .NET 4.x and 5.x, this is still a problem. One other way of working around this error is to basically deconstruct the code behind the foreach statement and make your own list to search. You need to get the Enumerator, then call MoveNext in a Try/Catch. I was originally afraid it would not move to the next one if it threw the exception, but it does, so this works. It's not pretty, but I have tested it and it works for me.
PrincipalContext principalContext;
GroupPrincipal groupPrincipal;
UserPrincipal userPrincipal;
bool hasItem;
// `members` is the new list that can be searched without errors.
List<Principal> members = new List<Principal>();
using (principalContext = new PrincipalContext(ContextType.Machine))
{
SecurityIdentifier adminsGroupSID = new SecurityIdentifier(WellKnownSidType.BuiltinAdministratorsSid, null);
using (groupPrincipal = GroupPrincipal.FindByIdentity(principalContext, IdentityType.Sid, adminsGroupSID.Value))
{
/*
* We will get our Enumerator. At this point, we are positioned before
* the current item, so we have to call MoveNext to get a valid Current
*/
var e = groupPrincipal.Members.GetEnumerator();
hasItem = false;
do
{
try
{
/*
* Try and move next. If it failes, it will be caught by the catch
* and ignored. At which point, we can try and call MoveNext again
* and get to the next one.
*/
hasItem = e.MoveNext();
if (hasItem)
{
members.Add(e.Current);
}
}
catch (PrincipalOperationException)
{
// We don't care about doing anything here--we just want to ignore the error
}
} while (hasItem);
}
}
One way to avoid the error is to go other way around.
Instead of checking if an user is member of a group, retrieve first all the groups and check the list for your target group. One drawback: is slower....
var groups = UserPrincipal.Current.GetAuthorizationGroups();
var found = groups.FirstOrDefault(principal => principal.Name == "Administrators");
var isMemberOfAdminGroup = found != null;
Thanks arus for your help :)
public static bool UserHasLocalAdminPrivledges(this UserPrincipal up)
{
SecurityIdentifier id = new SecurityIdentifier(WellKnownSidType.BuiltinAdministratorsSid, null);
return up.GetAuthorizationGroups().Any(g => g.Sid == id)
}
There are three possibile solutions (all untested, using the last myself for all sort of domain groups):
1) Load the group and enumerate the members yourself
2) Load the underlying object of the group and use properties["Members"], which is a list of SIDs.
3) Use GetAuthorizationGroups() of the user (which will also use your non-direct groups, Service-Account has to be member of "Windows Authorization Group" and "PreWindows 2000 Comaptible...." eventually) and use the group list to lookup your Admin group.

Categories