LDAP Best Practices

Use secure connections

Always use secure connections when sending credentials for authentication, and when reading or writing any data that is not public.

For LDAP applications, either connect to the directory server's LDAPS port (636), or if possible, begin each session with the StartTLS extended operation on the (cleartext) LDAP port (389).

See Best Practices for Client Applications | Example-Code

Reuse connections

The LDAP protocol is stateful. Typically you bind (connect), search or make an update, and then unbind (disconnect). The server maintains a context and enforces authorization decisions concerning your requests. Your application should reuse connections.

You can make multiple requests without having to set up a new connection and authenticate for every request. Many client libraries for LDAP allow your application to share connections in a pool, avoiding the overhead of setting up and tearing down connections if you use them often. This can significantly improve your application’s performance and reduce load on the directory server.

See Best Practices for Client Applications | Example-Code

Use paged searches

If you suspect that the result set from a given query may be very large, you should be able to retrieve a result set in small pieces. The Simple Paged Result extended control allows this type of retrieval by allowing a one-way traversal of the result set. Options on this control allow the client to set the initial page size and reset the page size with each subsequent request to the server.

The Simple Paged Result control is also used to access all of a large result set when there is a server-side administrative limit to the number of items returned from a query. For example, CalNet LDAP Directory servers have a default server-side limit of 1000 entries as the maximum number of results that are returned in a single request. If the results of a query exceed this limit, the Paged Results control is used with a page size equal to or less than the server-side limit in order to retrieve all of the results of the query.

See Best Practices for Client Applications | Example-Code

Apply resource limits

LDAP client applications can set time limits and size limits on search requests to avoid overuse of server resources. Setting limits is appropriate when the searches your application performs are not fixed, but instead partially or fully determined by user input.

The ldapsearch command has --sizeLimit and --timeLimit options

Request only what you need

Request the attributes you need explicitly and request all the attributes in the same search. This reduces the number of trips to the server and improves the speed of your application

Good Search:

ldapsearch -H ldaps://ldap.berkeley.edu -D "$LDAP_USER" -w $LDAP_PWD -b "dc=berkeley,dc=edu" -s sub "(&(objectClass=person)(uid=2))" uid, givenName


Bad Search:

ldapsearch -H ldaps://ldap.berkeley.edu -D "$LDAP_USER" -w $LDAP_PWD -b "dc=berkeley,dc=edu" -s sub "(&(objectClass=person)(uid=2))" "*"

Use specific LDAP filters

The difference between a general filter (berkeleyEduOfficialEmail=*@berkeley.edu) and a good, specific filter like (berkeleyEduOfficialEmail=agent.cooper@berkeley.edu) significant processing time and a very large number of entries, both for the directory server and for your application. In general, try to use short, specific filters. As a rule, prefer equality filters over substring filters.

Furthermore, always use & with ! to restrict the potential result set before returning all entries that do not match part of the filter. For example:
(&(berkeleyEduDept=FBI)(!(berkeleyEduOfficialEmail=agent.cooper@berkeley.edu)))

Use indexed searches

Some directory servers reject unindexed searches by default because unindexed searches are generally far more resource-intensive. The CalNet directory currently allows unindexed searches but that may change in the future. If your application needs to use a filter that results in an unindexed search, then work with us to find a solution, such as having the directory maintain the indexes required by your application.

See Currently Indexed Properties

Ask the directory server what it supports

Directory servers expose their capabilities, suffixes they support, and other information as attribute values on the root DSE.

This allows your application to discover a variety of information at run time, rather than storing configuration separately. Querying the directory about its configuration and the features it supports can make your application easier to deploy and maintain.

For example, rather than hard-coding dc=berkeley,dc=edu as a suffix DN in your configuration, you can search the root DSE on DS servers for namingContexts, and then search under the naming context DNs to locate the desired entries to initialize your configuration.

The directory servers also advertise supported security mechanisms such as the root DSE attributes supportedTLSCiphers and supportedTLSProtocols.

Directory servers expose their schema over LDAP. The root DSE attribute subschemaSubentry shows the DN of the entry holding LDAP schema definitions.

Trust result codes

The LDAP result codes that your application gets from the directory server is reliable and consistent. For example, if you request a modified operation and you get ResultCode.SUCCESS, then consider the operation a success rather than immediately issuing a search to get the modified entry.

The LDAP replication model is loosely convergent. The directory server will send you ResultCode.SUCCESS before replicating your change to every directory server instance. If you issue a read immediately after a write, and your request is sent to a different directory server instance, you could get an inconsistent result.

Avoid server-side sorting

Directory servers also support a resource-intensive operation called server-side sorting. When your application requests a server-side sort, the directory server retrieves all the entries matching your search, and then returns the whole set of entries in sorted order. For result sets of any size server-side sorting therefore ties up server resources that could be used elsewhere. Alternatives include both sorting the results after your application receives them, and working with our team to enable appropriate browsing (virtual list view) indexes on the directory server for applications that must regularly page through long lists of search results.

Check Result Codes

LDAP result codes are standard and clearly defined, and listed in "LDAP Result Codes". When your application receives a result, it must the result code value to determine what action to take. When the result is not what you expect, read or at least log the additional message information.

DS 7 > LDAP Result Codes

Indexed Properties

The following attributes are indexed and should be preferred for search filters. If your application requires another index please let us know.

