ФУНКЦИИ - МЕЖПРОЕКТНАЯ КОММУНИКАЦИЯ: Plane OIDC и план платформы

This commit is contained in:
DCCONSTRUCTIONS 2026-05-04 17:16:47 +03:00
parent 561d1eeef5
commit 55318f14e5
4 changed files with 302 additions and 23 deletions

View File

@ -61,6 +61,7 @@ x-app-env: &app-env
PLANE_OIDC_SCOPE: ${PLANE_OIDC_SCOPE:-openid email profile groups} PLANE_OIDC_SCOPE: ${PLANE_OIDC_SCOPE:-openid email profile groups}
PLANE_OIDC_REQUIRED_GROUPS: ${PLANE_OIDC_REQUIRED_GROUPS:-nodedc:superadmin,nodedc:taskmanager:admin,nodedc:taskmanager:user} PLANE_OIDC_REQUIRED_GROUPS: ${PLANE_OIDC_REQUIRED_GROUPS:-nodedc:superadmin,nodedc:taskmanager:admin,nodedc:taskmanager:user}
PLANE_OIDC_AUTO_LINK_EMAIL: ${PLANE_OIDC_AUTO_LINK_EMAIL:-0} PLANE_OIDC_AUTO_LINK_EMAIL: ${PLANE_OIDC_AUTO_LINK_EMAIL:-0}
PLANE_OIDC_SYNC_PROFILE: ${PLANE_OIDC_SYNC_PROFILE:-1}
GUNICORN_WORKERS: 1 GUNICORN_WORKERS: 1
POSTHOG_API_KEY: ${POSTHOG_API_KEY:-} POSTHOG_API_KEY: ${POSTHOG_API_KEY:-}
POSTHOG_HOST: ${POSTHOG_HOST:-} POSTHOG_HOST: ${POSTHOG_HOST:-}

View File

@ -12,9 +12,9 @@ LIVE_REPLICAS=1
LISTEN_HTTP_PORT=8090 LISTEN_HTTP_PORT=8090
LISTEN_HTTPS_PORT=8443 LISTEN_HTTPS_PORT=8443
WEB_URL=http://localhost:8090 WEB_URL=http://task.local.nodedc
DEBUG=0 DEBUG=0
CORS_ALLOWED_ORIGINS=http://localhost:8090 CORS_ALLOWED_ORIGINS=http://task.local.nodedc,http://localhost:8090
API_BASE_URL=http://api:8000 API_BASE_URL=http://api:8000
#DB SETTINGS #DB SETTINGS
@ -89,3 +89,15 @@ LIVE_SERVER_SECRET_KEY=
DOCKERHUB_USER=makeplane DOCKERHUB_USER=makeplane
PULL_POLICY=if_not_present PULL_POLICY=if_not_present
CUSTOM_BUILD=false CUSTOM_BUILD=false
# NODE.DC platform OIDC local dev
ENABLE_SIGNUP=0
ENABLE_EMAIL_PASSWORD=1
ENABLE_MAGIC_LINK_LOGIN=0
PLANE_OIDC_ISSUER=http://auth.local.nodedc/application/o/task-manager/
PLANE_OIDC_CLIENT_ID=nodedc-task-manager
PLANE_OIDC_CLIENT_SECRET=c510f7e389c95a610f34f7569c9ee7fbb744d214bc21e82734578d971e02e0aaa9812aeb83b33efdb76eb90c0a819b0a
PLANE_OIDC_REDIRECT_URI=http://task.local.nodedc/auth/oidc/callback
PLANE_OIDC_SCOPE=openid email profile groups
PLANE_OIDC_REQUIRED_GROUPS=nodedc:superadmin,nodedc:taskmanager:admin,nodedc:taskmanager:user
PLANE_OIDC_AUTO_LINK_EMAIL=1

View File

