среда, 27 ноября 2013 г.

Как работать с LDAP в PL/SQL, часть II

В первой части статьи я начал рассматривать работу с Active Directory сервером на языке PL/SQL с использованием пакета dbms_ldap. Были разобраны аутентификация, операция поиска, и написаны хранимые функция и процедуры ldap_open, ldap_print и ldap_search_and_print для облегчения дальнейшей работы. В данной статье будут рассмотрены операции модификации данных в LDAP каталоге, и попутно создан пакет ldap_helper на базе ранее написанных и новых PL/SQL функций и процедур.

Начнем с создания пакета ldap_helper, поместив в него, помимо уже упомянутых, новую функцию to_strcol для преобразования VARCHAR2 списка с разделителем в таблицу dbms_ldap.STRING_COLLECTION. Эта функция существенно упростит задание списка атрибутов для поиска:

CREATE OR REPLACE PACKAGE ldap_helper IS

    -- Разобрать и вывести результат поиска
    PROCEDURE print(
        p_session dbms_ldap.SESSION,
        p_results dbms_ldap.MESSAGE);

    -- Преобразовать VARCHAR2 список с разделителм в dbms_ldap.STRING_COLLECTION
    FUNCTION to_strcol(
        p_list IN VARCHAR2,
        p_separator IN VARCHAR2 DEFAULT ',')
    RETURN dbms_ldap.STRING_COLLECTION;

    -- открыть сеанс работы с LDAP сервером
    FUNCTION ldap_open
    RETURN dbms_ldap.SESSION;

    -- Выполнить поиск
    FUNCTION search(
        p_session dbms_ldap.SESSION,
        p_base   IN VARCHAR2,
        p_scope  IN PLS_INTEGER,
        p_filter IN VARCHAR2,
        p_attrs  IN VARCHAR2)
    RETURN dbms_ldap.MESSAGE;

    -- Выполнить поиск и вывести результат поиска
    PROCEDURE search_and_print(
        p_session dbms_ldap.SESSION,
        p_base   IN VARCHAR2,
        p_scope  IN PLS_INTEGER,
        p_filter IN VARCHAR2,
        p_attrs  IN VARCHAR2);

END ldap_helper;
/

