• saas
  • multi-tenancy
  • postgres
  • row-level security
  • rls
  • trigger function
  • multi-tenant architecture
  • single-tenant architecture

Реализация мультиарендной архитектуры с PostgreSQL: Изучение на простом примере из реальной жизни

Узнайте, как реализовать мультиарендную архитектуру с PostgreSQL Row-Level Security (RLS) и ролями базы данных через пример из реальной жизни для безопасной изоляции данных между арендаторами.

Yijun
Yijun
Developer

В некоторых из наших предыдущих статей мы углублялись в концепцию мультиарендности и её применение в продуктах и реальных бизнес-сценариях.

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

Что такое одноарендная архитектура?

Одноарендная архитектура относится к программной архитектуре, где каждый клиент имеет своё собственное выделенное экземпляр приложения и базы данных.

В этой архитектуре данные и ресурсы каждого арендатора полностью изолированы от других арендаторов.

Одноарендность

Что такое мультиарендная архитектура?

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

Мультиарендность

Одноарендная архитектура vs мультиарендная архитектура

Одноарендная архитектура и мультиарендная архитектура отличаются в таких аспектах, как изоляция данных, использование ресурсов, масштабируемость, управление и обслуживание, а также безопасность.

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

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

Мультиарендная архитектура часто является предпочтительной программной архитектурой, когда поставщики услуг предлагают стандартизированные услуги различным клиентам. Эти услуги, как правило, имеют низкий уровень кастомизации, и все клиенты используют один и тот же экземпляр приложения. Когда приложению требуется обновление, обновление одного экземпляра приложения равнозначно обновлению приложения для всех клиентов. Например, CRM (Управление Взаимоотношениями с Клиентами) — это стандартизированное требование. Эти системы обычно используют мультиарендную архитектуру для предоставления одной и той же услуги всем арендаторам.

Стратегии изоляции данных арендаторов в мультиарендной архитектуре

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

В дизайне архитектуры можно достичь различных степеней изоляции ресурсов между арендаторами:

Изолировано до общей

В общем, чем больше ресурсов делится между арендаторами, тем ниже стоимость итерации и обслуживания системы. И наоборот, чем меньше ресурсов делится, тем выше стоимость.

Начало реализации мультиарендности на примере из реальной жизни

В этой статье мы будем использовать систему CRM в качестве примера, чтобы представить простую, но практичную мультиарендную архитектуру.

Мы признаем, что все арендаторы используют одни и те же стандартизированные услуги, поэтому решили, что все арендаторы будут делить одни и те же базовые ресурсы, и мы реализуем изоляцию данных между различными арендаторами на уровне базы данных, используя Row-Level Security PostgreSQL.

Кроме того, мы создадим отдельное подключение к данным для каждого арендатора, чтобы облегчить управление разрешениями арендаторов.

Далее мы расскажем, как реализовать эту мультиарендную архитектуру.

Как реализовать мультиарендную архитектуру с PostgreSQL

Добавление идентификатора арендатора ко всем ресурсам

В системе CRM у нас будет много ресурсов, которые хранятся в разных таблицах. Например, информация о клиентах хранится в таблице customers.

До реализации мультиарендности эти ресурсы не связаны с каким-либо арендатором:

Чтобы различать арендаторов, владеющих разными ресурсами, мы вводим таблицу tenants для хранения информации о арендаторах (где db_user и db_user_password используются для хранения информации о подключении к базе данных для каждого арендатора, это будет подробно описано ниже). Кроме того, мы добавляем поле tenant_id к каждому ресурсу, чтобы идентифицировать, к какому арендатору он принадлежит:

Теперь каждый ресурс связан с tenant_id, теоретически позволяя нам добавлять условие where ко всем запросам, чтобы ограничить доступ к ресурсам для каждого арендатора:

На первый взгляд, это кажется простым и осуществимым. Однако это вызовет следующие проблемы:

  • Почти каждый запрос будет включать это условие where, загромождая код и усложняя его обслуживание, особенно при написании сложных операторов объединения.
  • Новички в кодовой базе могут легко забыть добавить это условие where.
  • Данные между различными арендаторами не изолированы на самом деле, так как каждый арендатор все еще имеет разрешение на доступ к данным, принадлежащим другим арендаторам.

Поэтому мы не будем использовать этот подход. Вместо этого мы будем использовать Row Level Security PostgreSQL для решения этих проблем. Однако прежде чем продолжить, мы создадим выделенную учетную запись базы данных для каждого арендатора для доступа к этой общей базе данных.

Настройка ролей базы данных для арендаторов

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

Поскольку у всех арендаторов одинаковое разрешение на операции с базой данных, мы можем создать базовую роль для управления этими разрешениями:

Затем, чтобы различать роли каждого арендатора, при создании для каждого арендатора назначается роль, наследуемая от базовой роли:

Далее информация о подключении к базе данных для каждого арендатора будет храниться в таблице tenants:

iddb_userdb_user_password
x2euiccrm_tenant_x2euicpa55w0rd

Этот механизм предоставляет каждому арендатору свою роль в базе данных, и эти роли разделяют права, предоставленные роли crm_tenant.

Мы можем затем определить область разрешений для арендаторов, используя роль crm_tenant:

  • Арендаторы должны иметь доступ на CRUD ко всем таблицам ресурсов системы CRM.
  • Таблицы, не относящиеся к ресурсам системы CRM, не должны быть видны арендаторам (предположительно только таблица systems).
  • Арендаторы не должны иметь возможность изменять таблицу tenants, и только поля id и db_user должны быть видимы для них при запросе их собственного идентификатора арендатора при выполнении операций с базой данных.

Как только роли для арендаторов настроены, когда арендатор запрашивает доступ к услуге, мы можем взаимодействовать с базой данных, используя роль базы данных, представляющую этого арендатора:

Защита данных арендаторов с использованием Row-Level Security PostgreSQL

На данный момент мы установили соответствующие роли базы данных для арендаторов, но это не ограничивает доступ данных между арендаторами. Далее мы будем использовать функцию Row-Level Security PostgreSQL для ограничения доступа каждого арендатора только к своим данным.

В PostgreSQL таблицы могут иметь политику безопасности строк, которая контролирует, какие строки могут быть доступны в запросах или изменены в командах манипуляции данными. Эта функция также известна как RLS (Row-Level Security).

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

Взяв таблицу customers в системе CRM в качестве примера, мы активируем RLS и создадим политику безопасности, чтобы ограничить каждого арендатора доступом только к своим данным о клиентах:

В заявлении создания политики безопасности:

  • for all (опционально) указывает, что эта политика доступа будет использоваться для операций select, insert, update и delete с таблицей. Вы можете указать политику доступа для конкретных операций, используя for, следующее за ключевым словом команды.
  • to crm_tenant указывает, что эта политика применяется к пользователям с ролью базы данных crm_tenant, то есть ко всем арендаторам.
  • as restrictive указывает режим принуждения политики, подразумевая, что доступ должен быть строго ограничен. По умолчанию таблица может иметь несколько политик, несколько permissive политик будут комбинированы с отношением OR. В этом сценарии мы объявляем эту политику как restrictive, потому что мы хотим, чтобы этот проверочный процесс был обязательным для пользователей, принадлежащих арендаторам системы CRM.
  • using выражение определяет условия для фактического доступа, ограничивая текущего пользователя базы данных средствами просмотра данных, принадлежащих только своему арендатору. Это ограничение применяется к строкам, выбранным командой (select, update или delete).
  • with check выражение определяет необходимое ограничение при модификации строк данных (insert или update), гарантируя, что арендаторы могут добавлять или обновлять записи только для себя.

