Когда ролей недостаточно: ReBAC и SpiceDB

Когда ролей недостаточно: ReBAC и SpiceDB

1. Введение: когда одних ролей недостаточно

В DevRime авторизация начинается так.

@AuthorizedRole(roles = { ScrumRoles.ROLE_PROJECT_MANAGER,
                          ScrumRoles.ROLE_PROJECT_MEMBER })
public class FindProductProjFetch extends FetchRequest<ProductRdo> {

«Только руководители проектов и участники проектов могут вызывать этот API». Это понятно, легко читается и в большинстве случаев вполне достаточно. Мы используем такой подход во всем DevRime.

Но в какой-то момент появляется такое требование: «Скрам-мастера должны иметь возможность изменять только эпики продуктов, за которые они отвечают».

Именно здесь начинается проблема. ROLE_SCRUM_MASTERпозволяет ответить на вопрос: «Является ли этот человек скрам-мастером?» Но «какого продукта» он является скрам-мастером, с помощью роли определить нельзя. Роль — это метка, присвоенная человеку, а нам нужно знать о связи между человеком и конкретным ресурсом.

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

ScrumTeam team = scrumTeamLogic.findByProductId(productId);
if (!team.getScrumMaster().getCitizenId().equals(currentCitizenId)) {
    throw new SecurityException("권한이 없습니다");
}

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

В этой статье представлены ReBAC и SpiceDB, которые мы внедрили для решения этой проблемы, а также обобщен наш практический опыт их использования в DevRime Scrum.

2. RBAC · ABAC · ReBAC: три подхода

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

RBAC    사람 ──▶ 역할 ──▶ 권한

ABAC    (사람 속성 + 자원 속성 + 환경) ──▶ 규칙 평가 ──▶ 허용 / 거부

ReBAC   사람 ──관계──▶ 자원 ──관계──▶ 자원 ──▶ 허용 / 거부

RBAC (Role-Based Access Control) — решения на основе ролей

Пользователям назначаются роли, а разрешения объединяются в эти роли. Это самый старый и наиболее широко используемый подход. Например: «Хон Гильдон → project.manager → может изменять настройки проекта».

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

Ограничение — различение ресурсов. Можно ответить на вопрос «Является ли этот человек администратором?», но не на вопрос «Является ли этот человек администратором этого документа?». Чтобы принудительно выразить такую связь, количество ролей должно расти вместе с количеством ресурсов — это называется взрывным ростом числа ролей.

ABAC (Attribute-Based Access Control) — решения на основе атрибутов

Правила вычисляются путем объединения атрибутов пользователя, ресурса и окружения. Например: «Отдел пользователя == отдел документа И текущее время находится в пределах рабочего времени → разрешить».

Преимущество — выразительность. Детализированные условия можно напрямую преобразовать в правила.

Ограничение — предсказуемость. По мере накопления правил становится трудно понять: «Почему этому человеку удалось получить доступ?». Правила также могут конфликтовать друг с другом, а поскольку результат зависит от значений атрибутов в момент проверки, воспроизвести принятое решение бывает непросто.

ReBAC (Relationship-Based Access Control) — решения на основе отношений

отношения между пользователями и ресурсами сохраняются, а решения принимаются путем прохождения по этим отношениям.

홍길동 ──editor──▶ 프로덕트 A
에픽 #12 ──parent──▶ 프로덕트 A
  ⇒ 홍길동은 에픽 #12를 수정할 수 있다

Преимущества — разрешения на уровне ресурсов и наследование. Поскольку мы напрямую сохраняем информацию о том, «какие отношения связывают этого человека с данным ресурсом», взрыва числа ролей не происходит. Если между ресурсами установлены отношения «родитель — потомок», разрешения естественным образом распространяются вниз. Это работает по тому же принципу, что и предоставление общего доступа к папке в Google Drive, при котором общий доступ получают и все находящиеся в ней файлы.

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

Необязательно выбирать только один из трех подходов

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

Категория

Основа для принятия решения

Лучше всего подходит для

Недостатки

RBAC

Роль

Доступ на уровне меню и функций

Разграничение доступа по ресурсам

ABAC

Атрибуты

Политики со сложными условиями

Трассируемость и воспроизводимость

ReBAC

Связи

Разрешения на уровне ресурсов и наследование

Согласованность инфраструктуры и данных

3. SpiceDB: база данных для хранения связей и выполнения запросов к ним

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

SpiceDB— это авторизационная база данных с открытым исходным кодом, которая решает эту проблему за вас. Это продукт, реализующий работу, Zanzibarопубликованную Google в 2019 году. Zanzibar— это система, которая фактически обрабатывает разрешения для Google Drive, YouTube и Calendar. Её целью было отвечать на миллионы запросов о разрешениях в секунду за считанные миллисекунды, и SpiceDBследует этому дизайну.

Схема и связи

SpiceDB содержит две составляющие. Во-первых, схема— она определяет, какие связи существуют и какие разрешения выводятся из этих связей.

definition resource {
    relation editor: actor | group#member
    relation viewer: citizen
    relation parent: resource

    permission edit = editor + parent->edit
    permission view = viewer
}

Вот как её следует читать. Связь editor может быть установлена с отдельным пользователем (актором) или участником группы. Разрешение edit предоставляется, если субъект является editor или имеет разрешение edit для родительского ресурса (parent->edit), тогда как разрешение view предоставляется при наличии связи viewer.

Ключевой момент здесь — строка parent->edit. Эта единственная строка выражает правило наследования: "Если вы можете редактировать родительский ресурс, вы также можете редактировать дочерний ресурс."

Далее идут данные о связях— фактическое хранилище информации о том, у кого какая связь с каким ресурсом. Если сохранить три приведённые ниже строки, SpiceDB обойдёт граф и ответит на вопрос: "Может ли Хон Гиль Дон редактировать эпик E12?"

resource:scrum_product-P1   editor   group:scrum_product_sm-P1#member
group:scrum_product_sm-P1   member   actor:홍길동
resource:scrum_epic-E12     parent   resource:scrum_product-P1

