Search code examples
python-3.4django-1.8python-ldap

ldap3 operation: SEARCH seems to fail (search_filter syntax vs RFC4515)


Edit: tl;dr - The search_filter argument used in SEARCH might be non conforming with RFC4515.

I've got a Django server running version 1.8.4 on Ubuntu 14.04. I'm using Python 3.4 and for that I'm trying to configure LDAP authentication using ldap3.

This is part of an upgrade to the versions named above from 1.6.2, 12.04, and 2.7.3, respectively. Everything was working correctly, so I assume that the problem is on my end and not with the authentication server.

The way this works is I've got a file called authenticate_user.py which receives the username and password passed through an HTML form as shown below.

def authenticateStudent(request):
    username = request.POST.get('username','')
    logger.info("User " + username + " has logged in.")
    password = request.POST.get('password','')
    x = Auth(username, password)
    retVal = x.AuthenticatePy()
    logger.info('retVale is '+str(retVal)) #this returns False
    #more code and more logging

The method instantiates an object from the Auth class (shown below), stores the username and password within, and then calls the AuthenticatePy() method in that class.

import logging
import sys
import os.path,subprocess
import ldap3 as ldap
from ldap3 import Connection, Server, SIMPLE, SYNC, SUBTREE, ALL


logger = logging.getLogger('Submission')

class Auth():

    studentName = ""
    studentEmail = ""
    studentMatrik = ""

    def __init__(self, username, password):
        self.username = username
        self.password = password

    def AuthenticatePy(self):
        user_dn = "cn="+self.username+",ou=users,ou=data,ou=prod,ou=authserver,dc=domain,dc=tld"
        base_dn = "dc=domain,dc=tld"
        server = Server("authserver.domain.tld", port=636, use_ssl=True)

        filter = "uid="+self.username #might be incorrect
        try:
            #if authentication successful, get the full user data
            connect = Connection(server, user=user_dn, password=self.password)
            connect.bind()
            logger.info('Connection Bind Complete!') #the last logged message from this method
            result = connect.search(search_base=base_dn, search_filter=filter, search_scope=SUBTREE)
            logger.info('SEARCHING COMPLETE') #does not appear in the log
            # return all user data results
            connect.unbind()
            uname = result[0][1]['cn'][0]
            studentName = result[0][1]['fullName'][0]
            studentEmail = result[0][1]['imHauptEMail'][0]
            studentMatrik = result[0][1]['imMatrikelNr'][0]
            logger.info('studentName is '+str(studentName))
            if uname == self.username :
                return studentName + '$' + studentEmail + '$' + studentMatrik
            else:
                return False
        except ldap.LDAPExceptionError:
            connect.unbind()
            return False

The last log message I'm seeing is 'Connection Bind Complete!' and I'm not really sure what's breaking. Any idea what I'm doing wrong?

Edit: I've been troubleshooting this for a while and I'm beginning to think that the problem could be in the search_filter argument I'm passing the search function. The ldap3 documentation on the SEARCH operation states that the filter string should be RFC4515 compliant, and I'm not sure I'm providing that.


Solution

  • I managed to solve this problem. My syntax for the search_filter argument was indeed wrong.

    It needed to be set as mentioned in the other answer:
    filter = "(uid="+self.username + ")"

    However, I also needed to indicate which attributes I wanted to return, hence connect.search()'s arguments needed to be changed:

    connect.search(search_base=base_dn, search_filter=filter, search_scope=SUBTREE, attributes=['cn', 'fullName', 'imHauptEmail', 'imMatrikelNr'])
    

    Also, accessing the returned attributes doesn't work the same way it did in the python-ldap library for Python 2.7 and can be accessed by connect.response, which is a list of dictionaries. You would have to access these attributes before you unbind the connection, so the connect.unbind() was moved until after storing the values needed from the attributes.

    uname = str(connect.response[0]['attributes']['cn'][0])
    studentName = str(connect.response[0]['attributes']['fullName'][0])
    studentEmail = str(connect.response[0]['attributes']['imHauptEMail'][0])
    studentMatrik = str(connect.response[0]['attributes']['imMatrikelNr'][0])
    connect.unbind()
    

    In reality all of this is explained in the documentation (See the examples section at the bottom). I just needed to read it a few times to get it.