CREATE OR REPLACE PACKAGE BODY ldap_helper IS

    -- Разобрать и вывести результат поиска
    PROCEDURE print(
        p_session dbms_ldap.SESSION,
        p_results dbms_ldap.MESSAGE)
    IS
        l_entry dbms_ldap.MESSAGE;
        l_attr VARCHAR2(256);
        l_values dbms_ldap.STRING_COLLECTION;
        l_berelem dbms_ldap.ber_element;
        i PLS_INTEGER;
    BEGIN
        l_entry := dbms_ldap.first_entry(p_session, p_results);
        WHILE l_entry IS NOT NULL LOOP
            dbms_output.put_line('DN = ' || dbms_ldap.get_dn(p_session, l_entry));
            l_attr := dbms_ldap.first_attribute(p_session, l_entry, l_berelem);
            WHILE l_attr IS NOT NULL LOOP
                l_values := dbms_ldap.get_values(p_session, l_entry, l_attr);
                i := l_values.first;
                WHILE i IS NOT NULL LOOP
                    dbms_output.put_line(LPAD(l_attr, 15) || ' = ' || l_values(i));
                    i := l_values.next(i);
                END LOOP;
                l_attr := dbms_ldap.next_attribute(p_session, l_entry, l_berelem);
            END LOOP;
            l_entry := dbms_ldap.next_entry(p_session, l_entry);
        END LOOP;
    END print;

    -- Преобразовать VARCHAR2 список с разделителм в dbms_ldap.STRING_COLLECTION
    FUNCTION to_strcol(
        p_list IN VARCHAR2,
        p_separator IN VARCHAR2 DEFAULT ',')
    RETURN dbms_ldap.STRING_COLLECTION
    IS
        i PLS_INTEGER := 0;
        beg PLS_INTEGER := 1;
        fin PLS_INTEGER := 0;
        strcol dbms_ldap.STRING_COLLECTION;
    BEGIN
        IF p_list IS NULL OR p_separator IS NULL THEN
            RETURN strcol;
        END IF;
        fin := instr(p_list, p_separator, beg);
        WHILE fin > 0 LOOP
            strcol(i) := TRIM(substr(p_list, beg, fin - beg));
            beg := fin + length(p_separator);
            fin := instr(p_list, p_separator, beg);
            i := i + 1;
        END LOOP;
        strcol(i) := TRIM(substr(p_list, beg));
        RETURN strcol;
    END to_strcol;

    -- открыть сеанс работы с LDAP сервером
    FUNCTION ldap_open
    RETURN dbms_ldap.SESSION
    IS
        LDAP_HOST CONSTANT VARCHAR2(20) := '192.168.0.16';
        LDAP_PORT CONSTANT VARCHAR2(20) := dbms_ldap.PORT;
        LDAP_USER CONSTANT VARCHAR2(20) := 'trofimov_a@SKY';
        LDAP_PSWD CONSTANT VARCHAR2(20) := 'nooneknows';
        l_dummy PLS_INTEGER;
        l_session dbms_ldap.SESSION;
    BEGIN
        l_session := dbms_ldap.init(LDAP_HOST, LDAP_PORT);
        l_dummy := dbms_ldap.simple_bind_s(l_session, LDAP_USER, LDAP_PSWD);
        RETURN l_session;
    END ldap_open;

    -- Выполнить поиск
    FUNCTION search(
        p_session dbms_ldap.SESSION,
        p_base   IN VARCHAR2,
        p_scope  IN PLS_INTEGER,
        p_filter IN VARCHAR2,
        p_attrs  IN VARCHAR2)
    RETURN dbms_ldap.MESSAGE
    IS
        l_dummy PLS_INTEGER;
        l_results dbms_ldap.MESSAGE;
    BEGIN
        l_dummy := dbms_ldap.search_s(
            p_session,
            p_base,
            p_scope,
            p_filter,
            to_strcol(p_attrs),
            0,
            l_results
        );
        RETURN l_results;
    END search;

    -- Выполнить поиск и вывести результат поиска
    PROCEDURE search_and_print(
        p_session dbms_ldap.SESSION,
        p_base   IN VARCHAR2,
        p_scope  IN PLS_INTEGER,
        p_filter IN VARCHAR2,
        p_attrs  IN VARCHAR2)
    IS
        l_dummy PLS_INTEGER;
        l_results dbms_ldap.MESSAGE;
    BEGIN
        print(p_session, search(p_session, p_base, p_scope, p_filter, p_attrs));
    EXCEPTION
    WHEN dbms_ldap.invalid_search_scope OR dbms_ldap.general_error THEN
        dbms_output.put_line('Не найдена запись с DN ' || p_base);
    END search_and_print;

END ldap_helper;
/

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

Добавим новую запись класса organizationalPerson в Производственный отдел. Функция add_s пакета dbms_ldap принимает в качестве параметров DN новой записи и массив атрибутов и их значений. Этот массив вначале нужно подготовить, для чего предназначены функция dbms_ldap.create_mod_array и процедура dbms_ldap.populate_mod_array. Обратите внимание, как пригодилась в данном случае новая функция ldap_helper.to_strcol для подготовки массива значений атрибутов.

-- Добавим новую запись в Active Directory
DECLARE
    l_sess dbms_ldap.session;
    l_dummy PLS_INTEGER;
    l_dn VARCHAR2(200);
    l_modarray dbms_ldap.MOD_ARRAY;
BEGIN
    l_sess := ldap_helper.ldap_open;

    -- подготовить данные для добавления сотрудника
    l_dn := 'CN=Обломов Илья,OU=Производственный отдел,O=Синее Небо,DC=org,DC=ru';
    l_modarray := dbms_ldap.create_mod_array(4);
    dbms_ldap.populate_mod_array(
        l_modarray, 
        dbms_ldap.MOD_ADD, 
        'cn', 
        ldap_helper.to_strcol('Обломов Илья'));
    dbms_ldap.populate_mod_array(
        l_modarray, 
        dbms_ldap.MOD_ADD, 
        'sn', 
        ldap_helper.to_strcol('Обломов'));
    dbms_ldap.populate_mod_array(
        l_modarray, 
        dbms_ldap.MOD_ADD, 
        'objectClass', 
        ldap_helper.to_strcol('organizationalPerson, person, top'));
    dbms_ldap.populate_mod_array(
        l_modarray, 
        dbms_ldap.MOD_ADD, 
        'telephoneNumber', 
        ldap_helper.to_strcol('2121212'));
    -- добавить сотрудника
    l_dummy := dbms_ldap.add_s(l_sess, l_dn, l_modarray);

    -- что получилось?
    ldap_helper.search_and_print(
        p_session => l_sess,
        p_base => l_dn,
        p_scope => dbms_ldap.SCOPE_BASE,
        p_filter => '(objectClass=*)',
        p_attrs => 'cn, givenName, sn, objectClass, telephoneNumber'
    );
    
    l_dummy := dbms_ldap.unbind_s(l_sess);
