Advanced Usage#

This document covers some of the more advanced features of Bonsai.

Authentication mechanisms#

There are several authentication mechanisms, which can be used with Bonsai. The selected mechanism with the necessary credentials can be set with the LDAPClient.set_credentials() method.

Simple bind#

The simplest way to authenticate with an LDAP server is using the SIMPLE mechanism which requires a bind DN and a password:

>>> import bonsai
>>> client = bonsai.LDAPClient()
>>> client.set_credentials("SIMPLE", user="cn=user,dc=bonsai,dc=test", password="secret")
>>> client.connect()
<bonsai.ldapconnection.LDAPConnection object at 0x7fed62b19828>


Be aware that during the authentication the password is sent to the server in clear text form. It is ill-advised to use simple bind without secure channel (TLS/SSL) in production.

Simple Authentication and Security Layer#

The OpenLDAP library uses the Simple Authentication and Security Layer (SASL) (while the WinLDAP uses the similar SSPI) to provide different authentication mechanisms. Of course to use a certain mechanism it has to be supported both the client and the server. To learn which mechanisms are accessible from the server, check the root DSE’s supportedSASLMechanisms value:

>>> client.get_rootDSE()['supportedSASLMechanisms']


On Unix systems, make sure that the necessary libraries of the certain mechanism are also installed on the client (e.g. libsasl2-modules-gssapi-mit or cyrus-sasl-gssapi for GSSAPI support).


The DIGEST-MD5 and the NTLM mechanisms are challenge-response based authentications. Nowadays they are considered as weak security protocols, but still popular ones. An example of using NTLM:

>>> client.set_credentials("NTLM", "user", "secret")
>>> client.connect().whoami()

The credentials consist of a username and a password, just like for the simple authentication. When using DIGEST-MD5 you can also use an authorization ID during the bind to perform operation under the authority of a different identity afterwards, if the necessary rights are granted for you. NTLM does not support this functionality.

>>> client.set_credentials("DIGEST-MD5", "user", "secret", authz_id="u:root")
>>> client.connect().whoami()


GSSAPI uses Kerberos tickets to authenticate to the server. To use GSSAPI or GSS-SPNEGO the client must be Kerberos-aware, which means the necessary Kerberos tools and libraries have to be installed, and the proper configuration has to be set. (Typically, the configuration is in the /etc/krb5.conf on a Unix system). GSS-SPNEGO mechanism can negotiate a common authentication method between server and client.

Basically, to start a GSSAPI authentication a ticket granting ticket (TGT) needs to be already acquired by the client with the help of the command-line kinit tool:

[noirello@bonsai.test ~]$ kinit admin@BONSAI.TEST
Password for admin@BONSAI.TEST:

The acquired TGT can be listed with klist:

[noirello@bonsai.test ~]$ klist
Ticket cache: FILE:/tmp/krb5cc_1000
Default principal: admin@BONSAI.TEST

Valid starting     Expires            Service principal
22/02/16 22:06:14  23/02/16 08:06:14  krbtgt/BONSAI.TEST@BONSAI.TEST
    renew until 23/02/16 22:06:12

After successfully acquire a TGT, the module can used it for authenticating:

>>> import bonsai
>>> client = bonsai.LDAPClient("ldap://bonsai.test")
>>> client.set_credentials("GSSAPI")
>>> client.connect().whoami()

In normal case the passed credentials with the exception of the authorization ID are irrelevant – at least on a Unix system, the underlying SASL library figures it out on its own. The module’s client can only interfere with the authorization ID:

>>> client.set_credentials("GSSAPI", authz_id="u:chuck")
>>> client.connect().whoami()

But on a Windows system (by default) or if Bonsai is built with the optional Kerberos headers, then it is possible to requesting a TGT with the module’s client if username, password and realm name are all provided:

>>> client = bonsai.LDAPClient("ldap://bonsai.test")
>>> client.set_credentials("GSSAPI", "admin", "secret", "BONSAI.TEST")
>>> client.connect().whoami()

It is also possible to use Kerberos keytabs when the module is built with Kerberos support:

>>> client.set_credentials("GSSAPI", user="chuck", realm="BONSAI.TEST", keytab="./user.keytab")
>>> client.connect().whoami()

