fix: allow protected hub self access management

This commit is contained in:
DCCONSTRUCTIONS
2026-05-23 11:39:24 +03:00
parent 55ab952ae8
commit 2c6efdd116
2 changed files with 157 additions and 35 deletions
+88 -18
View File
@@ -817,7 +817,7 @@ app.post("/api/admin/task-manager/workspace-memberships/ensure", requireLauncher
return;
}
if (!assertAdminCanManageClient(req, res, client.id) || !assertAdminCanManageUser(req, res, user.id)) {
if (!assertAdminCanManageClient(req, res, client.id) || !assertAdminCanManageAccessForUser(req, res, user.id)) {
return;
}
@@ -878,7 +878,7 @@ app.post("/api/admin/task-manager/workspace-memberships/remove", requireLauncher
return;
}
if (!assertAdminCanManageClient(req, res, client.id) || !assertAdminCanManageUser(req, res, user.id)) {
if (!assertAdminCanManageClient(req, res, client.id) || !assertAdminCanManageAccessForUser(req, res, user.id)) {
return;
}
@@ -959,7 +959,7 @@ app.post("/api/admin/task-manager/project-memberships/ensure", requireLauncherAd
return;
}
if (!assertAdminCanManageClient(req, res, client.id) || !assertAdminCanManageUser(req, res, user.id)) {
if (!assertAdminCanManageClient(req, res, client.id) || !assertAdminCanManageAccessForUser(req, res, user.id)) {
return;
}
@@ -1031,7 +1031,7 @@ app.post("/api/admin/task-manager/project-memberships/remove", requireLauncherAd
return;
}
if (!assertAdminCanManageClient(req, res, client.id) || !assertAdminCanManageUser(req, res, user.id)) {
if (!assertAdminCanManageClient(req, res, client.id) || !assertAdminCanManageAccessForUser(req, res, user.id)) {
return;
}
@@ -1190,7 +1190,15 @@ app.patch("/api/admin/memberships/:membershipId", requireLauncherAdmin, asyncRou
return;
}
if (!assertAdminCanManageMembership(req, res, membership)) {
if (!assertAdminCanManageMembership(req, res, membership, { allowProtectedSelf: true })) {
return;
}
if (isProtectedSelfManageRequest(req, membership.userId) && req.body?.status && req.body.status !== "active") {
res.status(403).json({
error: "protected_self_status_locked",
message: "Защищённый пользователь может менять себе роли, но не может отключить собственный контур.",
});
return;
}
@@ -1369,6 +1377,10 @@ app.post("/api/admin/groups", requireLauncherAdmin, asyncRoute(async (req, res)
return;
}
if (!assertAdminCanManageProtectedGroupMembers(req, res, [], req.body?.memberIds)) {
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);
@@ -1389,6 +1401,11 @@ app.patch("/api/admin/groups/:groupId", requireLauncherAdmin, asyncRoute(async (
}
const previousMemberIds = group.memberIds;
if (!assertAdminCanManageProtectedGroupMembers(req, res, previousMemberIds, req.body?.memberIds)) {
return;
}
const result = await controlPlaneStore.updateGroup(req.params.groupId, req.body, req.nodedcSession.user);
const syncResult = await syncUsersToAuthentik(
result.data,
@@ -1412,6 +1429,10 @@ app.delete("/api/admin/groups/:groupId", requireLauncherAdmin, asyncRoute(async
return;
}
if (!assertAdminCanManageProtectedGroupMembers(req, res, group.memberIds, [])) {
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);
@@ -1445,7 +1466,7 @@ app.delete("/api/admin/services/:serviceId", requireLauncherAdmin, requireRootLa
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)) {
if (!assertAdminCanManageGrantTarget(req, res, snapshot.data, req.body?.targetType, req.body?.targetId, { allowProtectedSelf: true })) {
return;
}
@@ -1460,7 +1481,7 @@ app.post("/api/admin/access/grants", requireLauncherAdmin, asyncRoute(async (req
}));
app.post("/api/admin/access/exceptions", requireLauncherAdmin, asyncRoute(async (req, res) => {
if (!assertAdminCanManageUser(req, res, req.body?.userId)) {
if (!assertAdminCanManageAccessForUser(req, res, req.body?.userId)) {
return;
}
@@ -1471,7 +1492,7 @@ app.post("/api/admin/access/exceptions", requireLauncherAdmin, asyncRoute(async
}));
app.post("/api/admin/access/user-service", requireLauncherAdmin, asyncRoute(async (req, res) => {
if (!assertAdminCanManageUser(req, res, req.body?.userId)) {
if (!assertAdminCanManageAccessForUser(req, res, req.body?.userId)) {
return;
}
@@ -1492,7 +1513,7 @@ app.post("/api/admin/access/user-service", requireLauncherAdmin, asyncRoute(asyn
}));
app.post("/api/admin/access/service-modules", requireLauncherAdmin, asyncRoute(async (req, res) => {
if (!assertAdminCanManageClient(req, res, req.body?.clientId) || !assertAdminCanManageUser(req, res, req.body?.userId)) {
if (!assertAdminCanManageClient(req, res, req.body?.clientId) || !assertAdminCanManageAccessForUser(req, res, req.body?.userId)) {
return;
}
@@ -3183,8 +3204,16 @@ function canAdminManageClient(req, clientId) {
return Boolean(req.nodedcAdminScope?.isRoot || req.nodedcAdminScope?.clientIds.has(clientId));
}
function canAdminManageUser(req, userId) {
if (protectedLauncherUserIds.has(userId)) {
function isProtectedLauncherUser(userId) {
return protectedLauncherUserIds.has(userId);
}
function isProtectedSelfManageRequest(req, userId) {
return isProtectedLauncherUser(userId) && req.nodedcAdminScope?.actorId === userId;
}
function canAdminManageUser(req, userId, options = {}) {
if (isProtectedLauncherUser(userId) && !(options.allowProtectedSelf && isProtectedSelfManageRequest(req, userId))) {
return false;
}
@@ -3206,8 +3235,8 @@ function assertAdminCanManageClient(req, res, clientId) {
return false;
}
function assertAdminCanManageUser(req, res, userId) {
if (canAdminManageUser(req, userId)) {
function assertAdminCanManageUser(req, res, userId, options = {}) {
if (canAdminManageUser(req, userId, options)) {
return true;
}
@@ -3215,17 +3244,48 @@ function assertAdminCanManageUser(req, res, userId) {
return false;
}
function assertAdminCanManageMembership(req, res, membership) {
function assertAdminCanManageAccessForUser(req, res, userId) {
return assertAdminCanManageUser(req, res, userId, { allowProtectedSelf: true });
}
function assertAdminCanManageProtectedGroupMembers(req, res, previousMemberIds = [], nextMemberIds = []) {
if (!Array.isArray(nextMemberIds)) {
return true;
}
const previous = new Set(previousMemberIds);
const next = new Set(nextMemberIds);
const changedProtectedUserIds = [...protectedLauncherUserIds].filter((userId) => previous.has(userId) !== next.has(userId));
const blockedUserId = changedProtectedUserIds.find((userId) => !isProtectedSelfManageRequest(req, userId));
if (!blockedUserId) {
return true;
}
res.status(403).json({
error: "protected_user_group_membership_locked",
message: "Защищённого пользователя может добавлять в группы или удалять из групп только он сам.",
});
return false;
}
function assertAdminCanManageMembership(req, res, membership, options = {}) {
if (!assertAdminCanManageClient(req, res, membership.clientId)) {
return false;
}
return assertAdminCanManageUser(req, res, membership.userId);
return assertAdminCanManageUser(req, res, membership.userId, options);
}
function assertAdminCanManageGrantTarget(req, res, data, targetType, targetId) {
function assertAdminCanManageGrantTarget(req, res, data, targetType, targetId, options = {}) {
if (req.nodedcAdminScope?.isRoot) {
return true;
if (targetType === "client") {
return true;
}
if (targetType === "user" && (!isProtectedLauncherUser(targetId) || isProtectedSelfManageRequest(req, targetId) || !options.allowProtectedSelf)) {
return true;
}
}
if (targetType === "client") {
@@ -3240,11 +3300,21 @@ function assertAdminCanManageGrantTarget(req, res, data, targetType, targetId) {
return false;
}
const blockedProtectedUserId = group.memberIds.find((userId) => isProtectedLauncherUser(userId) && !isProtectedSelfManageRequest(req, userId));
if (blockedProtectedUserId) {
res.status(403).json({
error: "protected_group_access_locked",
message: "Гранты группы с защищённым пользователем может менять только сам защищённый пользователь.",
});
return false;
}
return assertAdminCanManageClient(req, res, group.clientId);
}
if (targetType === "user") {
return assertAdminCanManageUser(req, res, targetId);
return assertAdminCanManageUser(req, res, targetId, options);
}
res.status(403).json({ error: "Недостаточно прав для управления этим доступом" });