ФУНКЦИИ - МЕЖПРОЕКТНАЯ КОММУНИКАЦИЯ: 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_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:-}

View File

@ -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

View File

@ -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}))

View File

@ -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.",