Please note that the Kerberos realm names are typically uppercase with few exceptions.


Automatic TGT requesting only accessible on Unix systems if the optional Kerberos headers are provided during the module’s build.


With EXTERNAL mechanism TLS certifications are used to authenticate the user. In certain cases (e.g the remote server is an OpenLDAP directory) the EXTERNAL option is presented as an available SASL mechanism only when the client have built up a TLS connection with the server and already set a client cert.

>>> client = bonsai.LDAPClient("ldap://bonsai.test", tls=True)
>>> client.set_ca_cert_dir('/etc/openldap/certs')
>>> client.set_ca_cert("RootCACert")
>>> client.set_client_cert("BonsaiTestUser")
>>> client.set_client_key("./key.txt")
>>> client.get_rootDSE()['supportedSASLMechanisms']
>>> client.set_credentials("EXTERNAL")
>>> client.connect()
<bonsai.ldapconnection.LDAPConnection object at 0x7f006ad3d888>

For EXTERNAL mechanism only the authorization ID is used in as credential information.

>>> client.set_credentials("EXTERNAL", authz_id=u:chuck")
>>> client.connect()
>>> client.connect().whoami()

The proper way of setting the certifications is depend on the TLS implementation that the LDAP library uses. Please for more information see TLS settings.

TLS settings#

There are two practices to use secure connection:

  • Either use the ldaps:// scheme in the LDAP URL, then the client will use the LDAP over SSL protocol (similar to HTTPS).

  • Or set the tls parameter of the LDAPClient to True, which will instruct the client to preform a StartTLS operation after connected to the LDAP server.

Both practices rely on well-set credentials with the TLS related methods – LDAPClient.set_ca_cert_dir(), LDAPClient.set_ca_cert(), LDAPClient.set_client_cert() and LDAPClient.set_client_key().

These are expecting different inputs depending on which TLS library is used by the LDAP library. To find out which TLS library is used call bonsai.get_tls_impl_name().

GnuTLS and OpenSSL

For GnuTLS and OpenSSL the LDAPClient.set_ca_cert() and LDAPClient.set_client_cert() are expecting file paths that link to certification files in PEM-format.

The LDAPClient.set_ca_cert_dir() works only for OpenSSL if the content of provided directory is symbolic links of certifications that are generated by the c_rehash utility.

Mozilla NSS

When using Mozilla NSS the input of LDAPClient.set_ca_cert_dir() is the path of the directory containing the NSS certificate database (that is created with the certutil command).

The LDAPClient.set_ca_cert() and LDAPClient.set_client_cert() can be used to select the certificate with their names in the certificate database.

If the client certificate is password protected, then the input of LDAPClient.set_client_key() should be a path to the file that contains the password in clear text format.

Microsoft Schannel

Unfortunately, none of the listed TLS modules are effective on Microsoft Windows. The WinLDAP library automatically searches for the corresponding certificates in the cert store. All of the necessary certificates have to be loaded manually before the client tries to use them.

LDAP controls#

Several LDAP controls can be used to extend and improve the basic LDAP operations. Bonsai is supporting the following controls. Always check (the root DSE’s supportedControls) that the server also supports the selected control.

Server side sort#

Using the server side sort control the result of the search is ordered based on the selected attributes. To invoke the control simply set the sort_order parameter of the method:

>>> conn = client.connect()
>>>"ou=nerdherd,dc=bonsai,dc=test", 2, sort_order=["-cn", "gn"])

Attributes that start with - are used for descending order.


Even if the server side sort control is supported by the server there is no guarantee that the results will be sorted for multiple attributes.


The OID of server side sort control is: 1.2.840.113556.1.4.473.

Paged search result#

Paged search can be used to reduce large search result into smaller pages. Page result can be used with the LDAPConnection.paged_search() method and the size of the page can be set with the page_size parameter:

>>> conn = client.connect()
>>> conn.paged_search("ou=nerdherd,dc=bonsai,dc=test", 2, page_size=3)
<_bonsai.ldapsearchiter object at 0x7f006ad455d0>

