06 Authorization

Авторизация и контроль доступа KSAILab

Авторизация и контроль доступа KSAILab

Статус документа

Этот документ является актуальным источником истины по auth- и permission-модели платформы после перехода с Zitadel на Keycloak.

По состоянию на 2026-03-16 runtime auth foundation уже фиксирован в backend-ksailab через MR !15, где canonical human/browser contract доведен до session-based bootstrap.

Отдельно по состоянию на 2026-03-24 следующая auth wave уже зафиксирована в cross-repo issue contracts, но ещё не должна описываться как везде landed runtime:

  • ksailab/backend-ksailab#14 — opaque browser cookie + server-side Redis-backed auth session + full platform logout;
  • ksailab/frontend-ksailab#8 — product logout UX и явное различение logged-out, session-expired, unauthorized;
  • ksailab/ksailab-keycloak-scripts#9 — Keycloak post-logout redirect contract для browser flow.

Это означает:

  • Keycloak уже является источником identity и coarse-grained realm roles;
  • canonical browser login entry теперь GET /api/v1/auth/login;
  • canonical callback endpoint теперь GET /api/v1/auth/callback;
  • canonical logout endpoint теперь POST /api/v1/auth/logout;
  • GET /api/v1/auth/me поднимает текущую human/browser session через backend-owned cookie;
  • отдельного /capabilities endpoint в этой фазе нет;
  • admin read layer для permission administration теперь живёт отдельно в GET /api/v1/permissions/catalog, GET /api/v1/permissions/role-mappings и GET /api/v1/permissions/bundles;
  • local POST /api/v1/auth/login и POST /api/v1/auth/refresh больше не являются рабочим auth flow и остаются только как 410 Gone;
  • onboarding orchestration уже вынесен в отдельный POST /api/v1/onboarding/* контур;
  • startup/health уже показывают Keycloak connectivity и diagnostic human identity readiness;
  • остаточный auth-foundation reconciliation ещё не завершён и остаётся в ksailab/backend-ksailab#1.

Источник истины для этого раздела теперь нужно читать в двух слоях:

  • landed baseline: backend-owned browser session уже существует и даёт bootstrap через GET /api/v1/auth/me;
  • target next wave: browser cookie должен быть opaque session key, auth session и OIDC tokens должны жить server-side, а platform logout должен завершать не только локальную app session, но и Keycloak SSO session.

Связанные репозитории:

  • backend-ksailab
  • frontend-ksailab
  • keycloak-scripts-ksailab
  • docs-ksailab

Keycloak-контур теперь разнесён по четырём отдельным репозиториям:

  • infra-keycloak-ksailab — физический deployment Keycloak и связанной инфраструктуры;
  • ci-pipelines/pipelines-ksailab — shared CI source of truth для bootstrap/smoke pipeline logic;
  • keycloak-scripts-ksailab — realm/bootstrap/provisioning scripts и IAM docs;
  • keycloak-theme-ksailab — отдельный repo для login theme.

Назначение

Система авторизации KSAILab должна решать сразу четыре задачи:

  • централизованная аутентификация пользователей;
  • централизованное управление ролями и жизненным циклом аккаунтов;
  • быстрая выдача UI-прав для фронта;
  • гибкая ABAC-авторизация на реальных данных платформы.

Для KSAILab этого нельзя добиться одной только ролью в токене. Поэтому модель строится из трёх слоёв:

  1. Identity — в Keycloak.
  2. Global capabilities — рассчитываются backend и отдаются фронту.
  3. Resource-scoped ABAC — исполняется backend по данным своей БД.

Ключевое архитектурное решение

Identity source of truth

Keycloak — единственный IAM-компонент платформы.

Он отвечает за:

  • login/logout;
  • пароль и credential lifecycle;
  • realm roles;
  • disable/enable accounts;
  • reset password;
  • OIDC/JWT токены;
  • admin API для platform orchestration.

В текущем KsaiLab-контуре для этого задействованы клиенты:

  • ksailab-frontend
  • ksailab-backend
  • ksailab-admin-service

Их роли нужно трактовать без двусмысленности:

  • ksailab-frontend — текущий transition browser-login client в Keycloak. Он может оставаться частью runtime-совместимости, но не делает frontend владельцем canonical browser auth flow.
  • ksailab-backend — текущий backend-facing audience client для bearer/manual сценариев и token validation contract.
  • ksailab-admin-service — service account path для provisioning и lifecycle-операций backend.

Важно: ksailab-admin-service — это service account для provisioning и lifecycle-операций backend, а не human admin пользователь. Первый нормальный human admin внутри realm ksailab должен создаваться отдельным Keycloak operator flow. Рекомендуемый username для этого bootstrap-пути — platform-admin.

Ownership boundaries

Ниже находится canonical разделение ответственности для human/browser auth:

ЗонаВладелецЧто именно означает
Identity providerKeycloakЛогин-форма, credential lifecycle, OIDC endpoints, realm roles, token issuance
Canonical browser auth flowBackend/api/v1/auth/login, /api/v1/auth/callback, opaque browser cookie as target contract, post-login redirect
Access bootstrap for UIBackendGET /api/v1/auth/me, principal, bundles, effective_capabilities
Menu / route / component gatingFrontendПотребление готового bootstrap-контракта, но не code exchange
Resource-scoped access decisionBackendABAC и повторная серверная валидация каждого реального действия

Практическое правило:

  • frontend не владеет canonical browser token exchange;
  • Keycloak не владеет platform-specific effective capabilities;
  • backend не должен перекладывать окончательное решение по доступу на UI.

Current baseline vs next logout/session wave

Чтобы команда не смешивала уже landed runtime и следующую wave, нужно фиксировать отдельные уровни контракта:

Current landed baseline

  • backend уже владеет canonical browser login/callback/bootstrap path;
  • frontend уже не должен владеть code exchange и canonical token lifecycle;
  • GET /api/v1/auth/me уже поднимает human/browser context из backend-owned session;
  • browser logout endpoint уже называется POST /api/v1/auth/logout, но текущий runtime baseline ещё нельзя автоматически трактовать как завершённый full platform logout.

Target next wave

  • browser cookie хранит только opaque session key;
  • browser auth session живёт server-side и целевым storage для неё является Redis;
  • access/refresh/id tokens живут только внутри server-side auth session и не считаются browser-owned state;
  • GET /api/v1/auth/me bootstrap-ит UI из server-side auth session, а не из frontend-managed tokens;
  • local app logout и full platform logout описываются как разные уровни;
  • продуктовая кнопка Выйти в KSAILab должна означать full platform logout.

Что не нужно утверждать раньше времени

  • что Keycloak post-logout redirect contract уже везде enforced;
  • что frontend logged-out workaround уже убран из runtime;
  • что Redis-backed auth session уже landed во всех средах только потому, что она является целевым контрактом следующей wave.

Provisioning source of truth

Первичный завод аккаунтов и onboarding пользователей выполняется через backend orchestration поверх Keycloak.

Для первой версии действует такой operating model:

  • аккаунты заводит только admin;
  • почта и внешние invite-сервисы не используются;
  • временный пароль генерируется системой;
  • create-операции возвращают credential_receipt, а не обычное inline-поле temporary_password;
  • временный пароль раскрывается только через одноразовый consume flow;
  • на первом входе пользователь обязан сменить пароль.

Подробный документ по этой модели:

Authorization source of truth

Backend остаётся точкой принятия решений по доступу к платформенным ресурсам.

Причина: Keycloak не знает платформенные отношения и состояния:

  • кто владеет курсом или темой;
  • кто является преподавателем конкретной группы;
  • кто назначен в группу или курс;
  • кто соавтор темы;
  • архивирован ли ресурс;
  • активен ли доступ к материалу;
  • какой у объекта статус жизненного цикла.

Поэтому в KSAILab:

  • RBAC задаёт coarse-grained базу;
  • capabilities задают доступ к экранам и действиям UI;
  • ABAC окончательно решает доступ к конкретному ресурсу.

Ролевая модель

Базовые роли хранятся в Keycloak как realm roles:

  • admin
  • teacher
  • student

Роль не должна использоваться как единственный механизм UI-разграничения.

Она нужна для:

  • coarse-grained классификации пользователя;
  • базового starter mapping;
  • понятной административной модели;
  • упрощения части policy checks.

Capability model

Где живёт capability catalog

Каталог capabilities не должен рождаться вручную в нескольких местах сразу.

Для KSAILab фиксируется такая модель:

  • capability definition живёт в backend registry;
  • документация в permission-catalog.md является canonical human-readable каталогом;
  • platform UI не создаёт новые capability keys, а использует уже опубликованный каталог;
  • Keycloak не хранит полный каталог platform permissions.

Это означает: capability key появляется только через контролируемый релиз, а не через ручную правку в IAM.

Как считается effective access

Повседневное администрирование прав выполняется в платформе, а не в Keycloak.

Backend должен рассчитывать effective access по модели:

principal + bundles + overrides

Практически это означает:

  • realm role приходит из Keycloak;
  • bundles, assignments и overrides живут в платформе;
  • frontend получает уже рассчитанный результат через backend bootstrap endpoint;
  • первой фазе достаточно только GET /api/v1/auth/me, без отдельного /capabilities endpoint.

Отдельно для admin-facing permissions UX backend может отдавать read-only metadata через:

  • GET /api/v1/permissions/catalog;
  • GET /api/v1/permissions/role-mappings;
  • GET /api/v1/permissions/bundles.

Этот слой не заменяет bootstrap /auth/me и не должен трактоваться как frontend-owned source of truth.

Операционная модель вынесена отдельно:

Что хранится в Keycloak, а что в платформе

В Keycloak

  • identity;
  • credentials;
  • login/logout endpoints и SSO session lifecycle;
  • realm roles admin, teacher, student;
  • clients;
  • protocol mappers;
  • service account ksailab-admin-service.

В платформе

  • capability definitions как product contract;
  • bundles;
  • assignments;
  • allow/deny overrides;
  • effective permission calculation;
  • resource-scoped ABAC;
  • browser auth session storage;
  • full platform logout orchestration поверх локальной app session и Keycloak logout path.

Это ключевое разделение, без которого Keycloak быстро превратится в неудобное хранилище сотен платформенных прав.

Контракт прав для фронта

Базовый bootstrap

Фронт должен получать права не из случайных полей UI и не из ручного парсинга токена, а через backend.

Текущий canonical bootstrap-контракт:

GET /api/v1/auth/me

Runtime response уже содержит:

  • principal
  • classification
  • realm_roles
  • bundles
  • effective_capabilities

Этого достаточно для:

  • initial render;
  • меню;
  • route guards;
  • component gating;
  • выбора доступных onboarding/admin действий.

В этой фазе capability bootstrap должен идти только через этот endpoint. Отдельный /capabilities endpoint не реализован и не должен описываться как существующий runtime contract.

Отдельно от bootstrap backend now exposes read-only admin metadata endpoints для Permissions section:

  • GET /api/v1/permissions/catalog — capability registry с пометкой catalog-vs-runtime baseline;
  • GET /api/v1/permissions/role-mappings — starter role mappings и текущий runtime baseline по role:*;
  • GET /api/v1/permissions/bundles — starter bundle composition.

Они нужны для Permissions -> Roles и Permissions -> Groups, но не подменяют GET /api/v1/auth/me как initial-render contract.

GET /api/v1/auth/me нужен не для прямой работы с Keycloak, а для backend-owned bootstrap текущего browser/session контекста:

  • поднять current principal из server-side auth session;
  • нормализовать role/classification и platform profile;
  • вернуть global capabilities для menu и route gating;
  • не смешивать browser session auth с explicit bearer/manual flow.

Для следующей wave это нужно читать ещё жёстче:

  • browser получает только opaque session key в cookie;
  • auth session и tokens остаются server-side;
  • /auth/me не должен интерпретироваться как чтение browser-side token storage.

Что сейчас больше не считается рабочим auth flow

Следующие endpoints не должны больше описываться как основа авторизации:

  • POST /api/v1/auth/login
  • POST /api/v1/auth/refresh

Они оставлены только как deprecated compatibility layer и возвращают 410 Gone.

Что не должен делать фронт

Фронт не должен:

  • считать наличие роли эквивалентом полного доступа;
  • парсить JWT как единственный источник permissions;
  • хранить бизнес-логику доступа в десятках компонентов;
  • хранить canonical access/refresh tokens в localStorage;
  • строить browser auth вокруг frontend-managed bearer lifecycle;
  • считать скрытие кнопки окончательной проверкой доступа.

Явный bearer flow

В runtime browser auth для человека строится только через backend-owned session/cookie path.

Явный bearer flow допустим только как clearly separated non-browser/manual flow:

  • bearer token должен быть выпущен настроенным Keycloak realm;
  • bearer path может использоваться для tooling, manual testing или других не-браузерных сценариев;
  • bearer path не должен описываться как canonical browser contract.

Canonical Browser Flow

Для платформы принимается схема backend-owned browser session.

Canonical browser flow

  1. Frontend отправляет browser на GET /api/v1/auth/login.
  2. Backend сохраняет state и redirect target, затем уводит browser в Keycloak.
  3. Keycloak возвращает browser на GET /api/v1/auth/callback.
  4. Backend принимает authorization code, сервер-серверно делает code exchange и валидирует token contract.
  5. Backend создаёт browser auth session server-side и выставляет browser cookie.
  6. Backend редиректит browser обратно на frontend route.
  7. Frontend bootstrap-ится через GET /api/v1/auth/me.
  8. Logout идёт через POST /api/v1/auth/logout.

Для target next wave шаг 5 нужно понимать так:

  • в cookie лежит только opaque session key;
  • сама auth session живёт server-side;
  • target storage для неё — Redis.
sequenceDiagram
    participant Browser
    participant Frontend
    participant Backend
    participant Keycloak

    Frontend->>Backend: GET /api/v1/auth/login
    Backend-->>Browser: Redirect to Keycloak
    Browser->>Keycloak: Browser login flow
    Keycloak-->>Browser: Redirect to /api/v1/auth/callback?code=...
    Browser->>Backend: GET /api/v1/auth/callback
    Backend->>Keycloak: Server-side code exchange
    Keycloak-->>Backend: Tokens / claims
    Backend-->>Browser: Set opaque session cookie + redirect to frontend
    Frontend->>Backend: GET /api/v1/auth/me
    Backend-->>Frontend: principal + bundles + effective_capabilities

Storage model for browser auth

Чтобы больше не спорить о browser/session ownership, нужно использовать такую формулировку:

  • browser auth owner — backend;
  • browser cookie — opaque session key, а не auth payload;
  • access/refresh/id tokens — server-side only внутри backend auth session;
  • Redis — target storage для server-side auth session следующей wave;
  • frontend не должен владеть canonical token storage и не должен требовать токены для bootstrap.

Logout levels

Logout теперь нужно описывать в двух уровнях, а не одним словом:

Local app logout

  • инвалидирует локальную KSAILab/backend session;
  • очищает browser cookie локального приложения;
  • не гарантирует завершение Keycloak SSO session;
  • не должен описываться как финальный продуктовый смысл кнопки Выйти.

Full platform logout

  • инвалидирует локальную KSAILab/backend session;
  • завершает связанный Keycloak/browser SSO path;
  • должен приводить к тому, что следующий login снова показывает реальную страницу входа, а не silent re-login под ещё живым аккаунтом;
  • является target contract для backend-ksailab#14, frontend-ksailab#8, ksailab/ksailab-keycloak-scripts#9.

Что делает кнопка Выйти в KSAILab

Source-of-truth формулировка должна быть такой:

  • продуктово кнопка Выйти означает full platform logout;
  • если runtime конкретной среды ещё находится в переходном состоянии и фактически выполняет только local app logout, это должно описываться как temporary mismatch, а не как canonical contract;
  • frontend logged-out UX и Keycloak post-logout redirect contract должны читаться как companion rollout pieces, а не как отдельные конкурирующие semantics.

Почему именно так

Эта схема лучше подходит под KSAILab, чем browser-managed token flow, потому что:

  • снижает зависимость UI от localStorage токенов;
  • убирает frontend-managed access/refresh lifecycle из canonical browser path;
  • даёт backend полный контроль над principal bootstrap;
  • позволяет централизованно считать capabilities и контекст.

Что не является canonical browser contract

Следующие трактовки считаются ошибочными:

  • direct frontend-to-Keycloak login как default browser path;
  • frontend-owned code exchange;
  • frontend-managed access/refresh lifecycle как основа browser auth;
  • трактовка ksailab-frontend как long-term owner всего browser auth flow.

Transition vs target client model

Transition model

Текущее переходное состояние нужно понимать так:

  • canonical browser flow уже принадлежит backend;
  • backend login entry и backend callback уже являются supported runtime contract;
  • при этом Keycloak browser login может временно опираться на существующий client config ksailab-frontend;
  • это допустимо только как transition-совместимость, а не как доказательство frontend ownership.

Target model

Архитектурный фаворит для следующего этапа:

  • отдельный backend browser-auth client в Keycloak;
  • явный redirect URI policy под backend callback;
  • отсутствие двусмысленности между browser-session auth и explicit bearer/manual client contract;
  • ksailab-frontend перестаёт трактоваться как долгосрочный canonical browser-login client.

Именно эту target-модель нужно считать preferred direction для frontend-ksailab#6, keycloak-scripts-ksailab#8 и последующих runtime cleanup changes.

Provisioning и onboarding

Что уже landed в backend

Первая рабочая версия onboarding orchestration уже поддерживает:

  • POST /api/v1/onboarding/teachers
  • POST /api/v1/onboarding/groups
  • POST /api/v1/onboarding/groups/{group_id}/teachers
  • POST /api/v1/onboarding/groups/{group_id}/students/bulk
  • POST /api/v1/onboarding/credentials/consume

Это покрывает три базовых сценария:

  1. создание teacher account;
  2. создание group и teacher assignment;
  3. bulk onboarding студентов в group.

Что важно не перепутать

  • teacher account creation не равно teacher assignment to group;
  • group creation не равно student provisioning;
  • credential receipt не равно обычному постоянному паролю;
  • ksailab-admin-service не является human admin.

Источники трассируемости

  • shared CI source-of-truth для KsaiLab живёт в ci-pipelines/pipelines-ksailab;
  • backend deployment/runtime source-of-truth живёт в infra/infra-backend-ksailab;
  • backend-ksailab!15 не должен превращаться в выдуманный infra diff: rollout review для ingress/callback/cookie semantics может потребовать отдельный follow-up в infra-репозитории, но не скрытый docs claim.

Runtime capability baseline

Текущий backend bootstrap уже возвращает минимальный набор onboarding-oriented capabilities:

  • auth.bootstrap.read
  • onboarding.teacher.create
  • onboarding.group.create
  • onboarding.group.assign_teacher
  • onboarding.student.bulk_create
  • onboarding.credentials.consume

Это не отменяет будущий полный capability catalog, но уже является действующим runtime-контрактом для первой волны фронтовой интеграции.

ABAC в KSAILab

Принцип

ABAC нужен там, где одного capability недостаточно.

Например, наличие education.courses.manage ещё не означает, что преподаватель может менять любой курс. Нужно проверить отношения пользователя к конкретному ресурсу.

Формула проверки

Любая защищённая операция должна раскладываться так:

  1. principal аутентифицирован;
  2. у principal есть нужный global capability;
  3. resource-scoped правило пропускает конкретный объект.

Backend остаётся единственным местом, где эта проверка считается окончательной.

Как быстро добавлять и убирать доступы

Добавить новую фичу

Целевой сценарий для новой фичи такой:

  1. Разработчик добавляет capability definition в backend registry.
  2. Документация обновляет permission-catalog.md.
  3. Backend получает policy check.
  4. Frontend получает новый guard или menu binding.
  5. После релиза администратор добавляет capability в нужные bundles через platform UI.

Это и есть целевая модель релиз + UI-настройка, которая даёт скорость без потери управляемости.

Быстро изменить доступ существующей фичи

Для уже опубликованной capability релиз не нужен, если требуется:

  • выдать или снять bundle;
  • добавить capability в существующий bundle;
  • убрать capability из bundle;
  • назначить override конкретному пользователю;
  • отключить или включить пользователя.

Как быстро настраивать Keycloak

Базовый принцип

Keycloak должен конфигурироваться как Realm as Code.

Ручная настройка через GUI допустима для диагностики или аварийного unblock, но не должна быть единственным источником истины.

Что должно быть описано as code

  • realm;
  • clients ksailab-frontend, ksailab-backend, ksailab-admin-service, а после выделения target browser client — и отдельный backend browser-auth client;
  • redirect URIs;
  • logout URIs и post-logout redirect contract;
  • realm roles;
  • protocol mappers;
  • service account permissions для backend orchestration.

Где именно живёт этот as-code контур:

  • realm/bootstrap/provisioning scripts и IAM docs — в keycloak-scripts-ksailab;
  • shared pipeline logic — в ci-pipelines/pipelines-ksailab;
  • deployment/ingress/runtime delivery для самого Keycloak — в infra-keycloak-ksailab;
  • login theme и branding — в keycloak-theme-ksailab.

Поддерживаемый scripting/ops path для scripts repo теперь только Python. PowerShell fallback больше не должен описываться как supported path.

Что нельзя объявлять завершённым

Сейчас нельзя писать, что:

  • первый human admin уже полностью bootstrap-ится через чистый Keycloak-first flow;
  • startup log про локального admin доказывает существование готового Keycloak-admin пользователя;
  • ksailab/backend-ksailab#1 закрыт без остатка.

Корректная формулировка: startup/health теперь показывают connectivity и diagnostic human identity readiness, но не заменяют отдельный operator flow для первого human admin.

Validation и rollout implications

Эта auth-модель уже опирается на реальные backend-проверки:

  • integration tests на GET /api/v1/auth/me;
  • tests на deprecated 410 Gone для local auth endpoints;
  • integration tests на missing/expired opaque session -> 401 для /api/v1/auth/me;
  • integration tests на local app logout vs full platform logout semantics по мере реализации next wave;
  • onboarding tests на teacher/group/student orchestration;
  • tests на receipt-based consume flow;
  • health/startup diagnostics по Keycloak issuer и JWKS.

Фронтовая миграция guards, menu gating и onboarding UI остаётся отдельной задачей во frontend-ksailab.

Связанные документы