Поиск данных в каталоге LDAP. Работа с LDAP в Qt. Часть 2.
В первой части мы рассмотрели инициализацию, подключение и отключение от LDAP сервера.
Сегодня мы рассмотрим поиск информации в AD через LDAP.
Строка поиска LDAP
Для поиска в LDAP нам придется использовать фильтры LDAP, которые имеют свой синтаксис, давайте его рассмотрим.
Фильтр поиска всегда обрамляется в скобки ()
Внутри скобок указывается одно или несколько условий поиска.
Например, фильтр:
(&(objectCategory=person))
Задает поиск всех объектов с категорией person в нашем случае – всех пользователей.
Корневой элемента для поиска
Для эффективного поиска в LDAP нужно правильно указать BaseDN – Базовое уникальное имя – фактически корневой элемент (как правило это OU – Organization Unit – организационная единица – далее Элемент или OU), в котором будет производится поиск. Поиск производится как в самом OU, так и в принадлежащих ему дочерних.
Например, строка:
OU=Company,DC=altuninvv,DC=local
Задает поиск по всей нашей виртуальной компании.
Поиск через командную строку
Чтобы разобраться как работает поиск мы воспользуемся утилитой командной строки:
ldapsearch
Так как у нас уже установлен Msys2 и необходимые пакеты OpenLDAP то и утилита должна быть доступна.
Просто запустите в консоли:
ldapsearch
ldap_sasl_interactive_bind_s: Can't contact LDAP server (-1)
Это означает что утилита установлена.
Синтаксис подключения к LDAP серверу таков:
ldapsearch -x -b "OU=Company,DC=altuninvv,DC=local" -H ldap://192.168.0.1
Попробуем соединиться:
ldapsearch -x -b "OU=Company,DC=altuninvv,DC=local" -H ldap://192.168.0.1 # extended LDIF #
# LDAPv3
# base <OU=Company,DC=altuninvv,DC=local> with scope subtree
# filter: (objectclass=*)
# requesting: ALL
#
# search result
search: 2
result: 1 Operations error
text: 000004DC: LdapErr: DSID-0C090A37, comment: In order to perform this operation a successful bind must be completed on the connection., data 0, v4563
# numResponses: 1
Далее я буду удалять лишний вывод утилиты.
Как видите, без авторизации на сервере у нас ничего не выйдет. Добавим имя пользователя и пароль:
ldapsearch -x -b "OU=Company,DC=altuninvv,DC=local" -H "ldap://192.168.0.1" -D "CN=ldap-bind,CN=Users,DC=altuninvv,DC=local" -w Pas#w0rds#1
# Company, altuninvv.local
dn: OU=Company,DC=altuninvv,DC=local
objectClass: top
objectClass: organizationalUnit
ou: Company
description: Altunin Soft
distinguishedName: OU=Company,DC=altuninvv,DC=local
instanceType: 4
whenCreated: 20210125063654.0Z
whenChanged: 20210125063654.0Z
uSNCreated: 13359
uSNChanged: 13363
name: Company
objectGUID:: fwKJseAITUue+tPywp27dA==
objectCategory: CN=Organizational-Unit,CN=Schema,CN=Configuration,DC=altuninvv
,DC=local
dSCorePropagationData: 20210125063654.0Z
dSCorePropagationData: 20210125063654.0Z
dSCorePropagationData: 20210125063654.0Z
dSCorePropagationData: 16010101000000.0Z
# ruk, Company, altuninvv.local
dn: OU=ruk,OU=Company,DC=altuninvv,DC=local
objectClass: top
objectClass: organizationalUnit
ou: ruk
description:: 0KDRg9C60L7QstC+0LTRgdGC0LLQvg==
distinguishedName: OU=ruk,OU=Company,DC=altuninvv,DC=local
instanceType: 4
whenCreated: 20210125063654.0Z
whenChanged: 20210125063654.0Z
uSNCreated: 13362
uSNChanged: 13364
name: ruk
objectGUID:: OEmFwzRCwkejwuVD5wysbQ==
objectCategory: CN=Organizational-Unit,CN=Schema,CN=Configuration,DC=altuninvv
,DC=local
dSCorePropagationData: 20210125063654.0Z
dSCorePropagationData: 20210125063654.0Z
dSCorePropagationData: 16010101000000.0Z
# PavlovaJAV, ruk, Company, altuninvv.local
dn: CN=PavlovaJAV,OU=ruk,OU=Company,DC=altuninvv,DC=local
objectClass: top
objectClass: person
objectClass: organizationalPerson
objectClass: user
cn: PavlovaJAV
sn:: 0J/QsNCy0LvQvtCy0LA=
title:: 0JTQuNGA0LXQutGC0L7RgA==
physicalDeliveryOfficeName: 101
telephoneNumber: +7(495)12300010
givenName:: 0K/RgdC80LjQvdCw
initials:: 0JIu
distinguishedName: CN=PavlovaJAV,OU=ruk,OU=Company,DC=altuninvv,DC=local
instanceType: 4
whenCreated: 20210125063654.0Z
whenChanged: 20210125063654.0Z
displayName:: 0J/QsNCy0LvQvtCy0LAg0K/RgdC80LjQvdCwINCS0YHQtdCy0L7Qu9C+0LTQvtCy
0L3QsA==
otherTelephone: 100
uSNCreated: 13378
uSNChanged: 13383
department:: 0KDRg9C60L7QstC+0LTRgdGC0LLQvg==
company: Altunin Soft
streetAddress:: 0JvQtdC90LjQvdCwIDE=
name: PavlovaJAV
objectGUID:: XkI/ntJ+lE+v+jgeg7bk3Q==
userAccountControl: 512
badPwdCount: 0
codePage: 0
countryCode: 0
badPasswordTime: 0
lastLogoff: 0
lastLogon: 0
pwdLastSet: 132560302148742868
primaryGroupID: 513
objectSid:: AQUAAAAAAAUVAAAASQEoNLoSzC+CQL4qXAQAAA==
accountExpires: 9223372036854775807
logonCount: 0
sAMAccountName: PavlovaJAV
sAMAccountType: 805306368
userPrincipalName: PavlovaJAV
objectCategory: CN=Person,CN=Schema,CN=Configuration,DC=altuninvv,DC=local
dSCorePropagationData: 16010101000000.0Z
mail: PavlovaJAV@altuninvv.local
mobile: +7(495)853-91-88
Вывод сокращен.
Как видите, поиск успешно произведен, правда возвращаются все атрибуты, что нам не слишком удобно, давайте сократим вывод:
ldapsearch -x -b "OU=Company,DC=altuninvv,DC=local" -H "ldap://192.168.0.1" -D "CN=ldap-bind,CN=Users,DC=altuninvv,DC=local" -w Pas#w0rds#1 objectClass cn DN title
…
# Company, altuninvv.local
dn: OU=Company,DC=altuninvv,DC=local
objectClass: top
objectClass: organizationalUnit
# ruk, Company, altuninvv.local
dn: OU=ruk,OU=Company,DC=altuninvv,DC=local
objectClass: top
objectClass: organizationalUnit
# PavlovaJAV, ruk, Company, altuninvv.local
dn: CN=PavlovaJAV,OU=ruk,OU=Company,DC=altuninvv,DC=local
objectClass: top
objectClass: person
objectClass: organizationalPerson
objectClass: user
cn: PavlovaJAV
title:: 0JTQuNGA0LXQutGC0L7RgA==
# KalininaVL, ruk, Company, altuninvv.local
dn: CN=KalininaVL,OU=ruk,OU=Company,DC=altuninvv,DC=local
objectClass: top
objectClass: person
objectClass: organizationalPerson
objectClass: user
cn: KalininaVL
title:: 0JfQsNC80LXRgdGC0LjRgtC10LvRjCDQtNC40YDQtdC60YLQvtGA0LA=
# SmirnovAV, ruk, Company, altuninvv.local
dn: CN=SmirnovAV,OU=ruk,OU=Company,DC=altuninvv,DC=local
objectClass: top
objectClass: person
objectClass: organizationalPerson
objectClass: user
cn: SmirnovAV
title:: 0JfQsNC80LXRgdGC0LjRgtC10LvRjCDQtNC40YDQtdC60YLQvtGA0LA=
Вывод сокращен.
Как вы могли заметить, все русские буквы заменены непонятным набором символов, на самом деле это просто кодировка BASE64
Вы можете использовать онлайн конвертер для проверки, например этот - https://www.base64decode.org/
Тогда строка
0JfQsNC80LXRgdGC0LjRgtC10LvRjCDQtNC40YDQtdC60YLQvtGA0LA=
Преобразуется в
Заместитель директора
Но нам не так важны русские надписи в данный момент, мы будем ориентироваться по DN.
Давайте попробуем найти всех пользователей:
ldapsearch -x -b "OU=Company,DC=altuninvv,DC=local" -H "ldap://192.168.0.1" -D "CN=ldap-bind,CN=Users,DC=altuninvv,DC=local" -w Pas#w0rds#1 "(&(objectCategory=person))" cn dn
# PavlovaJAV, ruk, Company, altuninvv.local
dn: CN=PavlovaJAV,OU=ruk,OU=Company,DC=altuninvv,DC=local
cn: PavlovaJAV
# KalininaVL, ruk, Company, altuninvv.local
dn: CN=KalininaVL,OU=ruk,OU=Company,DC=altuninvv,DC=local
cn: KalininaVL
# SmirnovAV, ruk, Company, altuninvv.local
dn: CN=SmirnovAV,OU=ruk,OU=Company,DC=altuninvv,DC=local
cn: SmirnovAV
# LebedevaVA, ruk, Company, altuninvv.local
dn: CN=LebedevaVA,OU=ruk,OU=Company,DC=altuninvv,DC=local
cn: LebedevaVA
Вывод сокращен.
На этот раз мы получили список пользователей.
Поиск по атрибутам
LDAP предоставляет возможность поиска по атрибутам, для этого нужно указать их в фильтре, например:
(&(objectCategory=person)(cn=PavlovaJAV))
Запустим:
ldapsearch -x -b "OU=Company,DC=altuninvv,DC=local" -H "ldap://192.168.0.1" -D "CN=ldap-bind,CN=Users,DC=altuninvv,DC=local" -w Pas#w0rds#1 "(&(objectCategory=person)(cn=PavlovaJAV))" cn dn
# PavlovaJAV, ruk, Company, altuninvv.local
dn: CN=PavlovaJAV,OU=ruk,OU=Company,DC=altuninvv,DC=local
cn: PavlovaJAV
Использование wildcards (звездочки *) в поиске
В запросах вы можете использовать звездочку. Для этого достаточно указать * после строки поиска:
ldapsearch -x -b "OU=Company,DC=altuninvv,DC=local" -H "ldap://192.168.0.1" -D "CN=ldap-bind,CN=Users,DC=altuninvv,DC=local" -w Pas#w0rds#1 "(&(objectCategory=person)(cn=pav*))" cn dn
# PavlovaJAV, ruk, Company, altuninvv.local
dn: CN=PavlovaJAV,OU=ruk,OU=Company,DC=altuninvv,DC=local
cn: PavlovaJAV
Логические операторы при поиске
Логические операторы позволяют осуществлять сложный поиск по атрибутам.
Логический оператор указывается перед выражением, члены этого выражения указываются, в скобках, после него.
Оператор И (AND)
Оператор «И» обозначается знаком & мы уже использовали в примерах выше, например:
(&(objectCategory=person)(cn=PavlovaJAV))
Оператор ИЛИ (OR)
Оператор или обозначается знаком |, давайте найдем сразу директора и его замов:
ldapsearch -x -b "OU=Company,DC=altuninvv,DC=local" -H "ldap://192.168.0.1" -D "CN=ldap-bind,CN=Users,DC=altuninvv,DC=local" -w Pas#w0rds#1 "(&(objectCategory=person)(|(cn=PavlovaJAV)(cn=KalininaVL)(cn=SmirnovAV)))" cn
# SmirnovAV, ruk, Company, altuninvv.local
dn: CN=SmirnovAV,OU=ruk,OU=Company,DC=altuninvv,DC=local
cn: SmirnovAV
# KalininaVL, ruk, Company, altuninvv.local
dn: CN=KalininaVL,OU=ruk,OU=Company,DC=altuninvv,DC=local
cn: KalininaVL
# PavlovaJAV, ruk, Company, altuninvv.local
dn: CN=PavlovaJAV,OU=ruk,OU=Company,DC=altuninvv,DC=local
cn: PavlovaJAV
Обратите внимание мы использовали два логических оператора «И» и «ИЛИ» одновременно:
(&(objectCategory=person)(|(cn=PavlovaJAV)(cn=KalininaVL)(cn=SmirnovAV)))
Оператор НЕТ (NOT)
Оператор НЕТ обозначается знаком !
Давайте найдем всех руководителей без директора:
ldapsearch -x -b "OU=Company,DC=altuninvv,DC=local" -H "ldap://192.168.0.1" -D "CN=ldap-bind,CN=Users,DC=altuninvv,DC=local" -w Pas#w0rds#1 "(&(objectCategory=person)(|(cn=PavlovaJAV)(cn=KalininaVL)(cn=SmirnovAV))(! (cn=PavlovaJAV)))" cn
# SmirnovAV, ruk, Company, altuninvv.local
dn: CN=SmirnovAV,OU=ruk,OU=Company,DC=altuninvv,DC=local
cn: SmirnovAV
# KalininaVL, ruk, Company, altuninvv.local
dn: CN=KalininaVL,OU=ruk,OU=Company,DC=altuninvv,DC=local
cn: KalininaVL
Поиск в LDAP через Qt
Мы рассмотрели поиск в LDAP с помощью командной строки, вернемся к Qt5 и реализуем поиск в каталоге LDAP.
Добавим метод search в класс QLdap
Объявим метод
int search(QString baseDN, QString filter, QLdapEntryList *searchResults, const QString codePage="utf-8");
Реализация
int QLdap::search(QString baseDN, QString filter, QLdapEntryList *searchResults, const QString codePage)
{
QTextCodec *codec = QTextCodec::codecForName(codePage.toUtf8());
timeval *timeout = new timeval();
timeout->tv_sec = 100;
LDAPMessage *searchResultMsg;
int result = ldap_search_ext_s( this->ldp,
(char*)baseDN.toStdString().c_str(),
LDAP_SCOPE_SUBTREE,
(char*)filter.toStdString().c_str(),
nullptr,
0,
nullptr,
nullptr,
nullptr,
LDAP_NO_LIMIT,
&searchResultMsg
);
qDebug() << "Search result = " << ldap_err2string(result);
if ( result != LDAP_SUCCESS )
{
qDebug() << "ldap_parse_result() error: " << ldap_err2string(result);
}
result = ldap_count_entries( this->ldp, searchResultMsg);
qDebug() << "Search results count = " << result;
result = ldap_count_references( this->ldp, searchResultMsg);
qDebug() << "Search results ref = " << result;
LDAPMessage *msg;
QLdapEntryList results;
for ( msg = ldap_first_message( this->ldp, searchResultMsg ); msg != NULL; msg = ldap_next_message( this->ldp, msg ) )
{
int msgtype = ldap_msgtype( msg );
//qDebug() << "Type" << msgtype;
switch( msgtype ) {
//This is a search result data
case LDAP_RES_SEARCH_RESULT:
{
char *matched_msg = NULL;
int errcodep = 0;
char *error_msg = NULL;
char **refs;
LDAPControl **serverctrls;
int aresult = ldap_parse_result( this->ldp,
msg,
&errcodep,
nullptr,
&error_msg,
nullptr,
nullptr,
0 );
if ( aresult != LDAP_SUCCESS )
{
qDebug() << "ldap_parse_result() error: " << ldap_err2string(aresult);
}
if ( errcodep != LDAP_SUCCESS )
{
qDebug() << "ldap_parse_result() error: " << error_msg;
}
if (error_msg)
{
ldap_memfree(error_msg);
return errcodep;
}
break;
}
//This is a data item of search
case LDAP_RES_SEARCH_ENTRY:
{
BerElement *ber;
char *a;
QLdapEntry entry;
for ( a = ldap_first_attribute( this->ldp, msg, &ber );
a != NULL;
a = ldap_next_attribute( this->ldp, msg, ber ) ) {
struct berval **values;
struct berval value;
//qDebug() << a;
if (( values = ldap_get_values_len( this->ldp, msg, a )) != NULL ) {
for ( int i = 0; values[ i ] != NULL; i++ ) {
value = *values[i];
QByteArray buf = value.bv_val;
QString text;
if (codePage.compare("utf-8") == 0) {
text = buf;
} else {
text = codec->toUnicode(buf);
}
entry.insert(a,text);
qDebug() << a << ":" << text;
}
ldap_value_free_len( values );
}
ldap_memfree( a );
}
searchResults->append(entry);
break;
}
}
}
ldap_msgfree(searchResultMsg);
ldap_msgfree(msg);
return LDAP_SUCCESS;
}
Так же добавим объявления типов в начало файла qldap.h
typedef QHash<QString, QVariant> QLdapEntry;
typedef QList<QLdapEntry> QLdapEntryList;
Они нам понадобятся, чтобы сохранять значения атрибутов.
Добавим в конструктор главной формы код:
QLdapEntryList *searchResults = new QLdapEntryList();
int searchResult = ldap->search("OU=Company,DC=altuninvv,DC=local",
"(&(objectCategory=person))",
searchResults);
if ( searchResult != LDAP_SUCCESS )
{
ldap->close();
return;
}
Перенесем код:
result = ldap->close();
if ( result != LDAP_SUCCESS )
{
QString msg = QString("QLDAP init() error: ") + QString(ldap_err2string(result));
qFatal("%s",msg.toLatin1().constData());
return;
}
qDebug() << "Close result = " << ldap_err2string(result);
В конец конструктора.
Запустим:
Init result = Success
Bind result = Success
Search result = Success
Search results count = 100
Search results ref = 0
Close result = Success
Поиск прошел успешно, было возвращено 100 пользователей, как и ожидалось.
Разбираем код метода search
Давайте подробнее рассмотрим код метода:
Первая строчка
QTextCodec *codec = QTextCodec::codecForName(codePage.toUtf8());
была добавлена из-за странного бага с русской кодировкой, который пропал так же внезапно как и появился, возможно я не до конца в тот момент разобрался, как работает библиотека, но тем не менее я решил пока что не убирать этот функционал.
В текущем виде она ничего не делает если параметр с кодировкой не указан.
Если вам потребуется, чтобы результаты поиска были представлены не в utf-8 вы можете указать название этой кодировки в качестве четвертого параметра
Далее мы вызываем функцию, запускающую поиск по LDAP:
int result = ldap_search_ext_s( this->ldp,
(char*)baseDN.toStdString().c_str(),
LDAP_SCOPE_SUBTREE,
(char*)filter.toStdString().c_str(),
nullptr,
0,
nullptr,
nullptr,
nullptr,
LDAP_NO_LIMIT,
&searchResultMsg);
Функция принимает несколько параметров, давайте пронумеруем их, для наглядности:
- 1 – Указатель на объект с текущим соединением
- 2 – базовый DN в котором происходит поиск
- 3 – Указывает как проводить поиск, я по умолчанию осуществляю поиск во всем поддереве указанного DN
- 4 – Указатель на строку с фильтром
- Параметры с 5 по 9 не используются или не важны в данный момент, так что мы оставляем их либо nullptr, либо = 0
- 10 – Мы хотим получить все результаты поиска
- 11 – Указатель на результат поиска. В LDAP все результаты возвращаются в виде сообщений с типом LDAPMessage.
Далее мы проверяем, не возникли ли в процессе поиска ошибки.
Получение результатов поиска
Методы ldap_count_entries и ldap_count_references позволяют получить количество результатов поиска и реферальных ссылок после поиска.
Далее мы начинаем загружать результаты поиска.
Результаты поиска с LDAP сервера передаются в виде списка с указателем на следующий элемент.
Для того, чтобы получить первый элемент списка используется метод ldap_first_message.
Затем, после обработки сообщения, мы можем использовать метод ldap_next_message, для того, чтобы получить следующий элемент и так далее, до тех пор, пока метод ldap_next_message не вернет пустой указатель – NULL, что означает, что в списке не осталось элементов.
Типы сообщений
Каждое сообщение имеет собственный тип, на данный момент нас интересуют два типа сообщений:
- LDAP_RES_SEARCH_RESULT – конечный результат поиска, возвращается в самом конце результатов поиска и содержит текст ошибки, переданный со стороны сервера и код ошибки, если они есть.
- LDAP_RES_SEARCH_ENTRY – данные с результатом поиска, по сути, при поиске пользователей – сообщение с найденными данными пользователя.
Для того, чтобы получить тип сообщения используется метод ldap_msgtype:
int msgtype = ldap_msgtype( msg );
Далее мы рассмотрим код обработки сообщений LDAP_RES_SEARCH_ENTRY.
Атрибуты LDAP
Атрибуты, как и сообщения, передаются c сервера в виде списка с указателем на следующий элемент.
Для получения первого атрибута мы используем функцию ldap_first_attribute, для получения, следующего ldap_next_attribute.
Атрибуты возвращаются в виде указателя на строку с текстом.
Чтобы получить непосредственно значение мы используем метод ldap_get_values_len.
В качестве параметров он принимает:
- 1 – указатель на LDAP соединение
- 2 – текущее сообщение, из которого извлекаются атрибуты
- 3 – имя атрибута, значение которого извлекается
Результатом работы функции ldap_get_values_len является массив указателей, поэтому его необходимо обрабатывать в цикле:
for ( int i = 0; values[ i ] != NULL; i++ )
Возьмем некоторые результат поиска:
objectClass : "top"
objectClass : "person"
objectClass : "organizationalPerson"
objectClass : "user"
cn : "SuvorovDJA"
sn : "Суворов"
В данном случае objectClass имеет 4 значения, а cn и sn по одному.
Все значения атрибутов возвращаются в виде указателей типа berval.
Далее мы просто заполняем атрибутами список entry. И, после того как к entry добавлены все атрибуты добавляем его к результатам поиска.
Я умышлено не обрабатываю атрибуты с множеством значений, так как в данный момент это неактуально.
Обратите внимание, после того как вы закончили работать с сообщениями, вы обязательно должны корректно освободить память, под них выделенную.
ldap_msgfree(searchResultMsg);
ldap_msgfree(msg);
В следующей статье мы рассмотрим обработку результатов поиска и добавим методы для расширенного поиска в LDAP.
Заключение
Сегодня мы осуществили поиск в LDAP с помощью библиотеки OpenLDAP.
Были рассмотрены фильтр и базовый DN для поиска.
Проведен поиск с помощью консольной утилиты ldapsearch
Мы рассмотрели логические операторы фильтрации.
Написали метод для поиска в LDAP и подробно изучили его код.
Узнали, что информация о результатах поиска и атрибута элементов передается с сервера в виде сообщений, представляющих собой список с указателем на следующий элемент.
Исходный код проекта вы можете скачать на GitFlic.
Добавить комментарий