Пример работы с LDAP-каталогом с помощью php API

Левинца Егор, 06 апреля 2012 года.

Обсуждение статьи на форуме проекта Pro-LDAP.ru.

В порядке эксперимента, а также чтобы оказать помощь человеку в его затруднении, решил написать небольшую утилитку для отображения пользователей (а точнее, учётных записей пользователей), хранящихся в каталоге LDAP, по группам. Эксперимент состоял в том, чтобы обратиться к каталогу и получить из него данные через php API, чего я раньше не делал. Зная общие принципы взаимодействия с LDAP-каталогами и имея опыт написания подобных программ на perl API (с использованием модуля Net::LDAP), сделать это оказалось несложно. Все же я решил описать свою работу — возможно, она поможет кому-нибудь в разработке php-программ, взаимодействующих с LDAP-каталогом.

Исходные данные: имеется каталог (базовый DN dc=mycompany,dc=ru) со "стандартной" структурой для интеграции с samba: сведения об учётных записях пользователей хранятся в ветке ou=Users,dc=myconpany,dc=ru, сведения о группах пользователей — в ветке ou=Groups,dc=mycompany,dc=ru. Основная задача: отобразить нестандартную древовидную структуру: на первом уровне список групп, на втором — принадлежащие этим группам пользователи. Факультативная задача: при выборе пользователя из списка вывести некоторые сведения по нему.

Для упрощения примера подразумевается, что группы строятся на структурном объектном классе posixGroup, атрибут членства в группе — memberUid, значение которого совпадает с атрибутом uid учётной записи пользователя. Если у Вас за членство в группах и учётные записи пользователей отвечают другие атрибуты — измените код программы под Ваши настройки.

LDAP — относительно простой протокол с небольшим количеством операций. В нашем примере мы не собираемся изменять содержимое каталога (осуществляем доступ только для чтения), поэтому вся работа с каталогом будет выполнена за 4 операции: соединение с сервером каталога (connect), подключение к каталогу (bind), выполнение поиска необходимых нам данных (search) с выводом результатов, и, наконец, отключение от сервера (unbind). Все эти операции реализованы в php функциями с "говорящими" названиями: ldap_connect(), ldap_bind(), ldap_search() и ldap_unbind(). Эти и другие функции работы с LDAP в php описаны в соответствующей странице документации по php. Описаны, кстати, довольно неплохо, с хорошими примерами.

Поскольку при выполнении основной и факультативной задач осуществляются абсолютно одинаковые операции соединения, подключения и отключения от сервера, я решил вынести их в основную часть программы, а различающиеся операции поиска и отображения полученных в результате поиска данных — в отдельные функции. В первой функции get_main() производится поиск сведений о группах в ветке ou=Groups,dc=mycompany,dc=ru. Найденные группы, а также члены каждой из групп сортируются по алфавиту и выводится html-страница с древовидной структурой. Чтобы не изобретать велосипедов с отображением древовидной структуры, я воспользовался готовым модулем dhtmlxTree библиотеки компонентов dhtmlx. Вызов второй функции get_userinfo() выполняется при обращении к программе из ajax-запроса, генерируемого при нажатии на пользователя — члена группы в выведенной в браузер html-странице. В этой функции выполняется поиск в ветке ou=Users,dc=myconpany,dc=ru на совпадение имени пользователя с содержимым атрибута uid. Выбранные в результате поиска атрибуты записи пользователя оформляются в виде таблицы и загружаются в соответствующее информационное поле на html-странице.

Текст программы:

<?php
/*
    Пример работы с каталогом LDAP из php (мой hello world).
    Представление "Пользователи разбиты на группы". Пример не претендует на
    универсальность, подразумевается, что записи групп построены на объектном
    классе posixGroup, атрибут членства memberUid совпадает с uid записи
    пользователя (как при связке с samba). Представление древовидной структуры
    выполнено с помощью компонента dhtmlxTree библиотеки dhtmlxSuite
    (см. http://docs.dhtmlx.com/doku.php?id=dhtmlxtree:toc)
*/

##################################################################################
# Параметры подключения к LDAP-каталогу
##################################################################################
$LDAP = array(
    'server' => '127.0.0.1',
    'port' => '389',
    'bindDN' => NULL, // анонимное подключение,
		      // в этом параметре можно указать DN подключения
    'bindPW' => NULL, // анонимное подключение,
		      // в этом параметре можно указать пароль для DN подключения
    'groupsDN' => 'ou=Groups,dc=mycompany,dc=ru',
    'usersDN' => 'ou=Users,dc=mycompany,dc=ru'
    );

##################################################################################
# Работа с LDAP-сервером: подключение/отключение
##################################################################################
// Подключаемся к серверу
$conn = ldap_connect($LDAP['server'], $LDAP['port'])
    or die('Не могу подключиться к LDAP-серверу: ' . $LDAP['server'] . ', порт ' . $LDAP['port']);
// Устанавливаем версию протокола (странно, но по умолчанию не 3-я версия)
ldap_set_option($conn, LDAP_OPT_PROTOCOL_VERSION, 3);

