Как работать с LDAP в Python

Недавно я написал о том, что такое LDAP и с чем его едят. Сегодня, в продолжение этой темы, расскажу, как работать с LDAP сервером из программы на языке программирования Python. Для создания примеров я использую Python 2.7 и MS Active Directory. В примерах все имена организаций, подразделений и пользователей являются вымышленными, а всякое совпадение с реальностью является совершенно случайным 🙂

Для работы с LDAP из Python нам понадобится пакет python-ldap. Пакет позволяят работать с различными LDAP-серверами, и с Active Directory, в частности.

>>> import ldap
>>> ad = ldap.initialize("ldap://192.168.0.16")
>>> ad.simple_bind_s("trofimov_a@SKY", "nooneknows")
(97, [], 1, [])

По LDAP URL ldap://192.168.0.16, который указывает на Active Directory сервер, получен объект LDAPObject и присвоен переменной ad. Далее используем метод simple_bind_s этого объекта для присоединения к серверу, передавая имя и пароль пользователя для аутентификации. (Символы _s в конце имени метода означают, что метод выполняется синхронно.)

Аутентификация пользователей — одна из функций LDAP сервера, реализуемая операцией bind. Другие функции: поиск, чтение и модификация данных в каталоге.

Найдем и выведем на экран атрибут name всех подразделений (objectClass=organizationalUnits) верхнего уровня (ldap.SCOPE_ONELEVEL) для организации «Синее Небо» (O=Синее Небо,DC=org,DC=ru):

>>> basedn = 'O=Синее Небо,DC=org,DC=ru'
>>> scope = ldap.SCOPE_ONELEVEL
>>> filterexp = 'objectClass=organizationalUnit'
>>> attrlist = ['name']
>>> results = ad.search_s(basedn, scope, filterexp, attrlist)
>>> for result in results:
... print result[0].decode('utf-8'), result[1]['name'][0].decode('utf-8')
...
OU=Коммерческий отдел,O=Синее Небо,DC=org,DC=ru Коммерческий отдел
OU=Общее руководство,O=Синее Небо,DC=org,DC=ru Общее руководство
OU=Отдел обеспечения,O=Синее Небо,DC=org,DC=ru Отдел обеспечения
OU=Производственный отдел,O=Синее Небо,DC=org,DC=ru Производственный отдел

Метод search_s возвращает результат поиска как список кортежей (DN записи, словарь с запрошенными атрибутами). А поскольку атрибут может иметь больше одного значения, то значения атрибутов представлены списком. В рассмотренном примере атрибут name имеет одно значение, то есть, список значений содержит единственный элемент result[1]['name'][0].

Значения, которые может принимать параметр scope (в соответствии со спецификацией LDAP):

  • ldap.SCOPE_BASE — запись base DN,
  • ldap.SCOPE_ONELEVEL — дочерние записи base DN,
  • ldap.SCOPE_SUBTREE — поддерево с вершиной base DN.

Найду запись о пользователе (objectClass=user), указав в списке атрибутов objectClass, — этот атрибут имеет несколько значений:

>>> basedn = 'O=Синее Небо,DC=org,DC=ru'
>>> scope = ldap.SCOPE_SUBTREE
>>> filterexp = "(&(cn=Трофимов Андрей*)(objectClass=user))"
>>> attrlist = ["sAMAccountName", "mail", "objectClass"]
>>> results = ad.search_s(basedn, scope, filterexp, attrlist)
>>> for result in results:
... print 'DN'.rjust(15) + ' = ' + result[0].decode('utf-8')
... print 'n'.join(x.rjust(15) + ' = ' + str(result[1][x]) for x in attrlist)
...
DN = CN=Трофимов Андрей,OU=Производственный отдел,O=Синее Небо,DC=org,DC=ru
sAMAccountName = ['trofimov_a']
mail = ['trofimov_a@sineenebo.org.ru']
objectClass = ['top', 'person', 'organizationalPerson', 'user']

Несколько значений атрибута objectClass отражают тот факт, что класс user явлется наследником классов organizationalPerson, person и top. Таким образом, запись класса user является одновременно записью каждого из классов-предков.

Атрибут sAMAccountName содержит имя пользователя, под которым пользователь регистрируется в корпоративном домене, атрибут mail — адрес электронной почты.

Следующий скрипт выводит на экран имя и адрес электронной почты всех активных пользователей организации «Синее Небо». Приблизительно то же самое делает адресная книга вашего почтового клиента, чтобы вывести доступных адресатов электронной почты.

# -*- coding: utf-8 -*-

import ldap

LDAP_URL = "ldap://192.168.0.16"
USERNAME = "trofimov_a@SKY"
PASSWORD = "nooneknows"

ad = ldap.initialize(LDAP_URL)
ad.simple_bind_s(USERNAME, PASSWORD)