Please note that the return value of LDAPConnection.paged_search() is an ldapsearchiter. This object can be iterated over the entries of the page. By default the next page of results is acquired automatically during the iteration. This behaviour can be changed by setting the LDAPClient.auto_page_acquire to False and using the ldapsearchiter.acquire_next_page() method which explicitly initiates a new search request to get the next page.


Avoid using server-side referral chasing with paged search. It’s likely to fail with invalid cookie error.


The OID of paged search control is: 1.2.840.113556.1.4.319.

Virtual list view#

Virtual list view (VLV) is also for reducing large search result, but with a more specific manner. Virtual list view mimics the scrolling view of an application: it can select a target entry of a large list (ordered search result) with an offset or an attribute value and receiving only a given number of entries before and after it as a partial result of the entire search.

The LDAPConnection.virtual_list_search() method’s offset or attrvalue can be used to select the target, the before_count and after_count for specifying the number of entries before and after the target.

Also need to set the est_list_count parameter: the estimated size of the entire list by the client. The server will adjust the position of the target entry based on the real list size, estimated size and the offset.

Virtual list view control cannot be used without a server side sort control thus a sort order always has to be set.

>>> conn.virtual_list_search("ou=nerdherd,dc=bonsai,dc=test", 2, attrlist=['cn', 'uidNumber'], sort_order=['-uidNumber'], offset=4, before_count=1, after_count=1, est_list_count=6)
([{'dn': <LDAPDN cn=sam,ou=nerdherd,dc=bonsai,dc=test>, 'cn': ['sam'], 'uidNumber': [4]},
{'dn': <LDAPDN cn=skip,ou=nerdherd,dc=bonsai,dc=test>, 'cn': ['skip'], 'uidNumber': [3]},
{'dn': <LDAPDN cn=jeff,ou=nerdherd,dc=bonsai,dc=test>, 'cn': ['jeff'], 'uidNumber': [2]}],
{'oid': '2.16.840.1.113730.3.4.10', 'target_position': 4, 'list_count': 7})

The return value of the search is a tuple of a list and a dictionary. The dictionary contains the VLV server response: the target position and the real list size.


The OID of virtual list view control is: 2.16.840.1.113730.3.4.9.

Password policy#

Password policy defines a set of rules about accounts and modification of passwords. It allows for the system administrator to set expiration time for passwords and a maximal number of failed login attempts before the account become locked. Is also specifies rules about the quality of password.

Enabling the password policy control with LDAPClient.set_password_policy() method, the client can receive additional information during connecting to a server or modifying a user’s password. Setting this control will change the return value of LDAPClient.connect() and to a tuple of LDAPConnection and a dictionary that contains the remaining seconds until the password’s expiration and the remaining grace logins. The client can also receive new exceptions related to password modifications.

>>> import bonsai
>>> client = bonsai.LDAPClient()
>>> client.set_credentials("SIMPLE", "cn=user,dc=bonsai,dc=test", "secret")
>>> client.set_password_policy(True)
>>> conn, ctrl = client.connect()
>>> conn
<bonsai.ldapconnection.LDAPConnection object at 0x7fa552ab4e28>
>>> ctrl
{'grace': 1, 'expire': 3612, 'oid': ''})

If the server does not support password policy control or the given credentials does not have policies (like anonymous or administrator user) the second item in the tuple will be None.


Because the password policy is not standardized, it is not listed by the server among the supportedControls even if it is available.


Password policy control cannot be used on MS Windows with WinLDAP. In this case after opening a connection the control dictionary will always be None.

Extended DN#

Setting LDAP_SERVER_EXTENDED_DN control with LDAPClient.set_extended_dn() will extend the standard DN format with the SID and GUID attributes to <GUID=xxxxxxxx>;<SID=yyyyyyyyy>;distinguishedName during the LDAP search. The method’s parameter can be either 0 which means that the GUID and SID strings will be in a hexadecimal string format or 1 for receiving the extended dn in a standard string format. This control is only supported by Microsoft’s Active Directory.

Regardless of setting the control, the LDAPEntry.dn still remains a simple LDAPDN object without the SID or GUID extensions. The extended DN will be set to the LDAPEntry.extended_dn as a string. The extended DN control also affects other LDAP attributes that use distinguished names (e.g. memberOf attribute).