// Подсоединение
if(isset($LDAP['bindDN'])) // с аутентификацией
{
    ldap_bind($conn, $LDAP['bindDN'], $LDAP['bindPW'])
	or die('Не могу подсоединиться к LDAP-серверу');
}
else // анонимное
{
    ldap_bind($conn)
	or die('Не могу подсоединиться к LDAP-серверу');
}

// Определимся, что будем делать
$action = isset($_REQUEST['action']) ? $_REQUEST['action'] : 'main';
switch ($action)
{
    case 'main': get_main(); break;
    case 'userinfo': get_user_info(); break;
    default: die('Определите корректный параметр action'); break;
}

// Когда всё сделано отключаемся от сервера
ldap_close($conn);

##################################################################################
# Функция построения страницы и вывода списка групп с пользователями
##################################################################################
function get_main()
{
    global $LDAP, $conn;
    
    // Выполним запрос на получение групп с объектным классом posixGroup,
    // возвращающий атрибут memberUid
    $search = ldap_search($conn, $LDAP['groupsDN'], '(objectClass=posixGroup)', array('memberUid'))
	or die('Запрос ничего не вернул, DN ветки группы: ' . $LDAP['groupsDN']);
    // Получим результат запроса в массив
    $entries = ldap_get_entries($conn, $search);
    // Массив довольно интересен, с претензией на всеобъемлемость,
    // что затрудняет работу с ним. Содержимое массива 
    // можно посмотреть с помощью функции print_r()
    
    // Отсортируем по алфавиту список групп и список членов к каждой группе
    // пример работы с массивом, возвращаемым функцией ldap_get_entries()
    $entries_sorted = array();
    // Цикл по группам из массива
    for($i = 0; $i < $entries['count']; $i++)
    {
	$members = array();
	// Получение атрибутов memberUid для текущей группы из массива
	if(isset($entries[$i]['memberuid']))
	{
	    for($j = 0; $j < $entries[$i]['memberuid']['count']; $j++)
		array_push($members, $entries[$i]['memberuid'][$j]);
	    sort($members);
	}
	else $members = NULL;
	$entries_sorted[$entries[$i]['dn']]=$members;
    }
    asort($entries_sorted);
    
    // Вывод страницы
    header('Content-type: text/html; charset=UTF-8');
?>
<html><head><title>Просмотр групп LDAP</title>
<meta http-equiv="Content-type" content="text/html; charset=utf-8">
<link rel="STYLESHEET" type="text/css" href="./dhx/dhtmlxtree.css">
<script  src="./dhx/dhtmlxcommon.js"></script>
<script  src="./dhx/dhtmlxtree.js"></script>
<script  src="./dhx/dhtmlxtree_start.js"></script>
<script>dhtmlx.skin = "dhx_skyblue";
window.onload = function ()
{
    // Инициируем дерево
    LDAPTree = dhtmlXTreeFromHTML('LDAPTree');
    // Обработка события onClick
    LDAPTree.attachEvent('onClick', function(id)
    {
	if(/,/.test(id)) document.getElementById('userInfo').innerHTML = 'Группа ' + id;
	else if(/[$](_|$)/.test(id)) document.getElementById('userInfo').innerHTML = 'Samba Trust Account ' + LDAPTree.getItemText(id);
	else // ajax-запрос сведений о пользователе
	{
	    var A = dhtmlxAjax.getSync('index.php?action=userinfo&user='+LDAPTree.getItemText(id));
	    var res = A.xmlDoc.status==200 ? A.xmlDoc.responseText : 'Er: BadRequest, status '+ A.xmlDoc.status;
	    if(/^Er/.test(res)) alert(res); else document.getElementById('userInfo').innerHTML = res;
	}
    });
}
</script>
<style>
h1 {font-family:Tahoma; font-size:16pt}
#LDAPTree {width:400px; height:400px; border:1px solid Silver}
#userInfo {width:400px; height:400px; border:1px solid Silver; font-family:Tahoma;font-size:11px}
#userInfo td{font-family:Tahoma; font-size:11px; vertical-align:top}
</style>
</head><body><h1>Группы в <?php echo $LDAP['groupsDN']; ?></h1>
<table><tr><td><div id="LDAPTree" setImagePath="./dhx/imgs/csh_bluefolders/">
<?php
    // Вывод списка групп и их членов в формате xml, который поддерживает
    // dhtmlxTree (см .http://docs.dhtmlx.com/doku.php?id=dhtmlxtree:syntax_templates)
    if(count($entries_sorted))
    {
	echo '<xmp>';
	foreach($entries_sorted as $groupDN=>$members)
	{
	    echo '<item id="' . $groupDN . '" im0="folderClosed.gif" im1="folderOpen.gif" im2="folderClosed.gif" text="' .
		preg_replace('/,' . $LDAP['groupsDN'] . '/', '', $groupDN) . '">';

	    if(is_array($members))
		echo join('', array_map(
		function($member)
		{
		    return '<item id="' . $member . '" text="' . $member . '"' .
			(preg_match('/[$]$/',$member) ? ' im0="computer.gif"' : '') . ' />';
		}, $members));

	    echo '</item>';
	}
	echo '</xmp>';
    }
    else
    {
	echo 'Групп нет';
    }
?>
</div></td><td><div id="userInfo"></div></td></tr><table></body></html>
<?php
}

