Недавно на работе у меня было такое задание - соединить NodeJS приложение с OpenLDAP сервером, обладающем следующей структурой данных: в корне - структура  Organization Unit с профилями пользователями, доступная, для примера, по DN:

ou=people,dc=vodolaz095,dc=life.

И имеющая следующую струкруту:


dn: ou=people,dc=vodolaz095,dc=life
objectclass: organizationalUnit
ou: people

В этой структуре - уже были профили пользователей - например, uid=user1,ou=people,dc=vodolaz095,dc=life, uid=user2,ou=people,dc=vodolaz095,dc=life и т.д.

Аккаунты пользователей принадлежали к типам posixAccount, inetOrgPerson,organizationalPerson,person.

Аккаунты пользователей выглядели примерно вот так:

dn: uid=user1,ou=people,dc=vodolaz095,dc=life
cn: Jane Doe
gidnumber: 1000
givenname:  Jane
homedirectory: /home/janedoe
initials: JS
loginshell: /bin/bash
objectclass: posixAccount
objectclass: inetOrgPerson
objectclass: organizationalPerson
objectclass: person
mail: [email protected]
sn: Doe
uid: user1
uidnumber: 1002
userpassword: {SSHA}HF3O9bpD+/a5iswMxRCwBgboZgV2Mkpp

При этом для каждого пользователя был задан пароль.

Задача состояла в том, что пользователь вводит логин (в виде user1) и пароль в NodeJS приложении. И программа должна проверить, угадал ли пользователь пароль, а потом программа должна загрузить все данные о пользователе из базы данных OpenLDAP.

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

Для работы по протоколу openLDAP я использовал следующую NodeJS библиотеку - https://www.npmjs.com/package/ldapjs
Минимальный работающий образец исходного кода представлен тут:

'use strict';

const ldap = require('ldapjs');

const ldapClient = ldap.createClient({
  url: 'ldap://127.0.0.1:389'
});

const username = 'cn=readonly,dc=vodolaz095,dc=life';
const password = 'readonly';

ldapClient.bind(
  username,
  password,
  function (error) {
    if (error) {
      throw error;
    }
    console.log('bind performed');

    ldapClient.search('ou=people,dc=vodolaz095,dc=life', {
      filter: `(uid=vodolaz095)`,
      scope: 'one',
      attributes: ['uid', 'dn', 'cn', 'mail']
    }, function (error, res) {
      if (error) {
        throw error;
      }
      res.on('searchEntry', function (data) {
        // console.log('Data found', data);
        console.log('Data object', JSON.stringify(data.object, null, 2));
      });
      res.once('error', function(error){
        throw error;
      });
      res.once('end', function () {
        console.log('Completed');
        process.exit(0)
      });
    });
  }
);

Как видно из кода, мы сначала "биндимся" к базе данных как пользователь readonly с DN cn=readonly,dc=vodolaz095,dc=life, потом ищем пользователя в
коллекции ou=people,dc=vodolaz095,dc=life по фильтру (uid=vodolaz095), и если мы его нашли, то можно попробывать "забиндиться" как этот пользователь к базе данных, тем самым проверив пароль, подходит ли он.

'use strict';

const ldap = require('ldapjs');

const ldapClient = ldap.createClient({
  url: 'ldap://127.0.0.1:389'
});

const username = 'uid=vodolaz095,ou=people,dc=vodolaz095,dc=life';
const password = 'superSecretPasswordOfVodolaz095!';

ldapClient.bind(
  username,
  password,
  function (error) {
    if (error) {
      throw error;
    }
    console.log('пароль подошёл, пользователь vodolaz095 авторизирован!');
});

Можно ли как то упростить данную процедуру?

Возможно ли не использовать пользователя readonly?

Можно ли минимизировать число запросов данных к базе данных ldap?

Ответ:

Да, можно

И сейчас я расскажу, как.
Оказывается, большинство openLDAP серверов позволяют авторизоваться ("забиндиться") как искомый пользователь, и даже получить его профиль.

Рассмотрим данный код:

'use strict';

const ldap = require('ldapjs');

const ldapClient = ldap.createClient({
  url: 'ldap://127.0.0.1:389'
});


const username = 'uid=vodolaz095,ou=people,dc=vodolaz095,dc=life'
const password = 'thisIsVerySecureSecretPassword';


ldapClient.bind(
  username,
  password,
  function (error) {
    if (error) {
      throw error;
    }
    console.log('bind performed');

    ldapClient.search('uid=vodolaz095,ou=people,dc=vodolaz095,dc=life', { 
      scope: 'base', // important
      attributes: ['uid', 'dn', 'cn', 'mail']
    }, function (error, res) {
      if (error) {
        throw error;
      }
      res.on('searchEntry', function (data) {
        // console.log('Data found', data);
        console.log('Data object', JSON.stringify(data.object, null, 2));
      });
      res.once('error', function (error){
        throw error;
      });
      res.once('end', function () {
        console.log('All passed');
        process.exit(0);
      });
    });
  }
);

С помощью этого кода мы напрямую пытаемся "забиндиться" к базе данных ldap как ограниченный пользователь, используя функцию
ldapClient.bind(username, password, function(error){....}).

Если "забиндиться" получилось, то это означает, что пароль подошёл. Теперь надо получить профиль пользователя, и по умолчанию, все пользователи ldap имеют права на чтение своего собственного профиля.
По крайней мере, так работает в контейнеризированной сборке openLDAP из https://github.com/osixia/docker-openldap

После того, как мы забиндились, мы можем попробывать найти свой собственный профиль. Это можно сделать с помощью данной функции:

    ldapClient.search('uid=vodolaz095,ou=people,dc=vodolaz095,dc=life', { 
      scope: 'base', 
      attributes: ['uid', 'dn', 'cn', 'mail']
    }, function (error, res) {....})

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