1716 lines
152 KiB
Python
1716 lines
152 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"
|
||
PLATFORM_OWNER_EMAIL = "dcctouch@gmail.com"
|
||
|
||
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: обнаружен второй logout gap — Authentik end-session может завершаться белой страницей "Logout successful" вместо возврата в продуктовый маршрут, а hidden fetch end-session не гарантирует закрытие SSO-cookie. Launcher logout handoff переведен на top-level IdP logout: после front-channel закрытия приложений браузер уходит в Authentik end-session с зарегистрированным post_logout_redirect_uri=http://launcher.local.nodedc/auth/logged-out, затем /auth/logged-out чистит platform cookies и переводит в /auth/login?prompt=login. Launcher frontend использует window.location.replace для logout и revalidates session on bfcache pageshow, чтобы browser Back не показывал старый залогиненный state после выхода.
|
||
|
||
Дополнение 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": True},
|
||
],
|
||
),
|
||
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": True},
|
||
],
|
||
),
|
||
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},
|
||
{"text": "Поддержать returnTo для прямых ссылок на приложения.", "checked": True},
|
||
{"text": "Поддержать forced login для диагностики.", "checked": True},
|
||
"Спроектировать recovery/enrollment/MFA без раскрытия Authentik UI.",
|
||
{"text": "Проверить, что password/service tokens не попадают во frontend.", "checked": True},
|
||
],
|
||
),
|
||
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 Launcher больше не показывает промежуточное окно с кнопкой "Войти": frontend делает прямой replace на /auth/login, а сам /auth/login сразу отдает 302 в Authentik authorize flow. Если пользователь пришел на непустой launcher path, frontend добавляет returnTo к login URL, чтобы после callback вернуть его на исходный маршрут. После 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 видна витрина Launcher, OPERATIONAL CORE открывает Task Manager, global logout закрывает Launcher и downstream session.
|
||
|
||
Граница готовности на текущий момент: Launcher готов как базовый OIDC/BFF portal и local control-plane prototype, но не является финально готовым production control plane. Остаются mock/dev элементы, JSON-backed persistence, recovery/MFA UX и production storage/profile work.
|
||
|
||
2026-05-04 добавлен явный logout в профильное меню Launcher: кнопка "Выйти" вызывает /auth/logout и чистит local BFF session без ухода в Authentik UI/admin. Это нужно, чтобы пользователь оставался в NODE.DC UX после выхода.
|
||
|
||
SSO-session у identity provider может оставаться активной, если не провести top-level logout через Authentik end-session. Поэтому для пользовательского выхода Launcher использует global logout handoff: чистит локальную BFF session, закрывает downstream app sessions, уводит браузер в Authentik end-session и возвращает через /auth/logged-out в /auth/login?prompt=login. Это убирает промежуточную Authentik "Logout successful" страницу и не дает прямому заходу в Task Manager молча перелогиниться через старую SSO session.
|
||
|
||
2026-05-04 login facade переведен на безопасную Authentik-native кастомизацию: Brand/CSS/template JS без proxy над password form. Окно приведено к NODE.DC/Plane визуальному канону, Authentik dashboard/logout application UI исключены из пользовательского маршрута, логотип синхронизирован с Launcher top bar по размеру и позиции.
|
||
|
||
Текущий 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.",
|
||
{"text": "Реализовать фактический server-side sync в Authentik.", "checked": True},
|
||
],
|
||
),
|
||
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 еще нужно вынести в отдельный полноценный этап.
|
||
""",
|
||
),
|
||
text_block(
|
||
"launcher",
|
||
"Актуализация 2026-05-09",
|
||
"""
|
||
За последние итерации Launcher фактически стал текущим control-plane для корпоративного контура NODE.DC. Через него уже проверяются: клиенты, участники, группы, инвайты, сервисный каталог, матрица доступов, Operational Core workspace binding, user profile, client avatar, fullscreen admin panels и service-specific role modal для Operational Core.
|
||
|
||
Зафиксирован продуктовый принцип: enterprise-контур управляется через Launcher. Для таких пользователей и workspace Launcher является source of truth по приглашениям, сервисному доступу и базовым ролям; downstream-приложения получают техническую проекцию и не должны становиться вторым независимым источником прав.
|
||
|
||
Отдельно вынесена новая большая карточка "Публичный контур пользователей". Она закрывает сценарий обычных внешних пользователей, которые приходят не через клиентскую компанию, а через запрос приглашения и дальше используют отдельные сервисы как standalone-продукты.
|
||
""",
|
||
),
|
||
checker(
|
||
"launcher-actual-20260509",
|
||
"Чекер актуализации 2026-05-09",
|
||
[
|
||
{"text": "Зафиксировать Launcher как source of truth для enterprise-доступов.", "checked": True},
|
||
{"text": "Зафиксировать Operational Core role modal как текущую модель детальных назначений.", "checked": True},
|
||
{"text": "Отделить public/self-service контур в самостоятельную карточку.", "checked": True},
|
||
"Заменить JSON-backed store на production persistence.",
|
||
"Описать recovery/MFA/email-change UX без раскрытия Authentik UI.",
|
||
"Вынести billing/limits в отдельный RFC после стабилизации public-контура.",
|
||
],
|
||
),
|
||
],
|
||
},
|
||
{
|
||
"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},
|
||
{"text": "Закрыть путь без mapping или app access.", "checked": True},
|
||
],
|
||
),
|
||
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},
|
||
{"text": "Закрыть лишние OAuth/magic-link обходы, если они нарушают invite/manual модель.", "checked": True},
|
||
{"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},
|
||
{"text": "Провести ручной browser acceptance: снять доступ и увидеть отзыв уже открытой Plane-сессии.", "checked": True},
|
||
],
|
||
),
|
||
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, затем top-level уводит браузер в Authentik end-session с post_logout_redirect_uri=/auth/logged-out и финально возвращает пользователя в /auth/login?prompt=login, а не на Authentik "Logout successful". Для 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.",
|
||
],
|
||
),
|
||
text_block(
|
||
"plane",
|
||
"Актуализация 2026-05-09",
|
||
"""
|
||
Текущая модель Operational Core разделена на два режима. Для workspace, которыми управляет Launcher, участники/инвайты/права должны быть скрыты или переведены в readonly внутри Task Manager, чтобы не получить конфликтующий source of truth. Для standalone/public workspace штатные механики Task Manager остаются включенными: пользователь может создавать workspace, приглашать участников и управлять проектами внутри продукта.
|
||
|
||
Практически проверено: корпоративные назначения из Launcher доходят до Operational Core, public-пользователь может создать workspace после выдачи доступа, а прямой доступ к сервису проходит через NODE.DC SSO. Safari-only падение workspace зафиксировано как отдельный deferred debug, потому что Chrome/Chromium flow работает и проблема не должна блокировать платформенную обвязку.
|
||
|
||
Открытая развилка: нужно формально добавить managedBy=launcher/managedBy=tasker или эквивалентный флаг в mapping workspace, чтобы интерфейс Task Manager понимал, когда скрывать собственное управление пользователями, а когда оставлять автономный SaaS-режим.
|
||
""",
|
||
),
|
||
checker(
|
||
"plane-actual-20260509",
|
||
"Чекер актуализации 2026-05-09",
|
||
[
|
||
{"text": "Сохранить автономность Task Manager как standalone-продукта.", "checked": True},
|
||
{"text": "Зафиксировать managedBy=launcher для enterprise workspace.", "checked": True},
|
||
{"text": "Зафиксировать managedBy=tasker для standalone/public workspace.", "checked": True},
|
||
"Скрыть или readonly-заблокировать Task Manager users/invites для managedBy=launcher.",
|
||
"Оставить Task Manager users/invites включенными для managedBy=tasker.",
|
||
"Очистить оставшиеся demo users/seed data без удаления живых связей.",
|
||
"Оформить Safari-only workspace crash как отдельный deferred debug.",
|
||
],
|
||
),
|
||
],
|
||
},
|
||
{
|
||
"slug": "design-guide-canon",
|
||
"name": "NDC дизайн-гайд: единый UI-код платформы",
|
||
"priority": "medium",
|
||
"state_group": "cancelled",
|
||
"assignees": [CODEX_EMAIL],
|
||
"description_html": html(
|
||
"Карточка сохранена как отложенная архитектурная ссылка: реальный дизайн-канон NODE.DC живет в HDESIGN-CODE.md, а не в теле канбан-карточки.",
|
||
"Цель актуализации: убрать конфликт источников истины и зафиксировать, что дальнейшие UI-работы должны сверяться с документом дизайн-канона и существующими launcher/tasker компонентами.",
|
||
"Статус: deferred. Карточка не является активной задачей разработки до решения выделять дизайн-систему в отдельный пакет.",
|
||
),
|
||
"blocks": [
|
||
text_block(
|
||
"design-guide",
|
||
"Текущая архитектура",
|
||
"""
|
||
Единый визуальный канон NODE.DC зафиксирован в репозитории Operational Core в документе HDESIGN-CODE.md. Он описывает glass/card shell, top-bar, admin overlay, auth screens, spacing, typography, motion, token usage и правила интеграции в Launcher/Task Manager.
|
||
|
||
Карточка NDCPLATFORM-7 больше не должна конкурировать с этим документом и не должна хранить длинный HTML-дизайн-гайд. Канбан-карточка остается навигационной отметкой: дизайн-канон существует, но вынос в отдельную design-system работу пока отложен.
|
||
""",
|
||
),
|
||
text_block(
|
||
"design-guide",
|
||
"Этап 1. Зафиксировать источник дизайн-канона",
|
||
"""
|
||
Статус: выполнено.
|
||
|
||
HDESIGN-CODE.md является source of truth для текущего UI-кода NODE.DC. При разработке Launcher/admin overlay, auth screens и Task Manager NODE.DC-слоев сверяемся с ним, а не с историческим HTML-телом карточки.
|
||
""",
|
||
),
|
||
checker(
|
||
"design-guide1",
|
||
"Чекер этапа 1. Источник дизайн-канона",
|
||
[
|
||
{"text": "Признать HDESIGN-CODE.md реальным дизайн-каноном.", "checked": True},
|
||
{"text": "Оставить NDCPLATFORM-7 в deferred/cancelled, без активной разработки.", "checked": True},
|
||
{"text": "Не использовать старое HTML-тело карточки как актуальный UI-спек.", "checked": True},
|
||
],
|
||
),
|
||
text_block(
|
||
"design-guide",
|
||
"Реализация этапа 1",
|
||
"""
|
||
Карточка приведена к короткой structured layout записи. Исторический смысл сохранен, но актуальный дизайн-канон вынесен в документ репозитория, чтобы UI-решения не расходились между карточкой, кодом и документацией.
|
||
""",
|
||
),
|
||
text_block(
|
||
"design-guide",
|
||
"Этап 2. Future design-system extraction",
|
||
"""
|
||
Статус: backlog/deferred.
|
||
|
||
Если появится необходимость переиспользовать NODE.DC UI между несколькими приложениями как библиотеку, эту работу нужно открывать отдельным этапом внутри той же крупной карточки: tokens, primitives, surfaces, modals, cards, auth shell и cross-app visual QA.
|
||
""",
|
||
),
|
||
checker(
|
||
"design-guide2",
|
||
"Чекер этапа 2. Future design-system extraction",
|
||
[
|
||
"Принять решение, нужен ли отдельный design-system package.",
|
||
"Выделить переиспользуемые primitives без ломки Launcher/Tasker UI.",
|
||
"Описать visual QA для Auth/Launcher/Tasker экранов.",
|
||
],
|
||
),
|
||
],
|
||
},
|
||
{
|
||
"slug": "tasker-provisioning-workspace-onboarding",
|
||
"name": "Tasker provisioning и workspace onboarding",
|
||
"priority": "high",
|
||
"state_group": "started",
|
||
"assignees": [CODEX_EMAIL],
|
||
"description_html": html(
|
||
"Архитектурный узел между Launcher control plane и Operational Core: пользователь должен попадать в Tasker через NODE.DC SSO без redirect-loop, 500-страниц и ручных Plane-инвайтов.",
|
||
"Цель: разделить доступ к приложению, workspace onboarding и доменные роли Tasker. Launcher выдает сервисный доступ и enterprise-назначения, Tasker сохраняет собственные workspace/project/task модели и standalone-режим.",
|
||
"Критерий приемки: пользователь с доступом к Operational Core получает предсказуемый исход: назначенный workspace/project, разрешенный self-service workspace или ожидание назначения; для managedBy=launcher нет второго источника прав внутри Tasker.",
|
||
),
|
||
"blocks": [
|
||
text_block(
|
||
"tasker-provisioning",
|
||
"Текущая архитектура",
|
||
"""
|
||
Launcher уже является рабочим control plane для клиентов, пользователей, групп, инвайтов, service grants, Operational Core workspace binding и project-level назначений. Authentik получает техническую group projection из Launcher и остается внутренним IdP.
|
||
|
||
Operational Core подключен как Plane fork: OIDC/handoff, ExternalIdentityLink, live access middleware, workspace policy hook и internal adapter endpoints живут в Tasker, а Launcher вызывает их через защищенный server-to-server API. Прямых записей Launcher в Plane DB нет.
|
||
|
||
Фактически реализованы: auto-create/link локального Tasker user по verified OIDC/handoff claims, workspace membership bridge, project membership bridge, policy check на создание workspace, NODE.DC create-workspace UX и очистка stale assignees после снятия пользователей из workspace/project.
|
||
|
||
Открытая архитектурная граница: нужно формально закрепить managedBy=launcher/managedBy=tasker для workspace, чтобы enterprise-workspace управлялись из Launcher, а standalone/public workspace сохраняли штатные Tasker users/invites/admin mechanics.
|
||
""",
|
||
),
|
||
text_block(
|
||
"tasker-provisioning",
|
||
"Этап 1. OIDC fail-safe и local user/link provisioning",
|
||
"""
|
||
Статус: реализовано.
|
||
|
||
Этап закрывает патологию первого входа: Authentik подтверждает пользователя, но Tasker не знает локального User/ExternalIdentityLink. Сейчас Tasker умеет idempotently создать или связать локального пользователя по verified OIDC/handoff claims и не уводит пользователя в бесконечный login loop.
|
||
""",
|
||
),
|
||
checker(
|
||
"tasker-provisioning1",
|
||
"Чекер этапа 1. OIDC fail-safe и local user/link provisioning",
|
||
[
|
||
{"text": "Развести ошибки: нет platform access, нет local user/link, нет workspace membership.", "checked": True},
|
||
{"text": "Добавить auto-link existing Plane user по email отдельным env-флагом.", "checked": True},
|
||
{"text": "Добавить auto-create Tasker user по verified OIDC claims отдельным env-флагом.", "checked": True},
|
||
{"text": "Создавать/обновлять ExternalIdentityLink provider=authentik + subject.", "checked": True},
|
||
{"text": "Синхронизировать display name/email/avatar без пересоздания старого Plane user.", "checked": True},
|
||
{"text": "Сохранить standalone Plane auth/API механизмы вне NODE.DC env-профиля.", "checked": True},
|
||
],
|
||
),
|
||
text_block(
|
||
"tasker-provisioning",
|
||
"Реализация этапа 1",
|
||
"""
|
||
Tasker OIDC слой живет в plane-src/apps/api/plane/authentication/views/app/oidc.py. Он проверяет state/nonce/JWKS/audience/issuer, принимает groups, резолвит или создает локального пользователя и обновляет ExternalIdentityLink.
|
||
|
||
Launcher service handoff ведет в /auth/nodedc/handoff/ и потребляется через защищенный /api/internal/handoff/consume. Handoff передает normalized user, avatarUrl и groups без раскрытия пароля или Authentik service token.
|
||
|
||
Профильная синхронизация идет из claims и Launcher avatar URL. Существующий Plane owner dcctouch@gmail.com не пересоздается: связь держится через ExternalIdentityLink и старый Plane user сохраняет workspace/project/task связи.
|
||
""",
|
||
),
|
||
text_block(
|
||
"tasker-provisioning",
|
||
"Этап 2. Launcher -> Tasker workspace adapter",
|
||
"""
|
||
Статус: реализовано.
|
||
|
||
Этап добавляет управляемый bridge для enterprise workspace: Launcher может назначить или снять пользователя в Operational Core workspace через internal Tasker API, а Tasker idempotently меняет WorkspaceMember без Plane email-invite.
|
||
""",
|
||
),
|
||
checker(
|
||
"tasker-provisioning2",
|
||
"Чекер этапа 2. Launcher -> Tasker workspace adapter",
|
||
[
|
||
{"text": "Добавить Tasker internal workspace catalog endpoint.", "checked": True},
|
||
{"text": "Добавить internal endpoint ensure workspace membership.", "checked": True},
|
||
{"text": "Добавить internal endpoint remove workspace membership.", "checked": True},
|
||
{"text": "Защитить endpoints внутренним token/secret.", "checked": True},
|
||
{"text": "Добавить Launcher admin routes для workspace membership.", "checked": True},
|
||
{"text": "Сохранять workspace membership projection в Launcher storage.", "checked": True},
|
||
{"text": "Не использовать Plane email-invite как основной enterprise flow.", "checked": True},
|
||
],
|
||
),
|
||
text_block(
|
||
"tasker-provisioning",
|
||
"Реализация этапа 2",
|
||
"""
|
||
Tasker adapter endpoints лежат в plane-src/apps/api/plane/authentication/views/nodedc_workspace_adapter.py и подключены в plane-src/apps/api/plane/urls.py. Они резолвят workspace/user по slug/id/email/subject, создают или деактивируют WorkspaceMember и возвращают normalized membership summary.
|
||
|
||
Launcher BFF вызывает adapter из server/dev-server.mjs, проверяет права администратора на client/user, пишет projection через server/control-plane-store.mjs и обновляет UI через admin overlay.
|
||
""",
|
||
),
|
||
text_block(
|
||
"tasker-provisioning",
|
||
"Этап 3. Workspace onboarding policy",
|
||
"""
|
||
Статус: частично реализовано.
|
||
|
||
Сервисный доступ к Operational Core и право создавать workspace разделены. Launcher уже возвращает workspacePolicy через internal access check, а Tasker спрашивает policy перед create-workspace flow. Полная трехрежимная модель client/user/group policy еще не оформлена как production data model.
|
||
""",
|
||
),
|
||
checker(
|
||
"tasker-provisioning3",
|
||
"Чекер этапа 3. Workspace onboarding policy",
|
||
[
|
||
{"text": "Добавить Launcher setting taskManager.workspaceCreationPolicy.", "checked": True},
|
||
{"text": "Возвращать workspacePolicy из /api/internal/access/check.", "checked": True},
|
||
{"text": "Добавить Tasker backend workspace policy resolver.", "checked": True},
|
||
{"text": "Сохранить self-service create-workspace flow в NODE.DC дизайне.", "checked": True},
|
||
"Оформить admin_managed ожидание назначения без create workspace.",
|
||
"Добавить production policy model на уровне client/service/user/group.",
|
||
"Связать policy с managedBy=launcher/managedBy=tasker.",
|
||
],
|
||
),
|
||
text_block(
|
||
"tasker-provisioning",
|
||
"Реализация этапа 3",
|
||
"""
|
||
Launcher хранит базовую настройку workspace creation policy в settings.taskManager. Tasker использует plane.authentication.nodedc_workspace_policy, чтобы получить решение из Launcher access-check endpoint и разрешить или запретить создание workspace.
|
||
|
||
Create workspace UI в Tasker приведен к NODE.DC auth-card layout без удаления штатной Plane формы: вариант nodedc-auth сохраняет standalone совместимость и не ломает остальные вызовы.
|
||
""",
|
||
),
|
||
text_block(
|
||
"tasker-provisioning",
|
||
"Этап 4. Project-level доступы Operational Core из Launcher",
|
||
"""
|
||
Статус: реализовано локально, ожидает ручной acceptance.
|
||
|
||
Этап закрывает следующий слой после workspace binding: Launcher может назначать роль пользователя внутри конкретного проекта Tasker. Это остается adapter-вызовом через internal API, а не прямой записью в Plane DB.
|
||
""",
|
||
),
|
||
checker(
|
||
"tasker-provisioning4",
|
||
"Чекер этапа 4. Project-level access bridge",
|
||
[
|
||
{"text": "Расширить Tasker workspace catalog проектами внутри workspace.", "checked": True},
|
||
{"text": "Добавить internal endpoint ensure project membership.", "checked": True},
|
||
{"text": "Добавить internal endpoint remove project membership.", "checked": True},
|
||
{"text": "Сохранять project membership projection в Launcher storage.", "checked": True},
|
||
{"text": "Показать workspaces и projects в Operational Core modal.", "checked": True},
|
||
{"text": "При project role гарантировать минимальный workspace membership.", "checked": True},
|
||
"Провести ручной acceptance: назначение роли проекта, refresh, вход пользователем в Tasker.",
|
||
],
|
||
),
|
||
text_block(
|
||
"tasker-provisioning",
|
||
"Реализация этапа 4",
|
||
"""
|
||
Tasker: добавлены endpoints /api/internal/nodedc/project-memberships/ensure/ и /api/internal/nodedc/project-memberships/remove/. Они защищены тем же internal token, резолвят workspace/project/user и idempotently создают или обновляют ProjectMember.
|
||
|
||
Launcher: добавлены admin routes для project memberships, control-plane projection taskManagerProjectMemberships и UI в Operational Core modal. Роль проекта меняется кликом: прочерк, гость, участник, админ.
|
||
|
||
Проверки этапа ранее проходили: npm run build в Launcher, node --check server/dev-server.mjs server/control-plane-store.mjs и python compile для Tasker adapter/routes.
|
||
""",
|
||
),
|
||
text_block(
|
||
"tasker-provisioning",
|
||
"Этап 5. Stale assignees cleanup после снятия пользователей",
|
||
"""
|
||
Статус: реализовано в рабочем дереве, ожидает финальную проверку и коммит после подтверждения.
|
||
|
||
После удаления, блокировки или снятия пользователя из workspace/project Tasker не должен продолжать показывать его исполнителем в карточках и группировках. Последняя локальная правка удаляет IssueAssignee на membership remove и фильтрует assignee arrays только по активным workspace/project membership.
|
||
""",
|
||
),
|
||
checker(
|
||
"tasker-provisioning5",
|
||
"Чекер этапа 5. Stale assignees cleanup",
|
||
[
|
||
{"text": "Удалять IssueAssignee при снятии workspace membership.", "checked": True},
|
||
{"text": "Удалять IssueAssignee при снятии project membership.", "checked": True},
|
||
{"text": "Покрыть admin/license/member remove paths.", "checked": True},
|
||
{"text": "Фильтровать backend assignee_ids по active workspace/project membership.", "checked": True},
|
||
{"text": "Добавить frontend guard в internal Kanban card.", "checked": True},
|
||
{"text": "Проверить текущую БД на реально stale IssueAssignee.", "checked": True},
|
||
"Прогнать целевой regression по issue list/kanban после restart backend/web.",
|
||
],
|
||
),
|
||
text_block(
|
||
"tasker-provisioning",
|
||
"Реализация этапа 5",
|
||
"""
|
||
Затронуты Tasker файлы: plane-src/apps/api/plane/authentication/views/nodedc_workspace_adapter.py, workspace/project/member/license views, issue list/detail endpoints, common grouper utilities и internal-contour-card.tsx.
|
||
|
||
Проверка БД 2026-05-09: запрос по активным IssueAssignee без active WorkspaceMember/ProjectMember вернул 0 записей. Значит текущий runtime не содержит реально stale assignee links после последней очистки.
|
||
|
||
Рабочее дерево Task Manager остается dirty: этот этап еще не закоммичен и требует финальной проверки перед переводом карточного пункта в полностью закрытое состояние.
|
||
""",
|
||
),
|
||
text_block(
|
||
"tasker-provisioning",
|
||
"Этап 6. Source-of-truth split managedBy",
|
||
"""
|
||
Статус: следующий критический этап.
|
||
|
||
Нужно формально закрепить источник управления для workspace. managedBy=launcher означает enterprise workspace: пользователи, инвайты и базовые роли идут из Launcher, а Tasker не должен давать конфликтующее управление. managedBy=tasker означает standalone/public workspace: штатные Tasker механизмы пользователей, инвайтов и ролей остаются включенными.
|
||
""",
|
||
),
|
||
checker(
|
||
"tasker-provisioning6",
|
||
"Чекер этапа 6. Source-of-truth split managedBy",
|
||
[
|
||
"Добавить managedBy в Launcher Tasker workspace binding.",
|
||
"Возвращать managedBy/workspacePolicy из Launcher internal access-check.",
|
||
"Передавать managedBy в Tasker adapter responses или workspace policy resolver.",
|
||
"Скрыть или readonly-заблокировать Tasker users/invites для managedBy=launcher.",
|
||
"Оставить Tasker users/invites включенными для managedBy=tasker.",
|
||
"Проверить enterprise client admin и public self-service user flows отдельно.",
|
||
"Зафиксировать правила в NDCPLATFORM-4 и NDCPLATFORM-10 после реализации.",
|
||
],
|
||
),
|
||
],
|
||
},
|
||
{
|
||
"slug": "safari-workspace-crash-debug",
|
||
"name": "Safari: workspace crash и storage/OIDC debug",
|
||
"priority": "high",
|
||
"state_group": "cancelled",
|
||
"assignees": [CODEX_EMAIL],
|
||
"description_html": html(
|
||
"Отложенный debug Safari-only падения workspace в Task Manager. Проблема не блокирует текущую платформенную архитектуру, потому что Chrome/Chromium flow работает.",
|
||
"Цель актуализации: сохранить симптом и границы диагностики, но не смешивать browser-specific bug с Launcher/Auth/Tasker source-of-truth работами.",
|
||
"Статус: deferred. Возвращаться после закрытия критичного managedBy/source-of-truth этапа или при воспроизводимом Safari regression report.",
|
||
),
|
||
"blocks": [
|
||
text_block(
|
||
"safari-crash",
|
||
"Текущая архитектура",
|
||
"""
|
||
Safari-only падение workspace относится к browser/runtime compatibility слою Task Manager. Оно не меняет целевую архитектуру: Authentik остается внутренним IdP, Launcher — source of truth для enterprise-доступов, Task Manager — standalone-capable Operational Core module.
|
||
|
||
Пока Chrome/Chromium flow работает, этот debug не должен блокировать платформенную работу по access matrix, workspace onboarding и managedBy split. Карточка остается отложенной, чтобы не смешивать runtime browser bug с архитектурными задачами.
|
||
""",
|
||
),
|
||
text_block(
|
||
"safari-crash",
|
||
"Этап 1. Сохранить симптом и границы debug",
|
||
"""
|
||
Статус: зафиксировано как deferred.
|
||
|
||
Нужно вернуться к карточке только при наличии воспроизводимого сценария: Safari version, URL, user, workspace slug, console trace, network trace, storage/cookie state и отличие от Chrome на том же аккаунте.
|
||
""",
|
||
),
|
||
checker(
|
||
"safari1",
|
||
"Чекер этапа 1. Deferred symptom",
|
||
[
|
||
{"text": "Не считать Safari-only падение блокером Launcher/Auth/Tasker архитектуры.", "checked": True},
|
||
{"text": "Сохранить карточку как отдельный debug bucket.", "checked": True},
|
||
"Снять Safari console trace.",
|
||
"Снять Safari network trace.",
|
||
"Сравнить localStorage/sessionStorage/cookies Safari vs Chrome.",
|
||
],
|
||
),
|
||
text_block(
|
||
"safari-crash",
|
||
"Этап 2. Future Safari diagnostics",
|
||
"""
|
||
Статус: deferred.
|
||
|
||
Будущий debug должен идти от воспроизводимого crash-path: auth/session sync, workspace bootstrap API, storage hydration, frontend route boundary, browser-specific cookie policy или WebKit-only JS/runtime issue.
|
||
""",
|
||
),
|
||
checker(
|
||
"safari2",
|
||
"Чекер этапа 2. Safari diagnostics",
|
||
[
|
||
"Воспроизвести падение на актуальном Safari.",
|
||
"Отделить auth/session issue от frontend runtime crash.",
|
||
"Проверить WebKit cookie/storage policy на task.local.nodedc.",
|
||
"Сформулировать минимальный фикс без изменения общей архитектуры.",
|
||
],
|
||
),
|
||
],
|
||
},
|
||
{
|
||
"slug": "public-user-entry-and-service-access",
|
||
"name": "Публичный контур пользователей",
|
||
"priority": "high",
|
||
"state_group": "backlog",
|
||
"assignees": [PLATFORM_OWNER_EMAIL, CODEX_EMAIL],
|
||
"description_html": html(
|
||
"Отдельный public/open-access контур для внешних пользователей, которые приходят не как участники клиентской компании, а как самостоятельные пользователи сервисов NODE.DC.",
|
||
"Цель: не плодить новый админский интерфейс, а расширить существующий Launcher control plane. Enterprise-компании остаются company-scoped, public users попадают в отдельный Public Access Pool и получают сервисные доступы вручную через root-admin flow.",
|
||
"Критерий приемки: пользователь может нажать Запросить доступ в Auth/Login, заявка появляется в Launcher в разделе Инвайты как вкладка Заявки, root admin вручную апрувит/отклоняет, генерирует ссылку, а после входа пользователь видит только витрину и свои разрешенные сервисы.",
|
||
),
|
||
"blocks": [
|
||
text_block(
|
||
"public-users",
|
||
"Текущая архитектура",
|
||
"""
|
||
Уже есть единый Authentik login, branded под NODE.DC, и Launcher как точка входа. Launcher умеет показывать витрину приложений, хранить клиентов, пользователей, инвайты, группы, матрицу доступов и привязку Operational Core workspace к клиенту.
|
||
|
||
Enterprise-сценарий частично реализован: root admin создает компанию, добавляет туда client admin, выдает доступы, а client admin дальше управляет пользователями, группами, инвайтами и назначениями в рамках своей компании через Launcher. В Operational Core такие пользователи не должны видеть управление users/invites/workspace creation для launcher-managed workspace; они работают только в среде, которую им задал админ.
|
||
|
||
Public/open-access контур должен быть отдельной группой пользователей, не привязанной к клиентской компании. Рабочее название: Public Access Pool / Свободный доступ. Это не новый enterprise-client и не хаотичный список отдельных компаний, а отдельный cohort в Launcher, видимый root admin. Public user не получает Launcher admin overlay, пока ему явно не выдали роль; он видит витрину и сервисы.
|
||
|
||
Глобальная identity должна быть одна на email/Auth subject. Если пользователь сначала попал в Public Access Pool, а потом его надо перевести в enterprise-компанию, правильная операция — не удаление и повторная регистрация, а административный перевод: создать company membership, выбрать роль/группы, при необходимости убрать public-cohort marker и пересчитать доступы.
|
||
|
||
Также появился standalone-сценарий Operational Core: public-пользователь после выдачи доступа к сервису может создать собственный workspace и управлять им через штатный Task Manager, но только для workspace managedBy=tasker. Создание NODE.DC identity и первичная выдача сервисного доступа остаются в Launcher.
|
||
|
||
Еще не реализованы: кнопка Запросить доступ в login flow, модель access requests с обязательными полями, вкладка Заявки рядом с текущими Инвайтами, Public Access Pool в Launcher admin, перевод public user в компанию, правила приглашения других людей в public Tasker workspace без почтовой инфраструктуры.
|
||
""",
|
||
),
|
||
text_block(
|
||
"public-users",
|
||
"Этап 1. Разделение enterprise и Public Access Pool",
|
||
"""
|
||
Статус: backlog.
|
||
|
||
Нужно формально разделить два режима: enterprise/direct-contract company scope и public/open-access pool. Enterprise-контур остается текущей реализацией: root admin создает компанию, назначает client admin, client admin управляет только своей компанией через Launcher.
|
||
|
||
Public Access Pool — отдельный cohort для внешних самостоятельных пользователей. Он должен быть доступен root admin в существующем Launcher admin UI как отдельный режим/область рядом с компаниями, но не должен засорять список компаний сотнями псевдо-клиентов.
|
||
|
||
Главное правило безопасности: у каждого Operational Core workspace должен быть ровно один источник управления правами. managedBy=launcher означает управление из Launcher и readonly/hidden member management в Tasker; managedBy=tasker означает штатное workspace management внутри Operational Core, но identity/service entitlement все равно выдаются через Launcher.
|
||
""",
|
||
),
|
||
checker(
|
||
"public-users1",
|
||
"Чекер этапа 1. Разделение enterprise и Public Access Pool",
|
||
[
|
||
"Зафиксировать типы контуров: enterprise/direct-contract и public/open-access.",
|
||
"Добавить модель Public Access Pool или эквивалентный user cohort без создания отдельной компании на каждого пользователя.",
|
||
"Сделать global user уникальным по email/Auth subject независимо от public или enterprise происхождения.",
|
||
"Показать Public Access Pool root admin в существующем Launcher admin selector/режиме.",
|
||
"Скрыть Public Access Pool от client admin компаний.",
|
||
"Описать managedBy=launcher для корпоративных workspace.",
|
||
"Описать managedBy=tasker для публичных standalone workspace.",
|
||
"Развести видимость админки Launcher для root admin, client admin и public user.",
|
||
"Описать, как public-пользователь видит витрину сервисов без администрирования Launcher.",
|
||
"Зафиксировать запрет прямого создания Auth/Launcher users из Tasker без Launcher approval.",
|
||
],
|
||
),
|
||
text_block(
|
||
"public-users",
|
||
"Этап 2. Запрос приглашения из окна входа",
|
||
"""
|
||
Статус: backlog.
|
||
|
||
В branded login нужно добавить безопасный сценарий запроса доступа. Это не регистрация с паролем и не обход Authentik: пользователь оставляет заявку, а учетная запись и invite создаются только после ручного решения root admin.
|
||
|
||
Заявка должна попадать в Launcher admin, а не теряться в почте. Почтовое уведомление можно добавить позже как транспорт, но source of truth для обработки заявок должен быть внутри Launcher.
|
||
|
||
UI-решение: не добавлять новое большое окно. Расширить существующий раздел Инвайты вкладками Заявки и Сгенерированные инвайты. Для root admin вкладка Заявки показывает public/open-access заявки. Для client admin текущие company-scoped инвайты остаются без доступа к global public queue.
|
||
""",
|
||
),
|
||
checker(
|
||
"public-users2",
|
||
"Чекер этапа 2. Запрос приглашения из окна входа",
|
||
[
|
||
"Добавить вторичную кнопку Запросить доступ под кнопкой Войти.",
|
||
"Добавить форму заявки с обязательными полями: email, имя, фамилия, отчество, телефон, компания.",
|
||
"Оставить интересующий сервис/задачу и комментарий опциональными полями.",
|
||
"Блокировать отправку, если любое обязательное поле пустое.",
|
||
"Дублировать required validation server-side в Launcher public endpoint.",
|
||
"Не запрашивать пароль на этапе заявки.",
|
||
"Сохранять заявку server-side в Launcher storage/backend как accessRequest.",
|
||
"Расширить текущий раздел Инвайты вкладками Заявки и Сгенерированные инвайты.",
|
||
"Добавить статусы заявки: новая, в работе, принята, отклонена, архив.",
|
||
"Добавить действие Принять: создать invite в Public Access Pool для указанного email.",
|
||
"Добавить действие Отклонить без создания пользователя/Auth identity.",
|
||
"Пока нет почты, показывать сгенерированную ссылку для ручной передачи админом.",
|
||
],
|
||
),
|
||
text_block(
|
||
"public-users",
|
||
"Этап 3. Public user access flow",
|
||
"""
|
||
Статус: backlog.
|
||
|
||
После апрува пользователь регистрируется по invite link, попадает в Launcher и видит витрину сервисов. Администрирования Launcher у него нет. Он находится в Public Access Pool, но не получает доступ к Operational Core или другим сервисам автоматически.
|
||
|
||
Доступы к сервисам на первом этапе выдает root admin вручную через матрицу доступов. Когда public user получает Operational Core, он может создать собственный workspace в Tasker. Такой workspace должен быть managedBy=tasker: Tasker управляет проектами и workspace membership, Launcher управляет только identity, service entitlement и глобальной блокировкой пользователя.
|
||
""",
|
||
),
|
||
checker(
|
||
"public-users3",
|
||
"Чекер этапа 3. Public user access flow",
|
||
[
|
||
"Проверить, что public user не видит администрирование Launcher.",
|
||
"Проверить, что public user после invite попадает в Public Access Pool.",
|
||
"Не выдавать Operational Core автоматически только по факту принятого invite.",
|
||
"Показывать все сервисы витрины с состояниями доступен/нет доступа.",
|
||
"Раздавать сервисный доступ public user через матрицу доступов Launcher.",
|
||
"Открывать Operational Core без повторной авторизации после доступа.",
|
||
"Разрешить создание workspace внутри Tasker для managedBy=tasker.",
|
||
"Оставить настройки workspace/project внутри Tasker включенными для managedBy=tasker.",
|
||
"Скрывать настройки участников/инвайтов Tasker только для managedBy=launcher.",
|
||
],
|
||
),
|
||
text_block(
|
||
"public-users",
|
||
"Этап 4. Перевод Public user в enterprise-компанию",
|
||
"""
|
||
Статус: backlog.
|
||
|
||
Нужна штатная операция root admin: перевести пользователя из Public Access Pool в конкретную компанию. Это безопаснее, чем удалять пользователя и заставлять его регистрироваться заново по enterprise-инвайту, потому что Authentik identity, session history, audit trail, accepted invites и будущие billing/usage links остаются консистентными.
|
||
|
||
Целевая модель: Launcher хранит global user отдельно от company memberships. Public Access Pool — cohort/source/status, а не единственный контейнер identity. При переводе root admin выбирает компанию, роль, группы и политику доступа. Система создает или обновляет ClientMembership, может архивировать public pool membership/request, пересчитывает Authentik groups и service grants. Если enterprise admin позже выписывает invite на email уже существующего public user, accept flow должен merge by email в тот же global user, а не создавать дубль.
|
||
|
||
Нужна также мягкая операция Убрать из Public Pool. Она не должна по умолчанию удалять global user/Auth identity: она архивирует public cohort и отзывает public-only grants. Полное удаление пользователя — отдельное destructive действие с явным cleanup-планом.
|
||
""",
|
||
),
|
||
checker(
|
||
"public-users4",
|
||
"Чекер этапа 4. Перевод Public user в компанию",
|
||
[
|
||
"Добавить root-admin действие Перевести в компанию для пользователя из Public Access Pool.",
|
||
"Выбирать target company, company role и группы при переводе.",
|
||
"Создавать или обновлять ClientMembership без создания второго global user.",
|
||
"Принимать enterprise invite на email существующего public user через merge by email.",
|
||
"Архивировать или отключать public cohort marker после успешного перевода по выбору admin.",
|
||
"Пересчитывать Authentik groups/service grants после перевода.",
|
||
"Логировать перевод в audit с source pool, target company, actor и ролью.",
|
||
"Добавить действие Убрать из Public Pool без удаления global Auth identity по умолчанию.",
|
||
"Оставить полное удаление user отдельным destructive flow с подтверждением и cleanup.",
|
||
],
|
||
),
|
||
text_block(
|
||
"public-users",
|
||
"Этап 5. Public workspace collaboration и invite boundary",
|
||
"""
|
||
Статус: backlog.
|
||
|
||
Самый опасный участок — как public-пользователи приглашают других людей в свой Operational Core workspace без готовой почтовой инфраструктуры. Нельзя позволять Tasker напрямую создавать Authentik/Launcher users: это ломает единый identity perimeter.
|
||
|
||
Правило для NODE.DC public mode: если приглашенный email уже принадлежит активному NODE.DC user с доступом к Operational Core, Tasker может добавить его в managedBy=tasker workspace по точному email/ID без глобального поиска людей. Если пользователя нет или у него нет entitlement на Operational Core, Tasker должен создать request в Launcher с контекстом workspace/requestedBy/targetEmail/role. Root admin вручную апрувит, генерирует invite link и после принятия доступ доводится до нужного workspace через Tasker adapter.
|
||
|
||
Для managedBy=launcher workspace любые Tasker-side invites/users остаются скрытыми или readonly. Приглашения в enterprise-контуре идут через Launcher company admin/root admin, как в текущей реализации.
|
||
""",
|
||
),
|
||
checker(
|
||
"public-users5",
|
||
"Чекер этапа 5. Public workspace collaboration",
|
||
[
|
||
"Запретить прямое создание Authentik/Launcher user из Tasker invite flow.",
|
||
"Разрешить добавление существующего NODE.DC user в public managedBy=tasker workspace по точному email/ID.",
|
||
"Проверять, что target user имеет Operational Core entitlement перед прямым добавлением.",
|
||
"Если target user отсутствует или не имеет entitlement, создавать Launcher accessRequest с Tasker workspace context.",
|
||
"Показывать такие заявки root admin во вкладке Заявки с источником Operational Core.",
|
||
"После approval создавать invite link и связывать accepted user с исходным Tasker workspace.",
|
||
"Оставить enterprise managedBy=launcher workspace без Tasker-side invites.",
|
||
"Отложить автоматическую email-доставку до отдельного mailer/email этапа.",
|
||
],
|
||
),
|
||
text_block(
|
||
"public-users",
|
||
"Этап 6. Billing-ready модель",
|
||
"""
|
||
Статус: backlog.
|
||
|
||
Биллинг пока не реализуется, но модель публичного контура должна не закрыть путь к оплатам. Для enterprise-клиентов остаются прямые договоры и ручные даты договора/оплаты. Для public/self-service пользователей позже появятся подписки, тарифы, лимиты и модульные entitlements.
|
||
""",
|
||
),
|
||
checker(
|
||
"public-users6",
|
||
"Чекер этапа 6. Billing-ready модель",
|
||
[
|
||
"Описать entitlement по каждому сервису отдельно.",
|
||
"Описать статус подписки public user: trial, active, expired, blocked.",
|
||
"Заложить лимиты Operational Core: workspace count, members count, storage.",
|
||
"Заложить лимиты Voice Tasker: minutes, requests, model tier.",
|
||
"Не внедрять платежный провайдер до отдельного billing RFC.",
|
||
"Не смешивать прямые договоры enterprise-клиентов и public subscriptions.",
|
||
],
|
||
),
|
||
],
|
||
},
|
||
{
|
||
"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. Базовый local acceptance по happy path и части negative path уже пройден: Launcher без session уводит в OIDC login, недоступные плитки отображаются как disabled/Нет доступа, снятие доступа отзывает открытую Plane-сессию, старый Plane admin проходит через OIDC migration, frontend bundle не содержит service-token маркеров. Остаются destructive/edge checks: direct deny без group access, deactivated user, audit log и staging-hardening.
|
||
""",
|
||
),
|
||
text_block(
|
||
"security",
|
||
"Этап 1. Security acceptance tests",
|
||
"""
|
||
Статус: backlog.
|
||
|
||
Нужно проверить не только happy path, но и отказы: нет логина, нет группы, прямой URL, deactivated user, отсутствие service token во frontend.
|
||
""",
|
||
),
|
||
checker(
|
||
"security1",
|
||
"Чекер этапа 1. Security acceptance tests",
|
||
[
|
||
{"text": "Проверить redirect Launcher без логина.", "checked": True},
|
||
{"text": "Проверить скрытие Task Manager без group access.", "checked": True},
|
||
"Проверить deny на прямой task.local.nodedc без group access.",
|
||
{"text": "Проверить отзыв уже открытой downstream-сессии после снятия доступа.", "checked": True},
|
||
{"text": "Проверить успешный вход пользователя с access.", "checked": True},
|
||
{"text": "Проверить старого Plane admin после OIDC migration.", "checked": True},
|
||
"Проверить deactivate user.",
|
||
"Проверить audit log admin actions.",
|
||
{"text": "Проверить отсутствие service tokens во frontend bundle.", "checked": True},
|
||
],
|
||
),
|
||
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.created_by = issue.created_by or codex_user
|
||
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()
|