File: advanced.rst

package info (click to toggle)
python-bonsai 1.5.3%2Bds-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 1,136 kB
  • sloc: python: 6,816; ansic: 5,549; makefile: 170; sh: 90
file content (741 lines) | stat: -rw-r--r-- 31,572 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
Advanced Usage
**************

.. module:: bonsai
    :noindex:

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

.. _auth-mechs:

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 :meth:`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>

.. warning::
    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']
    ['GSS-SPNEGO', 'GSSAPI', 'DIGEST-MD5', 'NTLM']

.. note::
    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).

.. _SASL: https://tools.ietf.org/html/rfc4422
.. _SSPI: https://msdn.microsoft.com/en-us/library/windows/desktop/aa380493%28v=vs.85%29.aspx


DIGEST-MD5 and NTLM
^^^^^^^^^^^^^^^^^^^

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()
    "dn:cn=user,dc=bonsai,dc=test"

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()
    "dn:cn=admin,dc=bonsai,dc=test"

GSSAPI and GSS-SPNEGO
^^^^^^^^^^^^^^^^^^^^^

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()
    'dn:cn=admin,dc=bonsai,dc=test'

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()
    'dn:cn=chuck,ou=nerdherd,dc=bonsai,dc=test'

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()
    'dn:cn=admin,dc=bonsai,dc=test'

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()
    'dn:cn=chuck,ou=nerdherd,dc=bonsai,dc=test'

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

.. note::
    Automatic TGT requesting only accessible on Unix systems if the optional Kerberos headers are
    provided during the module's build.
  
EXTERNAL
^^^^^^^^

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']
    ['GSS-SPNEGO', 'GSSAPI', 'DIGEST-MD5', 'EXTERNAL', 'NTLM']   
    >>> 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()
    'dn:cn=chuck,ou=nerdherd,dc=bonsai,dc=test'


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

.. _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 :class:`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 --
:meth:`LDAPClient.set_ca_cert_dir`, :meth:`LDAPClient.set_ca_cert`,
:meth:`LDAPClient.set_client_cert` and :meth:`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
:func:`bonsai.get_tls_impl_name`.

.. rubric:: GnuTLS and OpenSSL

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

The :meth:`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.

.. rubric:: Mozilla NSS

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

The :meth:`LDAPClient.set_ca_cert` and :meth:`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
:meth:`LDAPClient.set_client_key` should be a path to the file that contains the password in clear
text format.

.. rubric:: 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:

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
:meth:`LDAPConnection.search` method:

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

Attributes that start with `-` are used for descending order.

.. warning::
    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.

.. note::
    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 :meth:`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 :meth:`LDAPConnection.paged_search` is an :class:`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 
:attr:`LDAPClient.auto_page_acquire` to `False` and using the :meth:`ldapsearchiter.acquire_next_page`
method which explicitly initiates a new search request to get the next page.

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

.. note::
    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 :meth:`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.

.. note::
    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 :meth:`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 :meth:`LDAPClient.connect` and
:meth:`LDAPConnection.open` to a tuple of :class:`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': '1.3.6.1.4.1.42.2.27.8.5.1'})

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`.

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

.. note::
    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 :meth:`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 :attr:`LDAPEntry.dn` still remains a simple :class:`LDAPDN`
object without the SID or GUID extensions. The extended DN will be set to the :attr:`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 = conn.search("ou=nerdherd,dc=bonsai,dc=test", 1)
    >>> result[0].extended_dn
    <GUID=899e4e01-e88d-4dea-ba64-119ed386b61c>;<SID=S-1-5-21-101232111302-1767724339-724445543-12345>;cn=chuck,ou=nerdherd,dc=bonsai,dc=test
    >>> result[0].dn
    <LDAPDN cn=chuck,ou=nerdherd,dc=bonsai,dc=test>

.. note::
    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 :meth:`LDAPConnection.delete` and :meth:`LDAPEntry.delete` to `True` will send 
the control with the delete request automatically, no further settings are required.

.. note::
    The OID of server tree delete control is: 1.2.840.113556.1.4.805

ManageDsaIT
-----------

The ManageDsaIT control can be used to work with LDAP referrals as simple LDAP entries. After
setting it with the :meth:`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 = conn.search("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'>

.. note::
    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 :meth:`LDAPClient.set_sd_flags` method to set the value of the flag.

.. note::
    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.

.. code-block:: python3

    import bonsai
    import threading
    from bonsai.pool import ThreadedConnectionPool

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

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

Connections in a pool are opened when the pool is created or when a connection
is requested from the pool with :meth:`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).

.. code-block:: python3

    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:
                try:
                    result = conn.search(
                        base, scope, filter_exp, attrlist, timeout=10
                    )
                    # Use the results somehow....
                except bonsai.ConnectionError as e:
                    conn.close()
                    # If all of the idle connections have been tried and the
                    # call still failed, there is something deeper wrong.
                    if try == idle_count:
                        raise

When using :class:`bonsai.asyncio.AIOConnectionPool`, also catch
:class:`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 :class:`LDIFReader`,
then the reader object can be used as an iterator to get the entries from the LDIF file.

.. code-block:: python3

    from bonsai import LDIFReader

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


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

.. code-block:: python3

    from bonsai import LDAPClient
    from bonsai import LDIFWriter

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

.. note::

    As mentioned above :class:`LDIFReader` and :class:`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`_.

.. _asyncio: https://docs.python.org/3/library/asyncio.html
.. _gevent: http://www.gevent.org/
.. _Tornado: http://www.tornadoweb.org/en/stable/
.. _trio: https://trio.readthedocs.io/en/stable/

Using async out-of-the-box
--------------------------

To start asynchronous operations set the :meth:`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`_.

.. _official documentation: https://docs.python.org/3/library/asyncio.html

An example for asynchronous search and modify with `asyncio`:

.. code-block:: python3

    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.
        asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())

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

    asyncio.run(do())

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

.. note::
    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:

.. code-block:: python

    import gevent

    import bonsai
    from bonsai.gevent import GeventLDAPConnection

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

    gevent.joinall([gevent.spawn(do)])

.. note::
    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.

.. warning::
    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 :meth:`LDAPConnection.get_result()` periodically in the `_evaluate` method which
happens to be called in every other method that evaluates an LDAP operation.

.. code-block:: python3

    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 :meth:`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`:

.. code-block:: python3

        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:

.. code-block:: python3

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

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

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

    if __name__ == "__main__":
        curio.run(tasks)

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

.. _Curio: https://curio.readthedocs.io/en/latest/

Utilities for Active Directory
==============================

.. module:: bonsai.active_directory
    :noindex:

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 :meth:`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 = conn.search("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>

UserAccountControl
------------------

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 :class:`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>
    >>> uac.properties
    {'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}