1135 lines
97 KiB
Python
1135 lines
97 KiB
Python
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()
|