From 55ab952ae8de7f49228f7494a523dfa5d5b156b3 Mon Sep 17 00:00:00 2001 From: DCCONSTRUCTIONS Date: Sun, 17 May 2026 21:49:45 +0300 Subject: [PATCH] FEAT - LAUNCHER: wire Engine access roles --- server/authentik-sync.mjs | 38 +++++++++++++++++++++++++++++++++- server/control-plane-store.mjs | 27 ++++++++++++++++++++++++ server/dev-server.mjs | 21 +++++++++++++++++-- src/shared/api/mockData.ts | 12 ++++++----- 4 files changed, 90 insertions(+), 8 deletions(-) diff --git a/server/authentik-sync.mjs b/server/authentik-sync.mjs index 05648da..e03cb37 100644 --- a/server/authentik-sync.mjs +++ b/server/authentik-sync.mjs @@ -6,7 +6,14 @@ const platformGroups = { launcherUser: "nodedc:launcher:user", taskManagerAdmin: "nodedc:taskmanager:admin", taskManagerUser: "nodedc:taskmanager:user", + engineAdmin: "nodedc:engine:admin", + engineEditor: "nodedc:engine:editor", + engineViewer: "nodedc:engine:viewer", + engineLegacyAdmin: "nodedc_admin", + engineLegacyEditor: "nodedc_editor", + engineLegacyViewer: "nodedc_viewer", }; +const engineServiceSlugs = new Set(["nodedc", "engine", "nodedc-engine"]); const publicPoolClientId = "client_public_pool"; const publicPoolClient = { id: publicPoolClientId, @@ -200,6 +207,7 @@ export function resolveRequiredGroups(data, user) { groupNames.add(platformGroups.launcherAdmin); groupNames.add(platformGroups.taskManagerAdmin); groupNames.add(platformGroups.taskManagerUser); + addGroups(groupNames, resolveEngineRoleGroups("admin")); return [...groupNames]; } @@ -219,7 +227,9 @@ export function resolveRequiredGroups(data, user) { continue; } - if (service.slug === "task-manager") { + if (isEngineService(service)) { + addGroups(groupNames, resolveEngineRoleGroups(access.appRole)); + } else if (service.slug === "task-manager") { groupNames.add(platformGroups.taskManagerUser); if (access.appRole === "admin" || access.appRole === "owner") { @@ -234,6 +244,32 @@ export function resolveRequiredGroups(data, user) { return [...groupNames]; } +function isEngineService(service) { + return ( + engineServiceSlugs.has(service.slug) || + engineServiceSlugs.has(service.authentikApplicationSlug) || + service.id === "service_nodedc" + ); +} + +function resolveEngineRoleGroups(appRole) { + if (appRole === "admin" || appRole === "owner") { + return [platformGroups.engineAdmin, platformGroups.engineLegacyAdmin]; + } + + if (appRole === "viewer") { + return [platformGroups.engineViewer, platformGroups.engineLegacyViewer]; + } + + return [platformGroups.engineEditor, platformGroups.engineLegacyEditor]; +} + +function addGroups(target, groups) { + for (const group of groups) { + target.add(group); + } +} + function getUserRuntimeClients(data, userId) { const clients = [...data.clients]; const hasPublicPoolMembership = data.memberships.some( diff --git a/server/control-plane-store.mjs b/server/control-plane-store.mjs index 96ff929..51d7706 100644 --- a/server/control-plane-store.mjs +++ b/server/control-plane-store.mjs @@ -37,6 +37,14 @@ const accessRequestStatuses = new Set(["new", "approved", "rejected"]); const taskerInviteRequestStatuses = new Set(["new", "approved", "rejected", "cancelled"]); const taskManagerInviteRoles = new Set(["guest", "member", "admin"]); const publicPoolClientId = "client_public_pool"; +const engineAuthentikGroups = [ + "nodedc:engine:admin", + "nodedc:engine:editor", + "nodedc:engine:viewer", + "nodedc_admin", + "nodedc_editor", + "nodedc_viewer", +]; const publicPoolClient = { id: publicPoolClientId, type: "person", @@ -1811,6 +1819,7 @@ export function createControlPlaneStore({ projectRoot }) { "nodedc:superadmin", "nodedc:launcher:admin", "nodedc:launcher:user", + ...engineAuthentikGroups, ...data.services.flatMap((service) => (service.authentikGroupName ? [service.authentikGroupName] : [])), ...data.groups.map((group) => `client:${group.clientId}:group:${slugify(group.name)}`), ], @@ -1931,6 +1940,7 @@ function normalizeData(payload) { ...client, integrations: normalizeClientIntegrations(client.integrations), })); + data.services = data.services.map(normalizeService); data.accessRequests = data.accessRequests.map(normalizeAccessRequest).filter(Boolean); data.revokedAccounts = data.revokedAccounts.map(normalizeRevokedAccount).filter(Boolean); data.serviceModuleEntitlements = data.serviceModuleEntitlements.map(normalizeServiceModuleEntitlement).filter(Boolean); @@ -1938,6 +1948,23 @@ function normalizeData(payload) { return data; } +function normalizeService(service) { + if (typeof service !== "object" || service === null) return service; + if (service.id !== "service_nodedc" && service.slug !== "nodedc" && service.authentikApplicationSlug !== "nodedc") return service; + + return { + ...service, + title: service.title === "AGENT CORE" || service.title === "NodeDC" ? "ENGINE" : service.title, + url: service.url === "https://nodedc.ru/" || service.url === "https://dev.handhdc.ru/sso/launch" ? "https://engine.nodedc.ru/" : service.url, + launchUrl: + service.launchUrl === "https://nodedc.ru/" || service.launchUrl === "https://dev.handhdc.ru/sso/launch" + ? "https://engine.nodedc.ru/" + : service.launchUrl, + authentikApplicationSlug: service.authentikApplicationSlug === "nodedc" ? "nodedc-engine" : service.authentikApplicationSlug, + authentikGroupName: service.authentikGroupName === "service-nodedc" ? "nodedc:engine:viewer" : service.authentikGroupName, + }; +} + function normalizeServiceModuleEntitlement(payload) { if (typeof payload !== "object" || payload === null) return null; const clientId = nullableString(payload.clientId); diff --git a/server/dev-server.mjs b/server/dev-server.mjs index 38c6b2f..2f8fc3b 100644 --- a/server/dev-server.mjs +++ b/server/dev-server.mjs @@ -2134,7 +2134,7 @@ function getAppCatalog() { const launcherData = readLauncherData(); const services = Array.isArray(launcherData?.services) ? launcherData.services : []; const serviceCatalog = services.map((service) => { - const specialGroups = specialRequiredGroups(service.slug); + const specialGroups = specialRequiredGroups(service); const requiredGroups = specialGroups.length ? specialGroups : service.authentikGroupName @@ -2175,9 +2175,26 @@ function getAppCatalog() { ]; } -function specialRequiredGroups(slug) { +const engineRequiredGroups = [ + "nodedc:engine:admin", + "nodedc:engine:editor", + "nodedc:engine:viewer", + "nodedc_admin", + "nodedc_editor", + "nodedc_viewer", +]; + +function specialRequiredGroups(serviceOrSlug) { + const slug = typeof serviceOrSlug === "string" ? serviceOrSlug : serviceOrSlug?.slug; + const applicationSlug = typeof serviceOrSlug === "string" ? null : serviceOrSlug?.authentikApplicationSlug; + const serviceId = typeof serviceOrSlug === "string" ? null : serviceOrSlug?.id; + if (slug === "launcher") return ["nodedc:launcher:admin", "nodedc:launcher:user"]; if (slug === "task-manager") return ["nodedc:taskmanager:admin", "nodedc:taskmanager:user"]; + if (slug === "nodedc" || slug === "engine" || slug === "nodedc-engine" || applicationSlug === "nodedc-engine" || serviceId === "service_nodedc") { + return engineRequiredGroups; + } + return []; } diff --git a/src/shared/api/mockData.ts b/src/shared/api/mockData.ts index be672b5..3f61cd9 100644 --- a/src/shared/api/mockData.ts +++ b/src/shared/api/mockData.ts @@ -77,19 +77,19 @@ export const mockServices: Service[] = [ { id: "service_nodedc", slug: "nodedc", - title: "NodeDC", + title: "ENGINE", subtitle: "Агентная платформа", description: "Сборка, запуск и мониторинг агентных workflow.", fullDescription: "NodeDC используется для настройки агентных процессов, визуальной оркестрации, интеграций и runtime-мониторинга.", - url: "https://dev.handhdc.ru/sso/launch", - launchUrl: "https://dev.handhdc.ru/sso/launch", + url: "https://engine.nodedc.ru/", + launchUrl: "https://engine.nodedc.ru/", accentColor: "#B5FF5A", fallbackGradient: "linear-gradient(128deg, rgba(181, 255, 90, 0.84), rgba(37, 58, 36, 0.86) 42%, #0A0D10 82%)", status: "active", order: 10, - authentikApplicationSlug: "nodedc", - authentikGroupName: "service-nodedc", + authentikApplicationSlug: "nodedc-engine", + authentikGroupName: "nodedc:engine:viewer", createdAt: "2026-04-01T10:00:00Z", updatedAt: now, }, @@ -207,6 +207,8 @@ export const mockGrants: ServiceGrant[] = [ grant("grant_dctouch_task_admins", "service_task_manager", "group", "group_dctouch_admins", "admin"), grant("grant_dctouch_task_managers", "service_task_manager", "group", "group_dctouch_managers", "member"), grant("grant_dctouch_nodedc_admins", "service_nodedc", "group", "group_dctouch_admins", "admin"), + grant("grant_engine_user_silver_psih_yahoo_com", "service_nodedc", "user", "user_silver_psih", "member"), + grant("grant_engine_user_constr_dc_yahoo_com", "service_nodedc", "user", "user_constr_dc_yahoo_com", "viewer"), ]; export const mockExceptions: ServiceAccessException[] = [];