NODEDC_TASKMANAGER/scripts/bootstrap_nodedc_platform_p...

1135 lines
97 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

from datetime import date
from hashlib import sha1
from django.db import transaction
from plane.db.models import (
DEFAULT_STATES,
Issue,
IssueAssignee,
IssueView,
Project,
ProjectMember,
State,
User,
Workspace,
)
SOURCE = "nodedc-platform-plan"
WORKSPACE_SLUG = "nodedc"
PROJECT_IDENTIFIER = "NDCPLATFORM"
PROJECT_NAME = "NDC platform"
CODEX_EMAIL = "codex@nodedc.local"
STATE_TEMPLATES = [
{"group": "backlog", "name": "В обсуждении", "color": "#60646C", "default": True},
{"group": "unstarted", "name": "К выполнению", "color": "#60646C", "default": False},
{"group": "started", "name": "В работе", "color": "#F59E0B", "default": False},
{"group": "completed", "name": "Готово", "color": "#46A758", "default": False},
{"group": "cancelled", "name": "Отложено", "color": "#9AA4BC", "default": False},
{"group": "triage", "name": "Триаж", "color": "#4E5355", "default": False},
]
def html(*paragraphs):
return "".join(f"<p>{paragraph}</p>" for paragraph in paragraphs)
def stable_id(*parts):
value = "::".join(str(part) for part in parts)
return sha1(value.encode("utf-8")).hexdigest()[:12]
def text_block(slug, title, body):
return {
"id": f"{slug}-text-{stable_id(slug, title)}",
"type": "text",
"title": title,
"body": body.strip(),
}
def checker(slug, title, items, checked=False):
normalized_items = []
for index, item in enumerate(items):
if isinstance(item, dict):
item_text = item["text"]
item_checked = item.get("checked", checked)
else:
item_text = item
item_checked = checked
normalized_items.append(
{
"id": f"{slug}-item-{index + 1}-{stable_id(slug, title, item_text)}",
"text": item_text,
"checked": item_checked,
}
)
return {
"id": f"{slug}-checker-{stable_id(slug, title)}",
"type": "checker",
"title": title,
"items": normalized_items,
}
CARDS = [
{
"slug": "phase-0-discovery-platform-skeleton",
"name": "Platform skeleton и discovery",
"priority": "high",
"state_group": "completed",
"assignees": [CODEX_EMAIL],
"description_html": html(
"Нулевой этап платформы NODE.DC: зафиксировать текущую структуру Launcher и Task Manager, создать platform skeleton и перенести архитектурный план из ТЗ в рабочие документы.",
"Граница scope: не переносить Launcher и Plane физически, не менять бизнес-логику приложений, не трогать Plane auth/users до отдельного backup и миграционного этапа.",
"Критерий приемки: в NODEDC есть platform/docs, отдельный git remote NODEDC_PLATFORM, понятный discovery report, auth model, local deployment plan, security checklist и migration plan для существующего Plane user.",
),
"blocks": [
text_block(
"phase0",
"Текущая архитектура",
"""
NODEDC сейчас используется как внешний workspace-корень. Launcher находится отдельным git-репозиторием в /Users/dcconstructions/Downloads/mnt/data/nodedc_launcher и является Vite/React GUI без backend слоя.
Task Manager находится отдельным git-репозиторием в /Users/dcconstructions/Downloads/mnt/data/dc_taskmanager/NODEDC_TASKMANAGER. Plane runtime лежит в plane-app, source fork лежит в plane-src, локальный стенд доступен на http://localhost:8090.
Физический перенос репозиториев на нулевом этапе не нужен: он может сломать Plane runtime, env, volumes и backup. Платформенный слой создается рядом, а не вместо текущих приложений.
Платформенный слой подключен как отдельный git-репозиторий: https://git.dcserve.ru/SILVER/NODEDC_PLATFORM.git. Локальная ветка: main.
""",
),
text_block(
"phase0",
"Этап 0. Discovery + platform skeleton",
"""
Статус: реализовано.
Этап фиксирует baseline без изменения Launcher и Plane. Результат должен стать стартовой точкой для последующих infra/auth задач: вся архитектура и ограничения описаны в platform/docs, а будущая реализация разбита на крупные проектные карточки в Task Manager.
""",
),
checker(
"phase0",
"Чекер этапа 0. Discovery + platform skeleton",
[
"Проверить базовые ТЗ в NODEDC/DOC/BASE.",
"Найти текущие пути Launcher и Task Manager.",
"Подтвердить, что Launcher и Task Manager являются отдельными git-репозиториями.",
"Зафиксировать решение не переносить Plane внутрь Launcher.",
"Создать platform skeleton в NODEDC/platform.",
"Подключить NODEDC/platform к отдельной repo NODEDC_PLATFORM.",
"Добавить ARCHITECTURE.md, AUTH_MODEL.md и DEPLOYMENT_LOCAL.md.",
"Добавить SECURITY_CHECKLIST.md и MIGRATION_PLANE_USER.md.",
"Добавить DISCOVERY_REPORT.md с текущими путями, рисками и следующим шагом.",
"Создать проект NDC platform и крупные карточки roadmap в Task Manager.",
],
checked=True,
),
text_block(
"phase0",
"Реализация этапа 0",
"""
Создан platform skeleton в /Users/dcconstructions/Downloads/mnt/NODEDC/platform.
Папка platform инициализирована как отдельный git-репозиторий на ветке main. Remote origin: https://git.dcserve.ru/SILVER/NODEDC_PLATFORM.git.
Первый commit создан и отправлен в origin/main: 0f89c4d, "АРХ - NODEDC PLATFORM: каркас платформенного репозитория". Remote проверен через git smart HTTP endpoint: refs/heads/main указывает на 0f89c4d.
Добавлены документы: README.md, docs/DISCOVERY_REPORT.md, docs/ARCHITECTURE.md, docs/AUTH_MODEL.md, docs/DEPLOYMENT_LOCAL.md, docs/SECURITY_CHECKLIST.md, docs/MIGRATION_PLANE_USER.md, infra/README.md, infra/.env.example, packages/auth-sdk/README.md, tasks/CODEX_PLATFORM_AUTH_TASK.md.
Discovery подтвердил: Launcher сейчас Vite/React GUI без backend, Task Manager уже запущен как Plane CE self-host на localhost:8090, workspace nodedc существует, пользователь codex@nodedc.local доступен.
""",
),
],
},
{
"slug": "infra-authentik-reverse-proxy",
"name": "Platform infra: Authentik и proxy",
"priority": "high",
"state_group": "completed",
"assignees": [CODEX_EMAIL],
"description_html": html(
"Инфраструктурный слой для локального Authentik и reverse proxy. Этот блок должен дать единые локальные домены, маршрутизацию приложений и внешний защитный слой перед Launcher и Task Manager.",
"Критерий приемки: auth.local.nodedc, launcher.local.nodedc и task.local.nodedc открываются через proxy, Authentik работает за корректными forwarded headers, прямой пользовательский вход идет через домены, а не через внутренние порты.",
),
"blocks": [
text_block(
"infra",
"Текущая архитектура",
"""
Сейчас Launcher и Task Manager живут как отдельные localhost-сервисы. Task Manager доступен через Plane proxy на http://localhost:8090. Единого Authentik, app domains и reverse proxy layer пока нет.
Этот этап не меняет Plane auth flow. Он подготавливает внешний слой и Authentik bootstrap, к которому потом подключаются Launcher и Plane.
""",
),
text_block(
"infra",
"Этап 1. Local domains + reverse proxy",
"""
Статус: реализовано.
Локальная схема доменов и proxy routing описаны в platform/infra. На первом проходе текущие Launcher и Task Manager проксируются как внешние localhost-upstream через host.docker.internal, без физического переноса репозиториев.
Runtime launch закрыт через локальный Caddy-based image nodedc/plane-proxy:ru. Docker compose поднят, Authentik server/worker/Postgres healthy, reverse proxy слушает host port 80.
Локальные домены прописаны в /etc/hosts. Прямые URL auth.local.nodedc, launcher.local.nodedc и task.local.nodedc проходят через host DNS без ручной подмены Host header.
""",
),
checker(
"infra1",
"Чекер этапа 1. Local domains + reverse proxy",
[
{"text": "Согласовать список локальных доменов auth/launcher/task.", "checked": True},
{"text": "Подготовить /etc/hosts инструкцию.", "checked": True},
{"text": "Выбрать proxy: nginx, caddy или traefik.", "checked": True},
{"text": "Настроить routing auth.local.nodedc.", "checked": True},
{"text": "Настроить routing launcher.local.nodedc.", "checked": True},
{"text": "Настроить routing task.local.nodedc.", "checked": True},
{"text": "Прокинуть Host, X-Forwarded-Proto и X-Forwarded-For.", "checked": True},
{"text": "Проверить WebSocket headers для Authentik/Plane live.", "checked": True},
{"text": "Поднять docker compose и проверить auth/task через curl.", "checked": True},
{"text": "Прописать auth/launcher/task local domains в /etc/hosts для браузерного теста.", "checked": True},
],
),
text_block(
"infra",
"Этап 2. Authentik bootstrap",
"""
Статус: реализовано.
Добавлен local compose для Authentik 2026.2 по актуальной официальной схеме: PostgreSQL 16, server и worker. Redis из раннего ТЗ не добавлен, потому что в текущем официальном compose Authentik 2026.2 Redis отсутствует.
Создан временный локальный Authentik admin-пользователь под ручную проверку входа. Ручной login через http://auth.local.nodedc подтвержден пользователем 2026-05-04.
Добавлен воспроизводимый local bootstrap для NODE.DC groups, Launcher/Task Manager OAuth2 providers, application tiles, group access bindings и OIDC client secrets. На текущем Authentik bootstrap выполнен.
""",
),
checker(
"infra2",
"Чекер этапа 2. Authentik bootstrap",
[
{"text": "Добавить Authentik server, worker и postgres в local infra.", "checked": True},
{"text": "Зафиксировать, что Redis не входит в официальный compose Authentik 2026.2.", "checked": True},
{"text": "Добавить генератор infra/.env с локальными secrets и bootstrap credentials.", "checked": True},
{"text": "Создать локального Authentik admin-пользователя для ручного теста.", "checked": True},
{"text": "Создать группы nodedc:launcher:* и nodedc:taskmanager:*.", "checked": True},
{"text": "Создать Application/Provider для Launcher.", "checked": True},
{"text": "Создать Application/Provider для Task Manager.", "checked": True},
{"text": "Настроить app access policies по группам.", "checked": True},
{"text": "Описать bootstrap/export или blueprint strategy.", "checked": True},
{"text": "Проверить login/logout в Authentik за proxy.", "checked": True},
{"text": "Добавить безопасный NODE.DC branded login через Authentik Brand/CSS.", "checked": True},
{"text": "Зафиксировать запрет HTML-proxy/password facade в security RFC.", "checked": True},
],
),
text_block(
"infra",
"Реализация этапа 1",
"""
В platform repo добавлены infra/docker-compose.dev.yml, infra/reverse-proxy/Caddyfile, infra/authentik/README.md и infra/scripts/init-dev-env.sh.
Compose публикует только reverse-proxy port 80. Authentik server, worker и PostgreSQL остаются внутри docker network. Caddy routes: auth.local.nodedc -> authentik-server:9000, launcher.local.nodedc -> host.docker.internal:5173, task.local.nodedc -> host.docker.internal:8090.
Проверки: docker compose config проходит на infra/.env.example; init-dev-env.sh проходит sh -n и создает ignored infra/.env с правами 600. Docker Hub pull caddy:2-alpine зависал, поэтому compose переведен на локальный PLATFORM_PROXY_IMAGE=nodedc/plane-proxy:ru.
Фактический docker compose up выполнен. Контейнеры nodedc-platform-authentik-server-1, nodedc-platform-authentik-worker-1, nodedc-platform-postgresql-authentik-1 healthy; nodedc-platform-reverse-proxy-1 слушает 0.0.0.0:80. Проверки через Host header: auth.local.nodedc возвращает 302 на Authentik authentication flow, launcher.local.nodedc возвращает 200 от Vite launcher, task.local.nodedc возвращает 200 от Plane.
Для Vite launcher route зафиксирован отдельный upstream Host localhost:5173, иначе dev server отдавал 403 на внешний Host launcher.local.nodedc.
Проверен bootstrap-пользователь Authentik: akadmin / admin@nodedc.local / internal / active. Пароль хранится только в ignored platform/infra/.env.
Локальные домены прописаны в /etc/hosts. Прямые проверки без подмены Host header проходят: http://auth.local.nodedc/ -> 302, http://launcher.local.nodedc/ -> 200, http://task.local.nodedc/ -> 200.
Plane live WebSocket upgrade через proxy проверен: запрос с Upgrade headers на http://task.local.nodedc/live/ возвращает 101 Switching Protocols. Caddy reverse_proxy сохраняет Upgrade/Connection headers для live-трафика.
Изменения platform repo закоммичены и отправлены в origin/main: afa53d5, "АРХ - NODEDC PLATFORM: запуск локального proxy/Authentik стенда". Remote main обновлен с 55db22b до afa53d5.
""",
),
text_block(
"infra",
"Реализация этапа 2",
"""
В Authentik создан локальный internal user dcctouch@gmail.com с username dcctouch@gmail.com, активным статусом и usable password.
Пользователь добавлен в встроенную группу authentik Admins, у которой is_superuser=True. Это дает доступ к админке Authentik для ручной проверки и дальнейшей настройки applications/providers.
Пароль был задан из пользовательского ввода и не сохранялся в platform repo. Следующий открытый пункт: ручная проверка login/logout через http://auth.local.nodedc.
Ручной login через proxy подтвержден пользователем 2026-05-04: dcctouch@gmail.com успешно вошел в Authentik, видит My applications и кнопку Admin interface.
В platform repo добавлены infra/authentik/bootstrap-dev.py и infra/scripts/bootstrap-authentik-dev.sh. Скрипт idempotent, заполняет недостающие OIDC client secrets в ignored infra/.env и создает Authentik объекты через локальный ak shell.
Созданы группы nodedc:superadmin, nodedc:launcher:admin, nodedc:launcher:user, nodedc:taskmanager:admin и nodedc:taskmanager:user. Пользователь dcctouch@gmail.com добавлен во все NODE.DC группы для локальной проверки.
Созданы application tiles NODE.DC Launcher и NODE.DC Task Manager, OAuth2 providers NODE.DC Launcher OIDC и NODE.DC Task Manager OIDC. Provider discovery endpoints отвечают 200: /application/o/launcher/.well-known/openid-configuration и /application/o/task-manager/.well-known/openid-configuration.
Application access задан через group bindings: Launcher доступен nodedc:superadmin, nodedc:launcher:admin, nodedc:launcher:user; Task Manager доступен nodedc:superadmin, nodedc:taskmanager:admin, nodedc:taskmanager:user. OIDC tokens получают стандартные openid/email/profile/offline_access scopes и custom groups scope.
2026-05-04 добавлен безопасный branded login prototype без proxy над Authentik. В platform/docs/AUTH_BRANDED_LOGIN_RFC.md зафиксирована threat model: запрещены HTML-rewrite proxy, password form в Launcher/BFF, ROPC/password grant и обход MFA/recovery/audit. Разрешенный путь — Authentik-native Brand/CSS/Flow customization.
platform/infra/authentik/bootstrap-dev.py теперь idempotent настраивает Brand для auth.local.nodedc: title NODE.DC, default authentication flow default-authentication-flow, flow title "Работайте во всех измерениях.", layout stacked и branding_custom_css из /templates/branding/nodedc-login.css. CSS лежит в platform/infra/authentik/custom-templates/branding/nodedc-login.css и монтируется в Authentik существующим volume /templates.
Runtime bootstrap выполнен: curl страницы auth.local.nodedc подтверждает title NODE.DC, matched_domain auth.local.nodedc и наличие brand-css со строкой "Работайте во всех измерениях". Authentik shell подтверждает default Brand auth.local.nodedc, branding_title NODE.DC и flow title "Работайте во всех измерениях.". Password/MFA/recovery mechanics не тронуты.
Дополнение 2026-05-04: экран Authentik "My applications" признан не login flow, а пользовательским dashboard самого identity provider. Для обычного платформенного UX он исключен на proxy-уровне: auth.local.nodedc/ и auth.local.nodedc/if/user* возвращают 302 на launcher.local.nodedc. OIDC discovery/authorize/callback и /if/admin/ не редиректятся, чтобы не ломать SSO и служебную админку Authentik.
Дополнение 2026-05-04: экран "You've logged out of NODE.DC Launcher" признан Authentik provider invalidation dashboard. OAuth2 providers переключены на штатный default-invalidation-flow с UserLogoutStage, чтобы global logout закрывал Authentik session и возвращал пользователя в NODE.DC route без показа Authentik application logout UI.
Дополнение 2026-05-04: authentication flow приближен к старому Plane login в рамках Authentik-native customization. Brand attributes выставлены в settings.locale=ru и settings.theme.base=dark. Flow title изменен на "Работайте во всех измерениях.". IdentificationStage теперь использует только email как идентификатор и привязан к штатному PasswordStage, поэтому email/password находятся в одном Authentik challenge без передачи пароля в Launcher. Отдельный PasswordStage binding удален из flow, MFA и UserLoginStage остаются штатными.
Изменения platform repo закоммичены и отправлены в origin/main: 4a10726, "АРХ - NODEDC PLATFORM: bootstrap Authentik applications". Remote main обновлен с afa53d5 до 4a10726.
""",
),
],
},
{
"slug": "launcher-control-plane-oidc",
"name": "Launcher: backend, OIDC и app access",
"priority": "high",
"state_group": "started",
"assignees": [CODEX_EMAIL],
"description_html": html(
"Launcher должен перестать быть только меню ссылок и стать control plane: входная точка платформы, клиенты/компании, пользователи, группы, инвайты, профиль, app registry, access matrix и audit log.",
"Критерий приемки: пользователь входит через NODE.DC UI без публичного брендинга Authentik, Launcher хранит бизнес-модель доступа и профиля, а Authentik получает только техническую SSO/OIDC projection.",
),
"blocks": [
text_block(
"launcher",
"Текущая архитектура",
"""
Launcher сейчас является Vite/React GUI в отдельном репозитории /Users/dcconstructions/Downloads/mnt/data/nodedc_launcher.
Backend слоя не обнаружено. Значит OIDC callback, session handling, Authentik service token, app registry и audit log нужно добавлять отдельным backend/BFF слоем, а не держать во frontend.
""",
),
text_block(
"launcher",
"Этап 1. Backend/BFF shell",
"""
Статус: реализовано.
Нужно добавить минимальный backend слой для безопасной auth-интеграции и будущего admin API. Выбран путь: BFF живет в Launcher repo и в dev режиме обслуживает Vite как middleware на том же порту 5173, чтобы существующий reverse proxy launcher.local.nodedc не требовал нового внешнего домена или порта.
""",
),
checker(
"launcher1",
"Чекер этапа 1. Backend/BFF shell",
[
{"text": "Выбрать место backend слоя: Launcher repo или platform/services/launcher-api.", "checked": True},
{"text": "Добавить health endpoint.", "checked": True},
{"text": "Добавить server-side session storage.", "checked": True},
{"text": "Добавить env contract для OIDC и Authentik API.", "checked": True},
{"text": "Зафиксировать запрет service tokens во frontend.", "checked": True},
],
),
text_block(
"launcher",
"Этап 2. OIDC login/session",
"""
Статус: реализовано, ожидает ручной browser login/logout проверки.
Launcher должен проходить OIDC Authorization Code Flow + PKCE через Authentik и получать нормализованного текущего пользователя.
Важно: текущий hosted Authentik login является временным dev flow для проверки OIDC/JWKS/session/redirect. Целевой production login не должен показывать пользователю Authentik UI/brand.
""",
),
checker(
"launcher2",
"Чекер этапа 2. OIDC login/session",
[
{"text": "Добавить login route.", "checked": True},
{"text": "Добавить callback route.", "checked": True},
{"text": "Проверить state/nonce/token.", "checked": True},
{"text": "Загрузить JWKS и валидировать JWT.", "checked": True},
{"text": "Нормализовать sub/email/name/groups.", "checked": True},
{"text": "Добавить logout flow.", "checked": True},
{"text": "Подтвердить browser login/logout через launcher.local.nodedc.", "checked": False},
],
),
text_block(
"launcher",
"Этап 3. App registry и фильтрация плиток",
"""
Статус: частично реализовано.
Launcher показывает приложения не статическим списком, а через registry и access state. Источник истины по доступу — Launcher access model; Authentik groups/entitlements являются технической проекцией для SSO/enforcement.
Продуктовое правило от 2026-05-04: Launcher не должен скрывать все недоступные плитки. Он показывает каталог приложений платформы, а отсутствие доступа отражает как disabled/Нет доступа на карточке. Это уже заложено в текущий frontend-декор и должно сохраняться при backend-интеграции.
""",
),
checker(
"launcher3",
"Чекер этапа 3. App registry и фильтрация плиток",
[
{"text": "Спроектировать app_registry модель.", "checked": True},
{"text": "Добавить GET /api/me.", "checked": True},
{"text": "Добавить GET /api/apps.", "checked": True},
{"text": "Вычислять access state из runtime projection groups.", "checked": True},
{"text": "Не скрывать недоступные плитки, а показывать Нет доступа.", "checked": True},
{"text": "Проверить direct URL behavior через proxy.", "checked": False},
],
),
text_block(
"launcher",
"Целевой пользовательский flow",
"""
Production flow: пользователь открывает nodedc.ru, видит основной промо/маркетинговый сайт, нажимает "Войти на платформу", проходит окно логина/пароля и после успешного доступа попадает в NODE.DC Launcher.
Пользовательский UI платформы не должен светить название identity provider. В текстах и кнопках используется нейтральное "Войти", "Вход на платформу", "Сессия NODE.DC". Authentik остается внутренним identity/session/OIDC слоем; продуктовая админка живет в Launcher. Прямой Authentik UI допустим только через отдельную служебную ссылку для системной настройки.
Прямые ссылки на приложения остаются нормальным сценарием. Если пользователь открыл Task Manager напрямую без session, его нужно увести в login flow и вернуть к приложению после успешной авторизации.
""",
),
text_block(
"launcher",
"Этап 3.5. NODE.DC login facade",
"""
Статус: частично реализовано.
Ручная проверка 2026-05-04 подтвердила: при входе пользователь видел стандартное окно Authentik. Это допустимо только для dev/OIDC bootstrap, но не соответствует целевой UX-модели NODE.DC.
Выбран безопасный первый путь: полностью кастомизированный Authentik-native Brand/CSS/Flow, а не proxy и не password facade в Launcher. Это сохраняет Authentik как password/session/MFA/recovery/audit authority.
Безопасностная граница: не делать frontend-only password form, не хранить IdP/service secrets в browser bundle, не ломать MFA/recovery/rate-limit/audit и не обходить Authentik как password/session authority. Если Brand/CSS не даст pixel-level дизайн Plane login, следующий допустимый уровень — template override внутри Authentik deployment с отдельным security review, но не HTML-rewrite proxy.
""",
),
checker(
"launcher35",
"Чекер этапа 3.5. NODE.DC login facade",
[
{"text": "Выбрать безопасную стратегию: branded Authentik flow или Launcher/BFF facade.", "checked": True},
{"text": "Запретить reverse proxy HTML rewrite и password form в Launcher/BFF.", "checked": True},
{"text": "Добавить AUTH_BRANDED_LOGIN_RFC.md с threat model.", "checked": True},
{"text": "Настроить Authentik Brand/CSS для auth.local.nodedc.", "checked": True},
{"text": "Сверстать NODE.DC login по канону Task Manager/Launcher в пределах Brand/CSS.", "checked": True},
{"text": "Объединить email/password в одном Authentik challenge через IdentificationStage.password_stage.", "checked": True},
{"text": "Включить ru locale и dark theme на уровне Authentik Brand settings.", "checked": True},
{"text": "Скрыть Authentik brand в базовом authentication page title/brand.", "checked": True},
{"text": "Исключить Authentik My applications dashboard из пользовательского маршрута.", "checked": True},
{"text": "Исключить Authentik application logout dashboard из пользовательского маршрута.", "checked": True},
"Поддержать returnTo для прямых ссылок на приложения.",
"Поддержать forced login для диагностики.",
"Спроектировать recovery/enrollment/MFA без раскрытия Authentik UI.",
"Проверить, что password/service tokens не попадают во frontend.",
],
),
text_block(
"launcher",
"Реализация этапов 1-3",
"""
В Launcher repo добавлен local BFF server/dev-server.mjs. npm run dev теперь запускает Node/Express BFF, который обслуживает Vite как middleware на том же порту 5173. Старый чистый Vite dev режим сохранен как npm run dev:vite.
BFF автоматически подхватывает OIDC env из NODEDC/platform/infra/.env, не вшивая secrets в frontend или git. Добавлены routes: GET /healthz, GET /auth/login, GET /auth/callback, GET /auth/logout, GET /api/me, GET /api/apps, POST /api/storage/upload и POST /api/storage/data.
OIDC flow реализован как Authorization Code + PKCE: state хранится server-side и в HttpOnly cookie, nonce проверяется по id_token, JWKS загружается из discovery endpoint, JWT валидируется по issuer/audience. Session хранится server-side in-memory для локального dev стенда.
App registry возвращает полный каталог приложений из launcher storage и runtime access state: hasAccess, matchedGroups, accessReason. Frontend больше не скрывает недоступные плитки; он отключает переход и показывает "Нет доступа" через существующую механику карточек.
Frontend Launcher подключен к /api/me и /api/apps. Без session показывается нейтральный экран "Вход на платформу NODE.DC" и кнопка "Войти" без упоминания Authentik. После login пользователь нормализуется из OIDC claims, а плитки получают доступы из runtime app registry.
Проверки 2026-05-04: npm run build проходит; http://launcher.local.nodedc/healthz возвращает oidcConfigured=true; http://launcher.local.nodedc/api/me без session возвращает 401 и loginUrl; http://launcher.local.nodedc/auth/login возвращает 302 на Authentik authorize endpoint; discovery endpoint Authentik для launcher возвращает issuer и authorization_endpoint.
Ручная проверка 2026-05-04 выявила callback error {"error":"JSON Web Key Set malformed"}. Root cause: Authentik OAuth2 providers были созданы без signing_key, поэтому JWKS endpoint отдавал {}. Bootstrap исправлен: providers получают authentik Self-signed Certificate как signing key. После повторного bootstrap JWKS отдает RSA key.
Открытый приемочный пункт: ручной browser login/logout через http://launcher.local.nodedc и проверка, что после callback видна плитка Task Manager.
Граница готовности на текущий момент: Launcher готов как базовый OIDC/BFF portal, но не является финально готовым production control plane. Остаются mock/dev элементы и следующий обязательный блок — Plane OIDC. Пока Plane OIDC не реализован, переход из Launcher в task.local.nodedc ожидаемо приводит к старой авторизации Plane, потому что Task Manager еще не доверяет Authentik session.
2026-05-04 добавлен явный logout в профильное меню Launcher: кнопка "Выйти" вызывает /auth/logout и чистит local BFF session без ухода в Authentik UI/admin. Это нужно, чтобы пользователь оставался в NODE.DC UX после выхода.
SSO-session у identity provider может оставаться активной. Поэтому повторное нажатие "Войти" может вернуть пользователя в Launcher без ввода пароля — это ожидаемое SSO-поведение, а не ошибка. Для диагностики добавлен prompt=login на /auth/login?prompt=login и отдельный global logout через /auth/logout?global=1, но пользовательский logout по умолчанию остается локальным.
2026-05-04 повторная ручная проверка подтвердила целевой UX gap: после нажатия "Войти" пользователь все еще видит стандартное окно Authentik. Это зафиксировано как отдельный backlog-этап NODE.DC login facade; текущий hosted login не считать production-ready.
Текущий BFF/OIDC слой является переходной реализацией. Сейчас app access читается из OIDC groups как runtime projection, но целевая source-of-truth модель — Launcher backend: клиенты, членства, группы клиента, user grants, deny exceptions, профиль платформы и audit. Authentik должен получать из Launcher синхронизированную техническую проекцию для SSO/enforcement, а не быть ручной бизнес-админкой.
""",
),
text_block(
"launcher",
"Этап 4. Admin API и audit",
"""
Статус: частично реализовано.
Launcher Admin API должен владеть бизнес-администрированием платформы: клиенты/компании, пользователи, членства, группы клиента, инвайты, grants/exceptions, профиль платформы и audit log. Пароли и SSO-session остаются в Authentik, но Authentik синхронизируется server-side и не показывается обычным администраторам как frontend/control-plane.
Текущий проход реализует dev backend boundary поверх JSON-backed launcher-data.json. Это еще не production DB и не финальный Authentik sync, но уже выносит admin операции из frontend-only состояния в server-side BFF.
""",
),
checker(
"launcher4",
"Чекер этапа 4. Admin API и audit",
[
{"text": "Добавить backend store для launcher-data.json.", "checked": True},
{"text": "Закрыть admin endpoints Launcher roles.", "checked": True},
{"text": "Добавить clients CRUD endpoints.", "checked": True},
{"text": "Добавить users list/profile update endpoints.", "checked": True},
{"text": "Добавить memberships update/delete endpoints.", "checked": True},
{"text": "Добавить client groups CRUD endpoints.", "checked": True},
{"text": "Добавить service catalog CRUD/reorder endpoints.", "checked": True},
{"text": "Добавить invite CRUD endpoints из Launcher.", "checked": True},
{"text": "Добавить access grants/exceptions/user-service endpoints.", "checked": True},
{"text": "Добавить Authentik sync dry-run plan endpoint.", "checked": True},
{"text": "Добавить admin_audit_log запись для backend mutations.", "checked": True},
{"text": "Подключить frontend admin overlay к новым admin endpoints.", "checked": True},
"Заменить JSON-backed store на production persistence.",
"Реализовать фактический server-side sync в Authentik.",
],
),
text_block(
"launcher",
"Реализация этапа 4",
"""
В Launcher repo добавлен server/control-plane-store.mjs: dev control-plane repository поверх public/storage/launcher-data.json и dist/storage/launcher-data.json. Store нормализует доменные коллекции clients, users, memberships, groups, services, grants, exceptions, invites, syncStatuses и auditEvents.
BFF server/dev-server.mjs получил admin guard по группам nodedc:superadmin/nodedc:launcher:admin и backend endpoints для текущего overlay: GET /api/admin/control-plane, clients CRUD, users/profile update, memberships update/delete, groups CRUD, services CRUD/reorder, invites CRUD, access grants/exceptions/user-service, sync retry и GET /api/admin/sync/authentik/plan.
Backend mutations пишут auditEvents и помечают затронутые объекты как pending sync для target authentik. Endpoint /api/admin/sync/authentik/plan пока dry-run: он показывает users/groups/access projection, но не вызывает Authentik API.
Frontend admin overlay подключен к server-side admin API через src/shared/api/adminApi.ts. Mutation handlers в src/app/LauncherApp.tsx больше не собирают mock id/audit локально для поддержанных admin операций, а вызывают BFF и принимают обновленный LauncherData из backend response.
Проверки 2026-05-04: node --check server/dev-server.mjs и server/control-plane-store.mjs проходят; npm run build проходит; npm run test проходит; http://localhost:5173/healthz возвращает oidcConfigured=true; /api/admin/control-plane без session возвращает 401 через localhost и launcher.local.nodedc. Store mutations проверены на временной копии launcher-data.json: create client/group/invite и set user-service access обновляют counts и auditEvents.
""",
),
text_block(
"launcher",
"Этап 4.5. Live users seed и очистка demo-участников",
"""
Статус: частично реализовано.
Декоративные MVP-пользователи root@nodedc.local, ivan@romashka.ru, vera@romashka.ru, vasya@romashka.ru, lena@romashka.ru, maria@example.ru и blocked demo-user удаляются из active Launcher seed. Их можно восстановить из backup, но рабочий dev-runtime должен проверяться на живых пользователях.
Фактический runtime discovery 2026-05-04:
- Authentik содержит dcctouch@gmail.com / DC Touch в группах nodedc:superadmin, nodedc:launcher:admin/user, nodedc:taskmanager:admin/user.
- Plane содержит dcctouch@gmail.com / DC Touch как owner workspace nodedc.
- Plane содержит silver_psih@yahoo.com / dcMASSACRE как member workspace nodedc и owner workspace silver-psih-mo493nm7.
Перед реализацией нужно подтвердить канонические email/имена: пользователь подтвердил, что silver_psih@yahoo.com — правильная почта Silver Psy. Runtime Plane/Auth показывает dcctouch@gmail.com для текущего superadmin; этот email считается фактическим до отдельного указания заменить его на другой alias.
Цель этапа: заменить demo-участников Launcher на живой минимальный seed, где супер-админ администрирует клиентов, группы, инвайты и доступы через Launcher UI, а не через прямые правки JSON/Auth/Plane.
""",
),
checker(
"launcher45",
"Чекер этапа 4.5. Live users seed и очистка demo-участников",
[
{"text": "Подтвердить канонический email NODE.DC superadmin.", "checked": True},
{"text": "Подтвердить канонический email Silver Psy / менеджера.", "checked": True},
{"text": "Сделать backup public/storage/launcher-data.json.", "checked": True},
{"text": "Создать idempotent seed script для живых users/clients/groups/memberships.", "checked": True},
{"text": "Удалить декоративных MVP-участников из active Launcher seed.", "checked": True},
{"text": "Создать живой superadmin в Launcher profile model.", "checked": True},
{"text": "Создать живого Silver Psy/manager пользователя в Launcher profile model.", "checked": True},
{"text": "Синхронизировать недостающего пользователя в Authentik через server-side flow.", "checked": True},
{"text": "Не пересоздавать Plane users и не менять Plane workspace/task связи.", "checked": True},
{"text": "Показывать все видимые сервисы в Launcher даже без доступа к запуску.", "checked": True},
{"text": "Считать /api/apps из Launcher control-plane, а не из stale OIDC groups.", "checked": True},
{"text": "Синхронизировать affected users в Authentik при изменении memberships/groups/grants/exceptions.", "checked": True},
{"text": "Заменить polling открытых Launcher вкладок на event-driven runtime refresh.", "checked": True},
{"text": "Сделать atomic write launcher-data.json, чтобы reads не ловили полузаписанный JSON.", "checked": True},
{"text": "Добавить pending-state на ячейки access matrix, чтобы не было повторных кликов по in-flight mutation.", "checked": True},
{"text": "Отключить legacy frontend autosave launcher-data.json в authenticated runtime.", "checked": True},
{"text": "Добавить self-profile settings panel справа.", "checked": True},
{"text": "Добавить upload avatar в Launcher profile storage.", "checked": True},
{"text": "Добавить смену пароля через backend Authentik projection.", "checked": True},
"Проверить Launcher admin UI: клиенты, участники, группы, инвайты, доступы.",
{"text": "Проверить Task Manager SSO для обоих живых пользователей.", "checked": True},
],
),
text_block(
"launcher",
"Реализация этапа 4.5",
"""
В Launcher repo создан scripts/seed-live-control-plane.mjs. Скрипт читает текущий public/storage/launcher-data.json, сохраняет существующий каталог services и заменяет active control-plane домен на один live-клиент DCTOUCH и двух пользователей: dcctouch@gmail.com / DC Touch и silver_psih@yahoo.com / Silver Psy.
Перед применением создан backup public/storage/backups/launcher-data-live-seed-20260504-152728.json. Active storage после seed содержит 1 client, 2 users, 2 memberships, 2 groups, 3 grants, 0 invites. Silver Psy помещен в группу "Менеджеры" и получает доступ к OPERATIONAL CORE через group grant; DC Touch помещен в группу "Администраторы" и остается platform superadmin.
Из src/shared/api/mockApi.ts убраны декоративные profileOptions для Ivan/Vera/Vasya/Lena/Maria. src/app/LauncherApp.tsx больше не мапит любого non-superadmin OIDC-пользователя на user_vasya: активный Launcher profile выбирается по Authentik sub/email, затем fallback на user_root для superadmin.
Fallback src/shared/api/mockData.ts приведен к live seed, чтобы при отсутствии storage не всплывали старые demo-участники. Plane users/workspaces не пересоздавались и не менялись.
Проверки 2026-05-04: node --check scripts/seed-live-control-plane.mjs, server/dev-server.mjs и server/control-plane-store.mjs проходят; npm run build проходит; npm run test проходит; http://localhost:5173/storage/launcher-data.json возвращает 1 client, 2 users и emails dcctouch@gmail.com/silver_psih@yahoo.com; http://launcher.local.nodedc/healthz возвращает ok и oidcConfigured=true.
Дополнение 2026-05-04: реализован server-side Authentik provisioning flow. В Launcher repo добавлен server/authentik-sync.mjs: он работает только на backend, берет Authentik API token из server-side env, создает/обновляет пользователя, назначает группы и выставляет пароль только при явном provisioning/generatePassword сценарии.
BFF server/dev-server.mjs получил POST /api/admin/users для создания пользователя в Launcher control-plane с optional Authentik provisioning и POST /api/admin/users/:userId/provision-authentik для повторной синхронизации существующего Launcher-пользователя. Healthcheck теперь показывает authentikApiConfigured без раскрытия token.
server/control-plane-store.mjs получил createUser и markUserAuthentikProvisioned: Launcher остается source-of-truth, а Authentik UUID сохраняется в user.authentikUserId после успешной технической проекции.
Frontend admin overlay получил форму "Создать участника" в разделе "Участники": email/name/role/group -> backend creates Launcher user -> Authentik projection. При генерации временного пароля пароль показывается только одноразово в UI и не пишется в storage/task card.
Дополнительно исправлена проверка доступа к плитке AGENT CORE/NodeDC: service nodedc больше не считается доступным просто по nodedc:launcher:user. Для сервисных плиток используется service.authentikGroupName или специальная группа конкретного приложения; факт входа в Launcher не должен автоматически открывать все сервисы.
Дополнение после ручной проверки Silver Psy: buildLauncherServices больше не фильтрует denied-сервисы из rail. Обычный пользователь видит весь видимый каталог приложений, но у сервисов без доступа кнопка запуска заблокирована и показывает "Нет доступа". Hidden-сервисы остаются скрытыми для non-root.
Добавлен self-profile контур: /api/profile, PATCH /api/profile и POST /api/profile/password доступны любому authenticated пользователю для собственного профиля. Profile settings panel открывается из меню профиля справа налево, может работать одновременно с левым admin overlay, сохраняет name/email/phone/position/avatarUrl и умеет менять пароль через backend Authentik projection.
Avatar upload использует существующий server-side storage upload и сохраняет ссылку в Launcher profile model. Runtime user в Launcher теперь предпочитает данные Launcher profile model поверх OIDC claims, чтобы изменения имени/аватарки отображались без ожидания следующего login.
Фактически выполнено: silver_psih@yahoo.com создан в Authentik как active internal user, получил группы nodedc:launcher:user и nodedc:taskmanager:user. dcctouch@gmail.com также синхронизирован по Authentik UUID. public/storage/launcher-data.json теперь содержит Authentik UUID для обоих live-профилей, а syncStatuses для обоих users находятся в state synced.
Проверки 2026-05-04: node --check server/authentik-sync.mjs, server/dev-server.mjs и server/control-plane-store.mjs проходят; npm run build проходит; npm run test проходит; Authentik API подтверждает наличие silver_psih@yahoo.com с 2 группами; /healthz возвращает authentikApiConfigured=true. Пользователь вручную подтвердил: Silver Psy входит в Launcher, не видит администрирование, открывает OPERATIONAL CORE и в Task Manager видит корректный доступный проект.
Дополнение после проверки снятия доступа: /api/apps больше не берет app access из stale OIDC groups текущей BFF-сессии. Runtime-доступ рассчитывается из Launcher control-plane через resolveRequiredGroups, а OIDC groups остаются fallback только если пользователь еще не сопоставлен с Launcher profile model.
Admin mutations теперь проталкивают affected users в Authentik сразу после изменения профиля, memberships, groups, access grants, deny/allow exceptions и user-service matrix. Это закрывает кейс, когда админ снял Silver Psy из доступа к OPERATIONAL CORE, а Authentik еще держал старую nodedc:taskmanager:user группу.
Открытые Launcher вкладки authenticated пользователей больше не используют polling каждые 5 секунд. BFF держит /api/events через Server-Sent Events, а admin/profile/storage mutations публикуют событие control-plane.updated. Вкладки обновляют /api/me, /api/apps и storage/control-plane snapshot только по факту изменения control-plane состояния.
Дополнение после ручной проверки access matrix: выявлен race записи public/storage/launcher-data.json. Пока writeFile перезаписывал файл напрямую, параллельные runtime запросы иногда читали пустой/полузаписанный JSON и падали с Unexpected end of JSON input; визуально это выглядело как применение access select только с нескольких попыток.
server/control-plane-store.mjs переведен на atomic write: запись идет во временный файл в том же storage root, затем rename заменяет launcher-data.json. Это убирает окно, в котором reader может увидеть обнуленный файл.
В access matrix добавлен pending-state на конкретную ячейку user/service: после выбора роли ячейка сразу показывает выбранное значение и "Сохраняем...", а повторный клик блокируется до завершения mutation. Это убирает многократные ручные попытки и race между несколькими одинаковыми запросами.
Дополнение по итогам повторного теста: найден второй источник отката access matrix. Legacy frontend autosave сохранял весь LauncherData обратно в /api/storage/data после setData и мог перезаписать свежий backend-result старым состоянием из открытой вкладки. src/app/LauncherApp.tsx больше не вызывает persistLauncherData; authenticated runtime пишет control-plane только через admin/profile API mutations. /api/storage/data дополнительно закрыт requireLauncherAdmin и оставлен только как служебный dev endpoint.
Текущий live-sync выполнен вручную после исправления: dcctouch@gmail.com имеет nodedc:launcher:user, nodedc:superadmin, nodedc:launcher:admin, nodedc:taskmanager:admin, nodedc:taskmanager:user; silver_psih@yahoo.com получает nodedc:launcher:user, nodedc:taskmanager:user и service-digital-twin согласно текущей access matrix.
Дополнение 2026-05-04: Launcher BFF получил внутренний server-to-server endpoint POST /api/internal/access/check. Endpoint защищен bearer token, не доступен через пользовательскую session-модель и возвращает актуальный allow/deny по Launcher control-plane для конкретного serviceSlug, subject/email/userId.
Ограничение по уже открытой downstream-сессии вынесено в Plane live enforcement этап. Launcher остается source-of-truth, а downstream-приложения должны либо дергать внутренний access check, либо использовать общий будущий NODE.DC auth SDK/gateway.
Ограничение: email/password/profile update уже работает как dev-flow, но production UX для reset-password/invite-email и подтверждения смены email еще нужно вынести в отдельный полноценный этап.
""",
),
],
},
{
"slug": "plane-oidc-user-migration",
"name": "Plane: OIDC и миграция пользователя",
"priority": "urgent",
"state_group": "started",
"assignees": [CODEX_EMAIL],
"description_html": html(
"Интеграция Plane fork с Authentik OIDC без потери текущего пользователя и связанных данных. Это самый рискованный блок, потому что нельзя пересоздать существующего Plane admin и потерять связи задач.",
"Критерий приемки: старый Plane user связан с Authentik sub, после входа через NODE.DC/Authentik видит старые workspace, проекты, задачи, комментарии и назначения; signup закрыт; Plane остается вынимаемым standalone-продуктом с сохранением собственных auth/API механизмов.",
),
"blocks": [
text_block(
"plane",
"Текущая архитектура",
"""
Task Manager работает как Plane CE self-host. Runtime лежит в plane-app, fork исходников в plane-src.
В workspace nodedc уже есть данные, пользователи и проекты. Старые связи в Plane нельзя менять без отдельной проверенной миграции.
Архитектурное правило от 2026-05-04: Plane подключается к NODE.DC как приложение/адаптер, но не превращается в невынимаемый модуль платформы. Его workspace/project/task/comment модели остаются внутри Plane, стандартные Plane auth/API механизмы сохраняются через env/конфигурацию, а Launcher не читает и не пишет Plane DB напрямую.
""",
),
text_block(
"plane",
"Этап 1. Backup и auth discovery",
"""
Статус: реализовано.
Перед кодом нужен backup и точная карта Plane auth/users: модели пользователя, sessions, signup, login endpoints, env flags и все связи, которые нельзя трогать.
""",
),
checker(
"plane1",
"Чекер этапа 1. Backup и auth discovery",
[
{"text": "Сделать dump plane_db.", "checked": True},
{"text": "Сохранить plane.env.", "checked": True},
{"text": "Сохранить uploads/MinIO volumes.", "checked": True},
{"text": "Найти User/Profile/WorkspaceMember/ProjectMember связи.", "checked": True},
{"text": "Найти текущие login/signup endpoints.", "checked": True},
{"text": "Найти env flags для signup/OAuth/password auth.", "checked": True},
{"text": "Составить список файлов для OIDC integration.", "checked": True},
],
),
text_block(
"plane",
"Реализация этапа 1",
"""
Создан backup перед изменением Plane auth слоя: plane DB dump, plane.env/api env snapshots, docker compose/config copies и архив uploads/MinIO volume. Backup лежит в /Users/dcconstructions/Downloads/mnt/data/dc_taskmanager/NODEDC_TASKMANAGER/plane-app/backup/nodedc-platform-oidc-20260504-122924.
Discovery подтвердил критичный baseline: пользователь dcctouch@gmail.com уже существует в Plane как admin_nodedc, user id 844d7f18-285d-4671-8371-8ca9ca5ffa39, состоит owner в workspace nodedc и связанных проектах. Миграция не должна пересоздавать этого пользователя и не должна менять старые workspace/project/task связи.
Точки интеграции найдены в plane-src/apps/api/plane/authentication/urls.py, plane-src/apps/api/plane/authentication/utils/login.py и plane-src/apps/api/plane/authentication/utils/redirection_path.py. Env flags Plane подтверждены: ENABLE_SIGNUP, ENABLE_EMAIL_PASSWORD, ENABLE_MAGIC_LINK_LOGIN.
""",
),
text_block(
"plane",
"Этап 2. External identity link",
"""
Статус: реализовано.
Нужно добавить mapping Authentik identity на существующего Plane user. Это отдельный слой связи, а не массовое изменение старых owner/assignee/created_by.
""",
),
checker(
"plane2",
"Чекер этапа 2. External identity link",
[
{"text": "Спроектировать модель external_identity_link.", "checked": True},
{"text": "Добавить миграцию модели.", "checked": True},
{"text": "Обеспечить unique provider+sub.", "checked": True},
{"text": "Обеспечить unique provider+plane_user_id.", "checked": True},
{"text": "Добавить last_login_at/status.", "checked": True},
{"text": "Проверить idempotent поведение.", "checked": True},
],
),
text_block(
"plane",
"Реализация этапа 2",
"""
В Plane source добавлена модель ExternalIdentityLink в plane-src/apps/api/plane/db/models/user.py и экспорт в plane-src/apps/api/plane/db/models/__init__.py.
Применены миграции db.0137_external_identity_link и db.0138_external_identity_link_unique_user. Модель хранит provider, subject, user, email, groups, status и last_login_at. Уникальность закреплена по provider+subject и provider+user, чтобы один Authentik subject не связывался с несколькими Plane users и один Plane user не получал несколько Authentik identities одного provider.
Dry-run команды link_authentik_user проверен на dcctouch@gmail.com без изменения данных.
""",
),
text_block(
"plane",
"Этап 3. OIDC login flow",
"""
Статус: частично реализовано.
Plane должен принимать Authentik OIDC callback, валидировать token/state/nonce и логинить локального пользователя через mapping.
""",
),
checker(
"plane3",
"Чекер этапа 3. OIDC login flow",
[
{"text": "Добавить NODE.DC/Authentik login entrypoint.", "checked": True},
{"text": "Добавить OIDC callback.", "checked": True},
{"text": "Валидировать issuer/audience/exp/sub.", "checked": True},
{"text": "Проверять nodedc:taskmanager:access.", "checked": True},
{"text": "Искать link по authentik_sub.", "checked": True},
{"text": "Логинить существующего plane_user_id.", "checked": True},
"Закрыть путь без mapping или app access.",
],
),
text_block(
"plane",
"Реализация этапа 3",
"""
Добавлены backend routes /auth/oidc/login/ и /auth/oidc/callback/ в Plane API. Login entrypoint генерирует Authorization Code + PKCE, state/nonce хранит в server-side session, callback обменивает code на tokens, проверяет id_token через JWKS и валидирует issuer/audience/nonce.
Access check завязан на группы Authentik: nodedc:superadmin, nodedc:taskmanager:admin, nodedc:taskmanager:user. Для local-dev включен PLANE_OIDC_AUTO_LINK_EMAIL=1, чтобы первый успешный callback мог безопасно связать существующего Plane user по email без пересоздания пользователя.
Проверено: /auth/oidc/login/?next_path=/nodedc стабильно возвращает 302 на Authentik authorize endpoint для client_id nodedc-task-manager.
Ручной browser acceptance от 2026-05-04 закрыт: пользователь зашел в Launcher в incognito, прошел Authentik login, вернулся в Launcher, открыл Operational Core/Task Manager из плитки и получил доступ без Plane password. В Plane создан ExternalIdentityLink для dcctouch@gmail.com, status active, subject присутствует, linked Plane user остался старым user id 844d7f18-285d-4671-8371-8ca9ca5ffa39.
После проверки добавлен route alias /auth/oidc/callback без trailing slash, потому что Authentik сначала обращался к callback без слэша и получал 404 перед успешным запросом на /auth/oidc/callback/. Группы в Plane и Launcher теперь дедуплицируются при нормализации claims.
Закрыт базовый runtime-слой профильного контекста: Launcher читает avatarUrl из OIDC claims, а Plane синхронизирует display_name/first_name/last_name/avatar при OIDC login. Текущая Authentik mapping — временная dev-проекция до появления Launcher profile storage. Текущий Plane user dcctouch@gmail.com приведен к display name DC Touch, существующий Plane avatar сохранен. Полная avatar-синхронизация должна идти из Launcher как мастер-источника профиля, а не из ручной настройки Authentik.
""",
),
text_block(
"plane",
"Этап 4. Migration command и signup policy",
"""
Статус: частично реализовано.
После модели и login flow нужна управляемая команда связывания старого пользователя и отключение обходных входов, которые ломают модель доступа.
""",
),
checker(
"plane4",
"Чекер этапа 4. Migration command и signup policy",
[
{"text": "Добавить manage.py command link_authentik_user.", "checked": True},
{"text": "Поддержать dry-run.", "checked": True},
{"text": "Проверять конфликтующий mapping.", "checked": True},
{"text": "Не менять задачи/workspace/memberships.", "checked": True},
{"text": "Отключить публичный signup.", "checked": True},
"Закрыть лишние OAuth/magic-link обходы, если они нарушают invite/manual модель.",
{"text": "Проверить старый admin после OIDC login.", "checked": True},
],
),
text_block(
"plane",
"Реализация этапа 4",
"""
Добавлена management command plane-src/apps/api/plane/db/management/commands/link_authentik_user.py с параметрами --email, --sub и --dry-run. Команда проверяет существующего Plane user, конфликтующий provider+subject и конфликтующий provider+user mapping.
plane-app/plane.env настроен для local runtime: WEB_URL=http://task.local.nodedc, CORS_ALLOWED_ORIGINS включает task.local.nodedc, ENABLE_SIGNUP=0, ENABLE_MAGIC_LINK_LOGIN=0, Plane OIDC env указывает на Authentik provider task-manager. ENABLE_EMAIL_PASSWORD временно оставлен включенным как fallback до ручного подтверждения OIDC входа, чтобы не потерять доступ к текущему Task Manager.
""",
),
text_block(
"plane",
"Этап 5. Единый профильный контекст",
"""
Статус: частично реализовано.
Профиль пользователя должен быть одинаковым в Launcher и Task Manager. Источник истины — Launcher profile model; OIDC claims являются транспортной проекцией, через которую локальные приложения получают единый display name/avatar context.
""",
),
checker(
"plane5",
"Чекер этапа 5. Единый профильный контекст",
[
{"text": "Добавить профильные claims в Authentik mapping.", "checked": True},
{"text": "Передавать avatarUrl в Launcher session.", "checked": True},
{"text": "Показывать avatarUrl в Launcher top bar/profile menu.", "checked": True},
{"text": "Синхронизировать Plane user profile при OIDC login.", "checked": True},
{"text": "Сохранить существующий Plane avatar, если Authentik не отдает picture.", "checked": True},
{"text": "Проверить dcctouch@gmail.com после profile sync.", "checked": True},
"Реализовать Launcher profile storage и avatar upload как мастер-источник.",
],
),
text_block(
"plane",
"Реализация этапа 5",
"""
В platform/infra/authentik/bootstrap-dev.py добавлен NODE.DC OAuth Mapping: profile context. Он отдает name, given_name, family_name, preferred_username, picture и avatar_url из Authentik user.name и user.attributes. Это временная dev-проекция до появления Launcher backend profile storage.
Launcher BFF нормализует picture/avatar_url/avatar в avatarUrl и отдает его через /api/me. Frontend прокидывает avatarUrl в runtime user и показывает изображение в top bar/profile menu, если claim присутствует.
Plane OIDC callback получил PLANE_OIDC_SYNC_PROFILE=1 по умолчанию. При успешном OIDC login Plane обновляет display_name, first_name, last_name и avatar из claims. Если OIDC projection не отдает picture/avatar_url, существующий Plane avatar не очищается. Runtime проверен: dcctouch@gmail.com теперь display_name DC Touch, first_name DC, last_name Touch, avatar_url сохранен. Launcher готов отобразить avatarUrl, но фактический единый avatar должен появиться из будущего Launcher profile storage/avatar upload, а не из ручной настройки Authentik.
""",
),
text_block(
"plane",
"Этап 5.5. Live access enforcement",
"""
Статус: частично реализовано.
Plane должен не только пускать пользователя при OIDC login, но и регулярно проверять актуальный доступ в Launcher control-plane. Если админ снимает доступ к OPERATIONAL CORE, уже открытая Plane session должна быть отозвана на следующем API/request без ожидания logout/session expiry.
Интеграция должна быть отключаемой env-флагами, чтобы Plane можно было развернуть standalone без Launcher/Auth projection.
""",
),
checker(
"plane55",
"Чекер этапа 5.5. Live access enforcement",
[
{"text": "Добавить внутренний Launcher access-check endpoint.", "checked": True},
{"text": "Защитить endpoint server-side bearer token.", "checked": True},
{"text": "Добавить Plane middleware после AuthenticationMiddleware.", "checked": True},
{"text": "Проверять Launcher access по Authentik subject/email.", "checked": True},
{"text": "Удалять Plane sessions при denied access.", "checked": True},
{"text": "Сделать enforcement отключаемым env-флагом.", "checked": True},
{"text": "Убрать legacy Plane email/password экран из NODE.DC direct flow.", "checked": True},
{"text": "Редиректить page denied/revoked в Launcher вместо старого login UI.", "checked": True},
{"text": "Редиректить 401/403 frontend requests в NODE.DC OIDC handoff.", "checked": True},
{"text": "Сделать Task Manager sign-out сквозным NODE.DC logout.", "checked": True},
{"text": "Добавить Task Manager front-channel logout endpoint /logout.", "checked": True},
{"text": "Закрывать app sessions из Launcher global logout перед IdP logout.", "checked": True},
{"text": "Проверить Plane API -> Launcher check из контейнера.", "checked": True},
"Провести ручной browser acceptance: снять доступ и увидеть отзыв уже открытой Plane-сессии.",
],
),
text_block(
"plane",
"Реализация этапа 5.5",
"""
Launcher repo: server/dev-server.mjs получил POST /api/internal/access/check. Endpoint принимает serviceSlug, subject/email/userId, находит live Launcher user, считает groups через resolveRequiredGroups и возвращает allowed/matchedGroups/user без раскрытия frontend secrets. Token берется из server-side env: NODEDC_INTERNAL_ACCESS_TOKEN / NODEDC_PLATFORM_SERVICE_TOKEN / fallback PLANE_OIDC_CLIENT_SECRET.
Plane repo: добавлен plane-src/apps/api/plane/authentication/middleware/nodedc_access.py и подключение в plane-src/apps/api/plane/settings/common.py сразу после django.contrib.auth.middleware.AuthenticationMiddleware. Middleware активируется только при PLANE_NODEDC_ACCESS_ENFORCEMENT=1 и наличии PLANE_NODEDC_ACCESS_CHECK_URL + token. Standalone Plane без этих env не меняет поведение.
Если Launcher возвращает denied, middleware удаляет sessions текущего Plane user через модель Session, вызывает logout(request) и возвращает 403 JSON для API или redirect на PLANE_NODEDC_ACCESS_DENIED_REDIRECT_URL для page request. В local runtime denied page redirect ведет в Launcher, а не в legacy Plane login. При временной недоступности Launcher check middleware возвращает 503, но не удаляет session, чтобы не делать destructive logout из-за сетевого сбоя.
plane-app/docker-compose.yaml и plane-app/plane.env получили PLANE_NODEDC_ACCESS_* env, PLANE_NODEDC_ACCESS_DENIED_REDIRECT_URL и extra_hosts для launcher.local.nodedc. В local runtime API container пересоздан с новым env; patched middleware/settings скопированы в /code/plane и API container перезапущен.
Plane web получил NODE.DC handoff для прямых заходов на task.local.nodedc: app/(home)/page.tsx показывает NodeDCAuthRedirect вместо AuthBase на NODE.DC-доменах; AuthenticationWrapper больше не возвращает unauthenticated users на /, а отправляет в /auth/oidc/login с сохранением next_path; api.service.ts при 401 и nodedc_access_revoked также уводит в OIDC handoff. Старый email/password экран Plane сохранен как standalone/fallback для не-NODE.DC доменов, но не должен появляться в нормальном local.notdc/task.local.nodedc сценарии.
Дополнение по logout semantics: Launcher /api/me теперь отдает global logout URL /auth/logout?global=1&returnTo=/. Launcher /auth/logout?global=1 сначала закрывает app sessions через front-channel logout URLs, затем уводит браузер в Authentik end-session. Для Task Manager добавлен GET/POST /logout, который чистит Plane session и возвращает короткий технический ответ для front-channel. Стандартный Plane POST /auth/sign-out/ теперь после локального logout редиректит в PLANE_NODEDC_GLOBAL_LOGOUT_URL, поэтому кнопка выхода внутри Task Manager больше не должна мгновенно заводить пользователя обратно через активную SSO session.
plane-src/apps/proxy/Caddyfile.ce маршрутизирует /logout в API, а plane-app/docker-compose.yaml монтирует этот Caddyfile в local proxy runtime, чтобы route не терялся при restart контейнера. В plane-app/plane.env добавлен PLANE_NODEDC_GLOBAL_LOGOUT_URL=http://launcher.local.nodedc/auth/logout?global=1&returnTo=/.
Проверки 2026-05-04: node --check server/dev-server.mjs проходит; python3 -m py_compile nodedc_access.py/settings.common проходит; docker compose --env-file plane.env config проходит; http://127.0.0.1:5173/healthz и launcher.local.nodedc/healthz возвращают internalAccessApiConfigured=true; task.local.nodedc/auth/oidc/login/ возвращает 302 на Authentik; из plane-app-api-1 POST на http://launcher.local.nodedc/api/internal/access/check для silver_psih@yahoo.com возвращает allowed=True и matchedGroups=['nodedc:taskmanager:user'], для missing-user возвращает allowed=False. Web image nodedc/plane-frontend:ru пересобран, plane-app-web-1 пересоздан, собранный home asset содержит NodeDCAuthRedirect и строку «Переходим в NODE.DC».
Ограничение: полный browser acceptance именно на уже открытой вкладке после снятия доступа еще должен подтвердить пользователь. Для durable production deploy нужно пересобрать nodedc/plane-backend:local, потому что текущий локальный runtime применен через docker cp поверх контейнера.
""",
),
text_block(
"plane",
"Этап 6. Standalone compatibility и Plane API adapter",
"""
Статус: частично реализовано.
Plane должен оставаться самостоятельным продуктом, который можно развернуть клиенту без остального NODE.DC stack. NODE.DC-интеграция должна быть включаемым слоем: OIDC provider, external identity link, app access projection и будущий adapter через Plane API/API tokens.
Запрещено строить интеграцию через прямое владение Plane DB со стороны Launcher. Launcher хранит платформенную привязку client/user/access, но Plane продолжает владеть workspace/project/task/comment и собственными ролями внутри приложения.
Частично закрыто в live enforcement: NODE.DC access middleware включается только через env PLANE_NODEDC_ACCESS_ENFORCEMENT и не активируется в standalone-профиле без Launcher access-check URL/token.
""",
),
checker(
"plane6",
"Чекер этапа 6. Standalone compatibility и Plane API adapter",
[
{"text": "Зафиксировать env-флаг включения NODE.DC SSO слоя.", "checked": True},
"Описать standalone-профиль Plane без Launcher/Auth projection.",
"Проверить сохранение Plane API-token механизма.",
"Спроектировать Launcher -> Plane API adapter.",
"Добавить mapping launcher_client_id -> plane_workspace_id.",
"Добавить mapping launcher_user_id -> plane_user_id.",
"Запретить прямые записи Launcher в Plane DB.",
],
),
],
},
{
"slug": "auth-sdk-application-standards",
"name": "Auth SDK и стандарты приложений",
"priority": "medium",
"state_group": "backlog",
"assignees": [CODEX_EMAIL],
"description_html": html(
"Общий слой правил и SDK для будущих приложений NODE.DC, чтобы Tender, Agents, 1C, DM и новые сервисы не реализовывали auth каждый раз по-разному.",
"Критерий приемки: есть typed AuthUser, JWKS/JWT validation helpers, requireAppAccess, env contract и документация claims для Node.js/Next.js сервисов; для Plane описан Python/Django эквивалент.",
),
"blocks": [
text_block(
"sdk",
"Текущая архитектура",
"""
Единого auth-sdk пока нет. В platform/packages/auth-sdk создан только README и контракт будущего пакета.
Пока не подключен Launcher backend, SDK должен оставаться спецификацией, чтобы не плодить преждевременные абстракции.
""",
),
text_block(
"sdk",
"Этап 1. Claims contract",
"""
Статус: не реализовано.
Сначала нужно стабилизировать claims и naming групп, иначе SDK закрепит неправильный контракт.
""",
),
checker(
"sdk1",
"Чекер этапа 1. Claims contract",
[
"Зафиксировать AuthUser type.",
"Зафиксировать обязательные claims.",
"Зафиксировать app access group naming.",
"Зафиксировать error model для deny/unauthorized.",
"Синхронизировать AUTH_MODEL.md с Authentik bootstrap.",
],
),
text_block(
"sdk",
"Этап 2. TypeScript auth-sdk",
"""
Статус: backlog.
После Launcher backend можно вынести повторяемую JWT/JWKS логику в маленький пакет без привязки к конкретному web framework.
""",
),
checker(
"sdk2",
"Чекер этапа 2. TypeScript auth-sdk",
[
"Добавить JWKS loader/cache.",
"Добавить JWT validation.",
"Добавить normalizeAuthUser.",
"Добавить requireAppAccess.",
"Добавить unit tests.",
"Подключить SDK в Launcher backend.",
],
),
],
},
{
"slug": "security-acceptance-staging",
"name": "Security acceptance и staging path",
"priority": "high",
"state_group": "backlog",
"assignees": [CODEX_EMAIL],
"description_html": html(
"Финальный слой проверки, что платформа не только запускается, но и закрывает реальные security-сценарии: прямые ссылки, отключение пользователей, отсутствие наружных внутренних портов, audit и staging path.",
"Критерий приемки: security checklist закрыт проверками, staging compose или понятный staging plan готов, secrets не попали в frontend/git, Plane admin сохранил данные после OIDC migration.",
),
"blocks": [
text_block(
"security",
"Текущая архитектура",
"""
Security checklist создан в platform/docs/SECURITY_CHECKLIST.md. Реальных acceptance tests по новой auth architecture пока нет, потому что Authentik/proxy/Launcher OIDC/Plane OIDC еще не реализованы.
""",
),
text_block(
"security",
"Этап 1. Security acceptance tests",
"""
Статус: backlog.
Нужно проверить не только happy path, но и отказы: нет логина, нет группы, прямой URL, deactivated user, отсутствие service token во frontend.
""",
),
checker(
"security1",
"Чекер этапа 1. Security acceptance tests",
[
"Проверить redirect Launcher без логина.",
"Проверить скрытие Task Manager без group access.",
"Проверить deny на прямой task.local.nodedc без group access.",
"Проверить отзыв уже открытой downstream-сессии после снятия доступа.",
"Проверить успешный вход пользователя с access.",
"Проверить старого Plane admin после OIDC migration.",
"Проверить deactivate user.",
"Проверить audit log admin actions.",
"Проверить отсутствие service tokens во frontend bundle.",
],
),
text_block(
"security",
"Этап 2. Staging deployment path",
"""
Статус: backlog.
После локальной схемы нужен staging plan: HTTPS, secure cookies, реальные domains, secrets management, закрытые внутренние порты.
""",
),
checker(
"security2",
"Чекер этапа 2. Staging deployment path",
[
"Описать staging domains.",
"Подготовить docker-compose.staging.yml или эквивалентный deployment plan.",
"Включить COOKIE_SECURE=true.",
"Включить HTTPS/HSTS.",
"Закрыть Postgres/Redis/MinIO наружу.",
"Описать backup/restore runbook.",
"Описать secrets rotation policy.",
],
),
],
},
]
def ensure_project(workspace, owner, codex_user):
project, _ = Project.objects.get_or_create(
workspace=workspace,
identifier=PROJECT_IDENTIFIER,
defaults={
"name": PROJECT_NAME,
"description": "Платформенный проект NODE.DC: Authentik, Launcher control plane, Plane OIDC и future app auth foundation.",
"network": 0,
"project_lead": owner,
"created_by": codex_user,
"updated_by": codex_user,
},
)
project.name = PROJECT_NAME
project.description = "Платформенный проект NODE.DC: Authentik, Launcher control plane, Plane OIDC и future app auth foundation."
project.network = 0
project.project_lead = owner
project.timezone = "Europe/Moscow"
project.created_by = project.created_by or codex_user
project.updated_by = codex_user
project.save()
for index, state_spec in enumerate(STATE_TEMPLATES):
state = State.all_state_objects.filter(project=project, group=state_spec["group"]).first()
if state is None:
state = State(project=project, workspace=workspace)
state.name = state_spec["name"]
state.group = state_spec["group"]
state.color = state_spec["color"]
state.default = state_spec["default"]
state.is_triage = state_spec["group"] == "triage"
state.sequence = DEFAULT_STATES[index]["sequence"]
state.created_by = state.created_by or codex_user
state.updated_by = codex_user
state.save(disable_auto_set_user=True)
if state_spec["default"]:
project.default_state = state
project.save()
for user, role in [(owner, 20), (codex_user, 20)]:
member, _ = ProjectMember.objects.get_or_create(
project=project,
member=user,
defaults={"workspace": workspace, "role": role, "created_by": codex_user, "updated_by": codex_user},
)
member.workspace = workspace
member.role = role
member.is_active = True
member.updated_by = codex_user
member.save()
return project
def ensure_issue(workspace, project, codex_user, spec):
state = State.all_state_objects.get(project=project, group=spec["state_group"])
issue = Issue.objects.filter(project=project, workspace=workspace, external_source=SOURCE, external_id=spec["slug"]).first()
if issue is None:
issue = Issue.objects.filter(project=project, workspace=workspace, name=spec["name"]).first()
if issue is None:
issue = Issue(project=project, workspace=workspace, created_by=codex_user)
issue.name = spec["name"]
issue.description_html = spec["description_html"]
issue.detail_layout = {"nodedc_structured_blocks": spec["blocks"]}
issue.priority = spec["priority"]
issue.state = state
issue.start_date = date.today() if spec["state_group"] == "started" else None
issue.target_date = None
issue.external_source = SOURCE
issue.external_id = spec["slug"]
issue.updated_by = codex_user
issue.save(disable_auto_set_user=True)
IssueAssignee.objects.filter(issue=issue).delete()
for email in spec["assignees"]:
assignee = User.objects.get(email=email)
IssueAssignee.objects.get_or_create(
issue=issue,
assignee=assignee,
defaults={
"project": project,
"workspace": workspace,
"created_by": codex_user,
"updated_by": codex_user,
},
)
return issue
def ensure_view(workspace, project, codex_user):
view, _ = IssueView.objects.get_or_create(
workspace=workspace,
project=project,
name="Roadmap NODE.DC platform",
defaults={
"description": "Крупные архитектурные блоки платформенной auth/SSO работы",
"filters": {},
"rich_filters": {},
"owned_by": codex_user,
"access": 1,
"logo_props": {},
},
)
view.description = "Крупные архитектурные блоки платформенной auth/SSO работы"
view.filters = {}
view.rich_filters = {}
view.owned_by = codex_user
view.access = 1
view.save(disable_auto_set_user=True)
@transaction.atomic
def main():
workspace = Workspace.objects.get(slug=WORKSPACE_SLUG)
owner = workspace.owner
codex_user = User.objects.get(email=CODEX_EMAIL)
project = ensure_project(workspace, owner, codex_user)
issues = [ensure_issue(workspace, project, codex_user, card) for card in CARDS]
ensure_view(workspace, project, codex_user)
summary = {
"workspace": workspace.slug,
"project": f"{project.identifier} / {project.name}",
"issues": [f"{project.identifier}-{issue.sequence_id}: {issue.name}" for issue in issues],
}
print(summary)
main()