>>> client = bonsai.LDAPClient()
>>> client.set_extended_dn(1)
>>> result ="ou=nerdherd,dc=bonsai,dc=test", 1)
>>> result[0].extended_dn
>>> result[0].dn
<LDAPDN cn=chuck,ou=nerdherd,dc=bonsai,dc=test>


The OID of extended DN control is: 1.2.840.113556.1.4.529.

Server tree delete#

Server tree delete control allows the client to remove entire subtree with a single request if the user has appropriate permissions to remove every corresponding entry. Setting the recursive parameter of LDAPConnection.delete() and LDAPEntry.delete() to True will send the control with the delete request automatically, no further settings are required.


The OID of server tree delete control is: 1.2.840.113556.1.4.805


The ManageDsaIT control can be used to work with LDAP referrals as simple LDAP entries. After setting it with the LDAPClient.set_managedsait() method, the referrals can be added removed, and modified just like entries.

>>> client = bonsai.LDAPClient()
>>> client.set_managedsait(True)
>>> conn = client.connect()
>>> ref ="o=admin-ref,ou=nerdherd,dc=bonsai,dc=test", 0)[0]
>>> ref
{'dn': <LDAPDN o=admin-ref,ou=nerdherd,dc=bonsai,dc=test>, 'objectClass': ['referral',
'extensibleObject'], 'o': ['admin-ref']}
>>> type(ref)
<class 'bonsai.ldapentry.LDAPEntry'>


The OID of ManageDsaIT control is: 2.16.840.1.113730.3.4.2

SD Flags#

The SD Flags control can be used to manage the portion of a Windows security descriptor to retrieve. The flag can be the combination of the following:

  • 0x1 - Get the owner identifier of the object.

  • 0x2 - Get the primary group identifier.

  • 0x4 . Get the discretionary access control list (DACL) of the object.

  • 0x8 - Get the system access control list (SACL) of the object.

Use LDAPClient.set_sd_flags() method to set the value of the flag.


The OID of SD_FLAGS control is: 1.2.840.113556.1.4.801

Using connection pools#

When your application requires to use multiple open LDAP connections, Bonsai provides you connection pools to help you creating and accessing them. This way you can acquire an opened connection, do some operations and put it back into the pool for other threads/tasks to use.

import bonsai
import threading
from bonsai.pool import ThreadedConnectionPool

def work(pool):
    with pool.spawn() as conn:
        # Some other operations...

client = bonsai.LDAPClient()
pool = ThreadedConnectionPool(client, minconn=5, maxconn=10)
thr = threading.Thread(target=work, args=(pool,))
conn = pool.get()
res =
# After finishing up...

Connections in a pool are opened when the pool is created or when a connection is requested from the pool with bonsai.pool.ConnectionPool.get(). They then remain open until the entire pool is closed.

Connection timeouts#

Some LDAP servers, or some firewalls between a program and the LDAP server, will cut off connections if they have been idle for too long. Sometimes this is done silently and the only sign is that the next request will time out. You may therefore want to set a timeout when using connections from a pool so that your program doesn’t wait for a TCP timeout (which can take a very long time).

Bonsai does not detect that a connection has timed out or is otherwise in an error state and should not be used again. However, you can close the connection before returning it to the pool, and Bonsai will then discard it and not reuse it and will open a new replacement connection as needed.

One possible pattern for using a connection pool with an LDAP server that may silently time out connections is to close connections on failure and retry one more time than there are idle connections in the pool. In the worst case where all idle connections have been timed out, each of those idle connections will fail and be closed and then you’ll get a fresh connection that should not have timed out (and if that still fails, there is some deeper problem).

def work(pool, base, scope, filter_exp, attrlist):
    idle_count = pool.idle_connection
    for try in range(idle_count + 1):
        with pool.spawn() as conn:
                result =
                    base, scope, filter_exp, attrlist, timeout=10
                # Use the results somehow....
            except bonsai.ConnectionError as e:
                # If all of the idle connections have been tried and the
                # call still failed, there is something deeper wrong.
                if try == idle_count:

When using bonsai.asyncio.AIOConnectionPool, also catch asyncio.TimeoutError, which may be raised by a connection timeout.

Reading and writing LDIF files#

