Недавно на работе у меня было такое задание - соединить 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, при этом приложение использует только пароль, который предоставил сам пользователь, доступ на чтение ко всей базе данных не нужен.