END;
/

DN = CN=Обломов Илья,OU=Производственный отдел,O=Синее Небо,DC=org,DC=ru
    objectClass = top
    objectClass = person
    objectClass = organizationalPerson
             cn = Обломов Илья
             sn = Обломов
telephoneNumber = 2121212

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

Для изменения существующей записи нужно подготовить массив модификаций с помощью тех же dbms_ldap.create_mod_array и dbms_ldap.populate_mod_array. Режим модификации для каждого из атрибутов - одно из:

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

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

DECLARE
    l_sess dbms_ldap.session;
    l_dummy PLS_INTEGER;
    l_dn VARCHAR2(200);
    l_modarray dbms_ldap.MOD_ARRAY;
BEGIN
    l_sess := ldap_helper.ldap_open;

    -- подготовить данные для модификации сотрудника
    l_dn := 'CN=Обломов Илья,OU=Производственный отдел,O=Синее Небо,DC=org,DC=ru';
    l_modarray := dbms_ldap.create_mod_array(3);
    dbms_ldap.populate_mod_array(
        l_modarray, 
        dbms_ldap.MOD_ADD, 
        'givenName', 
        ldap_helper.to_strcol('Илья'));
    dbms_ldap.populate_mod_array(
        l_modarray, 
        dbms_ldap.MOD_REPLACE, 
        'sn', 
        ldap_helper.to_strcol('Обломоff'));
    dbms_ldap.populate_mod_array(
        l_modarray, 
        dbms_ldap.MOD_DELETE, 
        'telephoneNumber', 
        ldap_helper.to_strcol(NULL, NULL));
    -- модифицировать сотрудника
    l_dummy := dbms_ldap.modify_s(l_sess, l_dn, l_modarray);

    -- что получилось?
    ldap_helper.search_and_print(
        p_session => l_sess,
        p_base => l_dn,
        p_scope => dbms_ldap.SCOPE_BASE,
        p_filter => '(objectClass=*)',
        p_attrs => 'cn, givenName, sn, objectClass, telephoneNumber'
    );

    l_dummy := dbms_ldap.unbind_s(l_sess);
END;
/

DN = CN=Обломов Илья,OU=Производственный отдел,O=Синее Небо,DC=org,DC=ru
    objectClass = top
    objectClass = person
    objectClass = organizationalPerson
             cn = Обломов Илья
             sn = Обломоff
      givenName = Илья

Функция dbms_ldap.rename_s позволяет изменить RDN записи:

-- Переименовать (изменить RDN)
DECLARE
    l_sess dbms_ldap.session;
    l_dummy PLS_INTEGER;
    l_dn VARCHAR2(200);
BEGIN
    l_sess := ldap_helper.ldap_open;

    -- Изменить RDN
    l_dn := 'CN=Обломов Илья,OU=Производственный отдел,O=Синее Небо,DC=org,DC=ru';
    l_dummy := dbms_ldap.rename_s(
        ld => l_sess,
        dn => l_dn,
        newrdn => 'cn=Обломов Илья Ильич',
        newparent => 'OU=Производственный отдел,O=Синее Небо,DC=org,DC=ru',
        deleteoldrdn => 1);

    -- что получилось?
    ldap_helper.search_and_print(
        p_session => l_sess,
        p_base => l_dn,
        p_scope => dbms_ldap.SCOPE_BASE,
        p_filter => '(objectClass=*)',
        p_attrs => 'cn, givenName, sn, objectClass, telephoneNumber'
    );
    ldap_helper.search_and_print(
        p_session => l_sess,
        p_base => 'CN=Обломов Илья Ильич,OU=Производственный отдел,O=Синее Небо,DC=org,DC=ru',
        p_scope => dbms_ldap.SCOPE_BASE,
        p_filter => '(objectClass=*)',
        p_attrs => 'cn, givenName, sn, objectClass, telephoneNumber'
    );
    l_dummy := dbms_ldap.unbind_s(l_sess);
END;
/

