Данная статья является продолжением серии материалов по внедрению Keycloak в качестве провайдера авторизации. [спойлер: в конце вас ждет разочарование]

Первая часть

В прошлый раз мы настроили ABAC (Attribute Based Access Control) с использованием Keycloak, теперь реализуем проверку разрешений на уровне приложения.

Есть различные способы реализовать контроль доступа с использованием Keycloak на уровне вашего приложения, например использование средств Spring Security, как описано в статье от Baeldung. Однако в нашем случае не получится ограничиться исключительно группами / ролями пользователя так как требуется также учитывать его кастомные атрибуты доступа.

В прошлой части статьи мы добились возможности проверять разрешение пользователя через интерфейс Keycloak. Теперь нам нужно проделать то же самое, только используя API. Реализовав логику проверки разрешений с помощью API Keycloak мы сможем затем инкапсулировать ее в Spring Interceptor (перехватчик запросов).

Проверка разрешения представляет собой следующий алгоритм:

  1. Извлекаем из запроса URL;

  2. Используя URL получаем идентификатор ресурса, ассоциированного с нашим URL;

  3. Используя идентификатор ресурса и токен авторизации, проверяем разрешение для конкретного пользователя.

Схема проверки разрешения
Схема проверки разрешения

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

curl --location --request GET  $KEYCLOAK_HOST/realms/$REALM_NAME/authz/protection/resource_set?uri=/api/me' \
--header 'Authorization: Bearer $KEYCLOAK_TEC_USER_TOKEN'

Немного подробнее про конечную точку resource_set можно прочитать тут.

Чтобы проверить доступ к конкретному ресурсу нам понадобится та же конечная точка, что и для получения токена, однако со специфичным набором параметров:

curl --location --request POST '$KEYCLOAK_HOST/realms/$REALM_NAME/protocol/openid-connect/token/' \
--header 'Authorization: Bearer$KEYCLOAK_TEC_USER_TOKEN ' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'grant_type=urn:ietf:params:oauth:grant-type:uma-ticket' \
--data-urlencode 'audience=$CLIENT_NAME' \
--data-urlencode 'response_mode=decision' \
--data-urlencode 'permission=$RESOURCE_ID'

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

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

  1. Увеличение трафика в несколько раз, на каждый авторизованный запрос теперь приходится 2 дополнительных запроса в Keycloak, что в свою очередь замедляет систему и порождает существенные требования по RPS для инсталляции Keycloak.

  2. Сопровождение и миграции. Сопровождение политик написанных на JS оказалось чересчур трудоемким и повлекло существенные накладные расходы, более того, на момент реализации данного решения Keycloak не располагал средствами миграции, иными словами мне не удалось реализовать надежный способ миграции состояния Keycloak (ресурсы, разрешения, политики, мапперы и тд), который можно было бы встроить в СI/CD, что в свою очередь частично обесценило хранение настроек Keycloak в коде.

В конечном итоге было принято решение оставить ABAC на стороне приложения, а Keycloak использовать только для Identity пользователей.

