Tom Troughton
Tom Troughton

Reputation: 4325

Active directory LDAP query - want to filter out disabled users, but property missing

I'm writing some code to query Active Directory using an LDAP connection. I'm only interested in users and I'm testing against a dummy instance of AD.

It's working well - I'm specifying specific properties to return and getting back results with those properties.

Last challenge is to filter out disabled users. Around the web I've discovered that this requires the following clause in the DirectorySearcher's Filter property:

(!(userAccountControl:1.2.840.113556.1.4.803:=2))

However, this wasn't working. Certain disabled users were always being returned. To investigate I wrote a little console app to reveal all the user properties:

enter image description here

Notice that 2 users are returned, but only one has the 'Account control' property? (Note that the label 'Account control' is reporting the userAccountControl property.) The second user, no matter if enabled or disabled, never returns a userAccountControl property so I cannot filter it based on this.

Can anyone explain please?

* UPDATE *

Adding some of my code which performs the query:

            using (DirectoryEntry de = new DirectoryEntry(ConnectionString))
            {
                //de.Path = Path;
                de.Username = Username;
                de.Password = Password;

                DirectorySearcher directorySearcher = new DirectorySearcher(de);
                directorySearcher.PageSize = 1001;// To Pull up more than 100 records.

                // Note that the userAccountControl clause excludes disabled users
                directorySearcher.Filter = string.Format("(&(objectClass=user){0}{1})", DisabledUserFilter(), query);

                Console.WriteLine("------------");
                Console.WriteLine(directorySearcher.Filter);

                Attributes.ForEach(a => directorySearcher.PropertiesToLoad.Add(a.Key));

                directorySearcher.SearchScope = SearchScope.Subtree;

                try
                {
                    var result = directorySearcher.FindAll();
                    ...

Upvotes: 5

Views: 10391

Answers (2)

Matthias Loerke
Matthias Loerke

Reputation: 2187

Sounds like you find other object types that do not have the "userAccountControl"-attribute (e.g. Contacts). Try this saerch filter:

(&(userAccountControl=*)(!(userAccountControl:1.2.840.113556.1.4.803:=2)))

This one tests if the attribute is set at all and also that the disabled bit is not set.

Hint: You should also included a filter for "objectCategory=user" when searching for user accounts, because computer accounts also have the object class "user" and a "userAccountControl"-attribute.

Upvotes: 4

ninjaboy
ninjaboy

Reputation: 300

What classes do you use to get details of AD entry?

Here's some code that we use to detect AD entry status (sorry for VB.NET):

Private Sub verifyUser(ByVal username As String, ByVal password As String)
    '1. Check if username exists in AD
    Dim userPath As String = String.Empty
    If Not findUser(username, userPath) Then
        'not found
        Exit Sub
    End If

    '2. Get AD Entry
    Using adsUser As New DirectoryEntry(userPath, ADsUserName, ADsPassword)

    '3. Is account Locked
        If isLockedHack(adsUser) Then
            'account is locked
            Exit Sub
        End If

        '4. Is account Disabled 
        If isDisabled(adsUser) Then
            'account is disabled
            Exit Sub
        End If

        '5. Has account expired
        If hasAccountExpired(adsUser) Then
            'account expired
            Exit Sub
        End If

        '6. Check whether user has to change password at next login
        If hasToChangePassAtNextLoginHack(adsUser) Then
            'change pass next login                
            Exit Sub
        End If

        '7. Has password expired
        If hasPasswordExpired(adsUser) Then
            'password expired                
            Exit Sub
        End If

        '8. Check whether password entered is okay
        If isPassCorrect(userPath, username, password) Then
            'account is fine
        Else
            'account is fine but password is incorrect
        End If
    End Using
End Sub

Private strMaxPwdAge As String = "maxPwdAge"
Private strPwdLastSet As String = "pwdLastSet"
Private strLockoutTime As String = "lockoutTime"
Private strAccountDisabled As String = "AccountDisabled"

Private Function hasToChangePassAtNextLoginHack(ByVal adsUser As DirectoryEntry) As Boolean
    If adsUser.Properties.Contains(strPwdLastSet) Then
        Dim objPwdLastSet As Object = adsUser.Properties(strPwdLastSet)(0)
        If objPwdLastSet.LowPart = 0 Then
            Return True
        End If
    End If
    Return False

End Function

Private Function hasPasswordExpired(ByVal adsUser As DirectoryEntry) As Boolean
    Dim pwdLastSet As Object
    pwdLastSet = adsUser.Properties(strPwdLastSet).Value

    Dim pwdDate As DateTime = GetDateFromLargeInteger(pwdLastSet)
    Dim today As DateTime = Date.Now

    If pwdDate.AddDays(CInt(getMaxPwdAge())).CompareTo(today) < 0 Then
        Return True
    Else
        Return False
    End If
    Return False
End Function

Private Function isLockedHack(ByVal adsUser As DirectoryEntry) As Boolean
    Dim pcoll As PropertyCollection = adsUser.Properties

    If adsUser.Properties.Contains(strLockoutTime) Then
        Dim oli2 As Object = adsUser.Properties(strLockoutTime)(0)

        Dim timeVal As Long = (oli2.HighPart * &H100000000) + oli2.LowPart
        If timeVal > 0 Then
            Return True
        End If
    End If
    Return False
End Function

Private Function isPassCorrect(ByVal userPath As String, ByVal user As String, _
    ByVal pass As String) As Boolean

    Try
        Using adsUser As New DirectoryEntry(userPath, user, pass)
            Dim natObj As Object = adsUser.NativeObject
            adsUser.Dispose()
        End Using
    Catch ex As Exception
        Return False
    End Try
    Return True
End Function

Private Function isLocked(ByVal adsUser As DirectoryEntry) As Boolean

    Dim binIL As Boolean
    binIL = adsUser.InvokeGet("IsAccountLocked")
    If binIL Then
        Return True
    End If

    Return False

End Function

Private Function isDisabled(ByVal adsUser As DirectoryEntry) As Boolean
    If adsUser.InvokeGet(strAccountDisabled) Then
        Return True
    End If
    Return False
End Function

Private Function hasAccountExpired(ByVal adsUser As DirectoryEntry) As Boolean
    Dim myDate As DateTime = adsUser.InvokeGet("AccountExpirationDate")
    Dim defExpiredDate As New DateTime(1601, 1, 1)
    Dim passNeverExpiresDate As New DateTime(1970, 1, 1)
    Dim today As DateTime = Date.Now

    If (myDate.CompareTo(defExpiredDate) <> 0) And (myDate.CompareTo(passNeverExpiresDate) <> 0) Then
        If myDate.CompareTo(today) < 0 Then
            Return True
        Else
            Return False
        End If
    End If

    Return False

End Function

Private Function findUser(ByVal username As String, ByRef path As String) As Boolean
    path = ContainerFound(ContainerType.user, username)

    If path = "" Then
        Return False
    End If

    Return True
End Function

Private Function getMaxPwdAge() As String
    Using domainRoot As New DirectoryEntry(strLDAP & strADsPath, ADsUserName, ADsPassword)
        Dim maxPwdAge As LargeInteger
        Dim ticksDividerValue = -864000000000
        Dim numDays As Long
        If domainRoot.Properties.Contains(strMaxPwdAge) Then
            maxPwdAge = domainRoot.Properties(strMaxPwdAge)(0)
            numDays = ((maxPwdAge.HighPart * 2 ^ 32) + maxPwdAge.LowPart) / ticksDividerValue
        Else
            numDays = 0
        End If
        Return numDays.ToString()
    End Using
End Function

Private Function ContainerFound(ByVal Type As Byte, ByVal Item As String) As String
    'Finds the path of an object within the active directory
    Using objADObject As New DirectoryEntry

        objADObject.Path = strLDAP & strADsPath
        objADObject.Username = ADsUserName
        objADObject.Password = ADsPassword
        objADObject.AuthenticationType = AuthenticationTypes.Secure

        Using objADSearcher As New DirectorySearcher(objADObject)
            Dim Results As SearchResult
            Select Case Type
                Case ContainerType.organisationalUnit
                    objADSearcher.Filter = ("ou=" & Item)
                Case ContainerType.group, ContainerType.user
                    objADSearcher.Filter = ("cn=" & Item)
            End Select

            Try
                Results = objADSearcher.FindOne
            Catch ex As Exception
                'CannotConnectDomain
                Return ""
            End Try

            If IsNothing(Results) = True Then
                Return ""
            Else
                : Return Results.Path
            End If

        End Using
    End Using

End Function

Public Function GetDateFromLargeInteger(ByVal largeInteger As Object) As Date
    Try
        Return Date.FromFileTime(GetInt64FromLargeInteger(largeInteger))
    Catch e As ArgumentOutOfRangeException
        Throw New ArgumentException(strNotValidVal, e)
    End Try
End Function

Public Function GetInt64FromLargeInteger(ByVal largeInteger As Object) As Long
    Const highShift As Long = &H100000000
    Dim lowPart As Integer
    Dim highPart As Integer
    Dim longVal As Long
    Dim largeIntType As Type
    largeIntType = largeInteger.GetType()
    Try
        highPart = CType(largeIntType.InvokeMember("HighPart", BindingFlags.GetProperty Or BindingFlags.Public, Nothing, largeInteger, Nothing), Integer)
        lowPart = CType(largeIntType.InvokeMember("LowPart", BindingFlags.GetProperty Or BindingFlags.Public, Nothing, largeInteger, Nothing), Integer)
        longVal = (highPart * highShift) + lowPart 
        Return longVal
    Catch e As MissingMethodException
        Throw New ArgumentException(strInvalidCom, e)
    End Try
End Function

Hopefully it will give you a brief idea how this is done for our AD check. Let me know if that is helpful.

Upvotes: 0

Related Questions