##################################################################################
# Функция получения информации о пользователе
##################################################################################
function get_user_info()
{
    global $LDAP, $conn;
    
    // Обязательный параметр user
    $user = $_REQUEST['user'];
    if(empty($user)) die('Задайте параметр user');
    
    // Список нужных нам атрибутов 'Имя_атрибута' => 'Смысловое_описание'
    $LDAP['Attrs'] = array(
	'uid'=>'Login',
	'cn'=>'Имя',
	'uidnumber'=>'uid',
	'gidnumber'=>'gid',
	'telephonenumber'=>'Телефон',
	'mail'=>'Эл. почта');
    
    // Выполняем запрос, возвращающий заданные нами атрибуты
    $search = ldap_search($conn, $LDAP['usersDN'], '(uid=' . $user . ')', array_keys($LDAP['Attrs']))
	or die('Запрос ничего не вернул, user: ' . $user);
    // Получаем результаты запроса в массив
    $entries = ldap_get_entries($conn, $search);
    
    // Вывод заголовка
    header('Content-type: text/plain; charset:UTF-8');

    // Кому интересно посмотреть содержимое массива, раскомментируйте строку ниже
#    echo '<pre>'; print_r($entries); echo '</pre>';
    
    // Вывод таблицы с запрашиваемыми данными
    echo '<table>';
    foreach($LDAP['Attrs'] as $attr=>$desc)
    {
	echo '<tr><td>' . $desc . '</td><td>';
	if(isset($entries[0][$attr]))
	{
	    array_shift($entries[0][$attr]);
	    echo join('<br>', $entries[0][$attr]);
	}
	echo '</td></tr>';
    }
    echo '</table>';
}
?>

Выглядит это так:

Код программы с библиотеками dhtmlx и набором иконок можно скачать одним архивом здесь (~30 Kb).

Несколько замечаний напоследок.

  1. Как ни странно, но без вызова ldap_set_option($conn, LDAP_OPT_PROTOCOL_VERSION, 3) подключения произвести не удалось. Отсюда можно сделать вывод, что по умолчанию используется версия протокола, отличная от 3-ей (интересно было бы узнать мотивы такого решения). Значение по умолчанию опции LDAP_OPT_PROTOCOL_VERSION можно было бы проверить с помощью функции ldap_get_option(), но я что-то не удосужился.
  2. В функции поиска ldap_search() задаются, как и положено, базовый DN, поисковый фильтр, возвращаемые атрибуты и ещё ряд параметров, а вот диапазона поиска (scope) задать нельзя, он жёстко установлен в sub (LDAP_SCOPE_SUBTREE). В php определено еще 2 функции с аналогичным синтаксисом для поиска LDAP: ldap_list() - диапазон one (LDAP_SCOPE_ONELEVEL) и ldap_read() - диапазон base (LDAP_SCOPE_BASE). Для данной задачи, возможно, больше бы подошла функция ldap_list(), но я решил оставить в примере ldap_search() из-за более "говорящего" названия.
  3. По той же причине, - из-за "говорящего" названия, - я использовал для отключения от каталога функцию ldap_close() вместо ldap_unbind(). Две этих функции - синонимы.

Обсуждение статьи на форуме проекта Pro-LDAP.ru.

Эта страница

Содержание

Пример работы с LDAP-каталогом с помощью php API
OpenLDAP 2.4 Руководство

Содержание

Введение в службы каталогов OpenLDAPБыстрое развёртывание и начало работыОбщая картина - варианты конфигурацииСборка и установка OpenLDAPНастройка slapd

 

Конфигурационный файл slapdЗапуск slapdКонтроль доступаОграниченияИнструментыМеханизмы манипуляции даннымиНаложенияСпецификация схемы

 

БезопасностьSASLTLSРаспределённая служба каталоговРепликацияОбслуживаниеМониторингПроизводительностьУстранение неполадок
Перевод официального руководства OpenLDAP 2.4 Admin Guide
Полное содержание здесь
LDAP для учёных-ракетчиков

Содержание

О книгеКонцепции LDAPОбъекты LDAPУстановка LDAPПримерыНастройкаРепликация и отсылкиLDIF и DSMLПротоколLDAP API

 

HOWTOНеполадкиПроизводительностьИнструменты LDAPБезопасностьЗаметкиРесурсы LDAPRFC и X.500ГлоссарийОбъекты
Перевод "LDAP for Rocket Scientists"
Полное содержание здесь
Ресурсы

Книги

Руководство OpenLDAP 2.4LDAP для учёных-ракетчиков

Другие

СтатьиТермины LDAPman-страницы OpenLDAP 2.4Список RFCКлиенты LDAPФайлы наборов схемы
Полезные ресурсы
Форум

 

Разделы форумаНепрочитанные сообщенияПоследние сообщения
Форум проекта
Главная

Pro-LDAP.ru

О проектеНовости проектаУчастникиСтаньте участником!Сообщите об ошибке!Об авторских правахСоглашения проекта
Присоединяйсь!