diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index fea0c3a..ed9d9ba 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -7,30 +7,35 @@ Целевой пользовательский сценарий: 1. Пользователь открывает Launcher. -2. Если сессии нет, его отправляет в Authentik. -3. После логина Launcher получает identity и app access. -4. Launcher показывает только доступные приложения. +2. Если сессии нет, его отправляет в платформенный login flow без публичного брендинга Authentik. +3. После логина Launcher получает identity, профиль и access projection. +4. Launcher показывает каталог приложений и состояние доступа. 5. Переход в Task Manager не требует повторного логина. 6. Прямой URL Task Manager тоже защищен. ## Роли компонентов -`Authentik` является единым Identity Provider: +`Authentik` является внутренним Identity Provider / SSO layer: - логин; - пароль; -- активность пользователя; -- группы; -- app access; +- активность identity; +- технические SSO-группы/entitlements; +- OIDC app access projection; - OIDC claims; -- будущие invite/enrollment/MFA flows. +- будущие enrollment/MFA flows, вызываемые из Launcher. + +Authentik не является пользовательским control plane и не должен быть видим обычным пользователям/админам в штатном UI. + +Hosted Authentik login используется только как временный dev flow. Production-вход должен быть NODE.DC-branded login facade или полностью приведенный к NODE.DC UX Authentik flow без публичного упоминания Authentik. `Launcher` является control plane: - входная точка пользователя; -- список доступных приложений; -- admin UI управления пользователями и доступами; -- backend-интеграция с Authentik API; +- витрина всех приложений и состояния доступа; +- admin UI управления клиентами, пользователями, группами, инвайтами и доступами; +- мастер-данные пользователей и профиль платформы; +- backend-интеграция с Authentik API как внутренняя sync/projection; - audit log админских действий. `Task Manager / Plane` остается отдельным приложением: @@ -39,7 +44,8 @@ - собственные workspace/project/task/comment модели; - собственные роли workspace/project; - OIDC login через Authentik; -- mapping внешней identity на существующего Plane user. +- mapping внешней identity на существующего Plane user; +- возможность работать standalone вне NODE.DC stack при сохранении стандартных Plane auth/API механизмов. `Reverse proxy` является внешним защитным и маршрутизирующим слоем: @@ -75,14 +81,31 @@ NODEDC/ Логическая схема: ```text -authentik_db -> identity, groups, app access -launcher_db -> app registry, local profiles, audit +launcher_db -> clients, users, memberships, groups, invites, app registry, access model, profiles, audit +authentik_db -> identity/session/OIDC projection from Launcher plane_db -> workspace, projects, tasks, comments, app roles future_app_db -> доменная логика будущих приложений ``` Связь между identity и локальными пользователями выполняется через explicit mapping, а не через прямое чтение чужих таблиц. +Launcher является источником бизнес-пользователя. Authentik хранит техническую identity, необходимую для SSO, но не заменяет Launcher в администрировании клиентов, команд, доступов и профиля. + +## App standalone rule + +Подключаемые приложения не должны становиться невынимаемыми частями NODE.DC. + +Для Plane это означает: + +- NODE.DC интеграция добавляется как адаптерный слой: OIDC provider, external identity link, app access projection и будущий service adapter; +- доменные таблицы Plane не становятся частью Launcher DB; +- Launcher не читает и не пишет Plane DB напрямую; +- управление workspace/project/task/comment остается внутри Plane; +- интеграция с Plane выполняется через публичные Plane API, API tokens или явно выделенные adapter endpoints; +- стандартные Plane auth/API механизмы не удаляются без отдельного решения, чтобы можно было поставить Plane клиенту standalone. + +Если клиенту нужен только Task Manager, целевая поставка должна позволять развернуть Plane отдельно, отключить NODE.DC Launcher/Auth projection и оставить штатную модель Plane. + ## Auth flow Для приложений, которые контролируются кодом: @@ -91,7 +114,7 @@ future_app_db -> доменная логика будущих приложе - backend/session или BFF layer; - JWT/JWKS validation server-side; - проверка `issuer`, `audience`, `exp`, `sub`; -- проверка app access group; +- проверка app access projection group/entitlement; - локальный user profile или external identity link. Для legacy/временных приложений допускается reverse proxy forward-auth, но это временный внешний слой, а не единственная долгосрочная модель. diff --git a/docs/AUTH_MODEL.md b/docs/AUTH_MODEL.md index d52e8ed..6615db7 100644 --- a/docs/AUTH_MODEL.md +++ b/docs/AUTH_MODEL.md @@ -1,10 +1,16 @@ # Auth Model -## Identity source +## Identity и control-plane source -Единственный источник identity: Authentik. +Пользовательский и административный источник истины: Launcher backend. -Launcher, Plane и будущие приложения не хранят пароли пользователей и не становятся главным источником truth по логину. +Launcher владеет продуктовыми сущностями платформы: клиенты/компании, участники, роли клиента, группы клиента, инвайты, доступы к сервисам, audit log и пользовательский профиль платформы. + +Authentik остается внутренним identity/session слоем. Он хранит техническую identity, password/session/MFA/OIDC и получает из Launcher только синхронизированную проекцию, нужную для SSO: пользователей, enrollment/reset flows, группы/entitlements и профильные claims. + +Пользователь и обычный администратор не должны видеть бренд Authentik в платформенном UI. Прямой доступ к Authentik допускается только через отдельную внутреннюю/служебную ссылку для системной настройки. + +Plane и будущие приложения не хранят пароли пользователей и не становятся главным источником truth по платформенному пользователю. Они принимают OIDC/session claims и связывают их со своими локальными доменными моделями. ## User journey @@ -23,6 +29,25 @@ UI платформы не должен показывать пользоват Прямые ссылки на приложения остаются допустимым пользовательским сценарием. Если session нет, приложение или внешний proxy/auth layer должен увести пользователя в login flow и после успешного входа вернуть к исходному приложению. +## Platform login UX + +Текущий hosted Authentik login допустим только как dev/runtime bootstrap для проверки OIDC, JWKS, session и app redirect. + +Production login должен быть NODE.DC-branded: + +- пользователь видит только NODE.DC экран входа; +- тексты, ошибки, recovery/enrollment и logout не светят бренд Authentik; +- Authentik остается внутренним password/session/MFA/OIDC слоем; +- пароль, service tokens и refresh tokens не попадают во frontend bundle; +- прямые ссылки на Launcher/Task Manager/future apps ведут через этот же NODE.DC login facade и возвращают пользователя в исходное приложение. + +Допустимые реализации: + +1. Кастомизированный Authentik flow, если его можно привести к NODE.DC UX без компромиссов по дизайну. +2. Launcher/BFF login facade, который server-side вызывает внутренний Authentik flow/API и сохраняет безопасность IdP. + +Запрещено делать быстрый frontend-only password form, который обходит IdP protections, хранит секреты в браузере или ломает MFA/recovery/audit. + ## Required claims Минимальный normalized user object: @@ -32,6 +57,7 @@ type AuthUser = { sub: string; email: string; name?: string; + avatarUrl?: string | null; groups: string[]; entitlements?: string[]; }; @@ -44,7 +70,8 @@ type AuthUser = { - `exp` не истек; - `sub` непустой и стабилен; - `email` присутствует для user-facing приложений; -- `groups` или `entitlements` содержат требуемый app access. +- `name`, `picture`/`avatar_url` используются как единый профильный контекст, но мастер-данные профиля должны приходить из Launcher; +- `groups` или `entitlements` содержат требуемую техническую app access projection. ## Groups @@ -79,9 +106,11 @@ nodedc:dm:access ## Access levels -Уровень 1: Authentik app access. +Уровень 1: Launcher access model. -Отвечает на вопрос, можно ли открыть приложение. +Отвечает на вопрос, можно ли открыть приложение. Источник решения — данные Launcher: клиент, членство, группы клиента, user grants, deny exceptions, статус сервиса и период доступа. + +Authentik app access является технической проекцией этого решения для SSO/enforcement, а не местом ручного бизнес-администрирования. Launcher показывает каталог приложений как витрину, а не скрывает все недоступные плитки. Для приложения без доступа кнопка перехода отключена и отображается состояние "Нет доступа". Это важно для продаж, onboarding и понимания доступных модулей платформы. @@ -98,11 +127,28 @@ Launcher показывает каталог приложений как вит Backend должен: - хранить Authentik service token только server-side; -- выполнять admin calls к Authentik API; +- хранить клиентов, пользователей, членства, группы, инвайты, grants/exceptions и профиль платформы; +- выполнять server-side sync в Authentik API без раскрытия Authentik пользователю; +- создавать invite/enrollment flows из Launcher UI; - хранить audit log; - возвращать frontend только нормализованные данные пользователя и разрешенные действия; - не отдавать access/refresh/service tokens в browser bundle. +## Live control-plane data rule + +Демо-пользователи Launcher допустимы только как fixture на этапе frontend MVP. + +Как только появляется backend/admin API, Launcher control-plane должен перейти на живой seed: + +- реальные платформенные пользователи вместо `root@nodedc.local`, `ivan@romashka.ru` и прочих декоративных участников; +- явный superadmin NODE.DC; +- реальные клиентские пользователи и группы, подтвержденные по Plane/Auth/Launcher данным; +- idempotent seed/migration script с backup текущего `launcher-data.json`; +- отсутствие прямого изменения Plane users/workspace/task данных из Launcher seed; +- все дальнейшие проверки клиентов, групп, инвайтов и доступов выполняются через Launcher admin UI. + +Если пользователь существует в Plane, но еще не существует в Authentik, он должен быть заведен через Launcher/Auth sync flow, а не ручным рассинхроном. + ## Plane identity link Минимальная таблица или эквивалентная модель в Plane: @@ -124,3 +170,15 @@ external_identity_link - если link найден, логинить связанный `plane_user_id`; - если link не найден, но email совпадает и включен migration auto-link, создать link; - если доступа нет, вернуть deny. + +## Plane standalone compatibility + +Plane является подключаемым приложением NODE.DC, но не должен становиться технически невынимаемым модулем. + +Правила интеграции: + +- OIDC NODE.DC добавляется как отдельный auth provider path, а не как замена всей Plane auth системы; +- стандартные Plane session/API-token возможности сохраняются, пока нет отдельного решения их убрать; +- Launcher хранит только платформенные привязки и access projection, но не владеет Plane workspace/project/task/comment; +- связь с Plane строится через external identity link и будущий service adapter/API, а не через прямое изменение Plane DB из Launcher; +- env-флаги должны позволять включать NODE.DC SSO в составе платформы и выключать его для standalone-поставки Plane. diff --git a/infra/authentik/bootstrap-dev.py b/infra/authentik/bootstrap-dev.py index a988135..f011631 100644 --- a/infra/authentik/bootstrap-dev.py +++ b/infra/authentik/bootstrap-dev.py @@ -118,9 +118,42 @@ def ensure_groups_scope_mapping(): return mapping +def ensure_profile_scope_mapping(): + expression = """ +attributes = request.user.attributes or {} +display_name = request.user.name or request.user.username +name_parts = display_name.split(" ", 1) +avatar_url = attributes.get("picture") or attributes.get("avatar_url") or attributes.get("avatar") + +return { + "name": display_name, + "given_name": name_parts[0] if name_parts else display_name, + "family_name": name_parts[1] if len(name_parts) > 1 else "", + "preferred_username": request.user.username, + "nickname": request.user.username, + "picture": avatar_url, + "avatar_url": avatar_url, +} +""".strip() + mapping, _ = ScopeMapping.objects.get_or_create( + name="NODE.DC OAuth Mapping: profile context", + defaults={ + "scope_name": "profile", + "description": "Adds normalized NODE.DC profile claims to OIDC tokens.", + "expression": expression, + }, + ) + mapping.scope_name = "profile" + mapping.description = "Adds normalized NODE.DC profile claims to OIDC tokens." + mapping.expression = expression + mapping.save() + return mapping + + def default_scope_mappings(): - scope_names = ["openid", "email", "profile", "offline_access"] + scope_names = ["openid", "email", "offline_access"] mappings = list(ScopeMapping.objects.filter(scope_name__in=scope_names)) + mappings.append(ensure_profile_scope_mapping()) mappings.append(ensure_groups_scope_mapping()) return mappings