basedn = "O=Синее Небо,DC=org,DC=ru"
scope = ldap.SCOPE_SUBTREE
filterexp = "(&(objectClass=user)(!(userAccountControl:1.2.840.113556.1.4.803:=2))(cn=*)(mail=*))"
attrlist = ["cn", "mail"]
results = ad.search_s(basedn, scope, filterexp, attrlist)

for result in results:
print result[1]["cn"][0].decode("utf-8").rjust(35), result[1]["mail"][0]

ad.unbind_s()

Здесь в выражении фильтра расширенная проверка с атрибутом userAccountControl выбирает только пользователей с отключенной учетной записью (AccountDisabled)
, а ее отрицание, соответственно, выбирает активных пользователей. Выражение фильтра также требует наличия у записей атрибутов cn и mail.

В отличие от операции поиска, операции LDAP для изменения данных всегда выполняются ровно для одной записи в каталоге, идентифицированной ее уникальным именем DN.

Добавим новую запись класса organizationalPerson в Производственный отдел:

>>> dn = "CN=Обломов Илья,OU=Производственный отдел,O=Синее Небо,DC=org,DC=ru"
>>> addlist = [
... ("cn", "Обломов Илья"),
... ("sn", "Обломов"),
... ("objectClass", ["organizationalPerson","person","top"]),
... ("telephoneNumber", "2121212")
... ]
>>> ad.add_s(dn, addlist)
(105, [])

Метод add_s объекта LDAPObject принимает в качестве параметров DN новой записи и список кортежей (атрибут, значение). Запись успешно добавлена. Для чтения записи создам функцию:

>>> def show_entry(dn):
... try:
... results = ad.search_s(dn, ldap.SCOPE_BASE)
... print "DN".rjust(20), "=", results[0][0].decode("utf-8")
... for attr in ["cn", "sn", "givenName", "telephoneNumber"]:
... print attr.rjust(20), "=", (results[0][1][attr][0].decode("utf-8") if results[0][1].get(attr) else '')
... except:
... print ("Не найден DN %s" % dn).decode("utf-8")
...
>>> show_entry(dn)
DN = CN=Обломов Илья,OU=Производственный отдел,O=Синее Небо,DC=org,DC=ru
cn = Обломов Илья
sn = Обломов
givenName =
telephoneNumber = 2121212

Для изменения существующей записи нужно подготовить список модификаций:

>>> modlist = [
... (ldap.MOD_ADD, "givenName", "Илья"),
... (ldap.MOD_REPLACE, "sn", "Обломоff"),
... (ldap.MOD_DELETE, "telephoneNumber", None)
... ]

Первый элемент каждого кортежа в списке определяет тип модификации, одно из:

  • ldap.MOD_ADD — добавить атрибут
  • ldap.MOD_REPLACE — заменить атрибут
  • ldap.MOD_DELETE — удалить атрибут (или одно из его значений)

Изменяю запись и смотрю результат:

>>> ad.modify_s(dn, modlist)
(103, [])
>>> show_entry(dn)
DN = CN=Обломов Илья,OU=Производственный отдел,O=Синее Небо,DC=org,DC=ru
cn = Обломов Илья
sn = Обломоff
givenName = Илья
telephoneNumber =

Метод rename_s объекта LDAPObject позволяет изменить RDN записи:

>>> ad.rename_s(dn, "cn=Обломов Илья Ильич")
(109, [], 10, [])

>>> show_entry(dn)
Не найден DN CN=Обломов Илья Ильич,OU=Производственный отдел,O=Синее Небо,DC=org,DC=ru

>>> dn = "CN=Обломов Илья Ильич,OU=Производственный отдел,O=Синее Небо,DC=org,DC=ru"
>>> show_entry(dn)
DN = CN=Обломов Илья Ильич,OU=Производственный отдел,O=Синее Небо,DC=org,DC=ru
cn = Обломов Илья Ильич
sn = Обломоff
givenName = Илья
telephoneNumber =

Наконец, удаляю экспериментальную запись:

>>> ad.delete_s(dn)
(107, [], 12, [])
>>> show_entry(dn)
Не найден DN CN=Обломов Илья Ильич,OU=Производственный отдел,O=Синее Небо,DC=org,DC=ru

Закрываю соединение с Active Directory:

>>> ad.unbind_s()

Все примеры выше используют синхронные операции с LDAP. Объект LDAPObject предоставляет также асинхронные методы search, add, modify, rename, delete. Эти методы возвращают целочисленный идентификатор операции, который затем нужно передать методу result для получения результата операции, или методу abandon — для отмены операции.

Были рассмотрены базовые возможности, предоставлемые пакетом python-ldap, для чтения и изменения данных в LDAP каталоге.

Автор: Andrei Trofimov
Дата публикации: 2013-11-10T18:05:00.000+11:00