It is relatively straightforward to write .Net applications against Active Directory (AD) and Active Directory Lightweight Directory Services (ADLDS), but a number of challenges arise from writing applications against non-Microsoft directories.
More often than you would think, I am confronted with the use case of connecting to a non-Microsoft directory (ex. OpenLDAP, Oracle Internet Directory, IBM Directory Server, and Novell eDirectory). In a few cases, you can use the same classes that you would with AD or ADLDS that exist in the System.DirectoryServices namespace (ex. DirectoryEntry, DirectorySearcher, SearchResult, DirectoryEntries, etc), but in many cases you will run into an assortment of issues that usually ends in failure.
Digging into the namespace a little further, Microsoft has developed the System.DirectoryServices.Protocols namespace that gives you the ability to interact with LDAP directories at a lower level than the classes provided by the System.DirectoryServices namespace, but at a higher level than having to write your own LDAPv3 library from scratch.
Below is sample code for a sample C# .Net application and a sample Powershell script that allows interaction with non-Microsoft LDAP directories:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.DirectoryServices.Protocols;
using System.Collections;
namespace TestLDAPBind
{
class Program
{
//Application
Connects to and Searches OpenLDAP directory
static void Main(string[] args)
{
//
Connects to myopenldap.mikesblog.lan using SSL on a non-standard port
LdapConnection c = new LdapConnection("directory.mikesblog.lan:637");
//Set
session options
c.SessionOptions.SecureSocketLayer
= true;
// Pick
Authentication type:
//
Anonymous, Basic, Digest, DPA (Distributed Password Authentication),
//
External, Kerberos, Msn, Negotiate, Ntlm, Sicily
c.AuthType = AuthType.Basic;
// Gets
username and password. There are better ways to do this more securely...
// but
that's not the topic of this post.
Console.Write("Enter
Username: ");
string username = Console.ReadLine();
Console.WriteLine();
Console.Write("Enter
Password: ");
string password = Console.ReadLine();
// Bind
with the network credentials. Depending on the type of server,
// the
username will take different forms. Authentication type is controlled
// above
with the AuthType
c.Bind(new System.Net.NetworkCredential(username,
password));
SearchRequest r = new SearchRequest(
//Base
DN
"ou=users,dc=mikesblog,dc=lan",
//Filter
"(uid=burrm)",
//Search
scope
SearchScope.Subtree,
//params
string [] of attributes... in this case all
"*");
SearchResponse re = (SearchResponse)c.SendRequest(r);
//How
many results do we have?
Console.WriteLine(re.Entries.Count);
foreach (SearchResultEntry i in re.Entries)
{
//Do
something with each entry here, such as read attributes
}
}
}
}The example Powershell port follows:
#Mike Burr
#Script Connects to and Searches OpenLDAP directory
#Load the assemblies
[System.Reflection.Assembly]::LoadWithPartialName("System.DirectoryServices.Protocols")
[System.Reflection.Assembly]::LoadWithPartialName("System.Net")
#Connects to myopenldap.mikesblog.lan using SSL on a non-standard port
$c = New-Object System.DirectoryServices.Protocols.LdapConnection "myopenldap.mikesblog.lan:637"
#Set session options
$c.SessionOptions.SecureSocketLayer = $true;
# Pick Authentication type:
# Anonymous, Basic, Digest, DPA (Distributed Password Authentication),
# External, Kerberos, Msn, Negotiate, Ntlm, Sicily
$c.AuthType = [System.DirectoryServices.Protocols.AuthType]::Basic
# Gets username and password.
$user = Read-Host -Prompt "Username"
$pass = Read-Host -AsSecureString "Password"
$credentials = new-object "System.Net.NetworkCredential" -ArgumentList $user,$pass
# Bind with the network credentials. Depending on the type of server,
# the username will take different forms. Authentication type is controlled
# above with the AuthType
$c.Bind($credentials);
$basedn = "ou=users,dc=mikesblog,dc=lan"
$filter = "(uid=burrm)"
$scope = [System.DirectoryServices.Protocols.SearchScope]::Subtree
$attrlist = ,"*"
$r = New-Object System.DirectoryServices.Protocols.SearchRequest -ArgumentList `
$basedn,$filter,$scope,$attrlist
#$re is a System.DirectoryServices.Protocols.SearchResponse
$re = $c.SendRequest($r);
#How many results do we have?
write-host $re.Entries.Count
foreach ($i in $re.Entries)
{
#Do something with each entry here, such as read attributes
}
-->
Hello - great work
ReplyDeleteI was able to connect to the Oracle directory, how do I get the attribute values?
results:
PS C:\windows\system32> $re
MatchedDN :
Controls : {}
ResultCode : Success
ErrorMessage :
Referral : {}
References : {}
Entries : {uid=99_greg, ou=people, ou=FIRMA, o=abc,c=pl}
RequestId :
PS C:\windows\system32> $re.Entries
DistinguishedName Attributes Controls
----------------- ---------- --------
uid=99_greg, ou=people, ou=FIRMA, o=abc,c=pl {ntuserdeleteaccount, cn, caid, beginofwork...} {}
PS C:\windows\system32> $re | gm
TypeName: System.DirectoryServices.Protocols.SearchResponse
Name MemberType Definition
---- ---------- ----------
Equals Method bool Equals(System.Object obj)
GetHashCode Method int GetHashCode()
GetType Method type GetType()
ToString Method string ToString()
Controls Property System.DirectoryServices.Protocols.DirectoryControl[] Controls {get;}
Entries Property System.DirectoryServices.Protocols.SearchResultEntryCollection Entries {get;}
ErrorMessage Property System.String ErrorMessage {get;}
MatchedDN Property System.String MatchedDN {get;}
References Property System.DirectoryServices.Protocols.SearchResultReferenceCollection References {get;}
Referral Property System.Uri[] Referral {get;}
RequestId Property System.String RequestId {get;}
ResultCode Property System.DirectoryServices.Protocols.ResultCode ResultCode {get;}
It is sort of a mess to get attributes. The basic process is that you have to convert the attribute value to the type that you need. In the C# example below, GetValues gets an object [] that you have to cast to the type that you are using.
Deleteforeach (SearchResultEntry i in re.Entries)
{
string [] sn_string = (string [])i.Attributes["sn"].GetValues(typeof(String));
foreach (string j in sn_string)
{
Console.WriteLine(j);
}
}
In powershell, the way this works out is like this:
$type_string = [System.Type]::GetType("System.String")
foreach ($i in $re.Entries) {
$sn_string = $i.Attributes["sn"].GetValues($type_string)
foreach ($j in $sn_string)
{
Write-Host $j
}
}
Hope this helps...
Mike - you're GREAT!
ReplyDeleteThank you very much! it works perfectly!
is the first solution I've found on the net that really works
Mike - you described how to make a bind, search, view attributes
ReplyDeleteCan you describe how to change the values and save them to an LDAP (non $ MS)?
:)
Hi,
DeleteSorry it took a bit to get back to you (I was working on a project that took time away from my blog). I wrote a few posts that are linked that will help. For your specific question, see
http://mikemstech.blogspot.com/2013/05/updating-attributes-in-non-microsoft.html
Hope this helps