@ -81,7 +81,12 @@ class NodeDCOIDCCallbackEndpoint(View):
if not has_required_group(groups): if not has_required_group(groups):
return oidc_error_redirect(base_url, next_path, "oidc_access_denied") return oidc_error_redirect(base_url, next_path, "oidc_access_denied")
user = resolve_linked_user(claims=claims, groups=groups, auto_link=config["auto_link_email"]) user = resolve_linked_user(
claims=claims,
groups=groups,
auto_link=config["auto_link_email"],
sync_profile=config["sync_profile"],
)
if user is None or not user.is_active: if user is None or not user.is_active:
return oidc_error_redirect(base_url, next_path, "oidc_user_not_linked") return oidc_error_redirect(base_url, next_path, "oidc_user_not_linked")
@ -109,6 +114,7 @@ def get_oidc_config():
"redirect_uri": redirect_uri, "redirect_uri": redirect_uri,
"scope": os.environ.get("PLANE_OIDC_SCOPE", "openid email profile groups"), "scope": os.environ.get("PLANE_OIDC_SCOPE", "openid email profile groups"),
"auto_link_email": os.environ.get("PLANE_OIDC_AUTO_LINK_EMAIL", "0") == "1", "auto_link_email": os.environ.get("PLANE_OIDC_AUTO_LINK_EMAIL", "0") == "1",
"sync_profile": os.environ.get("PLANE_OIDC_SYNC_PROFILE", "1") == "1",
} }
@ -173,7 +179,7 @@ def has_required_group(groups):
return bool(required_groups.intersection(set(groups))) return bool(required_groups.intersection(set(groups)))
def resolve_linked_user(claims, groups, auto_link): def resolve_linked_user(claims, groups, auto_link, sync_profile):
subject = str(claims.get("sub") or "") subject = str(claims.get("sub") or "")
email = str(claims.get("email") or "").strip().lower() email = str(claims.get("email") or "").strip().lower()
@ -206,9 +212,53 @@ def resolve_linked_user(claims, groups, auto_link):
user = link.user user = link.user
user.last_login_medium = OIDC_PROVIDER user.last_login_medium = OIDC_PROVIDER
user.last_login_time = timezone.now() user.last_login_time = timezone.now()
user.save(update_fields=["last_login_medium", "last_login_time", "updated_at"]) update_fields = ["last_login_medium", "last_login_time", "updated_at"]
if sync_profile:
update_fields.extend(sync_user_profile_from_claims(user, claims))
user.save(update_fields=list(dict.fromkeys(update_fields)))
return user return user
def sync_user_profile_from_claims(user, claims):
updated_fields = []
display_name = first_string_claim(claims, "name", "preferred_username")
given_name = first_string_claim(claims, "given_name")
family_name = first_string_claim(claims, "family_name")
avatar_url = first_string_claim(claims, "picture", "avatar_url", "avatar")
if display_name and user.display_name != display_name:
user.display_name = display_name
updated_fields.append("display_name")
if not given_name and display_name:
name_parts = display_name.split(" ", 1)
given_name = name_parts[0]
family_name = family_name or (name_parts[1] if len(name_parts) > 1 else "")
if given_name and user.first_name != given_name:
user.first_name = given_name
updated_fields.append("first_name")
if family_name is not None and user.last_name != family_name:
user.last_name = family_name
updated_fields.append("last_name")
if avatar_url and user.avatar != avatar_url:
user.avatar = avatar_url
updated_fields.append("avatar")
return updated_fields
def first_string_claim(claims, *keys):
for key in keys:
value = claims.get(key)
if isinstance(value, str) and value:
return value
return None
def oidc_error_redirect(base_url, next_path, error_code): def oidc_error_redirect(base_url, next_path, error_code):
return HttpResponseRedirect(get_safe_redirect_url(base_url=base_url, next_path=next_path, params={"error": error_code})) return HttpResponseRedirect(get_safe_redirect_url(base_url=base_url, next_path=next_path, params={"error": error_code}))

View File

