Skip to content

Commit ef837a2

Browse files
spaceonedroideck
authored andcommitted
feat(ldap.dn): Add support for different formats in ldap.dn.dn2str() via flags
In C `dn2str()` supports `flags` which works by providing one of `LDAP_DN_FORMAT_UFN`, `LDAP_DN_FORMAT_AD_CANONICAL`, `LDAP_DN_FORMAT_DCE`, `LDAP_DN_FORMAT_LDAPV3`. These symbols do exist in Python, but could not be used ultimately because the Python counterpart was pure Python and did not pass to `dn2str(3)`. Fix #257
1 parent d466f39 commit ef837a2

File tree

3 files changed

+232
-3
lines changed

3 files changed

+232
-3
lines changed

Lib/ldap/dn.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,19 +48,25 @@ def str2dn(dn,flags=0):
4848
return ldap.functions._ldap_function_call(None,_ldap.str2dn,dn,flags)
4949

5050

51-
def dn2str(dn):
51+
def dn2str(dn, flags=0):
5252
"""
5353
This function takes a decomposed DN as parameter and returns
54-
a single string. It's the inverse to str2dn() but will always
55-
return a DN in LDAPv3 format compliant to RFC 4514.
54+
a single string. It's the inverse to str2dn() but will by default always
55+
return a DN in LDAPv3 format compliant to RFC 4514 if not otherwise specified
56+
via flags.
57+
58+
See also the OpenLDAP man-page ldap_dn2str(3)
5659
"""
60+
if flags:
61+
return ldap.functions._ldap_function_call(None, _ldap.dn2str, dn, flags)
5762
return ','.join([
5863
'+'.join([
5964
'='.join((atype,escape_dn_chars(avalue or '')))
6065
for atype,avalue,dummy in rdn])
6166
for rdn in dn
6267
])
6368