Bonsai has a limited support to read and write LDIF files. LDIF (LDAP Data Interchange Format) is a plain text file format for representing LDAP changes and updates. It can be used to exchange data between directory servers.

To read an LDIF file, simply open the file in read-mode, pass it to the LDIFReader, then the reader object can be used as an iterator to get the entries from the LDIF file.

from bonsai import LDIFReader

with open("users.ldif", "r") as data:
    reader = LDIFReader(data)
    for ent in reader:

Writing LDIF files is similar. The LDIFWriter needs an open file-object in write-mode, and the LDIFWriter.write_entry() expects an LDAPEntry object whose attributes will be serialised. It also possible to serialise the changes of an entry with LDIFWriter.write_changes().

from bonsai import LDAPClient
from bonsai import LDIFWriter

client = LDAPClient("ldap://bonsai.test")
with client.connect() as conn:
    res ="cn=jeff,ou=nerdherd,dc=bonsai,dc=test", 0)
    with open("user.ldif", "w") as data:
        writer = LDIFWriter(data)
    # Make some changes on the entry.
    res[0]["homeDirectory"] = "/opt/jeff"
    with open("changes.ldif", "w") as data:
        writer = LDIFWriter(data)


As mentioned above LDIFReader and LDIFWriter have their limitations. They can handle basic attribute changes (adding, modifying and removing), serialising attributes, but they’re not capable to cope with deleting and renaming entries, or processing LDAP controls that are presented in the LDIF file.

Asynchronous operations#

Asynchronous operations are first-class citizens in the underlying C API that Bonsai is built on. That makes relatively easy to integrate the module with popular Python async libraries. Bonsai is shipped with support to some: asyncio, gevent, Tornado and trio.

Using async out-of-the-box#

To start asynchronous operations set the LDAPClient.connect() method’s is_async parameter to True. By default the returned connection object can be used with Python’s asyncio library. For further details about how to use asyncio see the official documentation.

An example for asynchronous search and modify with asyncio:

import asyncio
import sys

import bonsai

if sys.platform == 'win32':
    # The current asyncio connection implementation requires
    # the old selector event loop when running on Windows.

async def do():
    cli = bonsai.LDAPClient("ldap://localhost")
    async with cli.connect(is_async=True) as conn:
        results = await"ou=nerdherd,dc=bonsai,dc=test", 1)
        for res in results:
        search = await"cn=chuck,ou=nerdherd,dc=bonsai,dc=test", 0)
        entry = search[0]
        entry['mail'] = ""
        await entry.modify()

To work with other non-blocking I/O modules the default asynchronous class has to be set to a different one with LDAPClient.set_async_connection_class().


The default asyncio event loop is changed with Python 3.8 on Windows to ProactorEventLoop. Unfortunately, bonsai’s asyncio connection requires the old SelectorEventLoop. Make sure to change it like in the example before using the module. Otherwise the add_reader method will raise NotImplementedError.

For example changing it to GeventLDAPConnection makes it possible to use the module with gevent:

import gevent

import bonsai
from bonsai.gevent import GeventLDAPConnection

def do():
    cli = bonsai.LDAPClient("ldap://localhost")
    # Change the default async conn class.
    with cli.connect(True) as conn:
        results ="ou=nerdherd,dc=bonsai,dc=test", 1)
        for res in results:
        search ="cn=chuck,ou=nerdherd,dc=bonsai,dc=test", 0)
        entry = search[0]
        entry['mail'] = ""



Since 1.2.1, to achieve non-blocking socket connection, the bonsai.set_connect_async(True) has to be called before connecting the server. But setting it and using TLS can cause errors even on newer Ubuntu releases (18.04+), thus it has been turned off by default on every platform.

Create your own async class#

If you would like to use an asynchronous library that is currently not supported by Bonsai, then you have to work a little bit more to make it possible. The following example will help you to achieve that by showing how to create a new async class for Curio. Inspecting the implementations of the supported libraries can also help.


This class is just for education purposes, the implementation is made after just scraping the surface of Curio. It’s far from perfect and not meant to use in production.

