Использование безопасности на уровне строк Postgres в Python и Django
Использование безопасности на уровне строк Postgres в Python и Django
Полный перевод, конспект и структурированный разбор материала
Перевод
Использование Postgres Row-Level Security в Python и Django
В 2016 году в Postgres появилась функция безопасности на уровне строк (Row-Level Security, RLS), которая дает администраторам баз данных возможность ограничивать доступ к строкам для конкретных пользователей, добавляя дополнительный уровень защиты данных. Прелесть RLS заключается в том, что если пользователь пытается выбрать или изменить строку, к которой у него нет доступа, его запрос просто вернет 0 строк вместо ошибки прав доступа. Таким образом, пользователь может выполнить SELECT * FROM table_name и получить только те строки, к которым у него есть доступ, даже не подозревая о существовании остальных.
Большинство примеров использования RLS ограничивают доступ к строкам на основе пользователя базы данных. Это очень мощная функция. В этой статье мы рассмотрим, как реализовать этот подход в вашем приложении на Django. Основная проблема, с которой сталкивается большинство разработчиков при попытке внедрить безопасность на уровне строк, заключается в том, что большинство веб-приложений (включая приложения Django) подключаются к базе данных под одним-единственным пользователем. Из-за этого использовать преимущества RLS становится затруднительно.
Один из способов обойти это ограничение — создавать пользователя в базе данных для каждого пользователя приложения. Мы начнем непосредственно с уровня базы данных. Мы создадим таблицы, добавим пару пользователей, а затем напишем нашу первую политику безопасности на уровне строк, чтобы ограничить доступ к записям. Как только мы разберемся, как RLS работает в Postgres, мы перенесем наш проект в Django и посмотрим, как работать с политиками и несколькими пользователями БД в веб-приложении.
Кстати, если вам интересно узнать об использовании Row-Level Security в Ruby on Rails, у нас есть отдельная статья на эту тему: [Using Postgres Row-Level Security in Ruby on Rails].
Как использовать RLS на уровне базы данных
Прежде чем перейти к Django, давайте посмотрим, как RLS работает в самом Postgres. Представим простой сценарий: мы строим приложение, которое помогает нашим менеджерам по продажам вести учет своих клиентов, и мы хотим гарантировать, что ни один менеджер не сможет получить доступ к клиентам своего коллеги. (Это крайне амбициозные и беспощадно конкурирующие между собой менеджеры по продажам).
Сначала давайте настроим наши таблицы и заполним их данными:
sqlCREATE TABLE salespeople (id serial primary key, name text); CREATE TABLE clients (id serial primary key, name text, salesperson_id integer); INSERT INTO salespeople (name) values ('Picard'); INSERT INTO salespeople (name) values ('Crusher'); INSERT INTO clients (name, salesperson_id) values ('client1', 1); INSERT INTO clients (name, salesperson_id) values ('client2', 2); INSERT INTO clients (name, salesperson_id) values ('client3', 2);
Итак, у нас есть два менеджера. У Пикара (Picard) один клиент, а у Крашер (Crusher) — два.
Далее нам понадобятся пользователи в базе данных — по одному для каждого менеджера. Поскольку у двух менеджеров могут быть одинаковые имена, мы будем использовать их id для создания пользователей Postgres. Мы также создадим роль с именем salespeople. Мы предоставим права этой роли, а все наши менеджеры будут наследоваться от нее.
sqlCREATE ROLE "1"; CREATE ROLE "2"; CREATE ROLE salespeople; GRANT select, insert ON clients TO salespeople; GRANT salespeople TO "1"; GRANT salespeople TO "2";
Такая структура пригодится нам в следующем разделе, когда помимо наших собственных таблиц нам придется иметь дело с таблицами самого Django.
Теперь мы готовы настроить RLS для таблицы clients. Наша политика будет ограничивать доступ для текущего пользователя Postgres (current_user) так, чтобы он мог видеть только те строки, где salesperson_id совпадает с его именем пользователя.
sqlALTER TABLE clients ENABLE ROW LEVEL SECURITY; CREATE POLICY salesperson_clients ON clients USING (salesperson_id::text = current_user);
При создании политики мы даем ей имя (salesperson_clients) и указываем таблицу, для которой она применяется (clients). Далее мы описываем саму политику. В данном случае она очень проста: значение salesperson_id в таблице должно быть равно значению current_user. Нам приходится приводить salesperson_id из целого числа к тексту, поскольку current_user возвращает строку (мы не можем создавать пользователей Postgres с именами в виде чистого числа без кавычек).
Сейчас мы авторизованы под суперпользователем postgres.
sqlSELECT session_user, current_user; session_user | current_user --------------+-------------- postgres | postgres (1 row)
Если мы сделаем запрос к таблице clients, мы увидим все строки, поскольку политики RLS не применяются к суперпользователям.
sqlSELECT * FROM clients; id | name | salesperson_id ----+---------+---------------- 1 | client1 | 1 2 | client2 | 2 3 | client3 | 2 (3 rows)
Но если мы изменим текущую роль, то получим только те строки, которые принадлежат этому пользователю.
sqlSET ROLE "1"; SELECT session_user, current_user; session_user | current_user --------------+-------------- postgres | 1 (1 row) SELECT * FROM clients; id | name | salesperson_id ----+---------+---------------- 1 | client1 | 1 (1 row)
Как использовать Postgres Row-Level Security в Django
Теперь как нам перенести это в приложение на Django?
Во-первых, нам нужно будет создавать пользователя в базе данных для каждого создаваемого пользователя приложения. Один из способов сделать это — переопределить метод save в модели Salesperson, но это отличная возможность воспользоваться сигналами Django (Django signals), поэтому мы создадим сигнал, который создает пользователя базы данных после сохранения нового менеджера.
Во-вторых, нам нужно понять, как переключаться на нужного пользователя БД, когда менеджер входит в систему. Для этого мы можем использовать промежуточное ПО (middleware), которое будет получать salesperson_id и устанавливать соответствующую роль в базе данных.
Модели
Наши модели в точности отражают то, что мы настроили ранее на уровне базы данных. Здесь я решил сделать Salesperson прокси-моделью для встроенной в Django модели User, но это не обязательно.
pythonfrom django.db import models from django.contrib.auth.models import User class Salesperson(User): class Meta: proxy = True class Client(models.Model): name = models.CharField(max_length=50) Salesperson = models.ForeignKey(Employee, on_delete=models.CASCADE)
Сигналы Django: создание пользователя базы данных
Мы хотим создавать нового пользователя базы данных каждый раз, когда создается новая запись менеджера по продажам. Мы можем использовать сигналы Django для выполнения кода после сохранения записи. Если вы не знакомы с сигналами, документация Django по этой теме очень проста для понимания. Если вас это заинтересовало, подробнее можно почитать в этой статье.
Вот код самого сигнала (однако вам нужно будет обратиться к упомянутой выше статье, чтобы зарегистрировать его в вашем приложении):
pythonfrom .models import Salesperson from django.db.models.signals import post_save from django.db import connection def create_db_user(sender, instance, created, **kwargs): if created: user_id = instance.id with connection.cursor() as cursor: cursor.execute(f'CREATE ROLE "{user_id}"') cursor.execute(f'GRANT salespeople TO "{user_id}"') post_save.connect(create_db_user, sender=Salesperson)
Сигнал post_save принимает именованный аргумент created, который является булевым значением. Это позволяет избежать выполнения кода при каждом обновлении записи и гарантирует, что он сработает только при создании нового менеджера. Далее мы получаем ID пользователя из экземпляра модели и используем django.db.connection для выполнения SQL-запроса, который создает роль и предоставляет ей права.
Очень важно отметить: если вы хотите использовать встроенную модель Django User и сопутствующую аутентификацию, вам нужно будет предоставить роли salespeople права доступа к таблицам django_admin_log и auth_user. Именно поэтому так удобно иметь родительскую роль, которую наследуют все индивидуальные пользователи.
Django Middleware: установка текущего пользователя
Теперь мы можем написать middleware для переключения пользователя базы данных на текущего пользователя приложения, выполняющего запрос.
pythonfrom django.db import connection class RlsMiddleware(object): def __init__(self, get_response): self.get_response = get_response def __call__(self, request): user_id = request.user.id with connection.cursor() as cursor: cursor.execute(f'SET ROLE "{user_id}" ') response = self.get_response(request) return response
Мы получаем ID пользователя из объекта запроса request. После этого код выглядит практически так же, как в нашем сигнале. Мы снова используем подключение к БД Django, чтобы установить роль для соответствующего пользователя базы данных, имя которого должно совпадать с ID пользователя приложения. Не забудьте зарегистрировать ваше middleware в файле settings.py.
Теперь мы можем использовать все встроенные методы запросов Django, сохраняя при этом безопасность на уровне строк в Postgres. Что особенно круто: с установленной ролью все, что нам нужно сделать для получения всех клиентов менеджера, — это вызвать Client.objects.all(). Мы можем быть уверены, что вернутся только те клиенты, которые связаны с этим менеджером. Если менеджер попытается запросить клиента, который ему не принадлежит, он получит пустой результат.
Заключение
В этой статье мы смогли создать простую, но мощную политику безопасности на уровне строк и с помощью Django middleware и сигналов внедрили её на уровне приложения. Мы увидели, как создавать пользователей базы данных при каждом создании нового пользователя приложения, и рассмотрели установку правильной роли в БД после входа в систему, гарантируя, что каждый пользователь приложения имеет доступ только к тем строкам, которые принадлежат ему.
Здесь есть несколько нюансов. Во-первых, использование идентификаторов вида 1, 2, 3 в качестве имен ролей в продакшене, скорее всего, не лучшая идея. Стоит использовать UUID или другой уникальный идентификатор. Кроме того, создание отдельного пользователя базы данных для каждого пользователя приложения на определенном этапе становится трудномасштабируемым. Безопасность на уровне строк — полезный инструмент для ограничения доступа на уровне БД, и мы лишь слегка коснулись его возможностей.
Тем не менее, перед внедрением RLS вам следует убедиться, что это действительно подходящее решение для вашего приложения. В частности, не стоит игнорировать влияние безопасности на уровне строк на производительность и то, как планировщик Postgres строит планы выполнения запросов. В Postgres 10 этот механизм был значительно улучшен, но мониторинг планов запросов в Postgres при использовании RLS по-прежнему необходим.
Во многих случаях RLS не требуется, и вы сможете защитить свои данные с помощью встроенных средств безопасности Django.