Комментарии (11)


  1. Ares_ekb
    05.11.2023 11:49
    +3

    Мы тоже пытались реализовать авторизацию (не путать с аутентификацией - с ней всё норм) через Keycloak, но не взлетело. Например, как банально получить перечень записей, но отфильтровать те, к которым у пользователя нет доступа? Запрашивать все записи и потом по каждой отправлять запрос в Keycloak? Как это всё будет стыковаться с пагинацией? Допустим на странице должно быть 10 записей, из них доступ есть только к 7. Нужно дозапросить ещё 3 записи?..

    Я просто не понимаю как это может работать хоть для сколько-нибудь сложного приложения. Хотя сам Keycloak клёвый. У нас приложение вместе с Keycloak, ApacheDS (для тестовых пользователей) и т.д. завернуто в Docker Compose. При запуске Keycloak автоматически импортируется json-конфиг с настройками realm, интеграцией с ApacheDS. Всё это легко разворачивается на стенде, на машинах разработчиков, в тестовом окружении.


    1. BlackSCORPION
      05.11.2023 11:49

      Как вариант можно написать свой плагин, который будет имплементировать нужный Вам АПИ ендпоинт.


      1. Ares_ekb
        05.11.2023 11:49

        Всё равно останется проблема, что записи в одном сервисе (само приложение), а разрешения для них - в другом (Keycloak). И нужно получать данные из двух источников, как-то мержить их. Здесь в принципе не сделать простую и быструю реализацию. Гораздо проще хранить и записи, и разрешения в одной базе и получать всё одним SQL-запросом.


    1. micronull
      05.11.2023 11:49

      Например, как банально получить перечень записей, но отфильтровать те, к которым у пользователя нет доступа? Запрашивать все записи и потом по каждой отправлять запрос в Keycloak? Как это всё будет стыковаться с пагинацией? Допустим на странице должно быть 10 записей, из них доступ есть только к 7. Нужно дозапросить ещё 3 записи?..

      Мы у себя запрашиваем доступные scope для нужного нам ресурса и дальше уже подставляем нужные фильтры в сам запрос.


      1. Ares_ekb
        05.11.2023 11:49

        Не очень понимаю... Если нужно ограничить доступ по типам ресурсов, то можно использовать роли из Keycloak. Например, одна роль позволяет просматривать новости, другая - статьи.

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

        А если ещё более сложный вариант типа Google Документов? Пользователи могут создавать документы, выдавать к ним доступ другим пользователям. Причём, этих документов в системе миллионы. В Keycloak должна храниться информация о каждом таком документе? Затем в него отправляется запрос с id документа и id пользователя и Keycloak отвечает есть ли у пользователя доступ к этому документу?


        1. micronull
          05.11.2023 11:49
          +1

          Вы меня не поняли.

          В Keycloak для авторизации есть две сущности:

          • Ресурсы.

          • Область действий.

          Например есть у нас ресурс (resource): статьи.
          И область действий (scope): читать и чтение за деньги. Которую назначаем для ресурса и привилегий.

          Представим что у нас есть пользователи с платным доступом и бесплатным.
          Соответственно с платным могут читать все статьи, а с бесплатным только бесплатные.

          В backend делаю запрос в keycloak на получение доступных областей пользования для текущего пользователя и ресурса. Keycloak возращает массив.

          Далее перед запросом списка статей из репозитория\БД, проверяю каждую область действий по отдельности:

          • Могу видеть все статьи? - Если да, то делаем запрос без фильтров и условий.

          • Могу видеть платные статьи? - Если нет, то ограничиваем тип статей.

          • Могу видеть бесплатные статьи? Если нет, то выдаю ошибку 403.

          Моя реализация может быть не правильной.

          Роли нам не совсем подходят, так как нужен гибкий инструмент для настройки политик и пермиссий.


          1. Ares_ekb
            05.11.2023 11:49

            Спасибо, теперь кажется понял. По сути resource и scope - это возможно чуть более правильный вариант по сравнению с ролями. Но пообъектную проверку доступа через него всё равно не сделать.

            Или пообъектной проверки и нет в Keycloak вообще? Интересно что имеется в виду под ресурсом в документации на сервер авторизации. Это типы ресурсов, например, "обычная статья", "платная статья" и т.д.? Или речь о конкретных ресурсах: "статья с идентификатором 123 про Keycloak", "статья с ид 234 про OIDC" и т.д.? Или ресурсом может быть что угодно: и тип ресурсов, и конкретный ресурс?

            То, что в Keycloak можно описать правила доступа к типам ресурсов это понятно. Либо через роли, либо через скоупы. Но если нужно разделение доступа на уровне отдельных объектов, то по-моему Keycloak уже не очень подходит.


            1. micronull
              05.11.2023 11:49

              Интересно что имеется в виду под ресурсом в документации на сервер авторизации. Это типы ресурсов, например, "обычная статья", "платная статья" и т.д.?

              Статья - это и есть ресурс. Это может быть что-то абстрактное.
              Например помимо статей, есть ещё новости. Можно разбить на меньшие понятия, например научные и развлекательные статьи.

              "платный", можно выделить к scope (мы так у себя сделали назвав примерно как view_paid), но ни что не мешает сделать как resource.

              Но если нужно разделение доступа на уровне отдельных объектов, то по-моему Keycloak уже не очень подходит.

              Мы в будущем планируем писать свой велосипед, из-за того что есть кейсы, которые Keycloak не может разрешить. Будем конечно ещё копаться.


              1. Ares_ekb
                05.11.2023 11:49

                Видимо это меня и сбивает, потому что обычно под ресурсом подразумевается конкретный объект. Например, в REST это может быть ресурсами:

                • https://example.com/api/articles/1

                • https://example.com/api/articles/2

                • https://example.com/api/articles/3

                • ...

                А статьи, новости и т.д. - это типы ресурсов, а не конкретные ресурсы. Или допустим в документации упоминается "fine-grained authorization", "fine-grained permissions" - у меня сразу возникают ассоциации с ограничением доступа на уровне отдельных объектов или даже отдельных атрибутов, с чем-нибудь типа Row-Level Security, но реально имеется в виду что-то более простое.


                1. micronull
                  05.11.2023 11:49

                  Ну либо я не совсем верно понял.

                  Вот гайд по которому мы реализовывали у себя пермиссии: https://www.keycloak.org/docs/latest/authorization_services/index.html#resource

                  Там они пишут что это может быть как в целом "банковский счёт", так и конкретный "банковский счёт Алисы".


                  1. Ares_ekb
                    05.11.2023 11:49

                    Да, по ссылке написано:

                    Every resource has a unique identifier that can represent a single resource or a set of resources. For instance, you can manage a Banking Account Resource that represents and defines a set of authorization policies for all banking accounts. But you can also have a different resource named Alice’s Banking Account, which represents a single resource owned by a single customer, which can have its own set of authorization policies.

                    Т.е. под ресурсом они имеют в виду и коллекцию (тип) ресурсов, и конкретные ресурсы.

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