Row-Level Security вРСУБД
АнтонЗлыгостев
Введение
Разграничение прав доступа является необходимой функциональностью любой корпоративной СУБД. Практические все современные СУБД предоставляют набор базовых средств по управлению правами доступа. Как правило, поддерживаются такие концепции, как пользователи и группы, а также так называемая декларативная безопасность – возможность предоставить этим пользователям и группам права доступа к определенным объектам базы данных. Немаловажным вопросом является гранулярность этой безопасности, т.е. насколько детально можно назначить права.
Основными объектами безопасности в СУБД являются таблицы, представления (view), и хранимые процедуры. В зависимости от типа объекта можно управлять правами на конкретные действия с ним. Например, в случае таблиц можно независимо управлять правами на чтение, добавление, удаление и изменение записей. В тех СУБД, где поддерживаются какие-либо другие объекты (например, пользовательские функции), доступом к ним можно управлять аналогичным образом. В некоторых системах можно управлять доступом на уровне отдельного столбца представления или таблицы.
Это достаточно гибкая и развитая система, позволяющая администратору СУБД настраивать права доступа пользователей в соответствии с их служебными обязанностями.
Однако в некоторых случаях встроенной в СУБД функциональности недостаточно для реализации требований корпоративной политики доступа к данным. Дело в том, что очень немногие СУБД позволяют ограничить доступ пользователей к отдельным строкам таблиц, хотя подобное требование достаточно часто встречается в прикладных задачах. Например, менеджеры по продажам не имеют права «видеть» накладные, выписанные в другом офисе той же компании, а директору по продажам все эти данные доступны. Рядовой бухгалтер не должен изменять документы задним числом, но главному бухгалтеру это позволительно. В англоязычной традиции данная функциональность называется Row-Level Security. Устоявшейся русскоязычной терминологии нет, поэтому мы будем пользоваться английским термином, иногда сокращая его до RLS.
Там, где встроенных средств поддержки такой функциональности нет, разработчику придется придумать свой способ гарантировать выполнение требований заказчика. Как и в любой другой задаче, существует большой выбор таких способов. В этой статье предлагается несколько из них.
Одним из наиболее популярных решений задачи RLS является встраивание правил безопасности в клиентское приложение. Поскольку в наши дни пользователи практически никогда не общаются с СУБД напрямую, это выглядит достаточно привлекательно. Все действия пользователя проходят через интерфейс приложения, и разработчик имеет возможность заложить сколь угодно сложные правила проверки допустимости действий. Основными преимуществами данного подхода являются:
большая гибкость – привычный процедурный язык программирования позволяет делать все, что угодно;
возможность определять, выполнимо ли некоторое действие, до попытки его выполнения (а все руководства по usability настоятельно рекомендуют заранее информировать пользователя об ограничениях на действия – например, выделяя серым соответствующий пункт меню);
Однако вместе с тем этот подход обладает несколькими существенными недостатками:
Небезопасность. Предполагается, что пользователи могут подключиться к СУБД только посредством конкретного клиентского приложения. Если злонамеренный пользователь достаточно квалифицирован, то он всегда сможет получить доступ к базе, минуя клиентское приложение, и игнорировать реализованные в нем правила безопасности. Еще более вероятным сценарием в корпоративных приложениях является одновременная работа нескольких версий клиентского приложения с отличающимися наборами правил безопасности. Таким образом, безопасность начинает зависеть уже не от разработчика, а от расторопности системных администраторов, осуществляющих эксплуатацию.
Производительность. Для запросов на изменение данных данный подход несколько повышает быстродействие, т.к. в случае запрета на действие вообще нет необходимости связываться с сервером. Однако запросы на чтение будут приводить к передаче по сети избыточных данных - тех строк, которые будут отброшены клиентским приложением в соответствии с правилами безопасности.
Целостность данных. Разграничение прав доступа на уровне СУБД выполняется в декларативном стиле, т.е. СУБД гарантированно проверяет одни и те же ограничения при любых операциях с данными. Но в клиентском приложении будет использован императивный стиль. Это означает, что потенциально больше возможностей сделать ошибку, или забыть применить единый набор правил для всех возможных способов обращения к данным. В вышеописанном примере с менеджерами продаж легко забыть внести соответствующие ограничения в какие-либо отчеты, основанные на данных из таблицы накладных, и дать пользователю возможность обойти правила безопасности.
Единственным способом борьбы с этими недостатками является реализация проверки всех правил безопасности на сервере.
Современные технологии разработки приложений предлагают два варианта реализации RLS на стороне сервера - средствами сервера баз данных и средствами сервера приложений.
Если для вашего приложения выбрана трехуровневая модель, то реализация RLS на уровне сервера приложений является вполне приемлемым, а возможно и наилучшим, решением.
Однако, если приложение разрабатывается в классической клиент-серверной модели, или предполагается наличие нескольких серверов приложений, работающих с одним сервером данных, то желательно реализовать RLS на уровне базы данных. Возможные подходы к решению этой задачи мы и рассмотрим в следующем разделе.
Реализация RLS средствами сервера БД
Итак, перед нами стоит задача - давать или не давать определенному пользователю право на выполнение тех или иных действий с различными строками таблицы. Теоретически, в самом общем случае, это означает, что перед выполнением любого действия проверяется значение некоторого предиката (булевой функции). Стандартные средства безопасности СУБД умеют проверять предикаты, параметрами которых являются идентификатор текущего пользователя, идентификатор(ы) таблиц, и, возможно, отдельных столбцов, к которым осуществляется доступ. Это позволяет вычислить значение один раз перед началом выполнения любого SQL запроса, и до окончания его обработки к вопросу безопасности не возвращаться. Таким образом, сводится к минимуму влияние проверки безопасности на производительность системы.
Нам придется расширить функциональность таким образом, чтобы предикат безопасности вычислялся независимо для каждой строки таблицы.
В терминах SQL это означает, например, что к каждому select-запросу нужно неявно добавить соответствующее условие:
select * from Clients where CompanyName like 'Micro%' AND <Security_Check_Ok> |
В данном случае под <Security_Check_Ok> подразумевается некоторое булево выражение. Далее мы будем исследовать различные варианты построения таких выражений, но перед этим стоит убедиться в том, что мы можем гарантировать проверку этого условия.
Нам необходимо изолировать пользователя от прямого доступа к данным и гарантировать применение установленных правил безопасности. Если используемая СУБД не предоставляет встроенных механизмов обеспечения безопасности, то получить полноценное решение задачи не удастся.
Обычной техникой для изоляции пользователя от хранимых в СУБД данных является построение соответствующего представления (view), и запрет прямого доступа к нижележащей таблице. В MS SQL Server это можно сделать примерно таким образом:
create view SecureClients as select * from Clients where <Security_Check_Ok>deny public read on Clientsgrant public read on SecureClients |
Тогда при выполнении запроса
select * from SecureClients where CompanyName like 'Micro%' |
пользователи автоматически увидят только те строки, для которых <Security_Check_Ok> возвращает TRUE.
Теперь рассмотрим различные варианты предикатов, которые могут использоваться для проверки доступа.
Теоретически, основой вычисления предиката безопасности является идентификатор текущего пользователя (он, как правило, доступен в любой СУБД, поддерживающей аутентификацию). Однако его прямое использование не рекомендуется, т.к. как корпоративная политика, в соответствии с которой должна строиться реализация системы, редко манипулирует персоналиями. В таком случае затруднительно сформулировать относительно стабильные правила, которые не придется пересматривать при каждом изменении списка сотрудников.
Обычно все правила построены на основе должностей. Их аналогом в программировании являются группы или роли. Поэтому в предикатах безопасности нам часто придется использовать выражения типа IsUserInRole(rolename). Если в используемую СУБД встроена подобная функциональность - прекрасно, лучше всего использовать именно ее. В таком случае субъекты безопасности будут образовывать единое пространство как для встроенной безопасности СУБД, так и для наших расширений.
Если же СУБД не предоставляет средств поддержки групп или ролей, то их тоже придется реализовывать вручную. Одним из простейших способов является создание специальной таблицы, содержащей список групп или ролей, и связь ее с таблицей пользователей. В зависимости от потребностей, можно выбрать различные схемы. Если пользователь может входить только в одну группу, то достаточно добавить ссылку на группу в таблицу пользователей. А если ему может быть назначено одновременно несколько ролей, то для связи надо будет создать отдельную таблицу.