Развитие Security Proxy. Динамические права
Всем привет! Классический подход к авторизации — когда её контроль помещают внутрь конкретного сервиса в виде статических правил. То есть зашивают в код проверку ролей и прав из JWT‑токена. В первых версиях наших сервисов так и было сделано. Позднее родилась идея снять с них эту нагрузку и передать её тонкому прокси‑сервису (SecurityProxy), который DevOps’ы развернут в виде sidecar у защищаемого сервиса. В качестве технологии выбрали GraphQL.
Меня зовут Сергей Котельников, я лид бэкенд-разработки в команде продукта под названием «Мицелий» компании «Дискавери Лабс». Расскажу о том, как мы обеспечивали безопасность готового бизнес‑сервиса без его изменения.
Недостатки предыдущего решения
У предыдущего решения с прокси-сервисом были недостатки:
Хотя мы и вынесли заботу авторизации пользователей на SecurityProxy, однако за выбор запросов для защиты в GraphQL API отвечал бизнес‑сервис. То есть в схему, которую предоставлял бизнес‑сервис, нужно было вносить разметку специальными директивами, которые мы сами выдумали. Их нет в стандарте.
Сервис «знал» об особенностях защиты, как минимум о необходимости разметки в схеме GraphQL и о значении прав для операции. То есть для защиты требуется добавлять и менять код самого сервиса с синхронизацией БД сервиса прав (grants).
Это не проблема для корпоративной разработки, когда сервисы пишут те же люди, что создавали и поддерживают само решение авторизации. Но если сервисы пишут другие (компании), без поддержки директив на уровне библиотек, которые хотят соответствовать стандарту, то управлять этим гораздо сложнее.
Решение
Чтобы избавиться от описанных выше недостатков, мы решили задавать правила авторизации для сервиса не из схемы самого сервиса, а через утилиту администрирования. Чтобы администратор с помощью ролей присваивал пользователям права, а также назначал права операциям самого сервиса, без вмешательства в его устройство. То есть мы априори обращаемся с бизнес‑сервисом как с чёрным ящиком, не пытаясь разобраться в его устройстве или поменять его.
Техническая реализация
Участники процесса: потребитель, бизнес‑сервис, его SecurityProxy, сервис Grants (с БД) и утилита администрирования.
Первый запуск. Регистрация сервиса
При первом запуске Security Proxy считывает схему у своего бизнес‑сервиса. У нас схема стандартная, не содержит никаких директив для авторизации. SecurityProxy запрашивает правила авторизации для этой схемы у сервиса Grants. Поскольку запуск первый, сервис Grants ничего «не знает» про эту схему. Тогда SecurityProxy передаёт информацию о ней в Grants. Тот записывает в БД: ID сервиса, хеш схемы, список полей для авторизации. Таким образом SecurityProxy оповещает систему о регистрации нового сервиса или схемы.
Примечание: по умолчанию доступ к неразмеченным полям отсутствует. То есть без присвоения прав сервис будет недоступен.
Назначение правил авторизации
Администратор назначает новому сервису правила авторизации и уведомляет об изменении SecurityProxy. После этого SecurityProxy считывает из Grants правила для своей схемы и «понимает», какие поля и по каким правилам необходимо проверять. Сервис становится защищённым.
Виды правил:
AllowAnonymous — доступ разрешён без проверки;
Authenticate — доступ разрешён по аутентификации;
Authorize — доступ разрешён при наличии специального права в JWT‑токене.
Например, есть сервис Clients, который позволяет получать и менять данные клиента. Допустим, у него есть операции (в терминах GraphQL‑поля):
query client(id)
— запрос данных клиента
mutation updateClient(id, data)
— изменение данных клиента
Администратор задаёт домен CLIENTS
, в нём операции GetClient
и UpdateClient
. Затем назначает эти права для роли, например, «Сотрудник офиса». Также администратор назначает сервису Clients
для query client
право Authorize(CLIENTS.GetClient)
, а для mutation updateClient
— Authorize(CLIENTS.UpdateClient)
.
После этого только сотрудник с указанной ролью будет иметь доступ к указанным операциям бизнес‑сервиса.
Изменение схемы бизнес-сервиса
На практике сервисы дорабатываются, меняется состав их методов. Для схемы GraphQL это означает добавление новых полей, запросов и мутаций. В этом случае формально схема меняется. Поэтому важно, чтобы те поля, которые не поменялись, сохраняли свою конфигурацию в правилах авторизации. А новые или изменившиеся поля потребовали бы новых настроек авторизации.
SecurityProxy, прочитав новую схему, отправляет в Grants её хеш. Grants, обнаружив, что уже имеет для этого сервиса правила авторизации, но только для предыдущей версии схемы (в БД другой хеш), информирует об этом SecurityProxy. Тот передаёт новую схему в Grants, который переносит из старой схемы не изменившиеся правила. Соответственно, для новых запросов и мутаций потребуется настройка из утилиты администрирования.
Например, в сервис Clients добавили новый query family
. После запуска сервиса с новой схемой этот запрос не будет доступен. Допустим, что он не требует защиты конкретным правом. Тогда администратор может задать для такого запроса тип авторизации Authenticate
. После изменения и уведомления SecurityProxy, при успешной аутентификации (токен прошёл проверку) этот запрос будет разрешён.
Если же какое‑либо поле изменилось (обратная несовместимость), тогда предыдущее правило для этого поля теряет актуальность и потребуется новая настройка админом.
Уведомление SecurityProxy об изменениях
Подход, при котором бизнес‑сервис уведомляет некий общий центр о своём существовании, называется Service Discovery. В данном случае от имени бизнес‑сервиса выступает его прокси — SecurityProxy. В качестве центрального сервиса системы безопасности выступает сервис Grants. SecurityProxy уведомляет Grants о своём существовании, передаёт схему (новую или изменённую) и читает правила авторизации.
Как Grants может уведомить SecurityProxy об изменениях правил авторизации? Варианты:
Самый простой: администратор просто уведомляет в рабочем порядке службу сопровождения о необходимости перезапустить SecurityProxy. При последующем запуске сервис перечитает информацию из Grants и начнёт работать по новым правилам. Недостаток подхода — человеческий фактор. Достоинство — надёжность.
Pooling. Можно задать через некоторый промежуток времени обращение из SecurityProxy в Grants для получения свежих изменений. Достоинства — автоматизация и надёжность. Недостаток — обновление будет не моментальным, а зависящим от таймаута пулинга.
События. Уведомлять об изменениях через некоторый механизм (Kafka, RabitMQ и т. п.). Достоинства — относительно моментальное изменение. Недостаток — ещё одна система = ещё одна точка отказа.
События через Webhooks. SP регистрируется в Grants, тот в свою очередь по вебхуку уведомляет нужный экземпляр SP. В данном случае новая система не задействована.
На практике можно комбинировать варианты.
Диаграмма последовательности
Подключаем REST
Описанный механизм для GraphQL‑сервиса работает и для сервисов REST. Принципиальное отличие в том, что если для GraphQL точки входа — это поля типов, начиная с базовых query и mutation, то для REST это операции (GET, POST и т. д.) и endpoint. Поэтому правила авторизации задаются в соответствии с REST‑сущностями.
В данном случае требование к сервису REST — поддержка схемы OpenAPI. Тут есть нюанс: если для GraphQL наличие схемы обязательно по стандарту, то для REST такого требования нет. Но для понимания, какие ресурсы предоставляет сервис REST, требуется именно схема. Наверное, можно и обойтись без неё, задавая в админке в явном виде операции и endpoint, но риск рассинхронизации при таком подходе слишком велик. Рассмотрим пример. Сервис Сlient предоставляет запросы:
GET /api/client/{id}
— чтение клиента
POST /api/client/{id}
— обновление клиента
SecurityProxy читает схему OpenAPI с этой информацией и передаёт её в Grants. Администратор назначает для этих запросов правила авторизации: Authorize(CLIENTS.GetClient)
и Authorize(CLIENTS.UpdateClient)
. SecurityProxy получает правила и начинает требовать для этих ресурсов определённые права в JWT‑токене. Сервис стал защищён.
В результате всех описанных изменений мы избавились от недостатков, присутствовавших в предыдущей версии «Мицелия». Теперь его система динамических прав позволяет работать со стандартными схемами GraphQL и OpenAPI и её легко сопровождать вне зависимости от происхождения защищаемых сервисов.