Property

Index Type

aci equality, presence
berkeleyEduAffID equality, presence
berkeleyEduAffiliations equality, presence
berkeleyEduAffType equality, presence
berkeleyeduafftypes equality, presence
berkeleyEduAlternateId equality, presence
berkeleyEduAppToken equality, presence
berkeleyEduCADSAffID equality, presence
berkeleyEduCalMailAccountOwner equality, presence
berkeleyEduCalNetAffID equality, presence
berkeleyeducalnetidupdateddate equality, presence
berkeleyEduCalNetIDUpdatedFlag equality, presence
berkeleyeducalnetuidconsolidationdate equality, presence
berkeleyEduCode equality, presence
berkeleyEduConfidentialFlag equality, presence
berkeleyEduCSID equality, presence
berkeleyEduEmailRelFlag equality, presence
berkeleyEduEmpTitleCode equality, presence
berkeleyeduexpdate equality, presence
berkeleyEduGuestSponsorUid equality, presence
berkeleyEduHCMID equality, presence
berkeleyEduIsMemberOf equality, presence
berkeleyEduKerberosPrincipalString equality, presence
berkeleyEduOfficialEmail equality, presence
berkeleyEduOrgUnitParent equality, presence
berkeleyEduOrgUnitProcessUnitFlag equality, presence
berkeleyEduPersonAddressPrimaryFlag equality, presence
berkeleyEduPrimaryDeptUnit equality, presence
berkeleyEduSPAUsersGroup equality, presence
berkeleyEduStuID equality, presence
berkeleyEduTestIDFlag equality, presence
berkeleyEduUCPathID equality, presence
cn equality, substring
createtimestamp equality, ordering
departmentNumber equality, presence
displayName equality, presence, substring
employeeNumber equality, presence
entryUUID equality
givenName equality, substring
mail equality, substring
modifytimestamp equality, ordering
objectClass equality
ou equality
sn equality, substring
telephoneNumber equality, substring
UCnetID equality, presence
uid equality

Example Code: Secure Search

# example secure connection using LDAPS
ldapsearch -H ldaps://ldap.berkeley.edu \
-D "$LDAP_USER" \
-w $LDAP_PWD \
-b "dc=berkeley,dc=edu" \
-s sub \
"(uid=2)"

# example secure connection using LDAP and StartTLS
ldapsearch -ZZ -H ldap://ldap.berkeley.edu \
-D "$LDAP_USER" \
-w $LDAP_PWD \
-b "dc=berkeley,dc=edu" \
-s sub \
"(uid=2)"

Example Code: Connection Reuse

from ldap3 import Server, Connection, SAFE_SYNC
from ldap3 import Tls
import ssl
import os

USER = 'uid=my_app_bind,ou=applications,dc=berkeley,dc=edu'
PASSWD = os.environ['LDAP_PWD']
HOST = 'ldap.berkeley.edu'
BASE = 'dc=berkeley,dc=edu'

def get_connection():
    # force TLSv1.2
     tls_configuration = Tls(validate=ssl.CERT_NONE, version=ssl.PROTOCOL_TLSv1_2)
     server = Server(HOST, use_ssl=True, tls=tls_configuration)
     c = Connection(server, USER, PASSWD, client_strategy=SAFE_SYNC, auto_bind=True)
     return c

def search(c, filter):
    # usually you don't need the original request (4th element of the returned tuple)
     return c.search(BASE, filter, attributes=['uid','berkeleyEduKerberosPrincipalString','berkeleyEduIsMemberOf','berkeleyEduAffiliations'],search_scope='SUBTREE')

def main():
    # get a connection
    c = get_connection()

    # process / loop through data that requires searches
    with open("search-list.txt", "r") as a_file:
        for line in a_file:
            filter = f"(&(objectClass=person)(berkeleyEduKerberosPrincipalString={line}))"
            status, result, response, _ = search(c,filter)
            # do more stuff with the response

    # close the connection after all searches are done
    c.unbind()

if __name__ == "__main__":
    main()

Example Code: Paged Searching

from ldap3 import Server, Connection, SAFE_SYNC
from ldap3 import Tls
import ssl
import os

USER = 'uid=my_app_bind,ou=applications,dc=berkeley,dc=edu'
PASSWD = os.environ['LDAP_PWD']
HOST = 'ldap.berkeley.edu'
BASE = 'dc=berkeley,dc=edu'

def get_connection():
    # force TLSv1.2, we can also do certificate validation here
    tls_configuration = Tls(validate=ssl.CERT_NONE, version=ssl.PROTOCOL_TLSv1_2)
    server = Server(HOST, use_ssl=True, tls=tls_configuration)
    c = Connection(server, USER, PASSWD, client_strategy=SAFE_SYNC, auto_bind=True)
    return c

def paged_search(c, filter):
    entries = c.extend.standard.paged_search(BASE, filter, attributes=['uid', 'givenName'], paged_size=99)
    return entries

def main():
    # get a connection
    c = get_connection()

   # perform a paged search when expecting a large number of returned resources
    # (e.g. all members of a department)
    filter = "(departmentNumber=VRSEC)"
    entries = paged_search(c,filter)
    for entry in entries:
       print(f"{entry}")

   # close the connection after all searches are done
    c.unbind()

if __name__ == "__main__":
    main()