Две входные двери: Доступ на уровне модулей в приложении Django GRC

Источник: GitLab•

Две входные двери: Доступ на уровне модулей в приложении Django GRC

Технический разбор реструктуризации внутреннего инструмента на Django для поддержки двух различных команд GitLab: проверки соблюдения нормативных требований и внутреннего аудита.

Инженерная команда GitLab разрабатывает множество внутренних инструментов собственными силами, но одна платформа заставила нас пересмотреть подход к управлению авторизацией: наш внутренний инструмент GRC, который обслуживает две совершенно разные категории пользователей под одной крышей — нашу команду по соблюдению нормативных требований (Security Compliance) и команду внутреннего аудита (Internal Audit).

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

Проблема

Наш исходный инструмент представлял собой единое приложение Django, созданное для команды Security Compliance с декоратором login_required для каждого представления. Это был правильный подход до тех пор, пока команда Internal Audit не нашла для нашего инструмента новый вариант использования, предполагающий совершенно отдельный рабочий процесс со своими собственными данными, моделями и конфиденциальной информацией, которую команда Security Compliance не должна видеть.

login_required отвечает ровно на один вопрос: вы вошли в систему? Он не отвечает на вопрос, который действительно важен во многопользовательском внутреннем инструменте: имеете ли вы право здесь находиться? Авторизованный пользователь Security Compliance, переходящий по URL-адресу Internal Audit, легко обходил проверку входа и попадал к данным, которые ему никогда не полагалось видеть.

Если в вашем приложении когда-либо появлялась вторая аудитория задним числом, это, вероятно, звучит знакомо. Вопрос, который стоит задать себе прямо сейчас, прост: проверяет ли каждое представление в вашем приложении, кто является пользователем, или только то, вошел ли он в систему? Если второе, у вас, скорее всего, тот же пробел, что и у нас.

Нам требовалось четкое разделение — не «не обращайте внимания на эту кнопку вон там», а строгая граница, которая сохраняется независимо от того, какой URL-адрес вводит пользователь.

Архитектурное решение

Когда сценарий использования для Internal Audit попал в наш план развития, перед нами встал классический выбор на распутье: прикрутить его к существующему приложению или провести реструктуризацию? Мы выбрали реструктуризацию.

Результатом стал основной проект Django — общая аутентификация, управление пользователями, журнал аудита, базовые компоненты интерфейса — с двумя независимыми модульными приложениями поверх него. Одно предназначено для Security Compliance, другое — для Internal Audit. Каждый модуль владеет собственными моделями, представлениями и интерфейсом API.

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

Контроль доступа

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

Наше архитектурное решение заключалось в использовании стандартных средств Django: встроенной системы аутентификации. Каждый модуль сопоставляется ровно с одной группой. Пользователь может принадлежать к одной группе, обеим или ни к одной, и система обеспечивает правильное разделение обязанностей на каждом уровне независимо от того, какая комбинация применяется. Панель навигации, представления Django и REST API независимо друг от друга контролируют членство в группах.

Реализация

В ядре есть один повторно используемый класс, от которого наследуются оба модуля: GroupRequiredMixin. Он проверяет членство в группе и вызывает PermissionDenied, если пользователь не состоит в необходимой группе.

Подклассы на уровне модулей

Каждый модуль получает подкласс из трех строк, задающий required_group_name — GasGroupRequiredMixin и IsGasGroupMember для стороны Security Compliance, причем тот же паттерн повторяется для Internal Audit. Такова цена добавления уровня проверок для модуля: один новый файл, один новый атрибут. Сама логика проверки находится в ядре и больше никогда не затрагивается.

Уровень пользовательского интерфейса

Наконец, существует фильтр шаблонов user_in_group, поэтому навигация отображает только то, что пользователю действительно разрешено просматривать. Он оборачивает проверку вида user|user_in_group:'erm' вокруг любого навигационного блока, который должен отображаться только для участников ERM.

Это средство защиты пользовательского интерфейса, а не граница безопасности. Скрытие навигационной ссылки не мешает никому ввести URL напрямую. Для этого предназначены миксины и класс разрешений Django REST Framework. Фильтр шаблонов просто поддерживает честность интерфейса в отношении того, что конкретный пользователь может видеть и делать.

Что ждет нас в будущем и с чего вы можете начать

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

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

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

  • Проверьте свои представления на наличие пробела между входом в систему и авторизацией. Выполните grep для login_required и LoginRequiredMixin и для каждого из них задайте вопрос, действительно ли он защищает конфиденциальные данные от других аутентифицированных пользователей, а не только от анонимных.
  • Определите границы модулей до написания кода. Решите, что является действительно общим (аутентификация, базовый интерфейс, логирование), а что принадлежит только одной аудитории.
  • Напишите один миксин, а не по одному на каждое представление. Один GroupRequiredMixin в вашем корневом приложении, от которого наследуются подклассы для каждого модуля, означает, что ваша логика проверки имеет ровно одно место для проверки и одно место для возможных ошибок.
  • Осуществляйте проверки на каждом уровне независимо — в представлениях, API и навигации, — чтобы сокрытие ссылки никогда не было вашей единственной линией защиты.

Ссылки

О чём эта статья

Ещё в разделе «Разработка ПО»

Все →

Ещё от GitLab