In my previous article Querying Active Directory on SQL Server using T-SQL I was showing the possibilities of querying the Active Directory using T-SQL, specifically using linked server and OPENQUERY statement or without linked server using the OPENROWSET statement.
This is an updated article and includes modification coming upon some of the comments to resolve some issues with large number of returned AD properties.
There ware mentioned some limitations of querying the AD using T-SQL and CLR will help us to bypass those limitations. Especially the limit of 1000 records returned as we can use pagination in the CLR code effectively.
For the purpose I’m creating a CLR Stored Procedure which will take several argument and return a result set. The reason I’m going to use a CLR Stored Procedure is, that stored procedure will allow me to return dynamic result set. It means I can specify properties of AD to return and those will be returned as columns of the result set. In case we go through the CLR Table Valued function, we had to create a separate function for each properties combination we would like to return.
So here is the CLR class for querying AD.
public class ActiveDirectory
{
/// <summary>
/// Queries Active directory according provided parameters
/// Current user credentials are used for authentication
/// </summary>
/// <param name="adRoot">AD Root for querying AD</param>
/// <param name="filter">Filter to be used for querying</param>
/// <param name="searchScope">Scope to be used for queryingg</param>
/// <param name="propertiesToLoad">List of properties to return</param>
/// <param name="pageSize">Represents a PageSise for the paged search of AD</param>
/// <param name="rowsLimit">Rrepresent limit for numbers of rows returned. NULL or value less than 1 represents unlimited</param>
[Microsoft.SqlServer.Server.SqlProcedure]
public static void QueryAD(string adRoot, string filter, string propertiesToLoad, string searchScope, int pageSize, SqlInt32 rowsLimit)
{
SearchAD(null, null, null, adRoot, filter, searchScope, propertiesToLoad, pageSize, rowsLimit);
}
/// <summary>
/// Queries Active directory according provided parameters
/// </summary>
/// <param name="userName">UserName to be used to authenticate AD</param>
/// <param name="password">Password to be used to authenticate to AD</param>
/// <param name="adRoot">AD Root for querying AD</param>
/// <param name="filter">Filter to be used for querying</param>
/// <param name="searchScope">Scope to be used for queryingg</param>
/// <param name="propertiesToLoad">List of properties to return</param>
/// <param name="pageSize">Represents a PageSise for the paged search of AD</param>
/// <param name="rowsLimit">Rrepresent limit for numbers of rows returned. NULL or value less than 1 represents unlimited</param>
[Microsoft.SqlServer.Server.SqlProcedure]
public static void QueryADUName(string userName, string password, string adRoot, string filter, string propertiesToLoad, string searchScope, int pageSize, SqlInt32 rowsLimit)
{
SearchAD(userName, password, null, adRoot, filter, searchScope, propertiesToLoad, pageSize, rowsLimit);
}
/// <summary>
/// Queries Active directory according provided parameters
/// </summary>
/// <param name="userName">UserName to be used to authenticate AD</param>
/// <param name="password">Password to be used to authenticate to AD</param>
/// <param name="authType">Authentication type to be used to authenticate to AD</param>
/// <param name="adRoot">AD Root for querying AD</param>
/// <param name="filter">Filter to be used for querying</param>
/// <param name="searchScope">Scope to be used for queryingg</param>
/// <param name="propertiesToLoad">List of properties to return</param>
/// <param name="pageSize">Represents a PageSise for the paged search of AD</param>
/// <param name="rowsLimit">Rrepresent limit for numbers of rows returned. NULL or value less than 1 represents unlimited</param>
[Microsoft.SqlServer.Server.SqlProcedure]
public static void QueryADAuth(string userName, string password, string authType, string adRoot, string filter, string propertiesToLoad, string searchScope, int pageSize, SqlInt32 rowsLimit)
{
SearchAD(userName, password, authType, adRoot, filter, searchScope, propertiesToLoad, pageSize, rowsLimit);
}
private static DirectoryEntry GetRootEntry(string adRoot, string userName, string password, string authType)
{
if (userName == null)
return new DirectoryEntry(adRoot);
else if (authType == null)
return new DirectoryEntry(adRoot, userName, password);
else
{
AuthenticationTypes at;
if (TryParseEnum<AuthenticationTypes>(authType, true, out at))
return new DirectoryEntry(adRoot, userName, password, at);
else
throw new System.InvalidCastException(string.Format("authType must be one of '{0}'", GetEnumNames<AuthenticationTypes>()));
}
}
private static string GetEnumNames<T>()
{
string[] names = Enum.GetNames(typeof(T));
StringBuilder sb = new StringBuilder();
for (int i = 0; i < names.Length; i++)
{
if (i > 0)
sb.Append(',');
sb.Append(names[i]);
}
return sb.ToString();
}
private static bool TryParseEnum<T>(string value, bool ignoreCase, out T outEnum)
{
try
{
outEnum = (T)Enum.Parse(typeof(T), value, ignoreCase);
return true;
}
catch
{
outEnum = (T)Enum.GetValues(typeof(T)).GetValue(0);
return false;
}
}
/// <summary>
/// Searches Active Directory according provided parameters
/// </summary>
/// <param name="userName">UserName to be used to authenticate AD</param>
/// <param name="password">Password to be used to authenticate to AD</param>
/// <param name="authType">Authentication type to be used to authenticate to AD</param>
/// <param name="adRoot">AD Root for querying AD</param>
/// <param name="filter">Filter to be used for querying</param>
/// <param name="searchScope">Scope to be used for queryingg</param>
/// <param name="propertiesToLoad">List of properties to return</param>
/// <param name="pageSize">Represents a PageSise for the paged search of AD</param>
/// <param name="rowsLimit">Rrepresent limit for numbers of rows returned. NULL or value less than 1 represents unlimited</param>
private static void SearchAD(string userName, string password, string authType, string adRoot, string filter, string searchScope, string propertiesToLoad, int pageSize, SqlInt32 rowsLimit)
{
string[] properties = propertiesToLoad.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
SqlMetaData[] recordMetaData = new SqlMetaData[properties.Length];
SearchScope scope;
int limit = rowsLimit.IsNull ? 0 : rowsLimit.Value;
int rowsCount = 0;
if (rowsLimit > 0 && pageSize > limit)
pageSize = limit;
if (!TryParseEnum<SearchScope>(searchScope, true, out scope))
throw new System.InvalidCastException(string.Format("searchScope must be one of '{0}'", GetEnumNames<SearchScope>()));
//Trim properties and prepare result set metadata, also process specified lengths
for (int i = 0; i < properties.Length; i++)
{
string[] propDetails = properties[i].Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
string propName = propDetails[0].Trim();
int len;
if (propDetails.Length < 2 || !int.TryParse(propDetails[1], out len) || len < 1 || len > 4000)
len = 4000;
recordMetaData[i] = new SqlMetaData(properties[i], System.Data.SqlDbType.NVarChar, len);
properties[i] = propName;
}
//Get Root Directory Entry
using (DirectoryEntry rootEntry = GetRootEntry(adRoot, userName, password, authType))
{
//Create a directory searcher with aproperiate filter, properties and search scope
using (DirectorySearcher ds = new DirectorySearcher(rootEntry, filter, properties, scope))
{
ds.PageSize = pageSize; //Set Page Size - without this we will not do a paged search and we will be limiited to 1000 results
//find all object from the rood, according the filter and search scope
using (SearchResultCollection results = ds.FindAll())
{
SqlDataRecord record = new SqlDataRecord(recordMetaData);
//Start pushing of records to client
SqlContext.Pipe.SendResultsStart(record);
foreach (SearchResult result in results)
{
record = new SqlDataRecord(recordMetaData);
for (int i = 0; i < properties.Length; i++)
{
ResultPropertyValueCollection props = result.Properties[properties[i]];
if (props.Count == 1) //if property collection contains single vallue, set the record field to that value
record.SetSqlString(i, props[0].ToString());
else if (props.Count == 0) //if property collection doesn't contain any value, set record field to NULL
record.SetSqlString(i, SqlString.Null);
else //In case of multiple value, separate the values by commas
{
StringBuilder sb = new StringBuilder();
bool firstItem = true;
foreach (object prop in props)
{
if (!firstItem)
sb.Append(',');
else
firstItem = false;
sb.Append(prop.ToString());
}
record.SetSqlString(i, sb.ToString());
}
}
//send record to client
SqlContext.Pipe.SendResultsRow(record);
//if rowsLimit was reached, break the loop
if (++rowsCount == rowsLimit)
break;
}
//stop sending records to client
SqlContext.Pipe.SendResultsEnd();
}
}
}
}
}
The class contains three public methods (CLR Stored Procedures) QueryAD
, QueryADUName
and QueryADAuth
. The first one will query AD using current user credentials and default authentication method, second will query AD using provided user credentials and default authentication method and in the third one we can specify also authentication method.
Those methods call a private method SearchAD, which takes care about the AD Searching and return the returns the result to client.
After the comment from Chris, I’ve updated my sample codes to have also the optional parameter pageSize
which allow reduce the size of a Page for Paged Search used for querying the AD and avoid insufficient memory problems when querying higher amount of AD attributes.
I’ve also added a parameter rowsLimit
which allows limit the maximum number of rows returned, which can be useful especially when querying very large AD. There is added a break
into the loop which iterates the results from AD when the number of precessed imtes reach the limit.
UPDATE:
Finally I’ve also updated the private static void SearchAD
method so now you pass the properties not as a comma separated list but a semicolon separated list. You can use a comma to specify the return length of each property. If the length is not specified, then the method will use a default 4000 characters length. This update should finally solve the issues with large number of properties returned when previously all were returned as nvarchar(4000)
The updated part of the code is the for loop
which is processing properties and generating result set metadata, starting with the comment //Trim properties and prepare result set metadata, also process specified lengths
END OF UPDATE:
To be able to compile the code for example using Visual C# Express it is necessary to add reference to the System.DirectoryServices
assembly in the project.
Once we compile the code and create say ADServices.dll
assembly, we can register that assembly and CLR Stored procedures in our database.
Because the ActiveDirectory class is using System.DirectoryServices
assembly, we will have to use UNSAFE
PERMISSION_SET
for our assembly and so the database using that assembly has to be TRUSTWORTHY
.
ALTER DATABASE TestDB3 SET TRUSTWORTHY ON WITH ROLLBACK IMMEDIATE;
As we are using the System.DirectoryServices
assembly, we have to register it in our database prior registering our assembly, otherwise we will not be able to register it.
--Register the NetFramework System.DirectoryServices assembly
CREATE ASSEMBLY [System.DirectoryServices]
AUTHORIZATION [dbo]
FROM 'C:WindowsMicrosoft.NETFrameworkv2.0.50727System.DirectoryServices.dll'
WITH PERMISSION_SET = UNSAFE
GO
--Register our assembly with ActiveDirectory CLR Stored Procedures
CREATE ASSEMBLY [ADServices]
AUTHORIZATION [dbo]
FROM 'C:CLRADServices.dll'
WITH PERMISSION_SET = UNSAFE
GO
Once we have successfully registered those assemblies, we can create the CLR Stored procedures in our DB.
--==========================================================================
-- @adRoot - root of searching eg. 'LDAP://OU=Sales,DC=Fabricam,DC=com'
-- @filter - filter to be used for searching eg. '(&(objectCategory=group)'
-- @propertiesToLoad - list of properties to be retrieved eg. 'cn,50;ADsPath'
-- - properties are separated by a semicolon and
-- - and comma can be used to specify return length of the property
-- @searchScope - scope to be used for searching: {Base,OneLevel,Subtree}
-- @pageSize - specifies the PageSize for paged search - default is 1000
-- it is possible to lower the value if there is a problem
-- retrieving such amount of records at once.
-- @rowsLimit - represents maximum number of rows returned.
-- NULL or value less than 1 represents unlimited
--==========================================================================
CREATE PROCEDURE usp_QueryAD
@adRoot nvarchar(255),
@filter nvarchar(255),
@propertiesToLoad nvarchar(255),
@searchScope nvarchar(8),
@pageSize int = 1000,
@rowsLimit int = 0
AS
EXTERNAL NAME [ADServices].[ActiveDirectory].[QueryAD]
GO
--==========================================================================
-- @userName - username to be used for authentificaiton to AD
-- @password - password to be used for authentification to AD
-- @adRoot - root of searching eg. 'LDAP://OU=Sales,DC=Fabricam,DC=com'
-- @filter - filter to be used for searching eg. '(&(objectCategory=group)'
-- @propertiesToLoad - list of properties to be retrieved eg. 'cn,50;ADsPath'
-- - properties are separated by a semicolon and
-- - and comma can be used to specify return length of the property
-- @searchScope - scope to be used for searching: {Base,OneLevel,Subtree}
-- @pageSize - specifies the PageSize for paged search - default is 1000
-- it is possible to lower the value if there is a problem
-- retrieving such amount of records at once.
-- @rowsLimit - represents maximum number of rows returned.
-- NULL or value less than 1 represents unlimited
--==========================================================================
CREATE PROCEDURE usp_QueryADUname
@userName nvarchar(255),
@password nvarchar(255),
@adRoot nvarchar(255),
@filter nvarchar(255),
@propertiesToLoad nvarchar(255),
@searchScope nvarchar(8),
@pageSize int = 1000,
@rowsLimit int = 0
AS
EXTERNAL NAME [ADServices].[ActiveDirectory].[QueryADUName]
GO
--==========================================================================
-- @userName - username to be used for authentificaiton to AD
-- @password - password to be used for authentification to AD
-- @authType - Authentification Type to be used for AD Authentification
-- {None,Secure,Encryption,SecureSocketsLayer,ReadonlyServer,Anonymous,
-- FastBind,Signing,Sealing,Delegation,ServerBind}
-- @adRoot - root of searching eg. 'LDAP://OU=Sales,DC=Fabricam,DC=com'
-- @filter - filter to be used for searching eg. '(&(objectCategory=group)'
-- @propertiesToLoad - list of properties to be retrieved eg. 'cn,50;ADsPath'
-- - properties are separated by a semicolon and
-- - and comma can be used to specify return length of the property
-- @searchScope - scope to be used for searching: {Base,OneLevel,Subtree}
-- @pageSize - specifies the PageSize for paged search - default is 1000
-- it is possible to lower the value if there is a problem
-- retrieving such amount of records at once.
-- @rowsLimit - represents maximum number of rows returned.
-- NULL or value less than 1 represents unlimited
--==========================================================================
CREATE PROCEDURE usp_QueryADAuth
@userName nvarchar(255),
@password nvarchar(255),
@authType nvarchar(20),
@adRoot nvarchar(255),
@filter nvarchar(255),
@propertiesToLoad nvarchar(255),
@searchScope nvarchar(8),
@pageSize int = 1000,
@rowsLimit int = 0
AS
EXTERNAL NAME [ADServices].[ActiveDirectory].[QueryADAuth]
GO
After successful creation of the stored procedures we can start querying the AD: The filter parameter is a LDAP filter according the Search Filter Syntax.
--Query AD Groups
--Result set will contain cn - common name, ADsPath
EXECUTE [usp_QueryAD]
@adRoot = 'LDAP://DC=Fabricam,DC=com'
,@filter = '(&(objectCategory=group))'
,@propertiesToLoad = 'cn,50;ADsPath'
,@searchScope = 'subtree'
GO
--Query users of Sales organization unit
--Result set will containt sn - surname, cn - common name and ADsPath
EXECUTE [usp_QueryAD]
@adRoot = 'LDAP://OU=Sales,DC=Fabricam,DC=com'
,@filter = '(&(objectCategory=user)(objectClass=user))'
,@propertiesToLoad = 'sn,cn,ADsPath'
,@searchScope = 'subtree'
GO
--Query al users belonging to Group "Sales Representatives" and providing user name and password
--for AD Authetication including authetication type
--Result set will containt sn - surname, cn - common name and ADsPath
EXECUTE [TestDB3].[dbo].[usp_QueryADAuth]
@userName = 'FabricamADUserName'
,@password = 'ADuserNamePassword'
,@authType = 'secure'
,@adRoot = 'LDAP://DC=Fabricam,DC=com'
,@filter = '(&(objectCategory=user)(objectClass=user)(memberOf=CN=Sales Representatives,OU=Security Groups,OU=Groups,DC=Fabricam,DC=com))'
,@propertiesToLoad = 'sn,100;cn,200;ADsPath,1000'
,@searchScope = 'subtree'
GO
As we use a stored procedure for querying AD, then we cannot work directly with the result further (OK.. On Denali it will be possible thanks to the EXECUTE WITH RESULT SETS. On SQL Server 2005 and 2008 we could store the results e.g. to table variable to temp table and then work with the results as normally. From the CLR code we can see, that the result set contains all the AD properties we have passed as parameter and the order is exactly the same as in the input parameter. The data type of each returned column is nvarchar
with length which was specified in the properties list. If no length was specified or the length was less than one or grater than 4000 then the return type is nvarchar(4000)
.
--Query All users from AD, store them in a table variable and then
--select all users with their cn (common name) starting by "C"
DECLARE @result TABLE (
sn nvarchar(100),
cn nvarchar(200),
ADsPath nvarchar(1000)
)
INSERT INTO @result
EXECUTE [TestDB3].[dbo].[usp_QueryADAuth]
@userName = 'FabricamADUserName'
,@password = 'ADuserNamePassword'
,@authType = 'secure'
,@adRoot = 'LDAP://DC=Fabricam,DC=com'
,@filter = '(&(objectCategory=user)(objectClass=user))'
,@propertiesToLoad = 'sn,100;cn,200;ADsPath,1000'
,@searchScope = 'subtree'
SELECT
*
FROM @result
WHERE cn LIKE 'C%'
From the examples above we can see, that once we create CLR stored procedures for querying the AD, the queries to AD are quite easy. Comparing the the T-SQL and Linked Server or OPENROWSET solution we have much greater possibilities and what is most important, we are not limited to 1000 results from our query, so we can easily query all the object in AD.
If you are interested, you can download sample ADServices Visual C# 2010 Express project. It contains all the latest updates I have mentioned.
Here you can download an updated sample PPSqlDirectoryServices Visual C# 2010 project with all the lasted updates implemented.
Like this:
Like Loading...