From 55318f14e555425209fb8d6eb747147c17661226 Mon Sep 17 00:00:00 2001 From: DCCONSTRUCTIONS Date: Mon, 4 May 2026 17:16:47 +0300 Subject: [PATCH] =?UTF-8?q?=D0=A4=D0=A3=D0=9D=D0=9A=D0=A6=D0=98=D0=98=20-?= =?UTF-8?q?=20=D0=9C=D0=95=D0=96=D0=9F=D0=A0=D0=9E=D0=95=D0=9A=D0=A2=D0=9D?= =?UTF-8?q?=D0=90=D0=AF=20=D0=9A=D0=9E=D0=9C=D0=9C=D0=A3=D0=9D=D0=98=D0=9A?= =?UTF-8?q?=D0=90=D0=A6=D0=98=D0=AF:=20Plane=20OIDC=20=D0=B8=20=D0=BF?= =?UTF-8?q?=D0=BB=D0=B0=D0=BD=20=D0=BF=D0=BB=D0=B0=D1=82=D1=84=D0=BE=D1=80?= =?UTF-8?q?=D0=BC=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- plane-app/docker-compose.yaml | 1 + plane-app/plane.env | 16 +- .../plane/authentication/views/app/oidc.py | 56 +++- scripts/bootstrap_nodedc_platform_plan.py | 252 ++++++++++++++++-- 4 files changed, 302 insertions(+), 23 deletions(-) diff --git a/plane-app/docker-compose.yaml b/plane-app/docker-compose.yaml index f0ceef7..ab51420 100644 --- a/plane-app/docker-compose.yaml +++ b/plane-app/docker-compose.yaml @@ -61,6 +61,7 @@ x-app-env: &app-env 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_AUTO_LINK_EMAIL: ${PLANE_OIDC_AUTO_LINK_EMAIL:-0} + PLANE_OIDC_SYNC_PROFILE: ${PLANE_OIDC_SYNC_PROFILE:-1} GUNICORN_WORKERS: 1 POSTHOG_API_KEY: ${POSTHOG_API_KEY:-} POSTHOG_HOST: ${POSTHOG_HOST:-} diff --git a/plane-app/plane.env b/plane-app/plane.env index 77c0288..298f0dc 100644 --- a/plane-app/plane.env +++ b/plane-app/plane.env @@ -12,9 +12,9 @@ LIVE_REPLICAS=1 LISTEN_HTTP_PORT=8090 LISTEN_HTTPS_PORT=8443 -WEB_URL=http://localhost:8090 +WEB_URL=http://task.local.nodedc DEBUG=0 -CORS_ALLOWED_ORIGINS=http://localhost:8090 +CORS_ALLOWED_ORIGINS=http://task.local.nodedc,http://localhost:8090 API_BASE_URL=http://api:8000 #DB SETTINGS @@ -89,3 +89,15 @@ LIVE_SERVER_SECRET_KEY= DOCKERHUB_USER=makeplane PULL_POLICY=if_not_present 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 diff --git a/plane-src/apps/api/plane/authentication/views/app/oidc.py b/plane-src/apps/api/plane/authentication/views/app/oidc.py index f261b6a..7ca52b2 100644 --- a/plane-src/apps/api/plane/authentication/views/app/oidc.py +++ b/plane-src/apps/api/plane/authentication/views/app/oidc.py @@ -81,7 +81,12 @@ class NodeDCOIDCCallbackEndpoint(View): if not has_required_group(groups): 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: return oidc_error_redirect(base_url, next_path, "oidc_user_not_linked") @@ -109,6 +114,7 @@ def get_oidc_config(): "redirect_uri": redirect_uri, "scope": os.environ.get("PLANE_OIDC_SCOPE", "openid email profile groups"), "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))) -def resolve_linked_user(claims, groups, auto_link): +def resolve_linked_user(claims, groups, auto_link, sync_profile): subject = str(claims.get("sub") or "") email = str(claims.get("email") or "").strip().lower() @@ -206,9 +212,53 @@ def resolve_linked_user(claims, groups, auto_link): user = link.user user.last_login_medium = OIDC_PROVIDER 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 +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): return HttpResponseRedirect(get_safe_redirect_url(base_url=base_url, next_path=next_path, params={"error": error_code})) diff --git a/scripts/bootstrap_nodedc_platform_plan.py b/scripts/bootstrap_nodedc_platform_plan.py index 49fa5d8..848918d 100644 --- a/scripts/bootstrap_nodedc_platform_plan.py +++ b/scripts/bootstrap_nodedc_platform_plan.py @@ -276,8 +276,8 @@ Application access задан через group bindings: Launcher доступе "state_group": "started", "assignees": [CODEX_EMAIL], "description_html": html( - "Launcher должен перестать быть только меню ссылок и стать control plane: login через Authentik, app registry, фильтрация доступных приложений, admin API для пользователей и audit log.", - "Критерий приемки: пользователь логинится через Authentik, Launcher видит sub/email/groups, показывает только доступные приложения, а admin действия идут через server-side backend и фиксируются в audit.", + "Launcher должен перестать быть только меню ссылок и стать control plane: входная точка платформы, клиенты/компании, пользователи, группы, инвайты, профиль, app registry, access matrix и audit log.", + "Критерий приемки: пользователь входит через NODE.DC UI без публичного брендинга Authentik, Launcher хранит бизнес-модель доступа и профиля, а Authentik получает только техническую SSO/OIDC projection.", ), "blocks": [ text_block( @@ -316,6 +316,8 @@ Backend слоя не обнаружено. Значит OIDC callback, 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( @@ -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-интеграции. """, @@ -349,8 +351,8 @@ Launcher показывает приложения не статическим {"text": "Спроектировать app_registry модель.", "checked": True}, {"text": "Добавить GET /api/me.", "checked": True}, {"text": "Добавить GET /api/apps.", "checked": True}, - {"text": "Фильтровать приложения по groups.", "checked": True}, - {"text": "Скрывать disabled apps.", "checked": True}, + {"text": "Вычислять access state из runtime projection groups.", "checked": True}, + {"text": "Не скрывать недоступные плитки, а показывать Нет доступа.", "checked": True}, {"text": "Проверить direct URL behavior через proxy.", "checked": False}, ], ), @@ -360,11 +362,37 @@ 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 и вернуть к приложению после успешной авторизации. """, ), + 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( "launcher", "Реализация этапов 1-3", @@ -377,7 +405,7 @@ OIDC flow реализован как Authorization Code + PKCE: state хран 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. @@ -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 после выхода. SSO-session у identity provider может оставаться активной. Поэтому повторное нажатие "Войти" может вернуть пользователя в Launcher без ввода пароля — это ожидаемое SSO-поведение, а не ошибка. Для диагностики добавлен prompt=login на /auth/login?prompt=login и отдельный global logout через /auth/logout?global=1, но пользовательский logout по умолчанию остается локальным. + +2026-05-04 повторная ручная проверка подтвердила целевой UX gap: после нажатия "Войти" пользователь все еще видит стандартное окно Authentik. Это зафиксировано как отдельный backlog-этап NODE.DC login facade; текущий hosted login не считать production-ready. + +Текущий BFF/OIDC слой является переходной реализацией. Сейчас app access читается из OIDC groups как runtime projection, но целевая source-of-truth модель — Launcher backend: клиенты, членства, группы клиента, user grants, deny exceptions, профиль платформы и audit. Authentik должен получать из Launcher синхронизированную техническую проекцию для SSO/enforcement, а не быть ручной бизнес-админкой. """, ), text_block( "launcher", "Этап 4. Admin API и audit", """ -Статус: 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( "launcher4", "Чекер этапа 4. Admin API и audit", [ - "Добавить список пользователей через Authentik API.", - "Добавить создание пользователя или enrollment flow.", - "Добавить disable пользователя.", - "Добавить reset/recovery flow.", - "Добавить add/remove group.", - "Добавить admin_audit_log.", - "Закрыть admin endpoints группами nodedc:superadmin/nodedc:launcher:admin.", + {"text": "Добавить backend store для launcher-data.json.", "checked": True}, + {"text": "Закрыть admin endpoints Launcher roles.", "checked": True}, + {"text": "Добавить clients CRUD endpoints.", "checked": True}, + {"text": "Добавить users list/profile update endpoints.", "checked": True}, + {"text": "Добавить memberships update/delete endpoints.", "checked": True}, + {"text": "Добавить client groups CRUD endpoints.", "checked": True}, + {"text": "Добавить service catalog CRUD/reorder endpoints.", "checked": True}, + {"text": "Добавить invite CRUD endpoints из Launcher.", "checked": True}, + {"text": "Добавить access grants/exceptions/user-service endpoints.", "checked": True}, + {"text": "Добавить Authentik sync dry-run plan endpoint.", "checked": True}, + {"text": "Добавить admin_audit_log запись для backend mutations.", "checked": True}, + {"text": "Подключить frontend admin overlay к новым admin endpoints.", "checked": True}, + "Заменить JSON-backed store на production persistence.", + "Реализовать фактический server-side sync в Authentik.", ], ), + text_block( + "launcher", + "Реализация этапа 4", + """ +В Launcher repo добавлен server/control-plane-store.mjs: dev control-plane repository поверх public/storage/launcher-data.json и dist/storage/launcher-data.json. Store нормализует доменные коллекции clients, users, memberships, groups, services, grants, exceptions, invites, syncStatuses и auditEvents. + +BFF server/dev-server.mjs получил admin guard по группам nodedc:superadmin/nodedc:launcher:admin и backend endpoints для текущего overlay: GET /api/admin/control-plane, clients CRUD, users/profile update, memberships update/delete, groups CRUD, services CRUD/reorder, invites CRUD, access grants/exceptions/user-service, sync retry и GET /api/admin/sync/authentik/plan. + +Backend mutations пишут auditEvents и помечают затронутые объекты как pending sync для target authentik. Endpoint /api/admin/sync/authentik/plan пока dry-run: он показывает users/groups/access projection, но не вызывает Authentik API. + +Frontend admin overlay подключен к server-side admin API через src/shared/api/adminApi.ts. Mutation handlers в src/app/LauncherApp.tsx больше не собирают mock id/audit локально для поддержанных admin операций, а вызывают BFF и принимают обновленный LauncherData из backend response. + +Проверки 2026-05-04: node --check server/dev-server.mjs и server/control-plane-store.mjs проходят; npm run build проходит; npm run test проходит; http://localhost:5173/healthz возвращает oidcConfigured=true; /api/admin/control-plane без session возвращает 401 через localhost и launcher.local.nodedc. Store mutations проверены на временной копии launcher-data.json: create client/group/invite и set user-service access обновляют counts и auditEvents. +""", + ), + text_block( + "launcher", + "Этап 4.5. Live users seed и очистка demo-участников", + """ +Статус: частично реализовано. + +Декоративные MVP-пользователи root@nodedc.local, ivan@romashka.ru, vera@romashka.ru, vasya@romashka.ru, lena@romashka.ru, maria@example.ru и blocked demo-user удаляются из active Launcher seed. Их можно восстановить из backup, но рабочий dev-runtime должен проверяться на живых пользователях. + +Фактический runtime discovery 2026-05-04: +- Authentik содержит dcctouch@gmail.com / DC Touch в группах nodedc:superadmin, nodedc:launcher:admin/user, nodedc:taskmanager:admin/user. +- Plane содержит dcctouch@gmail.com / DC Touch как owner workspace nodedc. +- Plane содержит silver_psih@yahoo.com / dcMASSACRE как member workspace nodedc и owner workspace silver-psih-mo493nm7. + +Перед реализацией нужно подтвердить канонические email/имена: пользователь подтвердил, что silver_psih@yahoo.com — правильная почта Silver Psy. Runtime Plane/Auth показывает dcctouch@gmail.com для текущего superadmin; этот email считается фактическим до отдельного указания заменить его на другой alias. + +Цель этапа: заменить demo-участников Launcher на живой минимальный seed, где супер-админ администрирует клиентов, группы, инвайты и доступы через Launcher UI, а не через прямые правки JSON/Auth/Plane. +""", + ), + checker( + "launcher45", + "Чекер этапа 4.5. Live users seed и очистка demo-участников", + [ + {"text": "Подтвердить канонический email NODE.DC superadmin.", "checked": True}, + {"text": "Подтвердить канонический email Silver Psy / менеджера.", "checked": True}, + {"text": "Сделать backup public/storage/launcher-data.json.", "checked": True}, + {"text": "Создать idempotent seed script для живых users/clients/groups/memberships.", "checked": True}, + {"text": "Удалить декоративных MVP-участников из active Launcher seed.", "checked": True}, + {"text": "Создать живой superadmin в Launcher profile model.", "checked": True}, + {"text": "Создать живого Silver Psy/manager пользователя в Launcher profile model.", "checked": True}, + {"text": "Синхронизировать недостающего пользователя в Authentik через server-side flow.", "checked": True}, + {"text": "Не пересоздавать Plane users и не менять Plane workspace/task связи.", "checked": True}, + {"text": "Показывать все видимые сервисы в Launcher даже без доступа к запуску.", "checked": True}, + {"text": "Считать /api/apps из Launcher control-plane, а не из stale OIDC groups.", "checked": True}, + {"text": "Синхронизировать affected users в Authentik при изменении memberships/groups/grants/exceptions.", "checked": True}, + {"text": "Заменить polling открытых Launcher вкладок на event-driven runtime refresh.", "checked": True}, + {"text": "Сделать atomic write launcher-data.json, чтобы reads не ловили полузаписанный JSON.", "checked": True}, + {"text": "Добавить pending-state на ячейки access matrix, чтобы не было повторных кликов по in-flight mutation.", "checked": True}, + {"text": "Отключить legacy frontend autosave launcher-data.json в authenticated runtime.", "checked": True}, + {"text": "Добавить self-profile settings panel справа.", "checked": True}, + {"text": "Добавить upload avatar в Launcher profile storage.", "checked": True}, + {"text": "Добавить смену пароля через backend Authentik projection.", "checked": True}, + "Проверить Launcher admin UI: клиенты, участники, группы, инвайты, доступы.", + {"text": "Проверить Task Manager SSO для обоих живых пользователей.", "checked": True}, + ], + ), + text_block( + "launcher", + "Реализация этапа 4.5", + """ +В Launcher repo создан scripts/seed-live-control-plane.mjs. Скрипт читает текущий public/storage/launcher-data.json, сохраняет существующий каталог services и заменяет active control-plane домен на один live-клиент DCTOUCH и двух пользователей: dcctouch@gmail.com / DC Touch и silver_psih@yahoo.com / Silver Psy. + +Перед применением создан backup public/storage/backups/launcher-data-live-seed-20260504-152728.json. Active storage после seed содержит 1 client, 2 users, 2 memberships, 2 groups, 3 grants, 0 invites. Silver Psy помещен в группу "Менеджеры" и получает доступ к OPERATIONAL CORE через group grant; DC Touch помещен в группу "Администраторы" и остается platform superadmin. + +Из src/shared/api/mockApi.ts убраны декоративные profileOptions для Ivan/Vera/Vasya/Lena/Maria. src/app/LauncherApp.tsx больше не мапит любого non-superadmin OIDC-пользователя на user_vasya: активный Launcher profile выбирается по Authentik sub/email, затем fallback на user_root для superadmin. + +Fallback src/shared/api/mockData.ts приведен к live seed, чтобы при отсутствии storage не всплывали старые demo-участники. Plane users/workspaces не пересоздавались и не менялись. + +Проверки 2026-05-04: node --check scripts/seed-live-control-plane.mjs, server/dev-server.mjs и server/control-plane-store.mjs проходят; npm run build проходит; npm run test проходит; http://localhost:5173/storage/launcher-data.json возвращает 1 client, 2 users и emails dcctouch@gmail.com/silver_psih@yahoo.com; http://launcher.local.nodedc/healthz возвращает ok и oidcConfigured=true. + +Дополнение 2026-05-04: реализован server-side Authentik provisioning flow. В Launcher repo добавлен server/authentik-sync.mjs: он работает только на backend, берет Authentik API token из server-side env, создает/обновляет пользователя, назначает группы и выставляет пароль только при явном provisioning/generatePassword сценарии. + +BFF server/dev-server.mjs получил POST /api/admin/users для создания пользователя в Launcher control-plane с optional Authentik provisioning и POST /api/admin/users/:userId/provision-authentik для повторной синхронизации существующего Launcher-пользователя. Healthcheck теперь показывает authentikApiConfigured без раскрытия token. + +server/control-plane-store.mjs получил createUser и markUserAuthentikProvisioned: Launcher остается source-of-truth, а Authentik UUID сохраняется в user.authentikUserId после успешной технической проекции. + +Frontend admin overlay получил форму "Создать участника" в разделе "Участники": email/name/role/group -> backend creates Launcher user -> Authentik projection. При генерации временного пароля пароль показывается только одноразово в UI и не пишется в storage/task card. + +Дополнительно исправлена проверка доступа к плитке AGENT CORE/NodeDC: service nodedc больше не считается доступным просто по nodedc:launcher:user. Для сервисных плиток используется service.authentikGroupName или специальная группа конкретного приложения; факт входа в Launcher не должен автоматически открывать все сервисы. + +Дополнение после ручной проверки Silver Psy: buildLauncherServices больше не фильтрует denied-сервисы из rail. Обычный пользователь видит весь видимый каталог приложений, но у сервисов без доступа кнопка запуска заблокирована и показывает "Нет доступа". Hidden-сервисы остаются скрытыми для non-root. + +Добавлен self-profile контур: /api/profile, PATCH /api/profile и POST /api/profile/password доступны любому authenticated пользователю для собственного профиля. Profile settings panel открывается из меню профиля справа налево, может работать одновременно с левым admin overlay, сохраняет name/email/phone/position/avatarUrl и умеет менять пароль через backend Authentik projection. + +Avatar upload использует существующий server-side storage upload и сохраняет ссылку в Launcher profile model. Runtime user в Launcher теперь предпочитает данные Launcher profile model поверх OIDC claims, чтобы изменения имени/аватарки отображались без ожидания следующего login. + +Фактически выполнено: silver_psih@yahoo.com создан в Authentik как active internal user, получил группы nodedc:launcher:user и nodedc:taskmanager:user. dcctouch@gmail.com также синхронизирован по Authentik UUID. public/storage/launcher-data.json теперь содержит Authentik UUID для обоих live-профилей, а syncStatuses для обоих users находятся в state synced. + +Проверки 2026-05-04: node --check server/authentik-sync.mjs, server/dev-server.mjs и server/control-plane-store.mjs проходят; npm run build проходит; npm run test проходит; Authentik API подтверждает наличие silver_psih@yahoo.com с 2 группами; /healthz возвращает authentikApiConfigured=true. Пользователь вручную подтвердил: Silver Psy входит в Launcher, не видит администрирование, открывает OPERATIONAL CORE и в Task Manager видит корректный доступный проект. + +Дополнение после проверки снятия доступа: /api/apps больше не берет app access из stale OIDC groups текущей BFF-сессии. Runtime-доступ рассчитывается из Launcher control-plane через resolveRequiredGroups, а OIDC groups остаются fallback только если пользователь еще не сопоставлен с Launcher profile model. + +Admin mutations теперь проталкивают affected users в Authentik сразу после изменения профиля, memberships, groups, access grants, deny/allow exceptions и user-service matrix. Это закрывает кейс, когда админ снял Silver Psy из доступа к OPERATIONAL CORE, а Authentik еще держал старую nodedc:taskmanager:user группу. + +Открытые Launcher вкладки authenticated пользователей больше не используют polling каждые 5 секунд. BFF держит /api/events через Server-Sent Events, а admin/profile/storage mutations публикуют событие control-plane.updated. Вкладки обновляют /api/me, /api/apps и storage/control-plane snapshot только по факту изменения control-plane состояния. + +Дополнение после ручной проверки access matrix: выявлен race записи public/storage/launcher-data.json. Пока writeFile перезаписывал файл напрямую, параллельные runtime запросы иногда читали пустой/полузаписанный JSON и падали с Unexpected end of JSON input; визуально это выглядело как применение access select только с нескольких попыток. + +server/control-plane-store.mjs переведен на atomic write: запись идет во временный файл в том же storage root, затем rename заменяет launcher-data.json. Это убирает окно, в котором reader может увидеть обнуленный файл. + +В access matrix добавлен pending-state на конкретную ячейку user/service: после выбора роли ячейка сразу показывает выбранное значение и "Сохраняем...", а повторный клик блокируется до завершения mutation. Это убирает многократные ручные попытки и race между несколькими одинаковыми запросами. + +Дополнение по итогам повторного теста: найден второй источник отката access matrix. Legacy frontend autosave сохранял весь LauncherData обратно в /api/storage/data после setData и мог перезаписать свежий backend-result старым состоянием из открытой вкладки. src/app/LauncherApp.tsx больше не вызывает persistLauncherData; authenticated runtime пишет control-plane только через admin/profile API mutations. /api/storage/data дополнительно закрыт requireLauncherAdmin и оставлен только как служебный dev endpoint. + +Текущий live-sync выполнен вручную после исправления: dcctouch@gmail.com имеет nodedc:launcher:user, nodedc:superadmin, nodedc:launcher:admin, nodedc:taskmanager:admin, nodedc:taskmanager:user; silver_psih@yahoo.com имеет nodedc:launcher:user и 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], "description_html": html( "Интеграция 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": [ text_block( @@ -434,6 +590,8 @@ Admin API должен управлять пользователями и гру 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( @@ -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. -Открытый продуктовый 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( @@ -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. """, ), + 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 без логина.", "Проверить скрытие Task Manager без group access.", "Проверить deny на прямой task.local.nodedc без group access.", + "Проверить отзыв уже открытой downstream-сессии после снятия доступа.", "Проверить успешный вход пользователя с access.", "Проверить старого Plane admin после OIDC migration.", "Проверить deactivate user.",