Понедельник, 25 января 2021 19:00

Поиск данных в каталоге LDAP. Работа с LDAP в Qt5. Часть 2.

Россия
Оцените материал
(0 голосов)

В первой части мы рассмотрели инициализацию, подключение и отключение от 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: Этот адрес электронной почты защищен от спам-ботов. Для просмотра адреса в вашем браузере должен быть включен Javascript.
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 через Qt5

Мы рассмотрели поиск в 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 и подробно изучили его код.

Узнали, что информация о результатах поиска и атрибута элементов передается с сервера в виде сообщений, представляющих собой список с указателем на следующий элемент.

Исходный код проекта вы можете скачать на Github.

Прочитано 474 раз Последнее изменение Четверг, 28 января 2021 15:36