Хочу описать один из способов разграничения доступа к данным в СУБД, который мне кажется довольно гибким и интересным. Этот способ позволяет получать информацию о текущем пользователе с помощью вызова простой хранимой процедуры. Но сперва рассмотрим известные существующие способы с их плюсами и минусами, среди которых можно выделить использование встроенных механизмов аутентификации СУБД и контроль доступа на уровне приложения.
Способ 1. Встроенные механизмы аутентификации
Для каждого бизнес-пользователя создаётся соответствующий пользователь в СУБД, которому раздаются необходимые права.
Плюсы такого подхода: его простота и прозрачность. По логам СУБД легко увидеть, какие запросы выполняют пользователи, несколько прав можно объединять в роли и раздавать их пользователям прямо "из коробки". Основной минус такого подхода — отсутствие контроля доступа на уровне строк. Да, в 9.5 появилась row-level security, но этот механизм работает не так быстро, как хотелось бы, особенно для JOIN.
К встроенным механизмам аутентификации также относятся LDAP, PAM, GSSAPI и прочие.
Способ 2. Проверка на уровне приложения
Многие осуществляют разграничение доступа прямо на уровне приложения. При этом можно использовать как внешний сервис для авторизации пользователей так и хранить хеши паролей непосредственно в базе и проверять их в приложении. Это не имеет значения. Главное то, что все пользователи в конечном итоге ходят в базу под одним пользователем. В таком подходе я вообще не вижу никаких плюсов, зато минусов предостаточно:
- Отсутствует контроль доступа на уровне строк, либо он становится очень сложным.
- В случае компрометации пароля пользователя СУБД злоумышленник получает полный доступ ко всем данным, причём он сможет не только читать их, но и изменять.
- Приложение становится единственной звеном, контролирующим доступ и если вы, допустим, захотите реализовать ещё какой-нибудь сервис, работающий с базой, вам придётся писать весь код, выполняющий проверки, заново.
Несмотря на такое большое количество минусов, по моим наблюдениям это самый распостранённый способ разграничения доступа на сегодняшний день.
Способ 3. Введение сессии на уровне СУБД
Об этом способе я сегодня и хочу рассказать поподробнее. Суть его проста: в базе данных создаётся процедура авторизации, которая проверяет логин и пароль пользователя и в случае успеха устанавливает значение некоторой сессионной переменной, которая была бы доступна на чтение до конца текущей сессии. Для хранения значения переменной будем использовать глобальный массив GD, доступный процедурам на языке Pl/Python:
create or replace
function set_current_user_id(user_id integer) as $$
GD['user_id'] = user_id
$$ language plpythonu;
Сама же процедура авторизации будет выглядеть следующим образом:
create or replace
function login(user_ text, password_ text) returns integer as $$
declare
vuser_id integer; vis_admin boolean;
begin
select id, is_admin
into vuser_id, is_admin
from users where login = login_ and password = password_;
if found then
perform set_current_user_id(vuser_id);
/* код функции set_is_admin() аналогичен
коду функции set_current_user_id() */
perform set_is_admin(vis_admin);
else
raise exception 'Invalid login or password';
end if;
return vuser_id;
end;
$$ language plpgsql security definer;
После этого осталось реализовать функцию, которая будет возвращать ID залогиненного пользователя:
create or replace
function get_current_user_id() returns integer as $$
return GD.get('user_id')
$$ language plpythonu stable;
Теперь о том, как это всё использовать. А использовать очень просто. После авторизации пользователя внутри любой функции теперь можно легко узнать, что за пользователь запрашивает доступ к данным и какие у него есть права. Например:
create or replace
function delete_branch(branch_id_ integer) returns void as $$
begin
if not current_user_is_admin() then
raise exception 'Access denied: this operation needs admin privileges';
end if;
...
end;
$$ language plpgsql;
Для демонстрации того, как будет работать разграничение доступа на уровне строк, напишем функцию, которая будет возвращать список счетов в банке, причём только тех, которые открыты в филиале, к которому принадлежит пользователь (branch_id).
create or replace
function get_accounts() returns table (account_number text) as $$
begin
return query
select a.account_number
from accounts a
join users u on u.branch_id = a.branch_id
where u.id = get_current_user_id();
end;
$$ language plpgsql;
В чём плюсы и минусы такого подхода? Плюсы:
- Удобство использования, гибкость, расширяемость.
- Обеспечение разграничения доступа на уровне строк практически без ущерба для производительности СУБД.
- Вся логика сосредоточена в СУБД, таким образом можно предоставлять доступ к базе данных нескольким приложениям, в которых придётся реализовать лишь механизм авторизации.
- Кроме информации о самом пользователе, можно оперативно получать любые метаданные, связанные с ним — например, является ли текущий пользоватль администратором, его имя для отображения в каком-нибудь личном кабинете, группы, к которым он принадлежит, и так далее.
Несмотря на это, есть также и минусы:
- Всю логику работы с данными необходимо оборачивать в хранимые процедуры (на самом деле, для меня это плюс).
- Необходимость авторизации пользователя в начале каждой сессии, а если код обёрнут в транзакции, то в начале каждой транзакции. Это может быть некритично для так называемых "толстых клиентов", но для веб приложений уже становится актуальным. В этом случае проблема решается оборачиванием драйвера, который предоставляет доступ к СУБД кастомным кодом таким образом, чтобы авторизация выполнялась перед выполнением каждого запроса. Звучит не очень красиво, но на самом деле всё не так страшно. Я в своих проектах использовал Flask и модуль flask_login, который сильно упрощает эту задачу.
Резюме
Конечно, наверняка существуют проекты где описанный мной подход будет неуместен и я буду рад, если вы поделитесь своими мыслями на этот счёт, — возможно, данный метод можно доработать и улучшить. Но, в целом, такой подход кажется мне довольно интересным.
Комментарии (6)
AlexZaharow
01.08.2017 01:48Простите, но мне лично контроль даже в хранимых процедурах кажется всё равно не эффективным, но дело не в производительности, а в том, что результат выполнения запросов известен только после их выполнения. Независимо от того, что выполнялось. В этом кроется дилемма безопасности. Если говорить прямо, то сегодня уровень безопасности обеспечивает не саму безопасность, а доступ на выполнение той или иной конструкции запроса. Например, если вы хотите удалить одну запись, а в результате удаляется 100, то вопрос к кому? К системе безопасности или к разработчику выполнимой процедуры? Понятно, что все запросы тестируются до попадания в продакшен, но по факту один разработчик должен доверяться другому и не может сам что-то настроить.
Я бы для себя поставил задачу безопасности по другому. Например:
1. Открыть транзакцию.
2. до выполнения запроса на удаления «выставил» в программе атрибут1=таблица1, процедура=удаление, количество записей=1.
3. Запуск запроса.
4. Проверка, что действительно из таблицы 1 было именно удалена запись в количестве именно 1шт. Если в результате выполнения процедуры произошло удаление больше одной записи или произошли изменения в других таблицах, то выполнить rollback, иначе — commit.
Вот теперь у программиста backend-а есть контроль над разработчиком БД. Класс! Вот такая система безопасности является именно безопасностью, а не системой распределения прав доступа. Вдруг разработчик БД «немного» накосячил и что-то упустил, ну так такая система безопасности не даст ему произвести действия, которые не планировал разработчик backend-а.
Но о такой системе я ещё не слышал )))pensnarik
01.08.2017 09:31+1Если бы результаты выполнения запросов были известны до их выполнения, их бы незачем было выполнять.
SergeyGershkovich
01.08.2017 12:53Статья про plpython-ориентированный механизм аутентификации, а не «разграничение доступа». Ваш GD.get('user_id') можно и RLS засунуть. Я не говорю о достоинствах и недостатках предложенного метода. Я лишь про название и содержание статьи.
bigtrot
02.08.2017 11:07Все эти надстройки разграничения доступа над штатной системой ни к чему хорошему не приведут. По сути придется обеспечивать единство правил в штатной системе и своей. Лучше использовать только штатную и научить приложение адекватно на это реагировать. В крайнем случае делать только ту часть, которой нет в штатной системе. И еще надо четко разделять понятия безопасности информации и разграничение доступа к данными, это не одно и тоже.
heleo
В принципе старо как мир, доводилось иметь дело с таким подходом и как раз на уровне работы с БД через процедуры и представления.
Основная проблема: надо писать во всех функциях. Тут всё зависит от тех кто этим занят, если проект большой, то за всеми этими проверками просто не уследить. Люди могут быть ленивы, глупы или просто торопиться. Как следствие проверок не будет в коде.
Если код пилят со «стороны» к примеру подрядчики, становится ещё хуже.
В последнем примере есть поле u.branch_id. Я так понимаю это некий идентификатор доступа к ресурсу?.. Так вот, если вам нужно хранить несколько идентификаторов доступа к одному ресурсу, то вы либо увеличите количество записей в таблице, либо будете использовать массивы для их хранения. Если у Вас несколько ресурсов с разными идентификаторами, тогда вам нужно дополнительное поле или таблицы заточенные под эти ресурсы. Тогда не ясно, каким образом Ваше решение будет быстрее чем контроль строк от postgres из коробки?
pensnarik
Спасибо за отклик. Как я упоминал в статье, для меня оборачивание кода в хранимые процедуры это не минус, а плюс, но тут, как говорится «на вкус и цвет». Ошибиться и забыть что-то при написании клиента при таком подходе просто невозможно — предполагается, что у пользователя, под которым ходит приложение просто нет прав ни на что, кроме вызова процедур.
А branch_id это бизнес поле — филиал. Просто для примера. Ничего в структуру таблиу спуиально добавлять не нужно.