Суббота, 23 января 2021 19:00

Подключаемся к LDAP серверу. Работа с LDAP в Qt5. Часть 1.

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

Сегодня мы рассмотрим подключение к серверу Active Directory (AD)  с помощью протокола LDAP. Для этого мы будем использовать библиотеку OpenLDAP (libldap).

Установка пакетов

Прежде всего установим необходимые для разработки библиотеки.

Запустим консоль msys2 и выполним команду:

pacman -S mingw-w64-x86_64-openldap openssl libopenssl openssl-devel

Она установит требуемые пакеты.

Добавление библиотеки в проект

Создадим новый проект Qt Widgets Application и назовем его QOpenLDAP1.

Внесем изменения в проект, в .pro файл добавьте строку

LIBS += -L"/lib" -lldap

После

FORMS += \
    mainwindow.ui

 Таким образом мы указываем среде сборки где у нас находится подключаемая библиотека и указываем её имя.

Создаем класс

Давайте сразу оформим код в форме класса - создадим новый класс QLdap.

Добавим в qldap.h строку

#include "ldap.h"

Таким образом мы используем заголовочный файл библиотеки OpenLDAP (libldap)

Инициализация подключения

Перед подключением к серверу нужно произвести инициализацию объектов и установить параметры подключения.

Нам понадобиться указатель на объект соединения - LDAP

Добавим приватное поле

private:
    LDAP *ldp;

 Добавим метод 

int QLdap::init(const QString url)
{

    int result;
    this->ldp = NULL;

    std::string str = url.toStdString();
    const char *aurl = str.c_str();
    result = ldap_create(&this->ldp);


    if ( result != LDAP_SUCCESS )
    {
        qCritical() << "ldap_create() error: " << ldap_err2string(result);
        return result;
    }

    if (url != NULL) {
        result = ldap_set_option(this->ldp, LDAP_OPT_URI, aurl);
        if ( result != LDAP_SUCCESS ) {
            qCritical() << "ldap_set_option() url error: " << ldap_err2string(result);
            return result;
        }
    }

    const int version = LDAP_VERSION3;

    result = ldap_set_option(this->ldp, LDAP_OPT_PROTOCOL_VERSION, &version);
    if ( result != LDAP_SUCCESS )
    {
        qCritical() << "ldap_set_option() ver LDAP_OPT_PROTOCOL_VERSION error: " << ldap_err2string(result);
    }

    const int reff = 0;
    ldap_set_option(this->ldp, LDAP_OPT_REFERRALS, &reff);
    if ( result != LDAP_SUCCESS )
    {
        qCritical() << "ldap_set_option() ref LDAP_OPT_REFERRALS error: " << ldap_err2string(result);
    }

    return LDAP_SUCCESS;
}

Рассмотрим код – сначала мы создаем объект LDAP и получаем на него ссылку.

После этого обязательно проверяем результат операции, в случае проблемы выводим сообщение в консоль и возвращаем код ошибки.

Затем мы устанавливаем URL-адрес до сервера.

Обратите внимание – адрес должен быть в формате:

ldap://ip_адрес:порт

Например:

ldap://192.168.0.1:389

Далее устанавливаем версию протокола, в данный момент это версия 3.

В самом конце мы отключаем переход по реферальным ссылкам на другие LDAP серверы. Дело в том, что записи сервера могу содержать ссылки на другие сервера LDAP. В этом случае клиент может автоматически пытаться соединяться со внешними серверами, используя текущие логин и пароль. Так как мы планируем работать с одним сервером, мы отключаем данный функционал.

Даже если ошибка возникла, мы просто возвращаем код ошибки, давайте обработаем саму ошибку.

В конструктор главной формы добавим код:

   QLdap *ldap = new QLdap();

    const QString url= "ldap://192.168.0.1:389";

    int result = ldap->init(url);

    if ( result != LDAP_SUCCESS )
    {
        QString msg = QString("QLDAP init() error: ") + QString(ldap_err2string(result));
        qFatal("%s",msg.toLatin1().constData());
        return;
    }

