ФУНКЦИИ - МЕЖПРОЕКТНАЯ КОММУНИКАЦИЯ: 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: {
logoLinkUrl: "/",
},
taskManager: {
workspaceCreationPolicy: "any_authorized_user",
},
};
export function createControlPlaneStore({ projectRoot }) {
@ -195,6 +198,10 @@ export function createControlPlaneStore({ projectRoot }) {
...(data.settings?.brand ?? {}),
...(patch.brand ?? {}),
},
taskManager: {
...(data.settings?.taskManager ?? {}),
...(patch.taskManager ?? {}),
},
});
data.settings = settings;
@ -203,7 +210,7 @@ export function createControlPlaneStore({ projectRoot }) {
objectType: "settings",
objectName: "Brand settings",
result: "success",
details: `Logo link: ${settings.brand.logoLinkUrl}`,
details: `Logo link: ${settings.brand.logoLinkUrl}; Tasker workspace policy: ${settings.taskManager.workspaceCreationPolicy}`,
});
await writeData(data);
@ -1051,11 +1058,19 @@ function normalizeData(payload) {
function normalizeSettings(payload) {
const settings = typeof payload === "object" && payload !== null ? payload : {};
const brand = typeof settings.brand === "object" && settings.brand !== null ? settings.brand : {};
const taskManager = typeof settings.taskManager === "object" && settings.taskManager !== null ? settings.taskManager : {};
return {
brand: {
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 app = getAppsForUser(groups).find((candidate) => candidate.slug === serviceSlug);
const allowed = Boolean(app?.hasAccess);
const workspacePolicy =
serviceSlug === "task-manager" ? resolveTaskManagerWorkspacePolicy(snapshot.data, groups, allowed) : null;
res.json({
ok: true,
@ -381,6 +383,7 @@ app.post("/api/internal/access/check", (req, res) => {
serviceSlug,
groups,
matchedGroups: app?.matchedGroups ?? [],
workspacePolicy,
user: {
id: user.id,
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() {
const urls = [config.taskLogoutUrl];
const launcherData = readLauncherData();

View File

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

View File

@ -62,6 +62,7 @@ import {
type LauncherData,
type LauncherSettings,
type MeResponse,
type TaskManagerWorkspaceCreationPolicy,
} from "../../shared/api/mockApi";
import { uploadStorageFile } from "../../shared/api/storageApi";
import { cn } from "../../shared/lib/cn";
@ -885,6 +886,27 @@ const accessAssignmentOptions: Array<NodeDcSelectOption<AccessAssignmentValue>>
{ 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 modalActionAccentRgb = [247, 248, 244] as const;
@ -2401,13 +2423,19 @@ function MiscSection({
onUpdateSettings: (patch: Partial<LauncherSettings>) => void;
}) {
const [logoLinkUrl, setLogoLinkUrl] = useState(data.settings.brand.logoLinkUrl);
const [workspaceCreationPolicy, setWorkspaceCreationPolicy] = useState(
data.settings.taskManager.workspaceCreationPolicy
);
useEffect(() => {
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 hasChanges = normalizedLogoLinkUrl !== data.settings.brand.logoLinkUrl;
const hasChanges =
normalizedLogoLinkUrl !== data.settings.brand.logoLinkUrl ||
workspaceCreationPolicy !== data.settings.taskManager.workspaceCreationPolicy;
return (
<GlassSurface className="table-shell admin-settings-panel">
@ -2423,7 +2451,12 @@ function MiscSection({
type="button"
icon={<Save size={16} />}
disabled={!hasChanges}
onClick={() => onUpdateSettings({ brand: { logoLinkUrl: normalizedLogoLinkUrl } })}
onClick={() =>
onUpdateSettings({
brand: { logoLinkUrl: normalizedLogoLinkUrl },
taskManager: { workspaceCreationPolicy },
})
}
>
Сохранить
</Button>
@ -2440,6 +2473,18 @@ function MiscSection({
/>
<small>Куда ведёт клик по логотипу NODE.DC. Можно указать относительный путь или полный URL.</small>
</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>
</GlassSurface>
);