  질의: "actor:홍길동이 resource:scrum_epic-E12를 edit할 수 있나?"

    epic E12 ──parent──▶ product P1 ──editor──▶ SM 그룹 ──member──▶ 홍길동
                                                                ⇒ 허용

Где это используется в DevRime

Мы не вызываем SpiceDB напрямую из наших сервисов. Перед SpiceDB располагается сервис Guard платформенного ядра, а каждый сервис обращается к Guard через библиотеку prologue с помощью GuardAuthorizer.

스크럼 서비스 ──▶ GuardAuthorizer ──▶ Guard ──gRPC──▶ SpiceDB
  (도메인 코드)      (prologue)       (커널)        (권한 저장소)

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

4. Как мы используем это в DevRime?

Scrum Master: зачем добавлять ещё один уровень групп

При создании продукта мы не связываем людей напрямую с продуктом; вместо этого сначала связываем его с группой.

// Product 생성 시 (최초 1회)
public void linkScrumMasterGroup(String productId) {
    guardAuthorizer.add(Relationship.builder()
            .resource(ProductRelationType.PRODUCT, productId)
            .relation(RelationName.EDITOR)
            .subject(SchemaType.GROUP,
                     ProductGroupType.SCRUM_MASTER.id(productId), "member")
            .build());
}

«Редакторы продукта P1 — это scrum_product_sm-P1 участники группы». Затем, когда создаётся Scrum-команда или меняется Scrum Master, мы просто добавляем участников группы или удаляем их.

public void addScrumMaster(String productId, String actorId) {
    guardAuthorizer.add(Relationship.builder()
            .resource(SchemaType.GROUP,
                      ProductGroupType.SCRUM_MASTER.id(productId))
            .relation(RelationName.MEMBER)
            .subject(SchemaType.ACTOR, actorId)
            .build());
}

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

[사람을 직접 연결한 경우]              교체 시 관계 3줄을 전부 수정

    홍길동 ──editor──▶ 프로덕트 P1
    홍길동 ──editor──▶ 에픽 E12
    홍길동 ──editor──▶ 릴리스 R3

[그룹을 한 겹 둔 경우]                 교체 시 멤버 1줄만 수정

    홍길동 ──member──▶ SM 그룹 ──editor──▶ 프로덕트 P1
                                              ▲        ▲
                                       parent │        │ parent
                                          에픽 E12   릴리스 R3

Совместное использование RBAC и ReBAC

Это наиболее распространённый шаблон. Давайте рассмотрим код создания эпика.

public String registerEpic(EpicCdo epicCdo) {
    if (!StageContext.get().getRoles()
                     .contains(ScrumRoles.ROLE_PROJECT_MANAGER)) {
        boolean canEdit = productAuthorizer.canEdit(
                ProductRelationType.PRODUCT, epicCdo.getProductId(),
                StageContext.get().getActorKey().getId());
        if (!canEdit) {
            throw new SecurityException(NOT_ALLOWED);
        }
    }
    ...
    parentAuthorizer.addParent(ProductRelationType.EPIC, epicId,
                               ProductRelationType.PRODUCT,
                               epicCdo.getProductId());
    return epicId;
}

Если пользователь является администратором проекта, доступ предоставляется на основании роли; в противном случае проверяется связь. Администраторы проекта отвечают за весь проект, поэтому нет необходимости оценивать доступ к каждому ресурсу. Для всех остальных нужно спросить: «Связан ли этот человек с данным продуктом?» RBAC служит широким и недорогим первым уровнем проверки, а ReBAC — узким и точным вторым уровнем.

Также стоит обратить внимание на последнюю строку. При создании эпика мы устанавливаем продукт в качестве его родительского элемента. Благодаря этой единственной строке, когда позднее мы проверяем право на редактирование эпика, схема автоматически проходит вверх к продукту через parent->edit. Нет необходимости снова связывать Scrum Master с каждым эпиком.

Заключение

Если выразить суть ReBAC одной строкой, то это хранение разрешений в виде связей между людьми и ресурсами, а не в виде меток, прикреплённых к людям.

До его внедрения мы задавались вопросом: «Действительно ли нам нужна отдельная система только для проверки одного разрешения?» Однако, когда ресурсы начинают образовывать иерархию — эпики внутри продуктов, а релизы внутри эпиков, — одних ролей уже недостаточно, чтобы выразить всё необходимое, и проверки разрешений начинают проникать в код сервиса. Если на этом этапе вынести связи наружу, доменный код сможет сосредоточиться только на том, «что нужно сделать».

Разумеется, это не бесплатно. Если данные о связях рассинхронизируются с исходными данными, разрешения могут незаметно стать некорректными, и заметить это сложнее, чем ошибку в интерфейсе. Если мы забудем удалить Scrum Master из группы при его замене, предыдущий владелец по-прежнему сможет вносить изменения. Именно поэтому мы объединили регистрацию и удаление Scrum Master в одном классе, ProductAuthorizer. Код, изменяющий связи, не должен быть разбросан по проекту, чтобы мы могли найти все места, где что-то было пропущено.

walrus

Site footer