Introduction
This article discusses working within the Active Directory (AD) using VB.NET, how to query the AD, query groups, members, adding users, suspending users, and changing user passwords. The Active Directory is the Windows directory service that provides a unified view of the entire network. Working with the Active Directory is a lot like working with a database, you write queries based on the information you want to retrieve.
There are three interfaces for accessing the Active Directory:
- LDAP: The Lightweight Directory Access Protocol (LDAP) is the service protocol that runs on a layer above the TCP/IP layer (or stack) and provides an interface for accessing, searching, and modifying Internet Directories, and is based on a client-server model.
- ADSI: The Active Directory Services Interface (ADSI) is a set of COM components (or Interfaces) designed to access the directory services from different network providers in a network setup; it is designed to provide a single, central interface for accessing and managing network resources.
- System.DirectoryServices: The
System.DirectoryServices
namespace built into the .NET Framework is designed to provide programming access to LDAP directories (Active Directory), and is built on the ASDI API.
Background
Recently, at work, I was tasked with creating a security system for the application I currently program. I took this a step further, and wanted to ensure that in the event the application was ever taken off-site, that it couldn't run; this is when I came up with the idea of basing my security on the Active Directory of the company network. This would prevent an individual from taking the application off-site and attempting to use it, either for malicious purposes or to just "play" around with it. This also seemed to be the most secure way as only a select group of people actually have the permissions to alter the Active Directory in any way. Though there is a ton of built-in functionality in the .NET Framework for working with the Active Directory, I decided to create my own object, a wrapper around the System.DirectoryServices
namespace in .NET.
The employees who use this application are all in a single department. They, in turn, have their own table in the database holding specific information about the user, i.e., access level, queue, and such. When we first launched this application, the security was based on the user's access level in the database. This determined what the user had access to. When the application launched, it checked the name the user was logged in as:
Environment.UserName.ToString
It then split the user name (at my company, your username is FirstName.LastName) to return the first and last name, and queried the aforementioned table to retrieve the user's access level based on the first and last name retrieved earlier. I wanted to keep the access level security as this ensured people didn't have access to areas of the application they shouldn't have, so I added a new column to the user table to hold their AD login (data type SysUser
). From there, I would use Environment.UserName.ToString
to once again retrieve the username they are logged in with. With this, I would retrieve their LoginName
from the users table, and query the Active Directory to ensure this is a valid network account and they have permissions to be using this application.
Working on this AD security, I discovered the true power of the System.DirectoryServices
namespace; with it you can:
- Add a new user to the network
- Suspend a user's account
- Enable a user's account
- Reset a user's password
- Update a user account
- Add a user to a specific group
- Remove a user from a group
- Retrieve the list of all groups a user is a member of
- Retrieve all computers connected to the network
- Determine if a user's account is disabled
- Check if a user account is active (perform a basic login)
and so much more. In this article, I will walk you through connecting to an Active Directory, searching for users in the Active Directory, disabling a user's account, resetting a user's password, setting up a mailbox for a new user, displaying all computers on the network, and adding a user to a specific group in the Active Directory.
Using the code
I will get this helper out of the way first, many of the procedures/functions will be calling this helper. The job of this helper is simply to set the properties of a user's AD account. This saves time and code when properties need to either be set or altered. This helper searches the provided property to see if it already exists in the user's account; if it doesn't, then it adds and sets the value; if it already exists, it updates the property's value:
Public Shared Sub SetADProperty(ByVal de As DirectoryEntry, _
ByVal pName As String, ByVal pValue As String)
If Not pValue Is Nothing Then
If de.Properties.Contains(pName) Then
de.Properties(pName)(0) = pValue
Else
de.Properties(pName).Add(pValue)
End If
End If
End Sub
The first thing to do when working with the Active Directory is to create a connection to the Active Directory:
Public Shared Function GetDirectoryEntry() As DirectoryEntry
Dim dirEntry As DirectoryEntry = New DirectoryEntry()
dirEntry.Path = "LDAP://192.168.1.1/CN=Users;DC=Yourdomain"
dirEntry.Username = "yourdomain\sampleuser"
dirEntry.Password = "samplepassword"
Return dirEntry
End Function
As stated above, when I added the new column to the users table to hold their AD login (the SysUser
data type built into SQL2000/2005), this data type holds the value in the format DOMAIN\USERNAME. When searching the Active Directory for a specific user, you need to strip the "DOMAIN\" off, so I created the following function to do this for me:
Public Function ExtractUserName(ByVal path As String) As String
Dim userPath As String() = path.Split(New Char() {"\"c})
Return userPath((userPath.Length - 1))
End Function
To search for the provided user in the Active Directory, I created the following function:
Public Function IsValidADLogin(ByVal loginName As String, _
ByVal givenName As String, ByVal surName As String) As Boolean
Try
Dim search As New DirectorySearcher()
search.Filter = String.Format("(&(SAMAccountName={0}) & _
(givenName={1})(sn={2}))", _
ExtractUserName(loginName), givenName, surName)
search.PropertiesToLoad.Add("cn")
search.PropertiesToLoad.Add("SAMAccountName")
search.PropertiesToLoad.Add("givenName")
search.PropertiesToLoad.Add("sn")
Dim result As SearchResult = search.FindOne()
If result Is Nothing Then
Return False
Else
Return True
End If
Catch ex As Exception
MessageBox.Show(ex.Message, "Active Directory Error", & _
MessageBoxButtons.OK, MessageBoxIcon.Error, MessageBoxDefaultButton.Button1)
End Try
End Function
With this function, if the selected user is found, then True
is returned, else False
is returned, letting the programmer know that this isn't a valid user in the Active Directory. As stated earlier, there are many things you can do when accessing the Active Directory; I will now walk you through just a sample of the things possible with the System.DirectoryServices
namespace.
The first example will be creating a new AD user; in order to use this functionality, you must have an account that has the proper permissions to add a user. (All accounts have access to search the Active Directory, only members of the Administrator Group can perform many of the functions, including creating a new AD user.) The easiest way to do this is to delegate the permissions of a user account with DomainAdmin permissions to the thread that is running the program. The following code will take care of this for you:
Public Shared Sub SetCultureAndIdentity()
AppDomain.CurrentDomain.SetPrincipalPolicy(PrincipalPolicy.WindowsPrincipal)
Dim principal As WindowsPrincipal = CType(Thread.CurrentPrincipal, WindowsPrincipal)
Dim identity As WindowsIdentity = CType(principal.Identity, WindowsIdentity)
System.Threading.Thread.CurrentThread.CurrentCulture = New CultureInfo("en-US")
End Sub
1. Creating a new Active Directory user account
There are four procedures required to perform this. The first procedure, CreateADAccount
, is the procedure that actually creates the new user. It relies on three other procedures: SetPassword
is used to set the password of the new user, EnableAccount
is used to enable the new account, and AddUserToGroup
is used to add the new user to the specified Active Directory Group.
First, let's look at the CreateADAccount
procedure:
Public Sub CreateAdAccount(ByVal sUserName As String, _
ByVal sPassword As String, _
ByVal sFirstName As String, ByVal sLastName As String, _
ByVal sGroupName As String)
Dim catalog As Catalog = New Catalog()
Dim dirEntry As New DirectoryEntry()
Dim adUsers As DirectoryEntries = dirEntry.Children
Dim newUser As DirectoryEntry = adUsers.Add("CN=" & sUserName, "user")
SetProperty(newUser, "givenname", sFirstName)
SetProperty(newUser, "sn", sLastName)
SetProperty(newUser, "SAMAccountName", sUserName)
SetProperty(newUser, "userPrincipalName", sUserName)
newUser.CommitChanges()
SetPassword(newUser, sPassword)
AddUserToGroup(dirEntry, newUser, sGroupName)
EnableAccount(newUser)
newUser.Close()
dirEntry.Close()
End Sub
The three highlighted lines at the end of the procedure are referring to the three additional procedures mentioned above, the first being SetPassword
:
Private Shared Sub SetPassword(ByVal dEntry As DirectoryEntry, _
ByVal sPassword As String)
Dim oPassword As Object() = New Object() {sPassword}
Dim ret As Object = dEntry.Invoke("SetPassword", oPassword)
dEntry.CommitChanges()
End Sub
The second procedure is the EnableAccount
, which is responsible for enabling the new account so the user can use it:
Private Shared Sub EnableAccount(ByVal de As DirectoryEntry)
Dim exp As Integer = CInt(de.Properties("userAccountControl").Value)
de.Properties("userAccountControl").Value = exp Or &H1
de.CommitChanges()
Dim val As Integer = CInt(de.Properties("userAccountControl").Value)
de.Properties("userAccountControl").Value = val And Not &H2
de.CommitChanges()
End Sub
The third and final procedure is the AddUserToGroup
, which adds the new user to the specified Active Directory Group:
Public Shared Sub AddUserToGroup(ByVal de As DirectoryEntry, _
ByVal deUser As DirectoryEntry, ByVal GroupName As String)
Dim deSearch As DirectorySearcher = New DirectorySearcher()
deSearch.SearchRoot = de
deSearch.Filter = "(&(objectClass=group) (cn=" & GroupName & "))"
Dim results As SearchResultCollection = deSearch.FindAll()
Dim isGroupMember As Boolean = False
If results.Count > 0 Then
Dim group As New DirectoryEntry(results(0).Path)
Dim members As Object = group.Invoke("Members", Nothing)
For Each member As Object In CType(members, IEnumerable)
Dim x As DirectoryEntry = New DirectoryEntry(member)
Dim name As String = x.Name
If name <> deUser.Name Then
isGroupMember = False
Else
isGroupMember = True
Exit For
End If
Next member
If (Not isGroupMember) Then
group.Invoke("Add", New Object() {deUser.Path.ToString()})
End If
group.Close()
End If
Return
End Sub
2. Disable a user account
With the System.DirectoryServices
namespace, you not only can create a new Active Directory user, but can also disable a user's Active Directory account. To disable a user's Active Directory account, use the following procedure:
Public Sub DisableAccount(ByVal sLogin As String)
Dim dirEntry As DirectoryEntry = GetDirectoryEntry()
Dim dirSearcher As DirectorySearcher = New DirectorySearcher(dirEntry)
dirSearcher.Filter = "(&(objectCategory=Person)(objectClass=user) _
(SAMAccountName=" & sLogin & "))"
dirSearcher.SearchScope = SearchScope.Subtree
Dim results As SearchResult = dirSearcher.FindOne()
If Not results Is Nothing Then
Dim dirEntryResults As DirectoryEntry = _
GetDirectoryEntry(results.Path)
Dim iVal As Integer = _
CInt(dirEntryResults.Properties("userAccountControl").Value)
dirEntryResults.Properties("userAccountControl").Value = iVal Or &H2
dirEntryResults.Properties("msExchHideFromAddressLists").Value = "TRUE"
dirEntryResults.CommitChanges()
dirEntryResults.Close()
End If
dirEntry.Close()
End Sub
3. Update/Modify a user's Active Directory account information
With the System.DirectoryServices
namespace, you not only can create a new Active Directory user and disable a user's Active Directory account, but can also update/modify a user's Active Directory account properties. The properties in this procedure will differ from the properties in your Active Directory, so you will need access to see what properties are available; the ones I have listed are just a small sample of the possible properties. To update/modify a user's Active Directory account, use the following procedure:
Public Sub UpdateUserADAccount(ByVal userLogin As String, _
ByVal userDepartment As String, _
ByVal userTitle As String, ByVal userPhoneExt As String)
Dim dirEntry As DirectoryEntry = GetDirectoryEntry()
Dim dirSearcher As DirectorySearcher = New DirectorySearcher(dirEntry)
dirSearcher.Filter = "(&(objectCategory=Person)(objectClass=user) _
(SAMAccountName=" & userLogin & "))"
dirSearcher.SearchScope = SearchScope.Subtree
Dim searchResults As SearchResult = dirSearcher.FindOne()
If Not searchResults Is Nothing Then
Dim dirEntryResults As New DirectoryEntry(results.Path)
SetProperty(dirEntryResults, "department", userDepartment)
SetProperty(dirEntryResults, "title", userTitle)
SetProperty(dirEntryResults, "phone", userPhoneExt)
dirEntryResults.CommitChanges()
dirEntryResults.Close()
End If
dirEntry.Close()
End Sub
4. Listing all computers in the Active Directory
Using the System.DirectoryServices
namespace in the .NET Framework, you can also create a list of all the computers listed in the Active Directory. When I created this function, I populated a Collection
with each computer name, then I could enumerate through the Collection
and populate a control (such as ListBox
, DataGridView
, and such) with the names returned from the Active Directory query. The function used is:
Public Shared Function ListAllADComputers() As Collection
Dim dirEntry As DirectoryEntry = GetDirectoryEntry()
Dim pcList As New Collection()
Dim dirSearcher As DirectorySearcher = New DirectorySearcher(dirEntry)
dirSearcher.Filter = ("(objectClass=computer)")
Dim dirSearchResults As SearchResult
For Each dirSearchResults In dirSearcher.FindAll()
If Not pcList.Contains(dirSearchResults.GetDirectoryEntry().Name.ToString()) Then
pcList.Add(dirSearchResults.GetDirectoryEntry().Name.ToString())
End If
Next
Return pcList
End Function
5. List all groups a user is in
The following function returns a Collection
of Groups the user is a member of. I went with the Collection rather than a string concatenated with the Group names, as this way, I can populate a control with the values:
Private Function GetGroups(ByVal _path As String, ByVal username As String, _
ByVal password As String) As Collection
Dim Groups As New Collection
Dim dirEntry As New _
System.DirectoryServices.DirectoryEntry(_path, username, password)
Dim dirSearcher As New DirectorySearcher(dirEntry)
dirSearcher.Filter = String.Format("(sAMAccountName={0}))", username)
dirSearcher.PropertiesToLoad.Add("memberOf")
Dim propCount As Integer
Try
Dim dirSearchResults As SearchResult = dirSearcher.FindOne()
propCount = dirSearchResults.Properties("memberOf").Count
Dim dn As String
Dim equalsIndex As String
Dim commaIndex As String
For i As Integer = 0 To propCount - 1
dn = dirSearchResults.Properties("memberOf")(i)
equalsIndex = dn.IndexOf("=", 1)
commaIndex = dn.IndexOf(",", 1)
If equalsIndex = -1 Then
Return Nothing
End If
If Not Groups.Contains(dn.Substring((equalsIndex + 1), _
(commaIndex - equalsIndex) - 1)) Then
Groups.Add(dn.Substring((equalsIndex + 1), & _
(commaIndex - equalsIndex) - 1))
End If
Next
Catch ex As Exception
If ex.GetType Is GetType(System.NullReferenceException) Then
MessageBox.Show("Selected user isn't a member of any groups " & _
"at this time.", "No groups listed", _
MessageBoxButtons.OK, MessageBoxIcon.Error)
Else
MessageBox.Show(ex.Message.ToString, "Search Error", & _
MessageBoxButtons.OK, MessageBoxIcon.Error)
End If
End Try
Return Groups
End Function
End Class
6. Determine if a user's account has been disabled
The following function can be used to determine if a selected user's account had been disabled:
Public Shared Function IsAccountActive(ByVal userAccountControl As Integer) _
As Boolean
Dim accountDisabled As Integer = & _
Convert.ToInt32(ADAccountOptions.UF_ACCOUNTDISABLE)
Dim flagExists As Integer = userAccountControl And accountDisabled
If flagExists > 0 Then
Return False
Else
Return True
End If
End Function
7. Remove a user from a specific group
The following function can be used to remove a user from a specific group:
Public Shared Sub RemoveUserFromGroup(ByVal UserName As String, _
ByVal GroupName As String)
Dim Domain As New String("")
Domain = "/CN=" + GroupName + ",CN=Users," + GetLDAPDomain()
Dim oGroup As DirectoryEntry = GetDirectoryObject(Domain)
Domain = "/CN=" + UserName + ",CN=Users," + GetLDAPDomain()
Dim oUser As DirectoryEntry = GetDirectoryObject(Domain)
oGroup.Invoke("Remove", New Object() {oUser.Path.ToString()})
oGroup.Close()
oUser.Close()
End Sub
8. Check a user account to see if it's active
The following process does a simulated login to a user's account to see if it's a valid account and if it's active. This method calls other functions to work properly: GetUser
and the overrides GetUser
, IsUserValid
, and IsAccountActive
; all of these will be listed here.
Public Shared Function Login(ByVal UserName As String, ByVal Password As String) _
As ADWrapper.LoginResult
If IsUserValid(UserName, Password) Then
Dim dirEntry As DirectoryEntry = GetUser(UserName)
If Not dirEntry Is Nothing Then
Dim accountControl As Integer = & _
Convert.ToInt32(dirEntry.Properties("userAccountControl")(0))
dirEntry.Close()
If Not IsAccountActive(accountControl) Then
Return LoginResult.LOGIN_USER_ACCOUNT_INACTIVE
Else
Return LoginResult.LOGIN_OK
End If
Else
Return LoginResult.LOGIN_USER_DOESNT_EXIST
End If
Else
Return LoginResult.LOGIN_USER_DOESNT_EXIST
End If
End Function
GetUser (+Overrides version)
Public Shared Function GetUser(ByVal UserName As String) As DirectoryEntry
Dim dirEntry As DirectoryEntry = GetDirectoryObject("/" + GetLDAPDomain())
Dim dirSearch As New DirectorySearcher(dirEntry)
dirSearch.SearchRoot = dirEntry
dirSearch.Filter = "(&(objectCategory=user)(cn=" + UserName + "))"
Dim searchResults As SearchResult = dirSearch.FindOne()
If Not searchResults Is Nothing Then
ADAdminPassword,AuthenticationTypes.Secure);
Return searchResults.GetDirectoryEntry()
Else
Return Nothing
End If
End Function
Public Shared Function GetUser(ByVal UserName As String, ByVal Password _
As String) As DirectoryEntry
Dim dirEntry As DirectoryEntry = GetDirectoryObject(UserName, Password)
Dim dirSearch As New DirectorySearcher()
dirSearch.SearchRoot = dirEntry
dirSearch.Filter = "(&(objectClass=user)(cn=" + UserName + "))"
dirSearch.SearchScope = SearchScope.Subtree
Dim searchResults As SearchResult = dirSearch.FindOne()
If Not searchResults Is Nothing Then
dirEntry = New DirectoryEntry(searchResults.Path, ADAdminUser, _
ADAdminPassword, AuthenticationTypes.Secure)
Return dirEntry
Else
Return Nothing
End If
End Function
IsUserValid
This procedure will check to make sure it is a valid user account:
Public Shared Function IsUserValid(ByVal UserName As String, _
ByVal Password As String) As Boolean
Try
Dim dirUser As DirectoryEntry = GetUser(UserName, Password)
dirUser.Close()
Return True
Catch generatedExceptionName As Exception
Return False
End Try
End Function
IsUserActive
This will check to see if it is an active user account:
Public Shared Function IsAccountActive(ByVal userAccountControl As Integer) _
As Boolean
Dim accountDisabled As Integer = _
Convert.ToInt32(ADAccountOptions.UF_ACCOUNTDISABLE)
Dim flagExists As Integer = userAccountControl And accountDisabled
If flagExists > 0 Then
Return False
Else
Return True
End If
End Function
Points of interest
This is really a high level overview of working with the Active Directory in VB.NET. There is more functionality in the wrapper DLL I have for download with this article, such as querying the AD for all active groups, returning all users who are a member of a Group you specify, etc. Working with the Active Directory can be daunting at first, but don't let it scare you off. Once you learn just a few items, it's no more complicated than querying a database for information.
*Special thanks to Erika Ehrli, whose article on VBDotNetHeaven.Com helped me solve some AD logic I struggled with.*
Happy coding!