АРХ - МЕЖПРОЕКТНАЯ КОММУНИКАЦИЯ: managedBy policy Launcher
This commit is contained in:
parent
fd1d5baef3
commit
01e0988031
|
|
@ -27,6 +27,7 @@ const appRoles = new Set(["viewer", "member", "admin", "owner"]);
|
|||
const grantStatuses = new Set(["active", "disabled"]);
|
||||
const exceptionTypes = new Set(["deny", "allow"]);
|
||||
const serviceStatuses = new Set(["active", "maintenance", "hidden", "disabled"]);
|
||||
const taskManagerWorkspaceManagedByValues = new Set(["launcher", "tasker"]);
|
||||
const defaultSettings = {
|
||||
brand: {
|
||||
logoLinkUrl: "/",
|
||||
|
|
@ -177,6 +178,7 @@ export function createControlPlaneStore({ projectRoot }) {
|
|||
workspaceSlug: workspace.slug ?? payload?.workspaceSlug,
|
||||
workspaceName: workspace.name ?? payload?.workspaceName ?? null,
|
||||
role: normalizeTaskManagerMembershipRole(payload?.role),
|
||||
managedBy: payload?.managedBy,
|
||||
planeUserId: membership.member?.id ?? null,
|
||||
planeRole: typeof membership.role === "number" ? membership.role : null,
|
||||
});
|
||||
|
|
@ -234,15 +236,16 @@ export function createControlPlaneStore({ projectRoot }) {
|
|||
upsertTaskManagerProjectMembership(data, {
|
||||
clientId: client.id,
|
||||
userId: user.id,
|
||||
workspaceSlug: workspace.slug ?? payload?.workspaceSlug,
|
||||
workspaceName: workspace.name ?? payload?.workspaceName ?? null,
|
||||
projectId: project.id ?? payload?.projectId,
|
||||
projectIdentifier: project.identifier ?? payload?.projectIdentifier ?? null,
|
||||
projectName: project.name ?? payload?.projectName ?? null,
|
||||
role: normalizeTaskManagerMembershipRole(payload?.role),
|
||||
planeUserId: membership.member?.id ?? null,
|
||||
planeRole: typeof membership.role === "number" ? membership.role : null,
|
||||
});
|
||||
workspaceSlug: workspace.slug ?? payload?.workspaceSlug,
|
||||
workspaceName: workspace.name ?? payload?.workspaceName ?? null,
|
||||
projectId: project.id ?? payload?.projectId,
|
||||
projectIdentifier: project.identifier ?? payload?.projectIdentifier ?? null,
|
||||
projectName: project.name ?? payload?.projectName ?? null,
|
||||
role: normalizeTaskManagerMembershipRole(payload?.role),
|
||||
managedBy: payload?.managedBy,
|
||||
planeUserId: membership.member?.id ?? null,
|
||||
planeRole: typeof membership.role === "number" ? membership.role : null,
|
||||
});
|
||||
|
||||
addAuditEvent(data, actor, {
|
||||
action: "Назначен Tasker project",
|
||||
|
|
@ -1252,11 +1255,12 @@ function normalizeTaskManagerWorkspaces(taskManager, fallbackTaskManager = {}) {
|
|||
slug,
|
||||
name: nullableStringWithFallback(item.name, null),
|
||||
isPrimary: item.isPrimary === true,
|
||||
managedBy: normalizeTaskManagerWorkspaceManagedBy(item.managedBy),
|
||||
});
|
||||
}
|
||||
|
||||
if (legacySlug && !bySlug.has(legacySlug)) {
|
||||
bySlug.set(legacySlug, { slug: legacySlug, name: legacyName, isPrimary: true });
|
||||
bySlug.set(legacySlug, { slug: legacySlug, name: legacyName, isPrimary: true, managedBy: "launcher" });
|
||||
}
|
||||
|
||||
const workspaces = [...bySlug.values()];
|
||||
|
|
@ -1274,10 +1278,15 @@ function normalizeTaskManagerWorkspaces(taskManager, fallbackTaskManager = {}) {
|
|||
slug: workspace.slug,
|
||||
name: workspace.name ?? null,
|
||||
isPrimary,
|
||||
managedBy: normalizeTaskManagerWorkspaceManagedBy(workspace.managedBy),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function normalizeTaskManagerWorkspaceManagedBy(value) {
|
||||
return taskManagerWorkspaceManagedByValues.has(value) ? value : "launcher";
|
||||
}
|
||||
|
||||
function normalizeTaskManagerMembershipRole(value) {
|
||||
return value === "guest" || value === "admin" || value === "member" ? value : "member";
|
||||
}
|
||||
|
|
@ -1294,6 +1303,7 @@ function upsertTaskManagerMembership(data, payload) {
|
|||
workspaceSlug,
|
||||
workspaceName: nullableStringWithFallback(payload.workspaceName, existingMembership?.workspaceName ?? null),
|
||||
role: normalizeTaskManagerMembershipRole(payload.role),
|
||||
managedBy: normalizeTaskManagerWorkspaceManagedBy(payload.managedBy ?? existingMembership?.managedBy),
|
||||
planeUserId: nullableStringWithFallback(payload.planeUserId, existingMembership?.planeUserId ?? null),
|
||||
planeRole: typeof payload.planeRole === "number" ? payload.planeRole : (existingMembership?.planeRole ?? null),
|
||||
updatedAt: isoNow(),
|
||||
|
|
@ -1330,6 +1340,7 @@ function upsertTaskManagerProjectMembership(data, payload) {
|
|||
projectIdentifier: nullableStringWithFallback(payload.projectIdentifier, existingMembership?.projectIdentifier ?? null),
|
||||
projectName: nullableStringWithFallback(payload.projectName, existingMembership?.projectName ?? null),
|
||||
role: normalizeTaskManagerMembershipRole(payload.role),
|
||||
managedBy: normalizeTaskManagerWorkspaceManagedBy(payload.managedBy ?? existingMembership?.managedBy),
|
||||
planeUserId: nullableStringWithFallback(payload.planeUserId, existingMembership?.planeUserId ?? null),
|
||||
planeRole: typeof payload.planeRole === "number" ? payload.planeRole : (existingMembership?.planeRole ?? null),
|
||||
updatedAt: isoNow(),
|
||||
|
|
|
|||
|
|
@ -374,7 +374,7 @@ app.post("/api/internal/access/check", (req, res) => {
|
|||
const app = getAppsForUser(groups).find((candidate) => candidate.slug === serviceSlug);
|
||||
const allowed = Boolean(app?.hasAccess);
|
||||
const workspacePolicy =
|
||||
serviceSlug === "task-manager" ? resolveTaskManagerWorkspacePolicy(snapshot.data, groups, allowed) : null;
|
||||
serviceSlug === "task-manager" ? resolveTaskManagerWorkspacePolicy(snapshot.data, groups, allowed, user) : null;
|
||||
|
||||
res.json({
|
||||
ok: true,
|
||||
|
|
@ -569,6 +569,7 @@ app.post("/api/admin/task-manager/workspace-memberships/ensure", requireLauncher
|
|||
|
||||
const membership = snapshot.data.memberships.find((candidate) => candidate.clientId === client.id && candidate.userId === user.id);
|
||||
const workspaceSlug = normalizeOptionalText(req.body?.workspaceSlug) ?? client.integrations?.taskManager?.workspaceSlug ?? null;
|
||||
const workspaceManagedBy = resolveTaskManagerWorkspaceManagedByForClient(client, workspaceSlug);
|
||||
|
||||
if (!workspaceSlug) {
|
||||
res.status(400).json({ ok: false, error: "task_manager_workspace_not_configured" });
|
||||
|
|
@ -585,6 +586,7 @@ app.post("/api/admin/task-manager/workspace-memberships/ensure", requireLauncher
|
|||
avatarUrl: resolveUserAvatarPublicUrl(user),
|
||||
role,
|
||||
companyRole: membership?.role ?? null,
|
||||
managedBy: workspaceManagedBy,
|
||||
setLastWorkspace: req.body?.setLastWorkspace !== false,
|
||||
},
|
||||
});
|
||||
|
|
@ -595,6 +597,7 @@ app.post("/api/admin/task-manager/workspace-memberships/ensure", requireLauncher
|
|||
userId: user.id,
|
||||
workspaceSlug,
|
||||
role,
|
||||
managedBy: workspaceManagedBy,
|
||||
taskManager,
|
||||
},
|
||||
req.nodedcSession.user
|
||||
|
|
@ -641,17 +644,19 @@ app.post("/api/admin/task-manager/workspace-memberships/remove", requireLauncher
|
|||
email: user.email,
|
||||
subject: user.authentikUserId ?? undefined,
|
||||
avatarUrl: resolveUserAvatarPublicUrl(user),
|
||||
role: "admin",
|
||||
companyRole: membership.role,
|
||||
setLastWorkspace: false,
|
||||
},
|
||||
});
|
||||
role: "admin",
|
||||
companyRole: membership.role,
|
||||
managedBy: resolveTaskManagerWorkspaceManagedByForClient(client, workspaceSlug),
|
||||
setLastWorkspace: false,
|
||||
},
|
||||
});
|
||||
const result = await controlPlaneStore.recordTaskManagerWorkspaceMembership(
|
||||
{
|
||||
clientId: client.id,
|
||||
userId: user.id,
|
||||
workspaceSlug,
|
||||
role: "admin",
|
||||
managedBy: resolveTaskManagerWorkspaceManagedByForClient(client, workspaceSlug),
|
||||
taskManager,
|
||||
},
|
||||
req.nodedcSession.user
|
||||
|
|
@ -707,6 +712,7 @@ app.post("/api/admin/task-manager/project-memberships/ensure", requireLauncherAd
|
|||
const workspaceSlug = normalizeOptionalText(req.body?.workspaceSlug);
|
||||
const projectId = normalizeOptionalText(req.body?.projectId);
|
||||
const role = normalizeTaskManagerRole(req.body?.role);
|
||||
const workspaceManagedBy = resolveTaskManagerWorkspaceManagedByForClient(client, workspaceSlug);
|
||||
|
||||
if (!workspaceSlug) {
|
||||
res.status(400).json({ ok: false, error: "task_manager_workspace_not_configured" });
|
||||
|
|
@ -732,6 +738,7 @@ app.post("/api/admin/task-manager/project-memberships/ensure", requireLauncherAd
|
|||
subject: user.authentikUserId ?? undefined,
|
||||
avatarUrl: resolveUserAvatarPublicUrl(user),
|
||||
role,
|
||||
managedBy: workspaceManagedBy,
|
||||
setLastWorkspace: false,
|
||||
},
|
||||
});
|
||||
|
|
@ -743,6 +750,7 @@ app.post("/api/admin/task-manager/project-memberships/ensure", requireLauncherAd
|
|||
workspaceSlug,
|
||||
projectId,
|
||||
role,
|
||||
managedBy: workspaceManagedBy,
|
||||
taskManager,
|
||||
},
|
||||
req.nodedcSession.user
|
||||
|
|
@ -1632,6 +1640,72 @@ function resolveTaskManagerRoleForMembership(role) {
|
|||
return role === "client_owner" || role === "client_admin" ? "admin" : "member";
|
||||
}
|
||||
|
||||
function normalizeTaskManagerWorkspaceManagedBy(value) {
|
||||
return value === "tasker" ? "tasker" : "launcher";
|
||||
}
|
||||
|
||||
function getClientTaskManagerWorkspaces(client) {
|
||||
const taskManager = client?.integrations?.taskManager;
|
||||
const workspaces = Array.isArray(taskManager?.workspaces) ? taskManager.workspaces : [];
|
||||
const legacySlug = normalizeOptionalText(taskManager?.workspaceSlug);
|
||||
|
||||
if (!legacySlug || workspaces.some((workspace) => normalizeOptionalText(workspace?.slug) === legacySlug)) {
|
||||
return workspaces;
|
||||
}
|
||||
|
||||
return [
|
||||
...workspaces,
|
||||
{
|
||||
slug: legacySlug,
|
||||
name: normalizeOptionalText(taskManager?.workspaceName),
|
||||
isPrimary: true,
|
||||
managedBy: "launcher",
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
function resolveTaskManagerWorkspaceBinding(client, workspaceSlug) {
|
||||
const normalizedWorkspaceSlug = normalizeOptionalText(workspaceSlug);
|
||||
if (!normalizedWorkspaceSlug) return null;
|
||||
|
||||
return (
|
||||
getClientTaskManagerWorkspaces(client).find((workspace) => normalizeOptionalText(workspace?.slug) === normalizedWorkspaceSlug) ?? null
|
||||
);
|
||||
}
|
||||
|
||||
function resolveTaskManagerWorkspaceManagedByForClient(client, workspaceSlug) {
|
||||
return normalizeTaskManagerWorkspaceManagedBy(resolveTaskManagerWorkspaceBinding(client, workspaceSlug)?.managedBy);
|
||||
}
|
||||
|
||||
function resolveTaskManagerWorkspaceAssignments(data, user) {
|
||||
if (!user?.id) return [];
|
||||
|
||||
const bySlug = new Map();
|
||||
for (const membership of data.taskManagerMemberships ?? []) {
|
||||
if (membership.userId !== user.id) continue;
|
||||
const workspaceSlug = normalizeOptionalText(membership.workspaceSlug);
|
||||
if (!workspaceSlug) continue;
|
||||
|
||||
const client = data.clients.find((candidate) => candidate.id === membership.clientId);
|
||||
const managedBy = normalizeTaskManagerWorkspaceManagedBy(
|
||||
membership.managedBy ?? resolveTaskManagerWorkspaceBinding(client, workspaceSlug)?.managedBy
|
||||
);
|
||||
const current = bySlug.get(workspaceSlug);
|
||||
if (current && current.managedBy === "launcher") continue;
|
||||
|
||||
bySlug.set(workspaceSlug, {
|
||||
slug: workspaceSlug,
|
||||
name: normalizeOptionalText(membership.workspaceName ?? resolveTaskManagerWorkspaceBinding(client, workspaceSlug)?.name),
|
||||
managedBy,
|
||||
clientId: client?.id ?? membership.clientId ?? null,
|
||||
clientName: client?.name ?? null,
|
||||
role: normalizeTaskManagerRole(membership.role) ?? "member",
|
||||
});
|
||||
}
|
||||
|
||||
return [...bySlug.values()];
|
||||
}
|
||||
|
||||
function createServiceHandoff(serviceSlug, user) {
|
||||
pruneExpiredServiceHandoffs();
|
||||
|
||||
|
|
@ -1679,15 +1753,21 @@ function pruneExpiredServiceHandoffs() {
|
|||
}
|
||||
}
|
||||
|
||||
function resolveTaskManagerWorkspacePolicy(data, groups, hasTaskManagerAccess) {
|
||||
function resolveTaskManagerWorkspacePolicy(data, groups, hasTaskManagerAccess, user) {
|
||||
const mode = data.settings?.taskManager?.workspaceCreationPolicy ?? "any_authorized_user";
|
||||
const groupSet = new Set(groups);
|
||||
const isSuperAdmin = groupSet.has("nodedc:superadmin");
|
||||
const isTaskManagerAdmin = groupSet.has("nodedc:taskmanager:admin");
|
||||
const workspaces = resolveTaskManagerWorkspaceAssignments(data, user);
|
||||
const hasLauncherManagedWorkspace = workspaces.some((workspace) => workspace.managedBy === "launcher");
|
||||
const defaultManagedBy = hasLauncherManagedWorkspace && !isSuperAdmin ? "launcher" : "tasker";
|
||||
|
||||
if (!hasTaskManagerAccess) {
|
||||
return {
|
||||
mode,
|
||||
managedBy: defaultManagedBy,
|
||||
defaultManagedBy,
|
||||
workspaces,
|
||||
canCreateWorkspace: false,
|
||||
reason: "Нет доступа к Operational Core.",
|
||||
};
|
||||
|
|
@ -1696,14 +1776,31 @@ function resolveTaskManagerWorkspacePolicy(data, groups, hasTaskManagerAccess) {
|
|||
if (mode === "disabled") {
|
||||
return {
|
||||
mode,
|
||||
managedBy: defaultManagedBy,
|
||||
defaultManagedBy,
|
||||
workspaces,
|
||||
canCreateWorkspace: false,
|
||||
reason: "Создание рабочих пространств отключено на уровне платформы.",
|
||||
};
|
||||
}
|
||||
|
||||
if (hasLauncherManagedWorkspace && !isSuperAdmin) {
|
||||
return {
|
||||
mode,
|
||||
managedBy: "launcher",
|
||||
defaultManagedBy: "launcher",
|
||||
workspaces,
|
||||
canCreateWorkspace: false,
|
||||
reason: "Рабочие пространства этого пользователя управляются через Launcher.",
|
||||
};
|
||||
}
|
||||
|
||||
if (mode === "task_admins_only" && !isSuperAdmin && !isTaskManagerAdmin) {
|
||||
return {
|
||||
mode,
|
||||
managedBy: defaultManagedBy,
|
||||
defaultManagedBy,
|
||||
workspaces,
|
||||
canCreateWorkspace: false,
|
||||
reason: "Создание рабочих пространств доступно только администраторам Operational Core.",
|
||||
};
|
||||
|
|
@ -1711,6 +1808,9 @@ function resolveTaskManagerWorkspacePolicy(data, groups, hasTaskManagerAccess) {
|
|||
|
||||
return {
|
||||
mode,
|
||||
managedBy: "tasker",
|
||||
defaultManagedBy: "tasker",
|
||||
workspaces,
|
||||
canCreateWorkspace: true,
|
||||
reason: "Создание рабочих пространств разрешено платформенной policy.",
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,10 +1,12 @@
|
|||
export type ClientType = "company" | "person";
|
||||
export type ClientStatus = "active" | "suspended" | "demo" | "expired";
|
||||
export type TaskManagerWorkspaceManagedBy = "launcher" | "tasker";
|
||||
|
||||
export interface ClientTaskManagerWorkspaceBinding {
|
||||
slug: string;
|
||||
name?: string | null;
|
||||
isPrimary?: boolean;
|
||||
managedBy?: TaskManagerWorkspaceManagedBy;
|
||||
}
|
||||
|
||||
export interface Client {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import type { ServiceAccessException, ServiceAppRole, ServiceGrant } from "../../entities/access/types";
|
||||
import type { Client } from "../../entities/client/types";
|
||||
import type { Client, TaskManagerWorkspaceManagedBy } from "../../entities/client/types";
|
||||
import type { Invite } from "../../entities/invite/types";
|
||||
import type { Service } from "../../entities/service/types";
|
||||
import type { SyncStatus } from "../../entities/sync/types";
|
||||
|
|
@ -35,6 +35,7 @@ export interface TaskManagerWorkspaceSummary {
|
|||
id: string;
|
||||
slug: string;
|
||||
name: string;
|
||||
managedBy?: TaskManagerWorkspaceManagedBy;
|
||||
ownerEmail: string | null;
|
||||
memberCount: number;
|
||||
projects?: TaskManagerProjectSummary[];
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { computeEffectiveAccess } from "../../entities/access/computeEffectiveAccess";
|
||||
import type { EffectiveAccessResult, ServiceAccessException, ServiceGrant } from "../../entities/access/types";
|
||||
import type { Client } from "../../entities/client/types";
|
||||
import type { Client, TaskManagerWorkspaceManagedBy } from "../../entities/client/types";
|
||||
import type { Invite } from "../../entities/invite/types";
|
||||
import { getServiceLaunchLink } from "../../entities/service/links";
|
||||
import type { LauncherServiceView, Service } from "../../entities/service/types";
|
||||
|
|
@ -72,6 +72,7 @@ export interface TaskManagerMembershipAssignment {
|
|||
workspaceSlug: string;
|
||||
workspaceName?: string | null;
|
||||
role: "guest" | "member" | "admin";
|
||||
managedBy?: TaskManagerWorkspaceManagedBy;
|
||||
planeUserId?: string | null;
|
||||
planeRole?: number | null;
|
||||
updatedAt: string;
|
||||
|
|
@ -87,6 +88,7 @@ export interface TaskManagerProjectMembershipAssignment {
|
|||
projectIdentifier?: string | null;
|
||||
projectName?: string | null;
|
||||
role: "guest" | "member" | "admin";
|
||||
managedBy?: TaskManagerWorkspaceManagedBy;
|
||||
planeUserId?: string | null;
|
||||
planeRole?: number | null;
|
||||
updatedAt: string;
|
||||
|
|
|
|||
|
|
@ -989,6 +989,7 @@ function getClientTaskManagerWorkspaces(client: Client): ClientTaskManagerWorksp
|
|||
slug: workspace.slug,
|
||||
name: workspace.name ?? null,
|
||||
isPrimary: workspace.isPrimary === true,
|
||||
managedBy: workspace.managedBy ?? "launcher",
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -997,6 +998,7 @@ function getClientTaskManagerWorkspaces(client: Client): ClientTaskManagerWorksp
|
|||
slug: taskManager.workspaceSlug,
|
||||
name: taskManager.workspaceName ?? null,
|
||||
isPrimary: true,
|
||||
managedBy: "launcher",
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -1055,6 +1057,7 @@ function normalizeClientTaskManagerWorkspaceDraft(workspaces: ClientTaskManagerW
|
|||
slug: workspace.slug,
|
||||
name: workspace.name ?? null,
|
||||
isPrimary: workspace.isPrimary === true,
|
||||
managedBy: workspace.managedBy ?? "launcher",
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -1781,6 +1784,7 @@ function ClientEditorModal({
|
|||
slug: workspace.slug,
|
||||
name: workspace.name,
|
||||
isPrimary: currentWorkspaces.length === 0,
|
||||
managedBy: "launcher",
|
||||
},
|
||||
];
|
||||
|
||||
|
|
@ -1919,7 +1923,7 @@ function ClientEditorModal({
|
|||
<span>
|
||||
<strong>{workspace.name}</strong>
|
||||
<small>
|
||||
{workspace.slug} · {workspace.memberCount} участников
|
||||
{workspace.slug} · {workspace.memberCount} участников · Launcher-managed
|
||||
</small>
|
||||
</span>
|
||||
{selected ? (
|
||||
|
|
|
|||
Loading…
Reference in New Issue