Теперь, например, при использовании неверного url:

    const QString url= "ldap1://192.168.0.1:389";

Мы получим ошибки:

ldap_set_option() url error:  Bad parameter to an ldap routine

QLDAP init() error: Bad parameter to an ldap routine

Сообщения умышленно выводиться два раза: первое - сообщение о критической ошибке с указание блока в котором она произошла, а второе – фатальная ошибка после которой работа программы принудительно завершается.

Обратите внимание – вы всегда должны сами обрабатывать потенциальные фатальные ошибки и освобождать память ото всех созданных объектов и ссылок.

Создаем пользователя для подключения к LDAP

Для подключения к AD с помощью LDAP нам потребуется создать отдельного пользователя.

Мы будем добавлять пользователя в виртуальный домен, созданный в этой статье - ссылка.

Откроем консоль PowerShell на нашем сервере и запустим:

Сначала добавим группу

New-ADGroup -Name gg-ldap-bind -GroupScope Global

Создадим пользователя

New-ADUser -Name ldap-bind -AccountPassword (ConvertTo-SecureString Pas#w0rds#1 -AsPlainText -force) -Enabled $true -ChangePasswordAtLogon $false

Добавим пользователя в группу

Add-ADGroupMember -Identity gg-ldap-bind -Members ldap-bind

Установим созданную группу для этого пользователя группой по умолчанию:

$group = get-adgroup "gg-ldap-bind" -properties @("primaryGroupToken") 

set-aduser -Identity ldap-bind -replace @{primaryGroupID=$group.primaryGroupToken}

Удалим пользователя из группы Domain Users

Remove-ADGroupMember -Identity "Domain Users" -Members ldap-bind

Мы удаляем пользователя из группы Domain Users чтобы исключить использование пользователя для авторизации в домене при доступе к ресурсам.

Вы можете просто запустить следующий скрипт для автоматического добавления пользователя:

#Remove-ADUser -Identity "ldap-bind"

New-ADGroup -Name gg-ldap-bind -GroupScope Global 

New-ADUser -Name ldap-bind -AccountPassword (ConvertTo-SecureString Pas#w0rds#1 -AsPlainText -force) -Enabled $true -ChangePasswordAtLogon $false

Add-ADGroupMember -Identity gg-ldap-bind -Members ldap-bind

$group = get-adgroup "gg-ldap-bind" -properties @("primaryGroupToken")

set-aduser -Identity ldap-bind -replace @{primaryGroupID=$group.primaryGroupToken} 

Remove-ADGroupMember -Identity "Domain Users" -Members ldap-bind  

Вы можете раскомментировать первую строчку, если вам потребуется пересоздавать пользователя.

Теперь пользователь создан и может быть использован для доступа через LDAP.

Синхронный и асинхронный методы доступа к LDAP

После того, как соединение инициализировано, нам необходимо подключиться к серверу.

Существуют два режима работы с LDAP – синхронный и асинхронный.

Синхронный проще в реализации и сегодня мы будет рассматривать его, асинхронный метод будет рассмотрен отдельно, в будущих статьях.

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

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

Сегодня мы будем использовать синхронный метод подключения.

Связывание (биндинг-bind)  с сервером LDAP

После инициализации пришла пора подключиться к серверу, в LDAP этот процесс называется привязкой или биндиногом.

Добавим новый метод:

int QLdap::bind(QString dn, QString password)
{
    const char *adn;
    adn = (char*)dn.toStdString().c_str();

    berval *creds = new berval();

    creds->bv_val = (char*)password.toStdString().c_str();
    creds->bv_len = password.length();

    int result  = ldap_sasl_bind_s(this->ldp,
                                   adn,
                                   LDAP_SASL_SIMPLE,
                                   creds,
                                   nullptr,
                                   nullptr,
                                   nullptr);

    if ( result != LDAP_SUCCESS )
    {
        qCritical() << "ldap_sasl_bind_s() error: " << ldap_err2string(result);
    }
    return result;
}

Рассмотрим этот код:

При подключении к LDAP серверу нам необходимо указать имя пользователя и пароль.

Так как мы используем внешнюю библиотеку,  нам необходимо, чтобы передаваемые параметры соответствовали описанным в заголовочном файле библиотеки OpenLDAP.

В данном случае логин и пароль ожидаются в формате const char *, поэтому мы преобразуем логин:

    const char *adn;
    adn = (char*)dn.toStdString().c_str();

С паролем всё труднее – функция требует от нас, чтобы этот параметр соответствовал типу berval.

Тип berval

Berval или BER – правила для представления структуры данных в двоичном виде. Т.е. по сути перекодирования текстовой информации в двоичное представление для передачи по линиям связи. В berval данные хранятся в виде октетов. Один октет всегда равен 8 битам!

Тип berval представляет собой простую структуру:

typedef struct berval {
                ber_len_t           bv_len;
                char                       *bv_val;
} BerValue;

В bv_val хранится указатель на строку, в bv_len длинна этой строки.

Преобразование к типу berval

Преобразовать данные к типу berval очень просто, например, для передачи пароля в функцию ldap_sasl_bind_s:

berval *creds = new berval();
creds->bv_val = (char*)password.toStdString().c_str();
creds->bv_len = password.length();

Закрытие соединения (unbind)

Не менее важным этапом работы является закрытие соединения до сервера. У сервера есть лимит на открытые соединения с одного адреса, когда их количество превышает некий порог, сервер отклоняет последующие соединения, вы можете получить ошибку Server is busy. Чтобы этого не произошло нужно закрывать соединения, это ко всему прочему позволяет освободить память, выделенную библиотекой libldap для своих нужд.

Напишем простой метод:

int QLdap::close()
{
    int result = ldap_unbind_ext_s(this->ldp,nullptr,nullptr);    
    if ( result != LDAP_SUCCESS )
    {
        qCritical() << "ldap_unbind_ext_s() error: " << ldap_err2string(result);
    }
    return result;
}

Здесь мы просто вызываем метод ldap_unbind_ext_s для того, чтобы освободить выделенную память и закрыть все открытые соединения.

Собираем всё вместе

Давайте попробуем подключиться к нашему виртуальному AD серверу через LDAP, добавим в конструктор главной формы код:

   

    QLdap *ldap = new QLdap();

    const QString url= "ldap://192.168.130.233:389";

    int result = ldap->init(url);
    if ( result != LDAP_SUCCESS )
    {
        QString msg = QString("QLDAP init() error: ") + QString(ldap_err2string(result));
        qFatal("%s",msg.toLatin1().constData());
        return;
    }

    qDebug() << "Init result = " << ldap_err2string(result);

    result = ldap->bind("CN=ldap-bind,CN=Users,DC=altuninvv,DC=local", "Pas#w0rds#1");

    if ( result != LDAP_SUCCESS )
    {

        QString msg = QString("QLDAP bind() error: ") + QString(ldap_err2string(result));
        qFatal("%s",msg.toLatin1().constData());
        ldap->close();
        return;
    }

    qDebug() << "Bind result = " << ldap_err2string(result);


    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);

    return;

 Обратите внимание, это важно, имя пользователя должно быть в формате DN: 

CN=ldap-bind,CN=Users,DC=altuninvv,DC=local

Запустим

Если все прошло удачно, в консоли вы увидите строчки:

Init result =  Success

Bind result =  Success

Close result =  Success

Заключение

Сегодня мы рассмотрели подключение к серверу Active Directory по протоколу LDAP  с помощью библиотеки OpenLDAP.

Мы инициализировали соединение, установили параметры подключения и задали URL подключения.

Добавили в AD пользователя, для биндинга по LDAP.

Подключились к серверу используя синхронный метод подключения.

Рассмотрели, что такое berval.

Реализовали метод для корректного отключения от сервера.

Протестировали соединение.

В следующей статье мы рассмотрим поиск в AD используя протокол LDAP.

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

Прочитано 1137 раз Последнее изменение Четверг, 28 января 2021 15:35
Другие материалы в этой категории: Поиск данных в каталоге LDAP. Работа с LDAP в Qt5. Часть 2. »