ФУНКЦИИ - МЕЖПРОЕКТНАЯ КОММУНИКАЦИЯ: policy создания workspace в Launcher

This commit is contained in:
DCCONSTRUCTIONS 2026-05-06 09:38:14 +03:00
parent 168010f05e
commit 6b002ec176
4 changed files with 126 additions and 4 deletions

View File

@ -29,6 +29,9 @@ const defaultSettings = {
brand: { brand: {
logoLinkUrl: "/", logoLinkUrl: "/",
}, },
taskManager: {
workspaceCreationPolicy: "any_authorized_user",
},
}; };
export function createControlPlaneStore({ projectRoot }) { export function createControlPlaneStore({ projectRoot }) {
@ -195,6 +198,10 @@ export function createControlPlaneStore({ projectRoot }) {
...(data.settings?.brand ?? {}), ...(data.settings?.brand ?? {}),
...(patch.brand ?? {}), ...(patch.brand ?? {}),
}, },
taskManager: {
...(data.settings?.taskManager ?? {}),
...(patch.taskManager ?? {}),
},
}); });
data.settings = settings; data.settings = settings;
@ -203,7 +210,7 @@ export function createControlPlaneStore({ projectRoot }) {
objectType: "settings", objectType: "settings",
objectName: "Brand settings", objectName: "Brand settings",
result: "success", result: "success",
details: `Logo link: ${settings.brand.logoLinkUrl}`, details: `Logo link: ${settings.brand.logoLinkUrl}; Tasker workspace policy: ${settings.taskManager.workspaceCreationPolicy}`,
}); });
await writeData(data); await writeData(data);
@ -1051,11 +1058,19 @@ function normalizeData(payload) {
function normalizeSettings(payload) { function normalizeSettings(payload) {
const settings = typeof payload === "object" && payload !== null ? payload : {}; const settings = typeof payload === "object" && payload !== null ? payload : {};
const brand = typeof settings.brand === "object" && settings.brand !== null ? settings.brand : {}; const brand = typeof settings.brand === "object" && settings.brand !== null ? settings.brand : {};
const taskManager = typeof settings.taskManager === "object" && settings.taskManager !== null ? settings.taskManager : {};
return { return {
brand: { brand: {
logoLinkUrl: optionalString(brand.logoLinkUrl, defaultSettings.brand.logoLinkUrl), logoLinkUrl: optionalString(brand.logoLinkUrl, defaultSettings.brand.logoLinkUrl),
}, },
taskManager: {
workspaceCreationPolicy: pickEnum(
taskManager.workspaceCreationPolicy,
new Set(["any_authorized_user", "task_admins_only", "disabled"]),
defaultSettings.taskManager.workspaceCreationPolicy
),
},
}; };
} }

View File

@ -373,6 +373,8 @@ app.post("/api/internal/access/check", (req, res) => {
const groups = resolveRequiredGroups(snapshot.data, user); const groups = resolveRequiredGroups(snapshot.data, user);
const app = getAppsForUser(groups).find((candidate) => candidate.slug === serviceSlug); const app = getAppsForUser(groups).find((candidate) => candidate.slug === serviceSlug);
const allowed = Boolean(app?.hasAccess); const allowed = Boolean(app?.hasAccess);
const workspacePolicy =
serviceSlug === "task-manager" ? resolveTaskManagerWorkspacePolicy(snapshot.data, groups, allowed) : null;
res.json({ res.json({
ok: true, ok: true,
@ -381,6 +383,7 @@ app.post("/api/internal/access/check", (req, res) => {
serviceSlug, serviceSlug,
groups, groups,
matchedGroups: app?.matchedGroups ?? [], matchedGroups: app?.matchedGroups ?? [],
workspacePolicy,
user: { user: {
id: user.id, id: user.id,
email: user.email, email: user.email,
@ -1234,6 +1237,43 @@ function pruneExpiredServiceHandoffs() {
} }
} }
function resolveTaskManagerWorkspacePolicy(data, groups, hasTaskManagerAccess) {
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");
if (!hasTaskManagerAccess) {
return {
mode,
canCreateWorkspace: false,
reason: "Нет доступа к Operational Core.",
};
}
if (mode === "disabled") {
return {
mode,
canCreateWorkspace: false,
reason: "Создание рабочих пространств отключено на уровне платформы.",
};
}
if (mode === "task_admins_only" && !isSuperAdmin && !isTaskManagerAdmin) {
return {
mode,
canCreateWorkspace: false,
reason: "Создание рабочих пространств доступно только администраторам Operational Core.",
};
}
return {
mode,
canCreateWorkspace: true,
reason: "Создание рабочих пространств разрешено платформенной policy.",
};
}
function getFrontchannelLogoutUrls() { function getFrontchannelLogoutUrls() {
const urls = [config.taskLogoutUrl]; const urls = [config.taskLogoutUrl];
const launcherData = readLauncherData(); const launcherData = readLauncherData();

View File

@ -67,8 +67,13 @@ export interface LauncherSettings {
brand: { brand: {
logoLinkUrl: string; logoLinkUrl: string;
}; };
taskManager: {
workspaceCreationPolicy: TaskManagerWorkspaceCreationPolicy;
};
} }
export type TaskManagerWorkspaceCreationPolicy = "any_authorized_user" | "task_admins_only" | "disabled";
export interface ProfileOption { export interface ProfileOption {
userId: string; userId: string;
label: string; label: string;
@ -94,6 +99,9 @@ export const defaultLauncherSettings: LauncherSettings = {
brand: { brand: {
logoLinkUrl: "/", logoLinkUrl: "/",
}, },
taskManager: {
workspaceCreationPolicy: "any_authorized_user",
},
}; };
export const initialLauncherData: LauncherData = normalizeLauncherData({ export const initialLauncherData: LauncherData = normalizeLauncherData({
@ -115,15 +123,29 @@ export function normalizeLauncherSettings(settings?: Partial<LauncherSettings> |
typeof settings?.brand === "object" && settings.brand !== null typeof settings?.brand === "object" && settings.brand !== null
? settings.brand ? settings.brand
: ({} as Partial<LauncherSettings["brand"]>); : ({} as Partial<LauncherSettings["brand"]>);
const taskManager =
typeof settings?.taskManager === "object" && settings.taskManager !== null
? settings.taskManager
: ({} as Partial<LauncherSettings["taskManager"]>);
const logoLinkUrl = typeof brand.logoLinkUrl === "string" && brand.logoLinkUrl.trim() ? brand.logoLinkUrl.trim() : "/"; const logoLinkUrl = typeof brand.logoLinkUrl === "string" && brand.logoLinkUrl.trim() ? brand.logoLinkUrl.trim() : "/";
const workspaceCreationPolicy = isTaskManagerWorkspaceCreationPolicy(taskManager.workspaceCreationPolicy)
? taskManager.workspaceCreationPolicy
: defaultLauncherSettings.taskManager.workspaceCreationPolicy;
return { return {
brand: { brand: {
logoLinkUrl, logoLinkUrl,
}, },
taskManager: {
workspaceCreationPolicy,
},
}; };
} }
function isTaskManagerWorkspaceCreationPolicy(value: unknown): value is TaskManagerWorkspaceCreationPolicy {
return value === "any_authorized_user" || value === "task_admins_only" || value === "disabled";
}
export function normalizeLauncherData(data: Partial<LauncherData> | null | undefined): LauncherData { export function normalizeLauncherData(data: Partial<LauncherData> | null | undefined): LauncherData {
const payload = data ?? {}; const payload = data ?? {};

View File

@ -62,6 +62,7 @@ import {
type LauncherData, type LauncherData,
type LauncherSettings, type LauncherSettings,
type MeResponse, type MeResponse,
type TaskManagerWorkspaceCreationPolicy,
} from "../../shared/api/mockApi"; } from "../../shared/api/mockApi";
import { uploadStorageFile } from "../../shared/api/storageApi"; import { uploadStorageFile } from "../../shared/api/storageApi";
import { cn } from "../../shared/lib/cn"; import { cn } from "../../shared/lib/cn";
@ -885,6 +886,27 @@ const accessAssignmentOptions: Array<NodeDcSelectOption<AccessAssignmentValue>>
{ value: "deny", label: "Deny", description: "Исключение", tone: "red" }, { value: "deny", label: "Deny", description: "Исключение", tone: "red" },
]; ];
const taskManagerWorkspacePolicyOptions: Array<NodeDcSelectOption<TaskManagerWorkspaceCreationPolicy>> = [
{
value: "any_authorized_user",
label: "Все с доступом",
description: "Пользователь с доступом к Operational Core может создать собственный workspace.",
tone: "green",
},
{
value: "task_admins_only",
label: "Только админы",
description: "Workspace создают только суперпользователь и админы Operational Core.",
tone: "yellow",
},
{
value: "disabled",
label: "Отключено",
description: "Создание workspace закрыто для всех через платформенную policy.",
tone: "red",
},
];
const mediaAccept = "image/*,video/*,.gif,.webm,.mov,.mp4,.m4v,.avi,.mkv"; const mediaAccept = "image/*,video/*,.gif,.webm,.mov,.mp4,.m4v,.avi,.mkv";
const modalActionAccentRgb = [247, 248, 244] as const; const modalActionAccentRgb = [247, 248, 244] as const;
@ -2401,13 +2423,19 @@ function MiscSection({
onUpdateSettings: (patch: Partial<LauncherSettings>) => void; onUpdateSettings: (patch: Partial<LauncherSettings>) => void;
}) { }) {
const [logoLinkUrl, setLogoLinkUrl] = useState(data.settings.brand.logoLinkUrl); const [logoLinkUrl, setLogoLinkUrl] = useState(data.settings.brand.logoLinkUrl);
const [workspaceCreationPolicy, setWorkspaceCreationPolicy] = useState(
data.settings.taskManager.workspaceCreationPolicy
);
useEffect(() => { useEffect(() => {
setLogoLinkUrl(data.settings.brand.logoLinkUrl); setLogoLinkUrl(data.settings.brand.logoLinkUrl);
}, [data.settings.brand.logoLinkUrl]); setWorkspaceCreationPolicy(data.settings.taskManager.workspaceCreationPolicy);
}, [data.settings.brand.logoLinkUrl, data.settings.taskManager.workspaceCreationPolicy]);
const normalizedLogoLinkUrl = logoLinkUrl.trim() || "/"; const normalizedLogoLinkUrl = logoLinkUrl.trim() || "/";
const hasChanges = normalizedLogoLinkUrl !== data.settings.brand.logoLinkUrl; const hasChanges =
normalizedLogoLinkUrl !== data.settings.brand.logoLinkUrl ||
workspaceCreationPolicy !== data.settings.taskManager.workspaceCreationPolicy;
return ( return (
<GlassSurface className="table-shell admin-settings-panel"> <GlassSurface className="table-shell admin-settings-panel">
@ -2423,7 +2451,12 @@ function MiscSection({
type="button" type="button"
icon={<Save size={16} />} icon={<Save size={16} />}
disabled={!hasChanges} disabled={!hasChanges}
onClick={() => onUpdateSettings({ brand: { logoLinkUrl: normalizedLogoLinkUrl } })} onClick={() =>
onUpdateSettings({
brand: { logoLinkUrl: normalizedLogoLinkUrl },
taskManager: { workspaceCreationPolicy },
})
}
> >
Сохранить Сохранить
</Button> </Button>
@ -2440,6 +2473,18 @@ function MiscSection({
/> />
<small>Куда ведёт клик по логотипу NODE.DC. Можно указать относительный путь или полный URL.</small> <small>Куда ведёт клик по логотипу NODE.DC. Можно указать относительный путь или полный URL.</small>
</label> </label>
<label className="admin-settings-field">
<span>Operational Core: создание workspace</span>
<NodeDcSelect
className="admin-modal-select-wrap"
triggerClassName="admin-modal-select-trigger"
value={workspaceCreationPolicy}
options={taskManagerWorkspacePolicyOptions}
label="Политика создания workspace в Operational Core"
onChange={(value) => setWorkspaceCreationPolicy(value)}
/>
<small>Платформенная policy для новых пользователей без назначенных рабочих пространств в Task Manager.</small>
</label>
</div> </div>
</GlassSurface> </GlassSurface>
); );