ABAC в микросервисах: сложная матрешка прав, простой API и никакой потери производительности

Внедрение атрибутивной модели доступа (ABAC) в крупной корпоративной системе на микросервисах — это всегда испытание для архитекторов, разработчиков и бизнес-аналитиков. ABAC — одна из самых сложных областей IAM (Identity and Access Management) в корпоративных платформах, и даже простая модель может сломать мозг и пользователям, и инженерам. Рассказываю, как я реализовал масштабируемую систему с миллионами сущностей без потери производительности и сохранили простоту API для конечного разработчика.


Какие задачи решали


Концепция системы прав (ABAC-матрешка)

Вся архитектура строится по принципу вложенных слоев — «матрешка»:

  1. Бизнес-роль

    • Управляется админом системы.

    • Определяет бизнес-логику (например, "Менеджер проекта").

  2. Системные роли

    • Задают наборы полномочий (например, "Документоисполнитель", "Согласующий").

    • Определяются бизнес-аналитиками и неизменяемы во время работы (меняется только изменением тестов и переносится как change request). Привязана к бизнес процессам.

  3. Полномочия

    • Жестко фиксированы (например, documents.view, documents.sign, documents.approver).

    • Разработчик добавляет их в коде и документации.

  4. Атрибуты полномочий

    • Определяют детали применения (например, список проектов, документов, срок действия).

    • Есть глобальные (назначаются при выдаче бизнес-роли пользователю) и локальные (выдаются динамически микросервисом).


Модель данных: матрешка и пользователь

1. Модель ролей и полномочий (шаблон, назначается пользователю):

{
  "business_role": "Менеджер_Alpha",
  "system_roles": [
    {
      "name": "Документоисполнитель",
      "permissions": [
        {
          "permission": "documents.view",
          "attributes": {}
        },
        {
          "permission": "documents.sign",
          "attributes": {}
        }
      ]
    }
  ]
}

2. После назначения пользователю (персонализированные атрибуты):

{
  "user_id": "petrov",
  "business_roles": [
    {
      "name": "Менеджер_Alpha",
      "system_roles": [
        {
          "name": "Документоисполнитель",
          "permissions": [
            {
              "permission": "documents.view",
              "attributes": {
                "projects": [101, 102]
              }
            },
            {
              "permission": "documents.sign",
              "attributes": {
                "projects": [101]
              }
            }
          ]
        }
      ]
    }
  ]
}

3. Локальные (динамические) права (например, права согласующего или доступ к секретному документу выдаются и читаются самим микросервисом):

[
  {
    "user_id": "petrov",
    "permission": "documents.approver",
    "attributes": {
      "documents": [999, 888]
    }
  },
  {
    "user_id": "ivanov",
    "permission": "documents.view",
    "attributes": {
      "secret": [123]
    }
  }
]

Примеры: как решаются кейсы

Задача: вывести пользователю только те документы, которые он может видеть.

Параметры для запроса

SQL-запрос

SELECT *
FROM documents
WHERE
    (:has_view_all)
    OR (project_id = ANY(:allowed_projects))
    OR (id = ANY(:allowed_documents))
    OR (id = ANY(:secret_documents))

Как это работает:

Пример на Python

def get_user_documents(user):
    # Получаем права пользователя (например, из кэша или реплики базы)
    perms = get_user_permissions(user)

    has_view_all = perms.get('documents.view_all', False)
    allowed_projects = perms.get('documents.view', {}).get('projects', [])
    allowed_documents = get_local_permissions(user, 'documents.approver').get('documents', [])
    secret_documents = get_local_permissions(user, 'documents.view').get('secret', [])

    query = """
        SELECT * FROM documents
        WHERE
            (%s)
            OR (project_id = ANY(%s))
            OR (id = ANY(%s))
            OR (id = ANY(%s))
    """
    params = (
        has_view_all,
        allowed_projects,
        allowed_documents,
        secret_documents
    )
    return db.execute(query, params)

Пример: назначение локальных прав (доступ к секретному документу)

def grant_secret_document_access(user_id, doc_id):
    # Динамически назначаем доступ к конкретному документу с признаком secret
    permission_record = {
        "user_id": user_id,
        "permission": "documents.view",
        "attributes": {
            "secret": [doc_id]
        }
    }
    save_permission(permission_record)

Проверка на отображение меню во фронте (React)

import { usePermissions } from '@company/shared-kernel';

function Sidebar() {
  const { hasPermission } = usePermissions();

  return (
    <nav>
      {hasPermission('documents.view') && (
        <MenuItem icon="documents">Документы</MenuItem>
      )}
      {hasPermission('documents.sign') && (
        <MenuItem icon="sign">Подписать</MenuItem>
      )}
    </nav>
  );
}

Почему система сложна внутри, но проста снаружи


Заключение

А как у вас реализованы сложные права? Делитесь кейсами и болями в комментариях!


@avangerus
07.07.2025 16:00 UTC
Первоисточник

Комментарии

@maksim_sitnikov
07.07.2025 11:36 UTC
0

а запрос от бд это эт постгрес?

@SergeyGershkovich
10.07.2025 19:27 UTC
+1

Красиво, но не соответствует реальным потребностям:

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

  2. Если у пользователя доступ предоставлен к миллиону документов, какого размера запрос отправится в СУБД чтобы показать список этих документов?

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

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

@avangerus
10.07.2025 19:51 UTC
0

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

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

2. если у пользователя доступ к миллиону документов (вполне реальная задача) это будет по факту всего одна выборка из базы. (тут надо сделать пометку что в этой системе реализован CQRS и eventsourcing поэтому чтение списка документов происходит из elasticsearch где объект собран сразу со всеми атрибутами, которые имеют на него право, если кому -то дали или забрали право - объект меняется в elasticsearch)

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

сейчас этот подход успешно лопатит 100млн документов с версионированием в пяти абсолютно разных системах.

12.07.2025 04:23 UTC
0

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

  1. При изменении прав нагрузка будет - чтобы забрать полномочия.

  2. К процессу выборки данных вопросов нет, смущает наличие функции в СУБД способной принять и переварить текст запроса с перечислением миллионов идентификаторов. При этом, с ростом объема данных размер текста запроса будет только увеличиваться.

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