Не найдена запись с DN CN=Обломов Илья,OU=Производственный отдел,O=Синее Небо,DC=org,DC=ru
DN = CN=Обломов Илья Ильич,OU=Производственный отдел,O=Синее Небо,DC=org,DC=ru
    objectClass = top
    objectClass = person
    objectClass = organizationalPerson
             cn = Обломов Илья Ильич
             sn = Обломоff
      givenName = Илья

Здесь первый вызов ldap_helper.search_and_print не нашел запись по прежнему DN, а второй вызов, с новым DN, нашел и вывел запись на экран.

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

-- Удалить запись
DECLARE
    l_sess dbms_ldap.session;
    l_dummy PLS_INTEGER;
    l_dn VARCHAR2(200);
BEGIN
    l_sess := ldap_helper.ldap_open;

    -- Изменить RDN
    l_dn := 'CN=Обломов Илья Ильич,OU=Производственный отдел,O=Синее Небо,DC=org,DC=ru';
    l_dummy := dbms_ldap.delete_s(l_sess, l_dn);

    -- что получилось?
    ldap_helper.search_and_print(
        p_session => l_sess,
        p_base => l_dn,
        p_scope => dbms_ldap.SCOPE_BASE,
        p_filter => '(objectClass=*)',
        p_attrs => 'cn, givenName, sn, objectClass, telephoneNumber'
    );

    l_dummy := dbms_ldap.unbind_s(l_sess);
END;
/

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

Все примеры выше используют синхронные операции с LDAP. Пакет dbms_ldap не предоставляет асинхронных функций.

Что касается пакета ldap_helper - фасада для dbms_ldap, - то чтобы сделать его еще полезнее, можно предложить следующие доработки:

  • Добавить опциональные параметры p_host, p_port, p_user, p_pswd для ldap_open. Тогда можно будет присоединяться к любому LDAP серверу. И добавить для симметрии ldap_close - обертку для dbms_ldap.unbind_s.
  • Попробовать запоминать открытый сеанс в переменной пакета и повторно его использовать, вместо открытия нового сенса каждый раз.
  • Создать функцию для возврата результата поиска как таблица таблиц таблиц (в смысле PL/SQL). Тогда значения атрибутов извлеченных записей можно будет получить как result(0)('name')(0), где первый 0 выбирает первую запись, 'name' выбирает атрибут, а последний 0 выбирает значение.
  • Создать функцию для подготовки массива модификаций dbms_ldap.MOD_ARRAY из таблицы записей PL/SQL вида (режим модификации, имя атрибута, список значений). И добавить процедуры add и modify, принимающие на вход массив модификаций в виде такой таблицы записей.

7 комментариев:

  1. добрый день, наткнулся на Ваш пост, у меня следующий вопрос по своему заданию, как можно получить objectGUID? когда я выбираю значение стандартными средставми возвращается строка в таком виде-
    ? ??K? Q8s?qp
    - как ее преобразовать или правильно получить средствами dbms_ldap?

    ОтветитьУдалить
  2. Добрый день, Ришат.

    Атрибут objectGUID бинарный. Для получения значений бинарных атрибутов есть функция dbms_ldap.get_values_len, подобная dbms_ldap.get_values, но возвращающая dbms_ldap.BINVAL_COLLECTION вместо dbms_ldap.STRING_COLLECTION. Если воспользоваться ей, то objectGUID выводится как строка, представлющая 16 байт в шестнадцатеричной записи, например: 29B6E8BA75AE5443A65CAC8BDEF9CFF4.

    Дальше можно преобразовать ее к нужному виду. Если ее разбить на части по 4-2-2-2-6 байт, в первых трех частях изменить порядок байт на противоположный, то получится то самое каноническое представление, которое берут в фигурные скобки: {BAE8B629-AE75-4354-A65C-AC8BDEF9CFF4}.

    ОтветитьУдалить
  3. Добрый время суток.
    Можно ли создать структуру DC/O/OU (objectclass=organizationalUnit) c помощью пакета dbms_ldap?

    ОтветитьУдалить
  4. Добрый день
    Можно, аналогично тому, как в приведенном мной примере создана запись organizationalPerson. Если нужно создать всю иерархию DC/O/OU, то последовательно: сначала DC, затем O, затем OU.

    ОтветитьУдалить
  5. Здравствуйте!
    А как добавить пользователя в определенную группу?

    ОтветитьУдалить
  6. Добрый день! Не решал такую задачу, так что не подскажу.

    ОтветитьУдалить