0
# LDIF Format Support
1
2
Read and write LDAP Data Interchange Format (LDIF) files for data import/export, backup operations, and batch processing. LDIF support includes parsing, writing, change records, and URL loading capabilities.
3
4
## Capabilities
5
6
### LDIF Reader
7
8
Parse LDIF files into LDAP entries with support for continuation lines, comments, and external URL references.
9
10
```python { .api }
11
from bonsai.ldif import LDIFReader, LDIFError
12
13
class LDIFReader:
14
def __init__(
15
self,
16
input_file: TextIO,
17
autoload: bool = True,
18
max_length: int = 76
19
) -> None:
20
"""
21
Initialize LDIF reader for parsing LDIF format files.
22
23
Parameters:
24
- input_file: File-like object in text mode containing LDIF data
25
- autoload: Allow automatic loading of external URL sources
26
- max_length: Maximum line length allowed in LDIF file
27
"""
28
29
def parse(self) -> Iterator[Tuple[str, Dict[str, List[Union[str, bytes]]]]]:
30
"""
31
Parse LDIF file and yield entries.
32
33
Yields:
34
Tuple of (dn, attributes_dict) for each entry
35
36
Where attributes_dict maps attribute names to lists of values.
37
Binary values are returned as bytes, text values as strings.
38
"""
39
40
def parse_entry_records(self) -> Iterator[LDAPEntry]:
41
"""
42
Parse LDIF file and yield LDAPEntry objects.
43
44
Yields:
45
LDAPEntry objects for each entry in the LDIF file
46
"""
47
48
def parse_change_records(self) -> Iterator[Tuple[str, str, Dict[str, Any]]]:
49
"""
50
Parse LDIF change records (add, modify, delete, modrdn operations).
51
52
Yields:
53
Tuple of (dn, changetype, change_data) for each change record
54
55
Where:
56
- dn: Distinguished name of the entry
57
- changetype: Operation type ("add", "modify", "delete", "modrdn")
58
- change_data: Dictionary with operation-specific data
59
"""
60
61
def parse_entries_and_change_records(self) -> Iterator[Union[
62
Tuple[str, Dict[str, List[Union[str, bytes]]]],
63
Tuple[str, str, Dict[str, Any]]
64
]]:
65
"""
66
Parse LDIF file yielding both entries and change records.
67
68
Yields:
69
Mixed entries and change records as tuples
70
"""
71
72
@property
73
def version(self) -> Optional[int]:
74
"""LDIF version number from version line."""
75
76
@property
77
def autoload(self) -> bool:
78
"""Whether automatic URL loading is enabled."""
79
80
@property
81
def max_length(self) -> int:
82
"""Maximum allowed line length."""
83
84
def set_resource_handler(self, scheme: str, handler: Callable[[str], bytes]) -> None:
85
"""
86
Set custom handler for loading external resources by URL scheme.
87
88
Parameters:
89
- scheme: URL scheme (e.g., "http", "ftp")
90
- handler: Function that takes URL string and returns bytes
91
"""
92
93
def get_resource_handler(self, scheme: str) -> Optional[Callable[[str], bytes]]:
94
"""Get resource handler for URL scheme."""
95
```
96
97
### LDIF Writer
98
99
Write LDAP entries and change records to LDIF format files with proper encoding and formatting.
100
101
```python { .api }
102
from bonsai.ldif import LDIFWriter
103
104
class LDIFWriter:
105
def __init__(
106
self,
107
output_file: TextIO,
108
base64_attrs: Optional[List[str]] = None,
109
cols: int = 76
110
) -> None:
111
"""
112
Initialize LDIF writer for creating LDIF format files.
113
114
Parameters:
115
- output_file: File-like object in text mode for writing LDIF data
116
- base64_attrs: List of attribute names to always base64 encode
117
- cols: Maximum column width for line wrapping
118
"""
119
120
def write_entry(self, dn: str, entry: Union[Dict[str, Any], LDAPEntry]) -> None:
121
"""
122
Write LDAP entry to LDIF file.
123
124
Parameters:
125
- dn: Distinguished name of the entry
126
- entry: Dictionary or LDAPEntry with attributes and values
127
"""
128
129
def write_entries(self, entries: Iterable[Union[LDAPEntry, Tuple[str, Dict]]]) -> None:
130
"""
131
Write multiple LDAP entries to LDIF file.
132
133
Parameters:
134
- entries: Iterable of LDAPEntry objects or (dn, attributes) tuples
135
"""
136
137
def write_change_record(
138
self,
139
dn: str,
140
changetype: str,
141
change_data: Dict[str, Any]
142
) -> None:
143
"""
144
Write LDIF change record to file.
145
146
Parameters:
147
- dn: Distinguished name
148
- changetype: Operation type ("add", "modify", "delete", "modrdn")
149
- change_data: Change-specific data dictionary
150
"""
151
152
def write_version(self, version: int = 1) -> None:
153
"""
154
Write LDIF version line.
155
156
Parameters:
157
- version: LDIF version number (default: 1)
158
"""
159
160
def write_comment(self, comment: str) -> None:
161
"""
162
Write comment line to LDIF file.
163
164
Parameters:
165
- comment: Comment text (without # prefix)
166
"""
167
168
def write_dn(self, dn: str) -> None:
169
"""
170
Write DN line to LDIF file.
171
172
Parameters:
173
- dn: Distinguished name
174
"""
175
176
def write_attribute(self, name: str, value: Union[str, bytes]) -> None:
177
"""
178
Write single attribute line to LDIF file.
179
180
Parameters:
181
- name: Attribute name
182
- value: Attribute value (string or bytes)
183
"""
184
185
def flush(self) -> None:
186
"""Flush output buffer to file."""
187
188
@property
189
def cols(self) -> int:
190
"""Maximum column width for line wrapping."""
191
192
@property
193
def base64_attrs(self) -> Optional[List[str]]:
194
"""List of attributes that are always base64 encoded."""
195
```
196
197
### LDIF Exceptions
198
199
Specialized exception for LDIF parsing and writing errors.
200
201
```python { .api }
202
from bonsai.ldif import LDIFError
203
204
class LDIFError(LDAPError):
205
"""Exception raised during LDIF file reading or writing operations."""
206
207
@property
208
def code(self) -> int:
209
"""Error code (-300 for LDIF errors)."""
210
```
211
212
## Usage Examples
213
214
### Reading LDIF Files
215
216
```python
217
from bonsai.ldif import LDIFReader
218
import io
219
220
# Read LDIF from file
221
with open('directory_export.ldif', 'r', encoding='utf-8') as f:
222
reader = LDIFReader(f)
223
224
# Parse entries
225
for dn, attributes in reader.parse():
226
print(f"Entry: {dn}")
227
for attr_name, values in attributes.items():
228
print(f" {attr_name}: {values}")
229
print()
230
231
# Read LDIF from string
232
ldif_data = '''
233
version: 1
234
235
dn: cn=John Doe,ou=people,dc=example,dc=com
236
objectClass: person
237
objectClass: organizationalPerson
238
cn: John Doe
239
sn: Doe
240
givenName: John
241
mail: john.doe@example.com
242
description: Software Engineer
243
244
dn: cn=Jane Smith,ou=people,dc=example,dc=com
245
objectClass: person
246
objectClass: organizationalPerson
247
cn: Jane Smith
248
sn: Smith
249
givenName: Jane
250
mail: jane.smith@example.com
251
telephoneNumber: +1-555-0123
252
'''
253
254
reader = LDIFReader(io.StringIO(ldif_data))
255
print(f"LDIF Version: {reader.version}")
256
257
# Parse into LDAPEntry objects
258
for entry in reader.parse_entry_records():
259
print(f"Entry DN: {entry.dn}")
260
print(f"Common Name: {entry['cn'][0]}")
261
print(f"Object Classes: {', '.join(entry['objectClass'])}")
262
if 'mail' in entry:
263
print(f"Email: {entry['mail'][0]}")
264
print()
265
```
266
267
### Writing LDIF Files
268
269
```python
270
from bonsai.ldif import LDIFWriter
271
from bonsai import LDAPEntry
272
import io
273
274
# Create LDIF writer
275
output = io.StringIO()
276
writer = LDIFWriter(output)
277
278
# Write version
279
writer.write_version(1)
280
281
# Write comment
282
writer.write_comment("Directory export created on 2024-01-15")
283
284
# Create and write entries
285
people = [
286
{
287
'dn': 'cn=Alice Johnson,ou=people,dc=example,dc=com',
288
'objectClass': ['person', 'organizationalPerson', 'inetOrgPerson'],
289
'cn': 'Alice Johnson',
290
'sn': 'Johnson',
291
'givenName': 'Alice',
292
'mail': 'alice.johnson@example.com',
293
'employeeNumber': '12345'
294
},
295
{
296
'dn': 'cn=Bob Wilson,ou=people,dc=example,dc=com',
297
'objectClass': ['person', 'organizationalPerson'],
298
'cn': 'Bob Wilson',
299
'sn': 'Wilson',
300
'givenName': 'Bob',
301
'telephoneNumber': '+1-555-0456'
302
}
303
]
304
305
for person in people:
306
dn = person.pop('dn')
307
writer.write_entry(dn, person)
308
309
# Get LDIF output
310
ldif_output = output.getvalue()
311
print(ldif_output)
312
313
# Write to file
314
with open('people_export.ldif', 'w', encoding='utf-8') as f:
315
writer = LDIFWriter(f)
316
writer.write_version(1)
317
318
# Write LDAPEntry objects directly
319
for person in people:
320
entry = LDAPEntry(person['dn'])
321
for attr, value in person.items():
322
if attr != 'dn':
323
entry[attr] = value
324
writer.write_entry(str(entry.dn), entry)
325
```
326
327
### Processing Change Records
328
329
```python
330
from bonsai.ldif import LDIFReader, LDIFWriter
331
import io
332
333
# LDIF with change records
334
change_ldif = '''
335
version: 1
336
337
# Add new user
338
dn: cn=New User,ou=people,dc=example,dc=com
339
changetype: add
340
objectClass: person
341
objectClass: organizationalPerson
342
cn: New User
343
sn: User
344
givenName: New
345
346
# Modify existing user
347
dn: cn=John Doe,ou=people,dc=example,dc=com
348
changetype: modify
349
replace: mail
350
mail: john.doe.new@example.com
351
-
352
add: telephoneNumber
353
telephoneNumber: +1-555-9999
354
-
355
356
# Delete user
357
dn: cn=Old User,ou=people,dc=example,dc=com
358
changetype: delete
359
360
# Rename user
361
dn: cn=Jane Smith,ou=people,dc=example,dc=com
362
changetype: modrdn
363
newrdn: cn=Jane Smith-Brown
364
deleteoldrdn: 1
365
newsuperior: ou=married,ou=people,dc=example,dc=com
366
'''
367
368
# Parse change records
369
reader = LDIFReader(io.StringIO(change_ldif))
370
371
for change in reader.parse_change_records():
372
dn, changetype, change_data = change
373
print(f"Change: {changetype} on {dn}")
374
375
if changetype == 'add':
376
print(" Adding new entry with attributes:")
377
for attr, values in change_data.items():
378
print(f" {attr}: {values}")
379
380
elif changetype == 'modify':
381
print(" Modifications:")
382
for mod in change_data['modifications']:
383
op, attr, values = mod
384
print(f" {op} {attr}: {values}")
385
386
elif changetype == 'delete':
387
print(" Deleting entry")
388
389
elif changetype == 'modrdn':
390
print(f" New RDN: {change_data['newrdn']}")
391
print(f" Delete old RDN: {change_data['deleteoldrdn']}")
392
if 'newsuperior' in change_data:
393
print(f" New superior: {change_data['newsuperior']}")
394
print()
395
```
396
397
### Binary Data Handling
398
399
```python
400
from bonsai.ldif import LDIFReader, LDIFWriter
401
import base64
402
import io
403
404
# LDIF with binary data (base64 encoded)
405
binary_ldif = '''
406
version: 1
407
408
dn: cn=user-with-photo,ou=people,dc=example,dc=com
409
objectClass: person
410
objectClass: organizationalPerson
411
cn: user-with-photo
412
sn: Photo
413
jpegPhoto:: /9j/4AAQSkZJRgABAQEAYABgAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNA...
414
userCertificate:: MIIDXTCCAkWgAwIBAgIJAKoK...
415
'''
416
417
# Read binary data
418
reader = LDIFReader(io.StringIO(binary_ldif))
419
420
for dn, attributes in reader.parse():
421
print(f"Entry: {dn}")
422
for attr, values in attributes.items():
423
if attr in ['jpegPhoto', 'userCertificate']:
424
# Binary attributes are returned as bytes
425
for value in values:
426
if isinstance(value, bytes):
427
print(f" {attr}: <binary data, {len(value)} bytes>")
428
else:
429
print(f" {attr}: {value}")
430
else:
431
print(f" {attr}: {values}")
432
433
# Write binary data
434
output = io.StringIO()
435
writer = LDIFWriter(output, base64_attrs=['jpegPhoto', 'userCertificate'])
436
437
# Binary data will be automatically base64 encoded
438
binary_data = b'\x89PNG\r\n\x1a\n...' # PNG image data
439
writer.write_entry('cn=test,dc=example,dc=com', {
440
'objectClass': ['person'],
441
'cn': 'test',
442
'sn': 'user',
443
'jpegPhoto': binary_data # Will be base64 encoded automatically
444
})
445
446
print(output.getvalue())
447
```
448
449
### Batch Import/Export Operations
450
451
```python
452
from bonsai import LDAPClient, LDAPEntry
453
from bonsai.ldif import LDIFReader, LDIFWriter
454
455
def export_to_ldif(client, base_dn, output_file):
456
"""Export LDAP directory tree to LDIF file."""
457
with client.connect() as conn:
458
# Search entire subtree
459
results = conn.search(base_dn, 2) # SUBTREE scope
460
461
with open(output_file, 'w', encoding='utf-8') as f:
462
writer = LDIFWriter(f)
463
writer.write_version(1)
464
writer.write_comment(f"Export of {base_dn} subtree")
465
466
for entry in results:
467
writer.write_entry(str(entry.dn), entry)
468
469
print(f"Exported {len(results)} entries to {output_file}")
470
471
def import_from_ldif(client, input_file):
472
"""Import entries from LDIF file to LDAP directory."""
473
with open(input_file, 'r', encoding='utf-8') as f:
474
reader = LDIFReader(f)
475
476
with client.connect() as conn:
477
imported_count = 0
478
479
for entry in reader.parse_entry_records():
480
try:
481
conn.add(entry)
482
imported_count += 1
483
print(f"Imported: {entry.dn}")
484
485
except Exception as e:
486
print(f"Failed to import {entry.dn}: {e}")
487
488
print(f"Successfully imported {imported_count} entries")
489
490
# Usage
491
client = LDAPClient("ldap://localhost")
492
client.set_credentials("SIMPLE", user="cn=admin,dc=example,dc=com", password="secret")
493
494
# Export directory to LDIF
495
export_to_ldif(client, "ou=people,dc=example,dc=com", "people_backup.ldif")
496
497
# Import from LDIF
498
import_from_ldif(client, "people_restore.ldif")
499
```
500
501
### Custom URL Handlers
502
503
```python
504
from bonsai.ldif import LDIFReader
505
import io
506
import requests
507
508
def http_handler(url):
509
"""Custom handler for HTTP URLs."""
510
response = requests.get(url)
511
response.raise_for_status()
512
return response.content
513
514
def ftp_handler(url):
515
"""Custom handler for FTP URLs."""
516
# Implementation for FTP URL loading
517
pass
518
519
# LDIF with external URL references
520
url_ldif = '''
521
version: 1
522
523
dn: cn=user-with-cert,ou=people,dc=example,dc=com
524
objectClass: person
525
cn: user-with-cert
526
sn: Cert
527
userCertificate:< http://example.com/certs/user.crt
528
jpegPhoto:< file:///path/to/photo.jpg
529
'''
530
531
reader = LDIFReader(io.StringIO(url_ldif), autoload=True)
532
533
# Register custom URL handlers
534
reader.set_resource_handler('http', http_handler)
535
reader.set_resource_handler('https', http_handler)
536
reader.set_resource_handler('ftp', ftp_handler)
537
538
# Parse will automatically load external resources
539
for dn, attributes in reader.parse():
540
print(f"Entry: {dn}")
541
for attr, values in attributes.items():
542
if attr in ['userCertificate', 'jpegPhoto']:
543
for value in values:
544
if isinstance(value, bytes):
545
print(f" {attr}: <loaded {len(value)} bytes from URL>")
546
else:
547
print(f" {attr}: {values}")
548
```