@ -276,8 +276,8 @@ Application access задан через group bindings: Launcher доступе
"state_group": "started", "state_group": "started",
"assignees": [CODEX_EMAIL], "assignees": [CODEX_EMAIL],
"description_html": html( "description_html": html(
"Launcher должен перестать быть только меню ссылок и стать control plane: login через Authentik, app registry, фильтрация доступных приложений, admin API для пользователей и audit log.", "Launcher должен перестать быть только меню ссылок и стать control plane: входная точка платформы, клиенты/компании, пользователи, группы, инвайты, профиль, app registry, access matrix и audit log.",
"Критерий приемки: пользователь логинится через Authentik, Launcher видит sub/email/groups, показывает только доступные приложения, а admin действия идут через server-side backend и фиксируются в audit.", "Критерий приемки: пользователь входит через NODE.DC UI без публичного брендинга Authentik, Launcher хранит бизнес-модель доступа и профиля, а Authentik получает только техническую SSO/OIDC projection.",
), ),
"blocks": [ "blocks": [
text_block( text_block(
@ -316,6 +316,8 @@ Backend слоя не обнаружено. Значит OIDC callback, session
Статус: реализовано, ожидает ручной browser login/logout проверки. Статус: реализовано, ожидает ручной browser login/logout проверки.
Launcher должен проходить OIDC Authorization Code Flow + PKCE через Authentik и получать нормализованного текущего пользователя. Launcher должен проходить OIDC Authorization Code Flow + PKCE через Authentik и получать нормализованного текущего пользователя.
Важно: текущий hosted Authentik login является временным dev flow для проверки OIDC/JWKS/session/redirect. Целевой production login не должен показывать пользователю Authentik UI/brand.
""", """,
), ),
checker( checker(
@ -337,7 +339,7 @@ Launcher должен проходить OIDC Authorization Code Flow + PKCE ч
""" """
Статус: частично реализовано. Статус: частично реализовано.
Launcher показывает приложения не статическим списком, а через registry с required_group. Источник truth по доступу остается в Authentik groups. Launcher показывает приложения не статическим списком, а через registry и access state. Источник истины по доступу Launcher access model; Authentik groups/entitlements являются технической проекцией для SSO/enforcement.
Продуктовое правило от 2026-05-04: Launcher не должен скрывать все недоступные плитки. Он показывает каталог приложений платформы, а отсутствие доступа отражает как disabled/Нет доступа на карточке. Это уже заложено в текущий frontend-декор и должно сохраняться при backend-интеграции. Продуктовое правило от 2026-05-04: Launcher не должен скрывать все недоступные плитки. Он показывает каталог приложений платформы, а отсутствие доступа отражает как disabled/Нет доступа на карточке. Это уже заложено в текущий frontend-декор и должно сохраняться при backend-интеграции.
""", """,
@ -349,8 +351,8 @@ Launcher показывает приложения не статическим
{"text": "Спроектировать app_registry модель.", "checked": True}, {"text": "Спроектировать app_registry модель.", "checked": True},
{"text": "Добавить GET /api/me.", "checked": True}, {"text": "Добавить GET /api/me.", "checked": True},
{"text": "Добавить GET /api/apps.", "checked": True}, {"text": "Добавить GET /api/apps.", "checked": True},
{"text": "Фильтровать приложения по groups.", "checked": True}, {"text": "Вычислять access state из runtime projection groups.", "checked": True},
{"text": "Скрывать disabled apps.", "checked": True}, {"text": "Не скрывать недоступные плитки, а показывать Нет доступа.", "checked": True},
{"text": "Проверить direct URL behavior через proxy.", "checked": False}, {"text": "Проверить direct URL behavior через proxy.", "checked": False},
], ],
), ),
@ -360,11 +362,37 @@ Launcher показывает приложения не статическим
""" """
Production flow: пользователь открывает nodedc.ru, видит основной промо/маркетинговый сайт, нажимает "Войти на платформу", проходит окно логина/пароля и после успешного доступа попадает в NODE.DC Launcher. Production flow: пользователь открывает nodedc.ru, видит основной промо/маркетинговый сайт, нажимает "Войти на платформу", проходит окно логина/пароля и после успешного доступа попадает в NODE.DC Launcher.
Пользовательский UI платформы не должен светить название identity provider. В текстах и кнопках используется нейтральное "Войти", "Вход на платформу", "Сессия NODE.DC". Authentik остается внутренним identity source и admin-инструментом. Пользовательский UI платформы не должен светить название identity provider. В текстах и кнопках используется нейтральное "Войти", "Вход на платформу", "Сессия NODE.DC". Authentik остается внутренним identity/session/OIDC слоем; продуктовая админка живет в Launcher. Прямой Authentik UI допустим только через отдельную служебную ссылку для системной настройки.
Прямые ссылки на приложения остаются нормальным сценарием. Если пользователь открыл Task Manager напрямую без session, его нужно увести в login flow и вернуть к приложению после успешной авторизации. Прямые ссылки на приложения остаются нормальным сценарием. Если пользователь открыл Task Manager напрямую без session, его нужно увести в login flow и вернуть к приложению после успешной авторизации.
""", """,
), ),
text_block(
"launcher",
"Этап 3.5. NODE.DC login facade",
"""
Статус: backlog.
Ручная проверка 2026-05-04 подтвердила: при входе пользователь пока видит стандартное окно Authentik. Это допустимо только для текущего dev/OIDC bootstrap, но не соответствует целевой UX-модели NODE.DC.
Нужно сделать production login без публичного Authentik UI: либо полностью кастомизированный Authentik flow, если он реально выдержит дизайн-канон NODE.DC, либо Launcher/BFF login facade, который server-side работает с внутренним Authentik flow/API и не раскрывает IdP пользователю.
Безопасностная граница: не делать frontend-only password form, не хранить IdP/service secrets в browser bundle, не ломать MFA/recovery/rate-limit/audit и не обходить Authentik как password/session authority.
""",
),
checker(
"launcher35",
"Чекер этапа 3.5. NODE.DC login facade",
[
"Выбрать безопасную стратегию: branded Authentik flow или Launcher/BFF facade.",
"Сверстать NODE.DC login по канону Task Manager/Launcher.",
"Скрыть Authentik brand во всех штатных login/logout/error текстах.",
"Поддержать returnTo для прямых ссылок на приложения.",
"Поддержать forced login для диагностики.",
"Спроектировать recovery/enrollment/MFA без раскрытия Authentik UI.",
"Проверить, что password/service tokens не попадают во frontend.",
],
),
text_block( text_block(
"launcher", "launcher",
"Реализация этапов 1-3", "Реализация этапов 1-3",
@ -377,7 +405,7 @@ OIDC flow реализован как Authorization Code + PKCE: state хран
App registry возвращает полный каталог приложений из launcher storage и runtime access state: hasAccess, matchedGroups, accessReason. Frontend больше не скрывает недоступные плитки; он отключает переход и показывает "Нет доступа" через существующую механику карточек. App registry возвращает полный каталог приложений из launcher storage и runtime access state: hasAccess, matchedGroups, accessReason. Frontend больше не скрывает недоступные плитки; он отключает переход и показывает "Нет доступа" через существующую механику карточек.
Frontend Launcher подключен к /api/me и /api/apps. Без session показывается нейтральный экран "Вход на платформу NODE.DC" и кнопка "Войти" без упоминания Authentik. После login пользователь нормализуется из Authentik claims, а плитки получают доступы из runtime app registry. Frontend Launcher подключен к /api/me и /api/apps. Без session показывается нейтральный экран "Вход на платформу NODE.DC" и кнопка "Войти" без упоминания Authentik. После login пользователь нормализуется из OIDC claims, а плитки получают доступы из runtime app registry.
Проверки 2026-05-04: npm run build проходит; http://launcher.local.nodedc/healthz возвращает oidcConfigured=true; http://launcher.local.nodedc/api/me без session возвращает 401 и loginUrl; http://launcher.local.nodedc/auth/login возвращает 302 на Authentik authorize endpoint; discovery endpoint Authentik для launcher возвращает issuer и authorization_endpoint. Проверки 2026-05-04: 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.
@ -390,30 +418,158 @@ Frontend Launcher подключен к /api/me и /api/apps. Без session п
2026-05-04 добавлен явный logout в профильное меню Launcher: кнопка "Выйти" вызывает /auth/logout и чистит local BFF session без ухода в Authentik UI/admin. Это нужно, чтобы пользователь оставался в NODE.DC UX после выхода. 2026-05-04 добавлен явный logout в профильное меню Launcher: кнопка "Выйти" вызывает /auth/logout и чистит local BFF session без ухода в Authentik UI/admin. Это нужно, чтобы пользователь оставался в NODE.DC UX после выхода.
SSO-session у identity provider может оставаться активной. Поэтому повторное нажатие "Войти" может вернуть пользователя в Launcher без ввода пароля это ожидаемое SSO-поведение, а не ошибка. Для диагностики добавлен prompt=login на /auth/login?prompt=login и отдельный global logout через /auth/logout?global=1, но пользовательский logout по умолчанию остается локальным. SSO-session у identity provider может оставаться активной. Поэтому повторное нажатие "Войти" может вернуть пользователя в Launcher без ввода пароля это ожидаемое SSO-поведение, а не ошибка. Для диагностики добавлен prompt=login на /auth/login?prompt=login и отдельный global logout через /auth/logout?global=1, но пользовательский logout по умолчанию остается локальным.
2026-05-04 повторная ручная проверка подтвердила целевой UX gap: после нажатия "Войти" пользователь все еще видит стандартное окно Authentik. Это зафиксировано как отдельный backlog-этап NODE.DC login facade; текущий hosted login не считать production-ready.
Текущий BFF/OIDC слой является переходной реализацией. Сейчас app access читается из OIDC groups как runtime projection, но целевая source-of-truth модель Launcher backend: клиенты, членства, группы клиента, user grants, deny exceptions, профиль платформы и audit. Authentik должен получать из Launcher синхронизированную техническую проекцию для SSO/enforcement, а не быть ручной бизнес-админкой.
""", """,
), ),
text_block( text_block(
"launcher", "launcher",
"Этап 4. Admin API и audit", "Этап 4. Admin API и audit",
""" """
Статус: backlog. Статус: частично реализовано.
Admin API должен управлять пользователями и группами через Authentik API. Launcher не хранит пароли и не заменяет Authentik как identity source. 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( checker(
"launcher4", "launcher4",
"Чекер этапа 4. Admin API и audit", "Чекер этапа 4. Admin API и audit",
[ [
"Добавить список пользователей через Authentik API.", {"text": "Добавить backend store для launcher-data.json.", "checked": True},
"Добавить создание пользователя или enrollment flow.", {"text": "Закрыть admin endpoints Launcher roles.", "checked": True},
"Добавить disable пользователя.", {"text": "Добавить clients CRUD endpoints.", "checked": True},
"Добавить reset/recovery flow.", {"text": "Добавить users list/profile update endpoints.", "checked": True},
"Добавить add/remove group.", {"text": "Добавить memberships update/delete endpoints.", "checked": True},
"Добавить admin_audit_log.", {"text": "Добавить client groups CRUD endpoints.", "checked": True},
"Закрыть admin endpoints группами nodedc:superadmin/nodedc:launcher:admin.", {"text": "Добавить service catalog CRUD/reorder endpoints.", "checked": True},
{"text": "Добавить invite CRUD endpoints из Launcher.", "checked": True},
{"text": "Добавить access grants/exceptions/user-service endpoints.", "checked": True},
{"text": "Добавить Authentik sync dry-run plan endpoint.", "checked": True},
{"text": "Добавить admin_audit_log запись для backend mutations.", "checked": True},
{"text": "Подключить frontend admin overlay к новым admin endpoints.", "checked": True},
"Заменить JSON-backed store на production persistence.",
"Реализовать фактический server-side sync в Authentik.",
], ],
), ),
text_block(
"launcher",
"Реализация этапа 4",
"""
В Launcher repo добавлен server/control-plane-store.mjs: dev control-plane repository поверх public/storage/launcher-data.json и dist/storage/launcher-data.json. Store нормализует доменные коллекции clients, users, memberships, groups, services, grants, exceptions, invites, syncStatuses и auditEvents.
BFF server/dev-server.mjs получил admin guard по группам nodedc:superadmin/nodedc:launcher:admin и backend endpoints для текущего overlay: GET /api/admin/control-plane, clients CRUD, users/profile update, memberships update/delete, groups CRUD, services CRUD/reorder, invites CRUD, access grants/exceptions/user-service, sync retry и GET /api/admin/sync/authentik/plan.
Backend mutations пишут auditEvents и помечают затронутые объекты как pending sync для target authentik. Endpoint /api/admin/sync/authentik/plan пока dry-run: он показывает users/groups/access projection, но не вызывает Authentik API.
Frontend admin overlay подключен к server-side admin API через src/shared/api/adminApi.ts. Mutation handlers в src/app/LauncherApp.tsx больше не собирают mock id/audit локально для поддержанных admin операций, а вызывают BFF и принимают обновленный LauncherData из backend response.
Проверки 2026-05-04: node --check server/dev-server.mjs и server/control-plane-store.mjs проходят; npm run build проходит; npm run test проходит; http://localhost:5173/healthz возвращает oidcConfigured=true; /api/admin/control-plane без session возвращает 401 через localhost и launcher.local.nodedc. Store mutations проверены на временной копии launcher-data.json: create client/group/invite и set user-service access обновляют counts и auditEvents.
""",
),
text_block(
"launcher",
"Этап 4.5. Live users seed и очистка demo-участников",
"""
Статус: частично реализовано.
Декоративные MVP-пользователи root@nodedc.local, ivan@romashka.ru, vera@romashka.ru, vasya@romashka.ru, lena@romashka.ru, maria@example.ru и blocked demo-user удаляются из active Launcher seed. Их можно восстановить из backup, но рабочий dev-runtime должен проверяться на живых пользователях.
Фактический runtime discovery 2026-05-04:
- Authentik содержит dcctouch@gmail.com / DC Touch в группах nodedc:superadmin, nodedc:launcher:admin/user, nodedc:taskmanager:admin/user.
- Plane содержит dcctouch@gmail.com / DC Touch как owner workspace nodedc.
- Plane содержит silver_psih@yahoo.com / dcMASSACRE как member workspace nodedc и owner workspace silver-psih-mo493nm7.
Перед реализацией нужно подтвердить канонические email/имена: пользователь подтвердил, что silver_psih@yahoo.com правильная почта Silver Psy. Runtime Plane/Auth показывает dcctouch@gmail.com для текущего superadmin; этот email считается фактическим до отдельного указания заменить его на другой alias.
Цель этапа: заменить demo-участников Launcher на живой минимальный seed, где супер-админ администрирует клиентов, группы, инвайты и доступы через Launcher UI, а не через прямые правки JSON/Auth/Plane.
""",
),
checker(
"launcher45",
"Чекер этапа 4.5. Live users seed и очистка demo-участников",
[
{"text": "Подтвердить канонический email NODE.DC superadmin.", "checked": True},
{"text": "Подтвердить канонический email Silver Psy / менеджера.", "checked": True},
{"text": "Сделать backup public/storage/launcher-data.json.", "checked": True},
{"text": "Создать idempotent seed script для живых users/clients/groups/memberships.", "checked": True},
{"text": "Удалить декоративных MVP-участников из active Launcher seed.", "checked": True},
{"text": "Создать живой superadmin в Launcher profile model.", "checked": True},
{"text": "Создать живого Silver Psy/manager пользователя в Launcher profile model.", "checked": True},
{"text": "Синхронизировать недостающего пользователя в Authentik через server-side flow.", "checked": True},
{"text": "Не пересоздавать Plane users и не менять Plane workspace/task связи.", "checked": True},
{"text": "Показывать все видимые сервисы в Launcher даже без доступа к запуску.", "checked": True},
{"text": "Считать /api/apps из Launcher control-plane, а не из stale OIDC groups.", "checked": True},
{"text": "Синхронизировать affected users в Authentik при изменении memberships/groups/grants/exceptions.", "checked": True},
{"text": "Заменить polling открытых Launcher вкладок на event-driven runtime refresh.", "checked": True},
{"text": "Сделать atomic write launcher-data.json, чтобы reads не ловили полузаписанный JSON.", "checked": True},
{"text": "Добавить pending-state на ячейки access matrix, чтобы не было повторных кликов по in-flight mutation.", "checked": True},
{"text": "Отключить legacy frontend autosave launcher-data.json в authenticated runtime.", "checked": True},
{"text": "Добавить self-profile settings panel справа.", "checked": True},
{"text": "Добавить upload avatar в Launcher profile storage.", "checked": True},
{"text": "Добавить смену пароля через backend Authentik projection.", "checked": True},
"Проверить Launcher admin UI: клиенты, участники, группы, инвайты, доступы.",
{"text": "Проверить Task Manager SSO для обоих живых пользователей.", "checked": True},
],
),
text_block(
"launcher",
"Реализация этапа 4.5",
"""
В Launcher repo создан scripts/seed-live-control-plane.mjs. Скрипт читает текущий public/storage/launcher-data.json, сохраняет существующий каталог services и заменяет active control-plane домен на один live-клиент DCTOUCH и двух пользователей: dcctouch@gmail.com / DC Touch и silver_psih@yahoo.com / Silver Psy.
Перед применением создан backup public/storage/backups/launcher-data-live-seed-20260504-152728.json. Active storage после seed содержит 1 client, 2 users, 2 memberships, 2 groups, 3 grants, 0 invites. Silver Psy помещен в группу "Менеджеры" и получает доступ к OPERATIONAL CORE через group grant; DC Touch помещен в группу "Администраторы" и остается platform superadmin.
Из src/shared/api/mockApi.ts убраны декоративные profileOptions для Ivan/Vera/Vasya/Lena/Maria. src/app/LauncherApp.tsx больше не мапит любого non-superadmin OIDC-пользователя на user_vasya: активный Launcher profile выбирается по Authentik sub/email, затем fallback на user_root для superadmin.
Fallback src/shared/api/mockData.ts приведен к live seed, чтобы при отсутствии storage не всплывали старые demo-участники. Plane users/workspaces не пересоздавались и не менялись.
Проверки 2026-05-04: node --check scripts/seed-live-control-plane.mjs, server/dev-server.mjs и server/control-plane-store.mjs проходят; npm run build проходит; npm run test проходит; http://localhost:5173/storage/launcher-data.json возвращает 1 client, 2 users и emails dcctouch@gmail.com/silver_psih@yahoo.com; http://launcher.local.nodedc/healthz возвращает ok и oidcConfigured=true.
Дополнение 2026-05-04: реализован server-side Authentik provisioning flow. В Launcher repo добавлен server/authentik-sync.mjs: он работает только на backend, берет Authentik API token из server-side env, создает/обновляет пользователя, назначает группы и выставляет пароль только при явном provisioning/generatePassword сценарии.
BFF server/dev-server.mjs получил POST /api/admin/users для создания пользователя в Launcher control-plane с optional Authentik provisioning и POST /api/admin/users/:userId/provision-authentik для повторной синхронизации существующего Launcher-пользователя. Healthcheck теперь показывает authentikApiConfigured без раскрытия token.
server/control-plane-store.mjs получил createUser и markUserAuthentikProvisioned: Launcher остается source-of-truth, а Authentik UUID сохраняется в user.authentikUserId после успешной технической проекции.
Frontend admin overlay получил форму "Создать участника" в разделе "Участники": email/name/role/group -> backend creates Launcher user -> Authentik projection. При генерации временного пароля пароль показывается только одноразово в UI и не пишется в storage/task card.
Дополнительно исправлена проверка доступа к плитке AGENT CORE/NodeDC: service nodedc больше не считается доступным просто по nodedc:launcher:user. Для сервисных плиток используется service.authentikGroupName или специальная группа конкретного приложения; факт входа в Launcher не должен автоматически открывать все сервисы.
Дополнение после ручной проверки Silver Psy: buildLauncherServices больше не фильтрует denied-сервисы из rail. Обычный пользователь видит весь видимый каталог приложений, но у сервисов без доступа кнопка запуска заблокирована и показывает "Нет доступа". Hidden-сервисы остаются скрытыми для non-root.
Добавлен self-profile контур: /api/profile, PATCH /api/profile и POST /api/profile/password доступны любому authenticated пользователю для собственного профиля. Profile settings panel открывается из меню профиля справа налево, может работать одновременно с левым admin overlay, сохраняет name/email/phone/position/avatarUrl и умеет менять пароль через backend Authentik projection.
Avatar upload использует существующий server-side storage upload и сохраняет ссылку в Launcher profile model. Runtime user в Launcher теперь предпочитает данные Launcher profile model поверх OIDC claims, чтобы изменения имени/аватарки отображались без ожидания следующего login.
Фактически выполнено: silver_psih@yahoo.com создан в Authentik как active internal user, получил группы nodedc:launcher:user и nodedc:taskmanager:user. dcctouch@gmail.com также синхронизирован по Authentik UUID. public/storage/launcher-data.json теперь содержит Authentik UUID для обоих live-профилей, а syncStatuses для обоих users находятся в state synced.
Проверки 2026-05-04: node --check server/authentik-sync.mjs, server/dev-server.mjs и server/control-plane-store.mjs проходят; npm run build проходит; npm run test проходит; Authentik API подтверждает наличие silver_psih@yahoo.com с 2 группами; /healthz возвращает authentikApiConfigured=true. Пользователь вручную подтвердил: Silver Psy входит в Launcher, не видит администрирование, открывает OPERATIONAL CORE и в Task Manager видит корректный доступный проект.
Дополнение после проверки снятия доступа: /api/apps больше не берет app access из stale OIDC groups текущей BFF-сессии. Runtime-доступ рассчитывается из Launcher control-plane через resolveRequiredGroups, а OIDC groups остаются fallback только если пользователь еще не сопоставлен с Launcher profile model.
Admin mutations теперь проталкивают affected users в Authentik сразу после изменения профиля, memberships, groups, access grants, deny/allow exceptions и user-service matrix. Это закрывает кейс, когда админ снял Silver Psy из доступа к OPERATIONAL CORE, а Authentik еще держал старую nodedc:taskmanager:user группу.
Открытые Launcher вкладки authenticated пользователей больше не используют polling каждые 5 секунд. BFF держит /api/events через Server-Sent Events, а admin/profile/storage mutations публикуют событие control-plane.updated. Вкладки обновляют /api/me, /api/apps и storage/control-plane snapshot только по факту изменения control-plane состояния.
Дополнение после ручной проверки access matrix: выявлен race записи public/storage/launcher-data.json. Пока writeFile перезаписывал файл напрямую, параллельные runtime запросы иногда читали пустой/полузаписанный JSON и падали с Unexpected end of JSON input; визуально это выглядело как применение access select только с нескольких попыток.
server/control-plane-store.mjs переведен на atomic write: запись идет во временный файл в том же storage root, затем rename заменяет launcher-data.json. Это убирает окно, в котором reader может увидеть обнуленный файл.
В access matrix добавлен pending-state на конкретную ячейку user/service: после выбора роли ячейка сразу показывает выбранное значение и "Сохраняем...", а повторный клик блокируется до завершения mutation. Это убирает многократные ручные попытки и race между несколькими одинаковыми запросами.
Дополнение по итогам повторного теста: найден второй источник отката access matrix. Legacy frontend autosave сохранял весь LauncherData обратно в /api/storage/data после setData и мог перезаписать свежий backend-result старым состоянием из открытой вкладки. src/app/LauncherApp.tsx больше не вызывает persistLauncherData; authenticated runtime пишет control-plane только через admin/profile API mutations. /api/storage/data дополнительно закрыт requireLauncherAdmin и оставлен только как служебный dev endpoint.
Текущий live-sync выполнен вручную после исправления: dcctouch@gmail.com имеет nodedc:launcher:user, nodedc:superadmin, nodedc:launcher:admin, nodedc:taskmanager:admin, nodedc:taskmanager:user; silver_psih@yahoo.com имеет nodedc:launcher:user и service-digital-twin, без nodedc:taskmanager:user.
Ограничение безопасности остается отдельным обязательным будущим этапом: уже открытая downstream-сессия Plane может жить до logout/session expiry, даже если Launcher уже заблокировал кнопку и Authentik projection сняла группу. Для жесткого realtime revoke нужен отдельный session-revocation/app-token слой в Plane или gateway.
Ограничение: email/password/profile update уже работает как dev-flow, но production UX для reset-password/invite-email и подтверждения смены email еще нужно вынести в отдельный полноценный этап.
""",
),
], ],
}, },
{ {
@ -424,7 +580,7 @@ Admin API должен управлять пользователями и гру
"assignees": [CODEX_EMAIL], "assignees": [CODEX_EMAIL],
"description_html": html( "description_html": html(
"Интеграция Plane fork с Authentik OIDC без потери текущего пользователя и связанных данных. Это самый рискованный блок, потому что нельзя пересоздать существующего Plane admin и потерять связи задач.", "Интеграция Plane fork с Authentik OIDC без потери текущего пользователя и связанных данных. Это самый рискованный блок, потому что нельзя пересоздать существующего Plane admin и потерять связи задач.",
"Критерий приемки: старый Plane user связан с Authentik sub, после входа через NODE.DC/Authentik видит старые workspace, проекты, задачи, комментарии и назначения; signup закрыт.", "Критерий приемки: старый Plane user связан с Authentik sub, после входа через NODE.DC/Authentik видит старые workspace, проекты, задачи, комментарии и назначения; signup закрыт; Plane остается вынимаемым standalone-продуктом с сохранением собственных auth/API механизмов.",
), ),
"blocks": [ "blocks": [
text_block( text_block(
@ -434,6 +590,8 @@ Admin API должен управлять пользователями и гру
Task Manager работает как Plane CE self-host. Runtime лежит в plane-app, fork исходников в plane-src. Task Manager работает как Plane CE self-host. Runtime лежит в plane-app, fork исходников в plane-src.
В workspace nodedc уже есть данные, пользователи и проекты. Старые связи в Plane нельзя менять без отдельной проверенной миграции. В workspace nodedc уже есть данные, пользователи и проекты. Старые связи в Plane нельзя менять без отдельной проверенной миграции.
Архитектурное правило от 2026-05-04: Plane подключается к NODE.DC как приложение/адаптер, но не превращается в невынимаемый модуль платформы. Его workspace/project/task/comment модели остаются внутри Plane, стандартные Plane auth/API механизмы сохраняются через env/конфигурацию, а Launcher не читает и не пишет Plane DB напрямую.
""", """,
), ),
text_block( text_block(
@ -537,7 +695,7 @@ Access check завязан на группы Authentik: nodedc:superadmin, node
После проверки добавлен route alias /auth/oidc/callback без trailing slash, потому что Authentik сначала обращался к callback без слэша и получал 404 перед успешным запросом на /auth/oidc/callback/. Группы в Plane и Launcher теперь дедуплицируются при нормализации claims. После проверки добавлен route alias /auth/oidc/callback без trailing slash, потому что Authentik сначала обращался к callback без слэша и получал 404 перед успешным запросом на /auth/oidc/callback/. Группы в Plane и Launcher теперь дедуплицируются при нормализации claims.
Открытый продуктовый gap: профильный контекст пока не синхронизируется полностью. Launcher показывает claims из Authentik, а Task Manager показывает локальный Plane profile/avatar. Следующий этап должен сделать единый источник display name/avatar. Закрыт базовый 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( text_block(
@ -571,6 +729,63 @@ Access check завязан на группы Authentik: nodedc:superadmin, node
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. 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",
"Этап 6. Standalone compatibility и Plane API adapter",
"""
Статус: backlog.
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 и собственными ролями внутри приложения.
""",
),
checker(
"plane6",
"Чекер этапа 6. Standalone compatibility и Plane API adapter",
[
"Зафиксировать env-флаг включения NODE.DC SSO слоя.",
"Описать 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.",
],
),
], ],
}, },
{ {
@ -670,6 +885,7 @@ Security checklist создан в platform/docs/SECURITY_CHECKLIST.md. Реал
"Проверить redirect Launcher без логина.", "Проверить redirect Launcher без логина.",
"Проверить скрытие Task Manager без group access.", "Проверить скрытие Task Manager без group access.",
"Проверить deny на прямой task.local.nodedc без group access.", "Проверить deny на прямой task.local.nodedc без group access.",
"Проверить отзыв уже открытой downstream-сессии после снятия доступа.",
"Проверить успешный вход пользователя с access.", "Проверить успешный вход пользователя с access.",
"Проверить старого Plane admin после OIDC migration.", "Проверить старого Plane admin после OIDC migration.",
"Проверить deactivate user.", "Проверить deactivate user.",