The C API’s asynchronous functions are designed to return a message ID immediately after calling them, and then polling the state of the executed operations. The BaseLDAPConnection class exposes the same functionality of the C API. Therefore it makes possible to start an operation then poll the result with LDAPConnection.get_result() periodically in the _evaluate method which happens to be called in every other method that evaluates an LDAP operation.

from typing import Optional
import bonsai
import curio

from bonsai.ldapconnection import BaseLDAPConnection

# You have to inherit from BaseLDAPConnection.
class CurioLDAPConnection(BaseLDAPConnection):
    def __init__(self, client: "LDAPClient"):
        super().__init__(client, is_async=True)

    async def _evaluate(self, msg_id: int, timeout: Optional[float] = None):
        while True:
            res = self.get_result(msg_id)
            if res is not None:
                return res
            await curio.sleep(1)

The constant polling can be avoided with voluntarily sleep, but it’s more efficient to register to an I/O event that will notify when the data is available. The LDAPConnection.fileno() method returns the socket’s file descriptor that can be used with the OS’s default I/O monitoring function (e.g select or epoll) for this purpose. In Curio you can wait until a socket becomes writable with curio.traps._write_wait:

async def _evaluate(self, msg_id: int, timeout: Optional[float] = None):
    while True:
        await curio.traps._write_wait(self.fileno())
        res = self.get_result(msg_id)
        if res is not None:
            return res

The following code is a simple litmus test for proving that the created class plays nice with other coroutines:

async def countdown(n):
    while n > 0:
        print(f"T-minus {n}")
        await curio.sleep(1)
        n -= 1

async def search():
    cli = bonsai.LDAPClient()
    conn = await cli.connect(is_async=True)
    res = await"ou=nerdherd,dc=bonsai,dc=test", 1)
    for ent in res:

async def tasks():
    tsk1 = await curio.spawn(countdown, 20)
    tsk2 = await curio.spawn(search)
    await tsk1.join()
    await tsk2.join()

if __name__ == "__main__":

This example class has the minimal functionalities only but hopefully gives you the basic idea how the asynchronous integration works.

Utilities for Active Directory#

Bonsai has some helper classes to work with Windows-specific attributes in Active Directory.

Security descriptor#

The Windows security descriptor is a data structure containing the security information associated with a securable object. It’s stored in the ntSecurityDescriptor attribute as a binary data. To parse it use the SecurityDescriptor.from_binary() method:

>>> import bonsai
>>> from bonsai.active_directory import SecurityDescriptor
>>> client = LDAPClient("ldap://localhost")
>>> client.set_credentials("SIMPLE", user="cn=chuck,ou=nerdherd,dc=bonsai,dc=test", password="secret")
>>> conn = client.connect()
>>> entry ="cn=chuck,ou=nerdherd,dc=bonsai,dc=test", 0, attrlist=["ntSecurityDescriptor"])[0]
>>> sec_desc = SecurityDescriptor.from_binary(entry["ntSecurityDescriptor"][0])
>>> print(sec_desc.owner_sid)
<SID: S-1-5-21-3623811015-3361044348-30300820-1013>
>>> print(sec_desc.dacl)
<bonsai.active_directory.acl.ACL object at 0x7f388bc4d6a0>


The UserAccountControl attribute contains a range of flags which define some important basic properties of a user object like whether the account is active or locked, whether the option of password change at the next logon is enabled, etc. The UserAccountControl helps to parse this information:

>>> from bonsai.active_directory import UserAccountControl
>>> uac = UserAccountControl(entry['userAccountControl'][0])
>>> uac
<bonsai.active_directory.UserAccountControl object at 0x00FC38D0>
{'script': False, 'accountdisable': False, 'homedir_required': False, 'lockout': False, 'passwd_notreqd': False, 'passwd_cant_change': False, 'encrypted_text_pwd_allowed': False, 'temp_duplicate_account': False, 'normal_account': True, 'interdomain_trust_account': False, 'workstation_trust_account': False, 'server_trust_account': False, 'dont_expire_password': True, 'mns_logon_account': False, 'smartcard_required': False, 'trusted_for_delegation': False, 'not_delegated': False, 'use_des_key_only': False, 'dont_req_preauth': False, 'password_expired': False, 'trusted_to_auth_for_delegation': False, 'partial_secrets_account': False}