Использование RLS для ограничения доступа арендатора к таблицам ресурсов предоставляет несколько преимуществ:

  • Эта политика эффективно добавляет where tenant_id = (select id from tenants where db_user = current_user) ко всем операциям запроса (select, update или delete). Например, когда вы выполняете select * from customers, это равнозначно выполнению select * from customers where tenant_id = (select id from tenants where db_user = current_user). Это устраняет необходимость явно добавлять условия where в коде приложения, упрощая его и сокращая вероятность ошибок.
  • Она централизованно контролирует разрешения доступа к данным между различными арендаторами на уровне базы данных, снижая риск уязвимостей или несоответствий в приложении, тем самым повышая безопасность системы.

Однако есть несколько моментов, на которые стоит обратить внимание:

  • Политики RLS исполняются для каждой строки данных. Если условия запроса в политике RLS слишком сложные, это может существенно повлиять на производительность системы. К счастью, наш запрос на проверку данных арендатора достаточно прост и не повлияет на производительность. Если вы планируете использовать другие функции с помощью RLS в будущем, вы можете следовать рекомендациям по производительности Row-Level Security от Supabase для оптимизации производительности RLS.
  • Политики RLS не автоматически заполняют tenant_id в insert операциях. Они только ограничивают арендаторов внесением данных от их имени. Это означает, что при внесении данных мы все же должны предоставить идентификатор арендатора, что не совпадает с процессом запроса и может привести к путанице в процессе разработки, увеличивая вероятность ошибок (это будет решено в следующих шагах).

В дополнение к таблица customers, нам нужно применить те же операции ко всем таблицам ресурсов системы CRM (этот процесс может быть немного утомительным, но мы можем написать программу для его настройки во время инициализации таблицы), тем самым изолируя данные от разных арендаторов.

Создание триггерной функции для внесения данных

Как уже упоминалось, RLS (Row-Level Security) позволяет нам выполнять запросы без заботы о наличии tenant_id, так как база данных обрабатывает это автоматически. Однако для операций insert мы все еще должны вручную указывать соответствующий tenant_id.

Чтобы достичь аналогичного удобства, как RLS для внесения данных, мы должны, чтобы база данных могла обрабатывать tenant_id автоматически во время внесения данных.

Это имеет очевидное преимущество: на уровне разработки приложений больше не нужно учитывать, к какому арендатору принадлежат данные, что уменьшает вероятность ошибок и снижает наше ментальное бремя при разработке мультиарендных приложений.

К счастью, PostgreSQL предоставляет мощные функции триггеров.

Триггеры — это специальные функции, связанные с таблицами, которые автоматически выполняют определенные действия (например, внести, обновить или удалить) при выполнении этих действий с таблицей. Эти действия могут вызываться на уровне строки (для каждой строки) или уровне оператора (для всего выражения). С помощью триггеров мы можем выполнять пользовательскую логику до или после выполнения определенных операций с базой данных, позволяя нам легко достичь нашей цели.

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

Далее свяжем эту триггерную функцию с таблицей customers для операций внесения (похожим образом на включение RLS для таблицы, эту триггерную функцию нужно связать со всеми релевантными таблицами):

Этот триггер гарантирует, что внесенные данные будут содержать правильный tenant_id. Если новые данные уже включают tenant_id, триггерная функция ничего не делает. В противном случае она автоматически заполняет поле tenant_id на основе информации текущего пользователя в таблице tenants.

Таким образом, мы достигаем автоматической обработки tenant_id на уровне базы данных во время внесения данных арендаторами.

Резюме

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

Мы обсудили управление ролями базы данных, контроль доступа и функцию Row-Level Security PostgreSQL для обеспечения изоляции данных между арендаторами. Кроме того, мы используем триггерные функции, чтобы уменьшить когнитивную нагрузку разработчиков при управлении различными арендаторами.

Это всё для этой статьи. Если вы хотите дополнительно улучшить своё мультиарендное приложение управлением доступа пользователей, вы можете обратиться к Простому руководству по началу работы с организациями Logto - для создания мультиарендного приложения для получения дополнительных знаний.