FIX - МЕЖПРОЕКТНАЯ КОММУНИКАЦИЯ: АДМИНКА ДОСТУПОВ И УЧАСТНИКОВ
This commit is contained in:
+315
-34
@@ -506,21 +506,26 @@ app.post("/api/invites/:token/accept", requireSession, asyncRoute(async (req, re
|
||||
}));
|
||||
|
||||
app.get("/api/admin/control-plane", requireLauncherAdmin, (req, res) => {
|
||||
res.json(controlPlaneStore.getSnapshot(req.nodedcSession.user));
|
||||
res.json(scopeAdminSnapshot(req));
|
||||
});
|
||||
|
||||
app.patch("/api/admin/settings", requireLauncherAdmin, asyncRoute(async (req, res) => {
|
||||
app.patch("/api/admin/settings", requireLauncherAdmin, requireRootLauncherAdmin, asyncRoute(async (req, res) => {
|
||||
const result = await controlPlaneStore.updateSettings(req.body, req.nodedcSession.user);
|
||||
publishControlPlaneEvent("admin.settings.updated");
|
||||
res.json(result);
|
||||
}));
|
||||
|
||||
app.get("/api/admin/clients", requireLauncherAdmin, (req, res) => {
|
||||
const snapshot = controlPlaneStore.getSnapshot(req.nodedcSession.user);
|
||||
const snapshot = scopeAdminSnapshot(req);
|
||||
res.json({ clients: snapshot.data.clients });
|
||||
});
|
||||
|
||||
app.get("/api/admin/task-manager/workspaces", requireLauncherAdmin, asyncRoute(async (_req, res) => {
|
||||
app.get("/api/admin/task-manager/workspaces", requireLauncherAdmin, asyncRoute(async (req, res) => {
|
||||
if (!req.nodedcAdminScope?.isRoot) {
|
||||
res.json({ ok: true, workspaces: [] });
|
||||
return;
|
||||
}
|
||||
|
||||
const taskManager = await requestTaskManagerInternalJson("/api/internal/nodedc/workspaces/");
|
||||
res.json(taskManager);
|
||||
}));
|
||||
@@ -542,6 +547,10 @@ app.post("/api/admin/task-manager/workspace-memberships/ensure", requireLauncher
|
||||
return;
|
||||
}
|
||||
|
||||
if (!assertAdminCanManageClient(req, res, client.id) || !assertAdminCanManageUser(req, res, user.id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
@@ -576,7 +585,7 @@ app.post("/api/admin/task-manager/workspace-memberships/ensure", requireLauncher
|
||||
);
|
||||
|
||||
publishControlPlaneEvent("admin.task-manager.workspace-membership.updated", [user.id]);
|
||||
res.json({ ...result, taskManager });
|
||||
res.json({ ...scopeAdminMutationResult(req, result), taskManager });
|
||||
}));
|
||||
|
||||
app.post("/api/admin/task-manager/workspace-memberships/remove", requireLauncherAdmin, asyncRoute(async (req, res) => {
|
||||
@@ -596,6 +605,10 @@ app.post("/api/admin/task-manager/workspace-memberships/remove", requireLauncher
|
||||
return;
|
||||
}
|
||||
|
||||
if (!assertAdminCanManageClient(req, res, client.id) || !assertAdminCanManageUser(req, res, user.id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const membership = snapshot.data.memberships.find((candidate) => candidate.clientId === client.id && candidate.userId === user.id);
|
||||
const workspaceSlug = client.integrations?.taskManager?.workspaceSlug ?? null;
|
||||
|
||||
@@ -629,7 +642,7 @@ app.post("/api/admin/task-manager/workspace-memberships/remove", requireLauncher
|
||||
);
|
||||
|
||||
publishControlPlaneEvent("admin.task-manager.workspace-membership.updated", [user.id]);
|
||||
res.json({ ...result, taskManager, protected: true });
|
||||
res.json({ ...scopeAdminMutationResult(req, result), taskManager, protected: true });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -651,30 +664,34 @@ app.post("/api/admin/task-manager/workspace-memberships/remove", requireLauncher
|
||||
);
|
||||
|
||||
publishControlPlaneEvent("admin.task-manager.workspace-membership.updated", [user.id]);
|
||||
res.json({ ...result, taskManager });
|
||||
res.json({ ...scopeAdminMutationResult(req, result), taskManager });
|
||||
}));
|
||||
|
||||
app.post("/api/admin/clients", requireLauncherAdmin, asyncRoute(async (req, res) => {
|
||||
app.post("/api/admin/clients", requireLauncherAdmin, requireRootLauncherAdmin, asyncRoute(async (req, res) => {
|
||||
const result = await controlPlaneStore.createClient(req.body, req.nodedcSession.user);
|
||||
res.status(201).json(result);
|
||||
}));
|
||||
|
||||
app.patch("/api/admin/clients/:clientId", requireLauncherAdmin, asyncRoute(async (req, res) => {
|
||||
app.patch("/api/admin/clients/:clientId", requireLauncherAdmin, requireRootLauncherAdmin, asyncRoute(async (req, res) => {
|
||||
const result = await controlPlaneStore.updateClient(req.params.clientId, req.body, req.nodedcSession.user);
|
||||
res.json(result);
|
||||
}));
|
||||
|
||||
app.delete("/api/admin/clients/:clientId", requireLauncherAdmin, asyncRoute(async (req, res) => {
|
||||
app.delete("/api/admin/clients/:clientId", requireLauncherAdmin, requireRootLauncherAdmin, asyncRoute(async (req, res) => {
|
||||
const result = await controlPlaneStore.deleteClient(req.params.clientId, req.nodedcSession.user);
|
||||
res.json(result);
|
||||
}));
|
||||
|
||||
app.get("/api/admin/users", requireLauncherAdmin, (req, res) => {
|
||||
const snapshot = controlPlaneStore.getSnapshot(req.nodedcSession.user);
|
||||
const snapshot = scopeAdminSnapshot(req);
|
||||
res.json({ users: snapshot.data.users, memberships: snapshot.data.memberships });
|
||||
});
|
||||
|
||||
app.post("/api/admin/users", requireLauncherAdmin, asyncRoute(async (req, res) => {
|
||||
if (!assertAdminCanManageClient(req, res, req.body?.clientId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await controlPlaneStore.createUser(req.body, req.nodedcSession.user);
|
||||
let provisioning = null;
|
||||
|
||||
@@ -691,17 +708,25 @@ app.post("/api/admin/users", requireLauncherAdmin, asyncRoute(async (req, res) =
|
||||
}
|
||||
|
||||
publishControlPlaneEvent("admin.user.created", [result.user.id]);
|
||||
res.status(201).json({ ...result, provisioning });
|
||||
res.status(201).json({ ...scopeAdminMutationResult(req, result), provisioning });
|
||||
}));
|
||||
|
||||
app.patch("/api/admin/users/:userId/profile", requireLauncherAdmin, asyncRoute(async (req, res) => {
|
||||
if (!assertAdminCanManageUser(req, res, req.params.userId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await controlPlaneStore.updateUserProfile(req.params.userId, req.body, req.nodedcSession.user);
|
||||
const syncResult = await syncUsersToAuthentik(result.data, [req.params.userId], req.nodedcSession.user);
|
||||
publishControlPlaneEvent("admin.user.updated", syncResult.userIds);
|
||||
res.json({ ...result, data: syncResult.data });
|
||||
res.json(scopeAdminMutationResult(req, { ...result, data: syncResult.data }));
|
||||
}));
|
||||
|
||||
app.post("/api/admin/users/:userId/provision-authentik", requireLauncherAdmin, asyncRoute(async (req, res) => {
|
||||
if (!assertAdminCanManageUser(req, res, req.params.userId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const snapshot = controlPlaneStore.getSnapshot(req.nodedcSession.user);
|
||||
const provisionedUser = await authentikSyncClient.provisionUser({
|
||||
data: snapshot.data,
|
||||
@@ -712,51 +737,118 @@ app.post("/api/admin/users/:userId/provision-authentik", requireLauncherAdmin, a
|
||||
const result = await controlPlaneStore.markUserAuthentikProvisioned(req.params.userId, provisionedUser, req.nodedcSession.user);
|
||||
|
||||
publishControlPlaneEvent("admin.user.provisioned", [req.params.userId]);
|
||||
res.json({ ...result, provisioning: toProvisioningResponse(provisionedUser) });
|
||||
res.json({ ...scopeAdminMutationResult(req, result), provisioning: toProvisioningResponse(provisionedUser) });
|
||||
}));
|
||||
|
||||
app.patch("/api/admin/memberships/:membershipId", requireLauncherAdmin, asyncRoute(async (req, res) => {
|
||||
const snapshot = controlPlaneStore.getSnapshot(req.nodedcSession.user);
|
||||
const membership = snapshot.data.memberships.find((candidate) => candidate.id === req.params.membershipId);
|
||||
|
||||
if (!membership) {
|
||||
res.status(404).json({ error: "membership_not_found" });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!assertAdminCanManageMembership(req, res, membership)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await controlPlaneStore.updateMembership(req.params.membershipId, req.body, req.nodedcSession.user);
|
||||
const syncResult = await syncUsersToAuthentik(result.data, [result.membership.userId], req.nodedcSession.user);
|
||||
publishControlPlaneEvent("admin.membership.updated", syncResult.userIds);
|
||||
res.json({ ...result, data: syncResult.data });
|
||||
res.json(scopeAdminMutationResult(req, { ...result, data: syncResult.data }));
|
||||
}));
|
||||
|
||||
app.delete("/api/admin/memberships/:membershipId", requireLauncherAdmin, asyncRoute(async (req, res) => {
|
||||
const snapshot = controlPlaneStore.getSnapshot(req.nodedcSession.user);
|
||||
const membership = snapshot.data.memberships.find((candidate) => candidate.id === req.params.membershipId);
|
||||
|
||||
if (!membership) {
|
||||
res.status(404).json({ error: "membership_not_found" });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!assertAdminCanManageMembership(req, res, membership)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await controlPlaneStore.deleteMembership(req.params.membershipId, req.nodedcSession.user);
|
||||
const syncResult = await syncUsersToAuthentik(result.data, [result.membership.userId], req.nodedcSession.user);
|
||||
publishControlPlaneEvent("admin.membership.deleted", syncResult.userIds);
|
||||
res.json({ ...result, data: syncResult.data });
|
||||
res.json(scopeAdminMutationResult(req, { ...result, data: syncResult.data }));
|
||||
}));
|
||||
|
||||
app.post("/api/admin/invites", requireLauncherAdmin, asyncRoute(async (req, res) => {
|
||||
if (!assertAdminCanManageClient(req, res, req.body?.clientId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await controlPlaneStore.createInvite(req.body, req.nodedcSession.user);
|
||||
publishControlPlaneEvent("admin.invite.created");
|
||||
res.status(201).json(result);
|
||||
res.status(201).json(scopeAdminMutationResult(req, result));
|
||||
}));
|
||||
|
||||
app.patch("/api/admin/invites/:inviteId", requireLauncherAdmin, asyncRoute(async (req, res) => {
|
||||
const snapshot = controlPlaneStore.getSnapshot(req.nodedcSession.user);
|
||||
const invite = snapshot.data.invites.find((candidate) => candidate.id === req.params.inviteId);
|
||||
|
||||
if (!invite) {
|
||||
res.status(404).json({ error: "invite_not_found" });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!assertAdminCanManageClient(req, res, invite.clientId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await controlPlaneStore.updateInvite(req.params.inviteId, req.body, req.nodedcSession.user);
|
||||
publishControlPlaneEvent("admin.invite.updated");
|
||||
res.json(result);
|
||||
res.json(scopeAdminMutationResult(req, result));
|
||||
}));
|
||||
|
||||
app.delete("/api/admin/invites/:inviteId", requireLauncherAdmin, asyncRoute(async (req, res) => {
|
||||
const snapshot = controlPlaneStore.getSnapshot(req.nodedcSession.user);
|
||||
const invite = snapshot.data.invites.find((candidate) => candidate.id === req.params.inviteId);
|
||||
|
||||
if (!invite) {
|
||||
res.status(404).json({ error: "invite_not_found" });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!assertAdminCanManageClient(req, res, invite.clientId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await controlPlaneStore.deleteInvite(req.params.inviteId, req.nodedcSession.user);
|
||||
publishControlPlaneEvent("admin.invite.deleted");
|
||||
res.json(result);
|
||||
res.json(scopeAdminMutationResult(req, result));
|
||||
}));
|
||||
|
||||
app.post("/api/admin/groups", requireLauncherAdmin, asyncRoute(async (req, res) => {
|
||||
if (!assertAdminCanManageClient(req, res, req.body?.clientId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await controlPlaneStore.createGroup(req.body, req.nodedcSession.user);
|
||||
const syncResult = await syncUsersToAuthentik(result.data, result.group.memberIds, req.nodedcSession.user);
|
||||
publishControlPlaneEvent("admin.group.created", syncResult.userIds);
|
||||
res.status(201).json({ ...result, data: syncResult.data });
|
||||
res.status(201).json(scopeAdminMutationResult(req, { ...result, data: syncResult.data }));
|
||||
}));
|
||||
|
||||
app.patch("/api/admin/groups/:groupId", requireLauncherAdmin, asyncRoute(async (req, res) => {
|
||||
const beforeSnapshot = controlPlaneStore.getSnapshot(req.nodedcSession.user);
|
||||
const previousMemberIds = beforeSnapshot.data.groups.find((group) => group.id === req.params.groupId)?.memberIds ?? [];
|
||||
const group = beforeSnapshot.data.groups.find((candidate) => candidate.id === req.params.groupId);
|
||||
|
||||
if (!group) {
|
||||
res.status(404).json({ error: "group_not_found" });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!assertAdminCanManageClient(req, res, group.clientId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const previousMemberIds = group.memberIds;
|
||||
const result = await controlPlaneStore.updateGroup(req.params.groupId, req.body, req.nodedcSession.user);
|
||||
const syncResult = await syncUsersToAuthentik(
|
||||
result.data,
|
||||
@@ -764,41 +856,59 @@ app.patch("/api/admin/groups/:groupId", requireLauncherAdmin, asyncRoute(async (
|
||||
req.nodedcSession.user
|
||||
);
|
||||
publishControlPlaneEvent("admin.group.updated", syncResult.userIds);
|
||||
res.json({ ...result, data: syncResult.data });
|
||||
res.json(scopeAdminMutationResult(req, { ...result, data: syncResult.data }));
|
||||
}));
|
||||
|
||||
app.delete("/api/admin/groups/:groupId", requireLauncherAdmin, asyncRoute(async (req, res) => {
|
||||
const snapshot = controlPlaneStore.getSnapshot(req.nodedcSession.user);
|
||||
const group = snapshot.data.groups.find((candidate) => candidate.id === req.params.groupId);
|
||||
|
||||
if (!group) {
|
||||
res.status(404).json({ error: "group_not_found" });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!assertAdminCanManageClient(req, res, group.clientId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await controlPlaneStore.deleteGroup(req.params.groupId, req.nodedcSession.user);
|
||||
const syncResult = await syncUsersToAuthentik(result.data, result.group.memberIds, req.nodedcSession.user);
|
||||
publishControlPlaneEvent("admin.group.deleted", syncResult.userIds);
|
||||
res.json({ ...result, data: syncResult.data });
|
||||
res.json(scopeAdminMutationResult(req, { ...result, data: syncResult.data }));
|
||||
}));
|
||||
|
||||
app.post("/api/admin/services", requireLauncherAdmin, asyncRoute(async (req, res) => {
|
||||
app.post("/api/admin/services", requireLauncherAdmin, requireRootLauncherAdmin, asyncRoute(async (req, res) => {
|
||||
const result = await controlPlaneStore.createService(req.body, req.nodedcSession.user);
|
||||
publishControlPlaneEvent("admin.service.created");
|
||||
res.status(201).json(result);
|
||||
}));
|
||||
|
||||
app.patch("/api/admin/services/reorder", requireLauncherAdmin, asyncRoute(async (req, res) => {
|
||||
app.patch("/api/admin/services/reorder", requireLauncherAdmin, requireRootLauncherAdmin, asyncRoute(async (req, res) => {
|
||||
const result = await controlPlaneStore.reorderServices(req.body, req.nodedcSession.user);
|
||||
publishControlPlaneEvent("admin.service.reordered");
|
||||
res.json(result);
|
||||
}));
|
||||
|
||||
app.patch("/api/admin/services/:serviceId", requireLauncherAdmin, asyncRoute(async (req, res) => {
|
||||
app.patch("/api/admin/services/:serviceId", requireLauncherAdmin, requireRootLauncherAdmin, asyncRoute(async (req, res) => {
|
||||
const result = await controlPlaneStore.updateService(req.params.serviceId, req.body, req.nodedcSession.user);
|
||||
publishControlPlaneEvent("admin.service.updated");
|
||||
res.json(result);
|
||||
}));
|
||||
|
||||
app.delete("/api/admin/services/:serviceId", requireLauncherAdmin, asyncRoute(async (req, res) => {
|
||||
app.delete("/api/admin/services/:serviceId", requireLauncherAdmin, requireRootLauncherAdmin, asyncRoute(async (req, res) => {
|
||||
const result = await controlPlaneStore.deleteService(req.params.serviceId, req.nodedcSession.user);
|
||||
publishControlPlaneEvent("admin.service.deleted");
|
||||
res.json(result);
|
||||
}));
|
||||
|
||||
app.post("/api/admin/access/grants", requireLauncherAdmin, asyncRoute(async (req, res) => {
|
||||
const snapshot = controlPlaneStore.getSnapshot(req.nodedcSession.user);
|
||||
|
||||
if (!assertAdminCanManageGrantTarget(req, res, snapshot.data, req.body?.targetType, req.body?.targetId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await controlPlaneStore.upsertGrant(req.body, req.nodedcSession.user);
|
||||
const syncResult = await syncUsersToAuthentik(
|
||||
result.data,
|
||||
@@ -806,30 +916,38 @@ app.post("/api/admin/access/grants", requireLauncherAdmin, asyncRoute(async (req
|
||||
req.nodedcSession.user
|
||||
);
|
||||
publishControlPlaneEvent("admin.access.grant.updated", syncResult.userIds);
|
||||
res.json({ ...result, data: syncResult.data });
|
||||
res.json(scopeAdminMutationResult(req, { ...result, data: syncResult.data }));
|
||||
}));
|
||||
|
||||
app.post("/api/admin/access/exceptions", requireLauncherAdmin, asyncRoute(async (req, res) => {
|
||||
if (!assertAdminCanManageUser(req, res, req.body?.userId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await controlPlaneStore.upsertException(req.body, req.nodedcSession.user);
|
||||
const syncResult = await syncUsersToAuthentik(result.data, [result.exception.userId], req.nodedcSession.user);
|
||||
publishControlPlaneEvent("admin.access.exception.updated", syncResult.userIds);
|
||||
res.json({ ...result, data: syncResult.data });
|
||||
res.json(scopeAdminMutationResult(req, { ...result, data: syncResult.data }));
|
||||
}));
|
||||
|
||||
app.post("/api/admin/access/user-service", requireLauncherAdmin, asyncRoute(async (req, res) => {
|
||||
if (!assertAdminCanManageUser(req, res, req.body?.userId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await controlPlaneStore.setUserServiceAccess(req.body, req.nodedcSession.user);
|
||||
const syncResult = await syncUsersToAuthentik(result.data, [req.body?.userId], req.nodedcSession.user);
|
||||
publishControlPlaneEvent("admin.access.user-service.updated", syncResult.userIds);
|
||||
res.json({ ...result, data: syncResult.data });
|
||||
res.json(scopeAdminMutationResult(req, { ...result, data: syncResult.data }));
|
||||
}));
|
||||
|
||||
app.post("/api/admin/sync/:syncId/retry", requireLauncherAdmin, asyncRoute(async (req, res) => {
|
||||
app.post("/api/admin/sync/:syncId/retry", requireLauncherAdmin, requireRootLauncherAdmin, asyncRoute(async (req, res) => {
|
||||
const result = await controlPlaneStore.retrySync(req.params.syncId, req.nodedcSession.user);
|
||||
publishControlPlaneEvent("admin.sync.retry");
|
||||
res.json(result);
|
||||
}));
|
||||
|
||||
app.get("/api/admin/sync/authentik/plan", requireLauncherAdmin, (_req, res) => {
|
||||
app.get("/api/admin/sync/authentik/plan", requireLauncherAdmin, requireRootLauncherAdmin, (_req, res) => {
|
||||
res.json(controlPlaneStore.buildAuthentikSyncPlan());
|
||||
});
|
||||
|
||||
@@ -838,7 +956,7 @@ app.post("/api/storage/upload", asyncRoute(async (req, res) => {
|
||||
res.json(result);
|
||||
}));
|
||||
|
||||
app.post("/api/storage/data", requireLauncherAdmin, asyncRoute(async (req, res) => {
|
||||
app.post("/api/storage/data", requireLauncherAdmin, requireRootLauncherAdmin, asyncRoute(async (req, res) => {
|
||||
await saveLauncherData(req.body);
|
||||
publishControlPlaneEvent("storage.data.updated");
|
||||
res.json({ ok: true, url: "/storage/launcher-data.json" });
|
||||
@@ -1844,13 +1962,24 @@ function requireLauncherAdmin(req, res, next) {
|
||||
}
|
||||
|
||||
const runtimeContext = getRuntimeSessionContext(session);
|
||||
const adminScope = resolveAdminScope(runtimeContext.user, runtimeContext.groups);
|
||||
|
||||
if (!isLauncherAdmin(runtimeContext.groups)) {
|
||||
if (!adminScope.isRoot && adminScope.clientIds.size === 0) {
|
||||
res.status(403).json({ error: "Недостаточно прав Launcher admin" });
|
||||
return;
|
||||
}
|
||||
|
||||
req.nodedcSession = { ...session, user: runtimeContext.user };
|
||||
req.nodedcAdminScope = adminScope;
|
||||
next();
|
||||
}
|
||||
|
||||
function requireRootLauncherAdmin(req, res, next) {
|
||||
if (!req.nodedcAdminScope?.isRoot) {
|
||||
res.status(403).json({ error: "Действие доступно только суперпользователю NODE.DC" });
|
||||
return;
|
||||
}
|
||||
|
||||
next();
|
||||
}
|
||||
|
||||
@@ -1935,6 +2064,158 @@ function isLauncherAdmin(groups) {
|
||||
return groups.includes("nodedc:superadmin") || groups.includes("nodedc:launcher:admin");
|
||||
}
|
||||
|
||||
const clientAdminMembershipRoles = new Set(["client_owner", "client_admin"]);
|
||||
const protectedLauncherUserIds = new Set(["user_root"]);
|
||||
|
||||
function resolveAdminScope(identity, groups) {
|
||||
const snapshot = controlPlaneStore.getSnapshot(identity);
|
||||
const actorId = snapshot.actor.source === "launcher" ? snapshot.actor.id : null;
|
||||
const isRoot = isLauncherAdmin(groups);
|
||||
const clientIds = new Set(
|
||||
isRoot
|
||||
? snapshot.data.clients.map((client) => client.id)
|
||||
: snapshot.data.memberships
|
||||
.filter((membership) => membership.userId === actorId && membership.status === "active" && clientAdminMembershipRoles.has(membership.role))
|
||||
.map((membership) => membership.clientId)
|
||||
);
|
||||
|
||||
return {
|
||||
actorId,
|
||||
clientIds,
|
||||
isRoot,
|
||||
snapshot,
|
||||
};
|
||||
}
|
||||
|
||||
function canAdminManageClient(req, clientId) {
|
||||
return Boolean(req.nodedcAdminScope?.isRoot || req.nodedcAdminScope?.clientIds.has(clientId));
|
||||
}
|
||||
|
||||
function canAdminManageUser(req, userId) {
|
||||
if (protectedLauncherUserIds.has(userId)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (req.nodedcAdminScope?.isRoot) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return req.nodedcAdminScope?.snapshot.data.memberships.some(
|
||||
(membership) => membership.userId === userId && req.nodedcAdminScope.clientIds.has(membership.clientId)
|
||||
);
|
||||
}
|
||||
|
||||
function assertAdminCanManageClient(req, res, clientId) {
|
||||
if (canAdminManageClient(req, clientId)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
res.status(403).json({ error: "Недостаточно прав для управления этим клиентом" });
|
||||
return false;
|
||||
}
|
||||
|
||||
function assertAdminCanManageUser(req, res, userId) {
|
||||
if (canAdminManageUser(req, userId)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
res.status(403).json({ error: "Недостаточно прав для управления этим пользователем" });
|
||||
return false;
|
||||
}
|
||||
|
||||
function assertAdminCanManageMembership(req, res, membership) {
|
||||
if (!assertAdminCanManageClient(req, res, membership.clientId)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return assertAdminCanManageUser(req, res, membership.userId);
|
||||
}
|
||||
|
||||
function assertAdminCanManageGrantTarget(req, res, data, targetType, targetId) {
|
||||
if (req.nodedcAdminScope?.isRoot) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (targetType === "client") {
|
||||
return assertAdminCanManageClient(req, res, targetId);
|
||||
}
|
||||
|
||||
if (targetType === "group") {
|
||||
const group = data.groups.find((candidate) => candidate.id === targetId);
|
||||
|
||||
if (!group) {
|
||||
res.status(404).json({ error: "group_not_found" });
|
||||
return false;
|
||||
}
|
||||
|
||||
return assertAdminCanManageClient(req, res, group.clientId);
|
||||
}
|
||||
|
||||
if (targetType === "user") {
|
||||
return assertAdminCanManageUser(req, res, targetId);
|
||||
}
|
||||
|
||||
res.status(403).json({ error: "Недостаточно прав для управления этим доступом" });
|
||||
return false;
|
||||
}
|
||||
|
||||
function scopeAdminSnapshot(req, snapshot = controlPlaneStore.getSnapshot(req.nodedcSession.user)) {
|
||||
return {
|
||||
...snapshot,
|
||||
data: scopeControlPlaneData(snapshot.data, req.nodedcAdminScope),
|
||||
};
|
||||
}
|
||||
|
||||
function scopeAdminMutationResult(req, result) {
|
||||
if (!result?.data) {
|
||||
return result;
|
||||
}
|
||||
|
||||
return {
|
||||
...result,
|
||||
data: scopeControlPlaneData(result.data, req.nodedcAdminScope),
|
||||
};
|
||||
}
|
||||
|
||||
function scopeControlPlaneData(data, scope) {
|
||||
if (!scope || scope.isRoot) {
|
||||
return data;
|
||||
}
|
||||
|
||||
const clientIds = scope.clientIds;
|
||||
const memberships = data.memberships.filter((membership) => clientIds.has(membership.clientId));
|
||||
const userIds = new Set(memberships.map((membership) => membership.userId));
|
||||
|
||||
if (scope.actorId) {
|
||||
userIds.add(scope.actorId);
|
||||
}
|
||||
|
||||
const groupIds = new Set(data.groups.filter((group) => clientIds.has(group.clientId)).map((group) => group.id));
|
||||
|
||||
return {
|
||||
...data,
|
||||
clients: data.clients.filter((client) => clientIds.has(client.id)),
|
||||
users: data.users.filter((user) => userIds.has(user.id)),
|
||||
memberships,
|
||||
groups: data.groups.filter((group) => clientIds.has(group.clientId)),
|
||||
invites: data.invites.filter((invite) => clientIds.has(invite.clientId)),
|
||||
grants: data.grants.filter((grant) => {
|
||||
if (grant.targetType === "client") return clientIds.has(grant.targetId);
|
||||
if (grant.targetType === "group") return groupIds.has(grant.targetId);
|
||||
if (grant.targetType === "user") return userIds.has(grant.targetId);
|
||||
return false;
|
||||
}),
|
||||
exceptions: data.exceptions.filter((exception) => userIds.has(exception.userId)),
|
||||
taskManagerMemberships: data.taskManagerMemberships.filter(
|
||||
(membership) => clientIds.has(membership.clientId) && userIds.has(membership.userId)
|
||||
),
|
||||
syncStatuses: data.syncStatuses.filter(
|
||||
(syncStatus) => clientIds.has(syncStatus.objectId) || userIds.has(syncStatus.objectId) || groupIds.has(syncStatus.objectId)
|
||||
),
|
||||
auditEvents: data.auditEvents.filter((event) => !event.clientId || clientIds.has(event.clientId)),
|
||||
};
|
||||
}
|
||||
|
||||
function cookieOptions(maxAgeMs) {
|
||||
const options = {
|
||||
httpOnly: true,
|
||||
|
||||
Reference in New Issue
Block a user