Purpose
The purpose of this tutorial is to show how to use a C# .Net 4.6 in order to get extended Active Directory attributes (or custom attributes someone has added to your AD schema) and push the results to a DataGridView. As a bonus, for the few which may work on cross-domain AD queries (such as an AD domain external to your own) I'll show how you can do such using the credentials of an account on that external domain.
To begin, let's look at what a request may look like:
using System;
using System.Data;
using System.Windows.Forms;
using System.DirectoryServices.AccountManagement; /* You may need to add reference to this from the Assemblies section */
public partial class Form1 : Form
{
private void getMembersBtn_Click(object sender, EventArgs e)
{
var rResultsLimit = 20;
var gDomain = "ad-domain";
var gGroup = "ad-group-to-get-members-of";
/* Get the members of an AD group and add to a variable as a string */
var result = extendedADAttribHandling.customADInterface.retrieveADGroupMembershipAndUserDetail(rResultsLimit, gDomain, gGroup);
/* Push the results (stored in dataSetContainer) to a DataViewGrid "grdGroupMembers" */
grdGroupMembers.DataSource = extendedADAttribHandling.customADInterface.dataSetContainer.Tables[0];
}
}
Next, Define the Extended Attribute(s) to Get (in this case "pancakes") and Functionality to Return the Data:
namespace extendedADAttribHandling
{
[DirectoryRdnPrefix("CN")]
[DirectoryObjectClass("person")]
public class PersonExtendedAttribs : UserPrincipal
{
}
[DirectoryProperty("pancakes")]
public string pancakes
{
get
{
if (ExtensionGet("pancakes").Length != 1) return null;
return (string)ExtensionGet("pancakes")[0];
}
set
{
ExtensionSet("pancakes", value);
}
}
public static new PersonExtendedAttribs FindByIdentity(PrincipalContext context, string identityValue)
{
return (PersonExtendedAttribs)FindByIdentityWithType(context, typeof(PersonExtendedAttribs), identityValue);
}
/* Functionality to get and return the data */
public class customADInterface
{
public static System.Data.DataSet dataSetContainer = new DataSet();
public static string retrieveADGroupMembershipAndUserDetail(int rLimit, string tDomain, string tGroup)
{
var rResultsLimit = rLimit;
var gDomain = tDomain;
var gGroup = tGroup;
var resultsLimitCount = 0;
var results = "";
PrincipalContext principalContext = new PrincipalContext(ContextType.Domain, gDomain);
GroupPrincipal group = GroupPrincipal.FindByIdentity(principalContext, gGroup);
/* For DataViewGrid, create DataTable and Column Names */
System.Data.DataTable table = new DataTable();
DataColumn column;
DataRow row;
column = new DataColumn();
column.DataType = System.Type.GetType("System.String");
column.ColumnName = "PanCakes";
column.ReadOnly = true;
column.Unique = false;
table.Columns.Add(column);
column = new DataColumn();
column.DataType = System.Type.GetType("System.String");
column.ColumnName = "FirstName";
column.ReadOnly = true;
column.Unique = false;
table.Columns.Add(column);
column = new DataColumn();
column.DataType = System.Type.GetType("System.String");
column.ColumnName = "LastName";
column.ReadOnly = true;
column.Unique = false;
table.Columns.Add(column);
dataSetContainer.Tables.Add(table);
/* Get the information from Active Directory */
foreach (Principal principal in group.Members)
{
if (resultsLimitCount < rResultsLimit)
{
resultsLimitCount += 1;
/* Get first and last name. Nothing special here. */
UserPrincipal usr = UserPrincipal.FindByIdentity(principalContext, IdentityType.Sid, principal.Sid.ToString());
results = results + "FirstName=" + usr.GivenName;
results = results + "LastName=" + usr.SurName;
/* Get extended AD attribute "pancakes" */
extendedADAttribHandling.PersonExtendedAttribs customADAttribute = new extendedADAttributeHandling.PersonExtendedAttribs(principalContext);
customADAttribute = extendedADAttribHandling.PersonExtendedAttribs.FindByIdentity(principalContext, principal.SamAccountName);
results = results + "Pancakes=" + customADAttribute.pancakes;
results = results + Environment.NewLine;
/* Add a row to the DataTable */
row = table.NewRow();
row["PanCakes"] = (object)customADAttribute.pancakes ?? DBNull.Value;
row["FirstName"] = (object)usr.GivenName ?? DBNull.Value;
row["LastName"] = (object)usr.SurName ?? DBNull.Value;
table.Rows.Add(row);
}
}
return results;
}
}
}
What About Searching by an Extended Attribute?
Searching by an extended attribute involves a bit more code to pull off (hopefully this will be addressed with future .Net versions where you don't need to do anything special for referencing extended attributes in an AD schema, but .Net has not yet evolved to reach such simplicity to reduce code volume a user must create and, thus, manage points of failure).
Changes for Searching by Extended Attribute(s) Using Filters:
namespace extendedADAttribHandling
{
[DirectoryRdnPrefix("CN")]
[DirectoryObjectClass("person")]
public class PersonExtendedAttribs : UserPrincipal
{
}
[DirectoryProperty("pancakes")]
public string pancakes
{
get
{
if (ExtensionGet("pancakes").Length != 1) return null;
return (string)ExtensionGet("pancakes")[0];
}
set
{
ExtensionSet("pancakes", value);
}
}
public static new PersonExtendedAttribs FindByIdentity(PrincipalContext context, string identityValue)
{
return (PersonExtendedAttribs)FindByIdentityWithType(context, typeof(PersonExtendedAttribs), identityValue);
}
PersonExtendedAttribsFilter searchFilter;
new public PersonExtendedAttribsFilter AdvancedSearchFilter
{
get
{
if (null == searchFilter) searchFilter = new PersonExtendedAttribsFilter(this);
return searchFilter;
}
}
public class PersonExtendedAttribsFilter : AdvancedFilters
{
public PersonExtendedAttribsFilter(Principal p) : base(p) { }
public void pancakes(string value, MatchType mt)
{
this.AdvancedFilterSet("pancakes", value, typeof(string), mt);
}
}
/* Functionality to get and return the data */
public class customADInterface
{
public static System.Data.DataSet dataSetContainer = new DataSet();
public static string retrieveADGroupMembershipAndUserDetail(int rLimit, string tDomain, string tGroup)
{
var rResultsLimit = rLimit;
var gDomain = tDomain;
var gGroup = tGroup;
var resultsLimitCount = 0;
var results = "";
PrincipalContext principalContext = new PrincipalContext(ContextType.Domain, gDomain);
//GroupPrincipal group = GroupPrincipal.FindByIdentity(principalContext, gGroup);
extendedADAttribHandling.PersonExtendedAttribs filter = new extendedADAttribHandling.PersonExtendedAttribs(principalContext);
filter.AdvancedSearchFilter.pancakes("some-value", MatchType.Equals);
PrincipalSearcher ps = new PrincipalSearcher(filter);
/* For DataViewGrid, create DataTable and Column Names */
System.Data.DataTable table = new DataTable();
DataColumn column;
DataRow row;
column = new DataColumn();
column.DataType = System.Type.GetType("System.String");
column.ColumnName = "PanCakes";
column.ReadOnly = true;
column.Unique = false;
table.Columns.Add(column);
column = new DataColumn();
column.DataType = System.Type.GetType("System.String");
column.ColumnName = "FirstName";
column.ReadOnly = true;
column.Unique = false;
table.Columns.Add(column);
column = new DataColumn();
column.DataType = System.Type.GetType("System.String");
column.ColumnName = "LastName";
column.ReadOnly = true;
column.Unique = false;
table.Columns.Add(column);
dataSetContainer.Tables.Add(table);
/* Get the information from Active Directory */
//foreach (Principal principal in group.Members)
foreach (extendedADAttribHandling.PersonExtendedAttribs p in ps.FindAll())
{
if (resultsLimitCount < rResultsLimit)
{
resultsLimitCount += 1;
/* Get first and last name. Nothing special here. */
//UserPrincipal usr = UserPrincipal.FindByIdentity(principalContext, IdentityType.Sid, principal.Sid.ToString());
UserPrincipal usr = UserPrincipal.FindByIdentity(principalContext, IdentityType.Sid, ps.Sid.ToString());
results = results + "FirstName=" + usr.GivenName;
results = results + "LastName=" + usr.SurName;
/* Get extended AD attribute "pancakes" */
//extendedADAttribHandling.PersonExtendedAttribs customADAttribute = new extendedADAttributeHandling.PersonExtendedAttribs(principalContext);
//customADAttribute = extendedADAttribHandling.PersonExtendedAttribs.FindByIdentity(principalContext, principal.SamAccountName);
results = results + "Pancakes=" + p.pancakes;
results = results + Environment.NewLine;
/* Add a row to the DataTable */
row = table.NewRow();
row["PanCakes"] = (object)p.pancakes ?? DBNull.Value;
row["FirstName"] = (object)usr.GivenName ?? DBNull.Value;
row["LastName"] = (object)usr.SurName ?? DBNull.Value;
table.Rows.Add(row);
}
}
return results;
}
}
}
Cross-Domain AD Queries
Performing a AD query against a domain separate from your own can be challenging. For one thing there is the code itself (shown below) using credentials from the external domain and other considerations such as network / firewall exemptions such as allowing port 636 (LDAPS) or 3269 (LDAPS) for the global catalog (the "old school" ports for regular LDAP were 389, 3268 respectively). Those aside, let's see the code:
var sDomain = "someother.domain.com"; /* Domain to contact using LDAP */
var username = "ADUserOnSomeOtherDomain"; /* The account on the domain to authenticate with */
var password = "somepwd"; /* The password of the account on the domain to authenticate with */
var workResult = "";
/* Specific account on the external domain to get details on */
var sSamAccountName = "username"; /* Account name */
try {
PrincipalContext dContext = new PrincipalContext(ContextType.Domain, sDomain, username, password);
UserPrincipal user = UserPrincipal.FindByIdentity(dContext, sSamAccountName);
/* User found on the external domain */
if (user != null) {
/* Get AccountName (but keep in mind you can get other information) */
AccountName = user.SamAccountName;
/* Get DistinguishedName */
DistinguishedName = user.DistinguishedName;
workResult = "Account Found.";
}
else {
/* User not found on the external domain */
workResult = "Failure: The account specified could not be found in the external domain.";
}
}
catch (Exception ex) {
if (ex.InnerException.Message.Length > 0) {
/* Get the failure message */
workResult = ex.InnerException.Message;
}
}
Cross-Domain AD Password Setting
Setting the password of an account in an external domain is also possible using credentials (such as from the example above):
var sDomain = "someother.domain.com"; /* Domain to contact using LDAP */
var username = "ADUserOnSomeOtherDomain"; /* The account on the domain to authenticate with */
var password = "somepwd"; /* The password of the account on the domain to authenticate with */
var workResult = "";
/* Specific account on the external domain to set the password of */
var sSamAccountName = "username"; /* Account name */
var tmpPwd = "simplepwd"; /* The password value to set */
try {
PrincipalContext dContext = new PrincipalContext(ContextType.Domain, sDomain, username, password);
UserPrincipal user = UserPrincipal.FindByIdentity(dContext, sSamAccountName);
/* User found in the external domain */
if (user != null) {
if (user.Enabled == true) {
user.SetPassword(tmpPwd);
user.Save();
workResult = "Password set.";
}
else {
/* The account is not enabled */
workResult = "Failure: The user account is not enabled.";
}
}
else {
/* The account was not found */
workResult = "Failure: The account specified could not be found in the external domain.";
}
}
catch (Exception ex) {
if (ex.InnerException.Message.Length > 0) {
/* Get the failure message */
/* One possibility is: Exception has been thrown by the target of an invocation -> Access is denied. (Exception from HRESULT: 0x8007000S (E_ACCESSDENIED)). In this case, can the credentials used with the query set or update the password of accounts on the external domain? */
workResult = ex.InnerException.Message;
}
}