69+
6470
def explode_dn(dn, notypes=False, flags=0):
6571
"""
6672
explode_dn(dn [, notypes=False [, flags=0]]) -> list

Modules/functions.c

Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,222 @@ l_ldap_str2dn(PyObject *unused, PyObject *args)
155155
return result;
156156
}
157157

158+
/* ldap_dn2str */
159+
160+
static void
161+
_free_dn_structure(LDAPDN dn)
162+
{
163+
if (dn == NULL)
164+
return;
165+
166+
for (LDAPRDN *rdn = dn; *rdn != NULL; rdn++) {
167+
for (LDAPAVA **avap = *rdn; *avap != NULL; avap++) {
168+
LDAPAVA *ava = *avap;
169+
170+
if (ava->la_attr.bv_val) {
171+
free(ava->la_attr.bv_val);
172+
}
173+
if (ava->la_value.bv_val) {
174+
free(ava->la_value.bv_val);
175+
}
176+
free(ava);
177+
}
178+
free(*rdn);
179+
}
180+
free(dn);
181+
}
182+
183+
/*
184+
* Convert a Python list-of-list-of-(str, str, int) into an LDAPDN and
185+
* call ldap_dn2bv to build a DN string.
186+
*
187+
* Python signature: dn2str(dn: list[list[tuple[str, str, int]]], flags: int) -> str
188+
* Returns the DN string on success, or raises TypeError or RuntimeError on error.
189+
*/
190+
static PyObject *
191+
l_ldap_dn2str(PyObject *self, PyObject *args)
192+
{
193+
PyObject *dn_list = NULL;
194+
int flags = 0;
195+
LDAPDN dn = NULL;
196+
LDAPAVA *ava;
197+
LDAPAVA **rdn;
198+
BerValue str = { 0, NULL };
199+
PyObject *py_rdn_seq = NULL, *py_ava_item = NULL;
200+
PyObject *py_name = NULL, *py_value = NULL, *py_encoding = NULL;
201+
PyObject *result = NULL;
202+
Py_ssize_t nrdns = 0, navas = 0, name_len = 0, value_len = 0;
203+
int i = 0, j = 0;
204+
int ldap_err;
205+
const char *name_utf8, *value_utf8;
206+
207+
const char *type_error_message = "expected list[list[tuple[str, str, int]]]";
208+
209+
if (!PyArg_ParseTuple(args, "Oi:dn2str", &dn_list, &flags)) {
210+
return NULL;
211+
}
212+
213+
if (!PySequence_Check(dn_list)) {
214+
PyErr_SetString(PyExc_TypeError, type_error_message);
215+
return NULL;
216+
}
217+
218+
nrdns = PySequence_Size(dn_list);
219+
if (nrdns < 0) {
220+
PyErr_SetString(PyExc_TypeError, type_error_message);
221+
return NULL;
222+
}
223+
224+
/* Allocate array of LDAPRDN pointers (+1 for NULL terminator) */
225+
dn = (LDAPRDN *) calloc((size_t)nrdns + 1, sizeof(LDAPRDN));
226+
if (dn == NULL) {
227+
PyErr_NoMemory();
228+
return NULL;
229+
}
230+
231+
for (i = 0; i < nrdns; i++) {
232+
py_rdn_seq = PySequence_GetItem(dn_list, i); /* New reference */
233+
if (py_rdn_seq == NULL) {
234+
goto error_cleanup;
235+
}
236+
if (!PySequence_Check(py_rdn_seq)) {
237+
PyErr_SetString(PyExc_TypeError, type_error_message);
238+
goto error_cleanup;
239+
}
240+
241+
navas = PySequence_Size(py_rdn_seq);
242+
if (navas < 0) {
243+
PyErr_SetString(PyExc_TypeError, type_error_message);
244+
goto error_cleanup;
245+
}
246+
247+
/* Allocate array of LDAPAVA* pointers (+1 for NULL terminator) */
248+
rdn = (LDAPAVA **)calloc((size_t)navas + 1, sizeof(LDAPAVA *));
249+
if (rdn == NULL) {
250+
PyErr_NoMemory();
251+
goto error_cleanup;
252+
}
253+
254+
for (j = 0; j < navas; j++) {
255+
py_ava_item = PySequence_GetItem(py_rdn_seq, j); /* New reference */
256+
if (py_ava_item == NULL) {
257+
goto error_cleanup;
258+
}
259+
/* Expect a 3‐tuple: (name: str, value: str, encoding: int) */
260+
if (!PyTuple_Check(py_ava_item) || PyTuple_Size(py_ava_item) != 3) {
261+
PyErr_SetString(PyExc_TypeError, type_error_message);
262+
goto error_cleanup;
263+
}
264+
265+
py_name = PyTuple_GetItem(py_ava_item, 0); /* Borrowed reference */
266+
py_value = PyTuple_GetItem(py_ava_item, 1); /* Borrowed reference */
267+
py_encoding = PyTuple_GetItem(py_ava_item, 2); /* Borrowed reference */
268+
269+
if (!PyUnicode_Check(py_name) || !PyUnicode_Check(py_value) || !PyLong_Check(py_encoding)) {
270+
PyErr_SetString(PyExc_TypeError, type_error_message);
271+
goto error_cleanup;
272+
}
273+
274+
name_len = 0;
275+
value_len = 0;
276+
name_utf8 = PyUnicode_AsUTF8AndSize(py_name, &name_len);
277+
value_utf8 = PyUnicode_AsUTF8AndSize(py_value, &value_len);
278+
if (name_utf8 == NULL || value_utf8 == NULL) {
279+
goto error_cleanup;
280+
}
281+
282+
ava = (LDAPAVA *) calloc(1, sizeof(LDAPAVA));
283+
284+
if (ava == NULL) {
285+
PyErr_NoMemory();
286+
goto error_cleanup;
287+
}
288+
289+
ava->la_attr.bv_val = (char *)malloc((size_t)name_len + 1);
290+
if (ava->la_attr.bv_val == NULL) {
291+
free(ava);
292+
PyErr_NoMemory();
293+
goto error_cleanup;
294+
}
295+
memcpy(ava->la_attr.bv_val, name_utf8, (size_t)name_len);
296+
ava->la_attr.bv_val[name_len] = '\0';
297+
ava->la_attr.bv_len = (ber_len_t) name_len;
298+
299+
ava->la_value.bv_val = (char *)malloc((size_t)value_len + 1);
300+
if (ava->la_value.bv_val == NULL) {
301+
free(ava->la_attr.bv_val);
302+
free(ava);
303+
PyErr_NoMemory();
304+
goto error_cleanup;
305+
}
306+
memcpy(ava->la_value.bv_val, value_utf8, (size_t)value_len);
307+
ava->la_value.bv_val[value_len] = '\0';
308+
ava->la_value.bv_len = (ber_len_t) value_len;
309+
310+
ava->la_flags = (int)PyLong_AsLong(py_encoding);
311+
if (PyErr_Occurred()) {
312+
/* Encoding conversion failed */
313+
free(ava->la_attr.bv_val);
314+
free(ava->la_value.bv_val);
315+
free(ava);
316+
goto error_cleanup;
317+
}
318+
319+
rdn[j] = ava;
320+
Py_DECREF(py_ava_item);
321+
py_ava_item = NULL;
322+
}
323+
324+
/* Null‐terminate the RDN */
325+
rdn[navas] = NULL;
326+
327+
dn[i] = rdn;
328+
Py_DECREF(py_rdn_seq);
329+
py_rdn_seq = NULL;
330+
}
331+
332+
/* Null‐terminate the DN */
333+
dn[nrdns] = NULL;
334+
335+
/* Call ldap_dn2bv to build a DN string */
336+
ldap_err = ldap_dn2bv(dn, &str, flags);
337+
if (ldap_err != LDAP_SUCCESS) {
338+
PyErr_SetString(PyExc_RuntimeError, ldap_err2string(ldap_err));
339+
goto error_cleanup;
340+
}
341+
342+
result = PyUnicode_FromString(str.bv_val);
343+
if (result == NULL) {
344+
goto error_cleanup;
345+
}
346+
347+
/* Free the memory allocated by ldap_dn2bv */
348+
ldap_memfree(str.bv_val);
349+
str.bv_val = NULL;
350+
351+
/* Free our local DN structure */
352+
_free_dn_structure(dn);
353+
dn = NULL;
354+
355+
return result;
356+
357+
error_cleanup:
358+
/* Free any partially built DN structure */
359+
_free_dn_structure(dn);
360+
dn = NULL;
361+
362+
/* If ldap_dn2bv allocated something, free it */
363+
if (str.bv_val) {
364+
ldap_memfree(str.bv_val);
365+
str.bv_val = NULL;
366+
}
367+
368+
/* Cleanup Python temporaries */
369+
Py_XDECREF(py_ava_item);
370+
Py_XDECREF(py_rdn_seq);
371+
return NULL;
372+
}
373+
158374
/* ldap_set_option (global options) */
159375

160376
static PyObject *
@@ -191,6 +407,7 @@ static PyMethodDef methods[] = {
191407
{"initialize_fd", (PyCFunction)l_ldap_initialize_fd, METH_VARARGS},
192408
#endif
193409
{"str2dn", (PyCFunction)l_ldap_str2dn, METH_VARARGS},
410+
{"dn2str", (PyCFunction)l_ldap_dn2str, METH_VARARGS},
194411
{"set_option", (PyCFunction)l_ldap_set_option, METH_VARARGS},
195412
{"get_option", (PyCFunction)l_ldap_get_option, METH_VARARGS},
196413
{NULL, NULL}

Tests/t_ldap_dn.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,12 @@ def test_dn2str(self):
255255
], ldap.DN_FORMAT_LDAPV3),
256256
r'cn=\C3\A4\C3\B6\C3\BC\C3\84\C3\96\C3\9C\C3\9F,dc=example,dc=com'
257257
)
258+
self.assertEqual(
259+
ldap.dn.dn2str([
260+
[('c', 'DEU', 1)], # country code only allow two-letters
261+
], ldap.DN_FORMAT_LDAPV3),
262+
r'c=DEU'
263+
)
258264

259265
def test_dn_various_lengths(self):
260266
base = [

0 commit comments

Comments
 (0)