Compare commits

..

No commits in common. "e2c70649c7516a75360522e83504fd53a0520915" and "55ab952ae8de7f49228f7494a523dfa5d5b156b3" have entirely different histories.

5 changed files with 35 additions and 164 deletions

View File

@ -1960,10 +1960,6 @@ function normalizeService(service) {
service.launchUrl === "https://nodedc.ru/" || service.launchUrl === "https://dev.handhdc.ru/sso/launch" service.launchUrl === "https://nodedc.ru/" || service.launchUrl === "https://dev.handhdc.ru/sso/launch"
? "https://engine.nodedc.ru/" ? "https://engine.nodedc.ru/"
: service.launchUrl, : service.launchUrl,
logoutUrl:
!service.logoutUrl || service.logoutUrl === "https://nodedc.ru/logout"
? "https://engine.nodedc.ru/logout"
: service.logoutUrl,
authentikApplicationSlug: service.authentikApplicationSlug === "nodedc" ? "nodedc-engine" : service.authentikApplicationSlug, authentikApplicationSlug: service.authentikApplicationSlug === "nodedc" ? "nodedc-engine" : service.authentikApplicationSlug,
authentikGroupName: service.authentikGroupName === "service-nodedc" ? "nodedc:engine:viewer" : service.authentikGroupName, authentikGroupName: service.authentikGroupName === "service-nodedc" ? "nodedc:engine:viewer" : service.authentikGroupName,
}; };
@ -2857,7 +2853,6 @@ function sanitizeServicePatch(payload, service) {
"fullDescription", "fullDescription",
"url", "url",
"launchUrl", "launchUrl",
"logoutUrl",
"iconUrl", "iconUrl",
"coverImageUrl", "coverImageUrl",
"coverMediaKind", "coverMediaKind",

View File

@ -817,7 +817,7 @@ app.post("/api/admin/task-manager/workspace-memberships/ensure", requireLauncher
return; return;
} }
if (!assertAdminCanManageClient(req, res, client.id) || !assertAdminCanManageAccessForUser(req, res, user.id)) { if (!assertAdminCanManageClient(req, res, client.id) || !assertAdminCanManageUser(req, res, user.id)) {
return; return;
} }
@ -878,7 +878,7 @@ app.post("/api/admin/task-manager/workspace-memberships/remove", requireLauncher
return; return;
} }
if (!assertAdminCanManageClient(req, res, client.id) || !assertAdminCanManageAccessForUser(req, res, user.id)) { if (!assertAdminCanManageClient(req, res, client.id) || !assertAdminCanManageUser(req, res, user.id)) {
return; return;
} }
@ -959,7 +959,7 @@ app.post("/api/admin/task-manager/project-memberships/ensure", requireLauncherAd
return; return;
} }
if (!assertAdminCanManageClient(req, res, client.id) || !assertAdminCanManageAccessForUser(req, res, user.id)) { if (!assertAdminCanManageClient(req, res, client.id) || !assertAdminCanManageUser(req, res, user.id)) {
return; return;
} }
@ -1031,7 +1031,7 @@ app.post("/api/admin/task-manager/project-memberships/remove", requireLauncherAd
return; return;
} }
if (!assertAdminCanManageClient(req, res, client.id) || !assertAdminCanManageAccessForUser(req, res, user.id)) { if (!assertAdminCanManageClient(req, res, client.id) || !assertAdminCanManageUser(req, res, user.id)) {
return; return;
} }
@ -1190,15 +1190,7 @@ app.patch("/api/admin/memberships/:membershipId", requireLauncherAdmin, asyncRou
return; return;
} }
if (!assertAdminCanManageMembership(req, res, membership, { allowProtectedSelf: true })) { if (!assertAdminCanManageMembership(req, res, membership)) {
return;
}
if (isProtectedSelfManageRequest(req, membership.userId) && req.body?.status && req.body.status !== "active") {
res.status(403).json({
error: "protected_self_status_locked",
message: "Защищённый пользователь может менять себе роли, но не может отключить собственный контур.",
});
return; return;
} }
@ -1377,10 +1369,6 @@ app.post("/api/admin/groups", requireLauncherAdmin, asyncRoute(async (req, res)
return; return;
} }
if (!assertAdminCanManageProtectedGroupMembers(req, res, [], req.body?.memberIds)) {
return;
}
const result = await controlPlaneStore.createGroup(req.body, req.nodedcSession.user); const result = await controlPlaneStore.createGroup(req.body, req.nodedcSession.user);
const syncResult = await syncUsersToAuthentik(result.data, result.group.memberIds, req.nodedcSession.user); const syncResult = await syncUsersToAuthentik(result.data, result.group.memberIds, req.nodedcSession.user);
publishControlPlaneEvent("admin.group.created", syncResult.userIds); publishControlPlaneEvent("admin.group.created", syncResult.userIds);
@ -1401,11 +1389,6 @@ app.patch("/api/admin/groups/:groupId", requireLauncherAdmin, asyncRoute(async (
} }
const previousMemberIds = group.memberIds; 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 result = await controlPlaneStore.updateGroup(req.params.groupId, req.body, req.nodedcSession.user);
const syncResult = await syncUsersToAuthentik( const syncResult = await syncUsersToAuthentik(
result.data, result.data,
@ -1429,10 +1412,6 @@ app.delete("/api/admin/groups/:groupId", requireLauncherAdmin, asyncRoute(async
return; return;
} }
if (!assertAdminCanManageProtectedGroupMembers(req, res, group.memberIds, [])) {
return;
}
const result = await controlPlaneStore.deleteGroup(req.params.groupId, req.nodedcSession.user); const result = await controlPlaneStore.deleteGroup(req.params.groupId, req.nodedcSession.user);
const syncResult = await syncUsersToAuthentik(result.data, result.group.memberIds, req.nodedcSession.user); const syncResult = await syncUsersToAuthentik(result.data, result.group.memberIds, req.nodedcSession.user);
publishControlPlaneEvent("admin.group.deleted", syncResult.userIds); publishControlPlaneEvent("admin.group.deleted", syncResult.userIds);
@ -1466,7 +1445,7 @@ app.delete("/api/admin/services/:serviceId", requireLauncherAdmin, requireRootLa
app.post("/api/admin/access/grants", requireLauncherAdmin, asyncRoute(async (req, res) => { app.post("/api/admin/access/grants", requireLauncherAdmin, asyncRoute(async (req, res) => {
const snapshot = controlPlaneStore.getSnapshot(req.nodedcSession.user); const snapshot = controlPlaneStore.getSnapshot(req.nodedcSession.user);
if (!assertAdminCanManageGrantTarget(req, res, snapshot.data, req.body?.targetType, req.body?.targetId, { allowProtectedSelf: true })) { if (!assertAdminCanManageGrantTarget(req, res, snapshot.data, req.body?.targetType, req.body?.targetId)) {
return; return;
} }
@ -1481,7 +1460,7 @@ app.post("/api/admin/access/grants", requireLauncherAdmin, asyncRoute(async (req
})); }));
app.post("/api/admin/access/exceptions", requireLauncherAdmin, asyncRoute(async (req, res) => { app.post("/api/admin/access/exceptions", requireLauncherAdmin, asyncRoute(async (req, res) => {
if (!assertAdminCanManageAccessForUser(req, res, req.body?.userId)) { if (!assertAdminCanManageUser(req, res, req.body?.userId)) {
return; return;
} }
@ -1492,7 +1471,7 @@ app.post("/api/admin/access/exceptions", requireLauncherAdmin, asyncRoute(async
})); }));
app.post("/api/admin/access/user-service", requireLauncherAdmin, asyncRoute(async (req, res) => { app.post("/api/admin/access/user-service", requireLauncherAdmin, asyncRoute(async (req, res) => {
if (!assertAdminCanManageAccessForUser(req, res, req.body?.userId)) { if (!assertAdminCanManageUser(req, res, req.body?.userId)) {
return; return;
} }
@ -1513,7 +1492,7 @@ app.post("/api/admin/access/user-service", requireLauncherAdmin, asyncRoute(asyn
})); }));
app.post("/api/admin/access/service-modules", requireLauncherAdmin, asyncRoute(async (req, res) => { app.post("/api/admin/access/service-modules", requireLauncherAdmin, asyncRoute(async (req, res) => {
if (!assertAdminCanManageClient(req, res, req.body?.clientId) || !assertAdminCanManageAccessForUser(req, res, req.body?.userId)) { if (!assertAdminCanManageClient(req, res, req.body?.clientId) || !assertAdminCanManageUser(req, res, req.body?.userId)) {
return; return;
} }
@ -3204,16 +3183,8 @@ function canAdminManageClient(req, clientId) {
return Boolean(req.nodedcAdminScope?.isRoot || req.nodedcAdminScope?.clientIds.has(clientId)); return Boolean(req.nodedcAdminScope?.isRoot || req.nodedcAdminScope?.clientIds.has(clientId));
} }
function isProtectedLauncherUser(userId) { function canAdminManageUser(req, userId) {
return protectedLauncherUserIds.has(userId); if (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; return false;
} }
@ -3235,8 +3206,8 @@ function assertAdminCanManageClient(req, res, clientId) {
return false; return false;
} }
function assertAdminCanManageUser(req, res, userId, options = {}) { function assertAdminCanManageUser(req, res, userId) {
if (canAdminManageUser(req, userId, options)) { if (canAdminManageUser(req, userId)) {
return true; return true;
} }
@ -3244,48 +3215,17 @@ function assertAdminCanManageUser(req, res, userId, options = {}) {
return false; return false;
} }
function assertAdminCanManageAccessForUser(req, res, userId) { function assertAdminCanManageMembership(req, res, membership) {
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)) { if (!assertAdminCanManageClient(req, res, membership.clientId)) {
return false; return false;
} }
return assertAdminCanManageUser(req, res, membership.userId, options); return assertAdminCanManageUser(req, res, membership.userId);
} }
function assertAdminCanManageGrantTarget(req, res, data, targetType, targetId, options = {}) { function assertAdminCanManageGrantTarget(req, res, data, targetType, targetId) {
if (req.nodedcAdminScope?.isRoot) { if (req.nodedcAdminScope?.isRoot) {
if (targetType === "client") { return true;
return true;
}
if (targetType === "user" && (!isProtectedLauncherUser(targetId) || isProtectedSelfManageRequest(req, targetId) || !options.allowProtectedSelf)) {
return true;
}
} }
if (targetType === "client") { if (targetType === "client") {
@ -3300,21 +3240,11 @@ function assertAdminCanManageGrantTarget(req, res, data, targetType, targetId, o
return false; 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); return assertAdminCanManageClient(req, res, group.clientId);
} }
if (targetType === "user") { if (targetType === "user") {
return assertAdminCanManageUser(req, res, targetId, options); return assertAdminCanManageUser(req, res, targetId);
} }
res.status(403).json({ error: "Недостаточно прав для управления этим доступом" }); res.status(403).json({ error: "Недостаточно прав для управления этим доступом" });

View File

@ -13,7 +13,6 @@ export interface Service {
fullDescription?: string | null; fullDescription?: string | null;
url: string; url: string;
launchUrl?: string | null; launchUrl?: string | null;
logoutUrl?: string | null;
iconUrl?: string | null; iconUrl?: string | null;
coverImageUrl?: string | null; coverImageUrl?: string | null;
coverMediaKind?: MediaKind | null; coverMediaKind?: MediaKind | null;

View File

@ -84,7 +84,6 @@ export const mockServices: Service[] = [
"NodeDC используется для настройки агентных процессов, визуальной оркестрации, интеграций и runtime-мониторинга.", "NodeDC используется для настройки агентных процессов, визуальной оркестрации, интеграций и runtime-мониторинга.",
url: "https://engine.nodedc.ru/", url: "https://engine.nodedc.ru/",
launchUrl: "https://engine.nodedc.ru/", launchUrl: "https://engine.nodedc.ru/",
logoutUrl: "https://engine.nodedc.ru/logout",
accentColor: "#B5FF5A", accentColor: "#B5FF5A",
fallbackGradient: "linear-gradient(128deg, rgba(181, 255, 90, 0.84), rgba(37, 58, 36, 0.86) 42%, #0A0D10 82%)", fallbackGradient: "linear-gradient(128deg, rgba(181, 255, 90, 0.84), rgba(37, 58, 36, 0.86) 42%, #0A0D10 82%)",
status: "active", status: "active",

View File

@ -163,20 +163,6 @@ const publicPoolSections: Array<{ id: AdminSection; label: string; icon: React.R
{ id: "misc", label: "Разное", icon: <SlidersHorizontal size={16} /> }, { id: "misc", label: "Разное", icon: <SlidersHorizontal size={16} /> },
]; ];
const protectedLauncherUserIds = new Set(["user_root"]);
function isProtectedLauncherUser(userId: string): boolean {
return protectedLauncherUserIds.has(userId);
}
function canSelfManageProtectedUser(me: MeResponse, userId: string): boolean {
return isProtectedLauncherUser(userId) && me.user.id === userId;
}
function isProtectedFromCurrentActor(me: MeResponse, userId: string): boolean {
return isProtectedLauncherUser(userId) && !canSelfManageProtectedUser(me, userId);
}
export function AdminOverlay({ export function AdminOverlay({
data, data,
me, me,
@ -513,7 +499,6 @@ export function AdminOverlay({
<GroupsSection <GroupsSection
data={data} data={data}
clientId={scopedClientId} clientId={scopedClientId}
me={me}
onCreateGroup={onCreateGroup} onCreateGroup={onCreateGroup}
onUpdateGroup={onUpdateGroup} onUpdateGroup={onUpdateGroup}
onDeleteGroup={onDeleteGroup} onDeleteGroup={onDeleteGroup}
@ -532,7 +517,6 @@ export function AdminOverlay({
{activeSection === "access" ? ( {activeSection === "access" ? (
<AccessSection <AccessSection
data={data} data={data}
me={me}
matrix={accessMatrix} matrix={accessMatrix}
selectedCell={selectedAccessCell} selectedCell={selectedAccessCell}
onSelectCell={(cell) => setSelectedCell({ userId: cell.userId, serviceId: cell.serviceId })} onSelectCell={(cell) => setSelectedCell({ userId: cell.userId, serviceId: cell.serviceId })}
@ -1190,14 +1174,12 @@ function PlatformUsersSection({
function GroupsSection({ function GroupsSection({
data, data,
clientId, clientId,
me,
onCreateGroup, onCreateGroup,
onUpdateGroup, onUpdateGroup,
onDeleteGroup, onDeleteGroup,
}: { }: {
data: LauncherData; data: LauncherData;
clientId: string; clientId: string;
me: MeResponse;
onCreateGroup: (clientId: string) => void; onCreateGroup: (clientId: string) => void;
onUpdateGroup: (groupId: string, patch: Partial<ClientGroup>) => void; onUpdateGroup: (groupId: string, patch: Partial<ClientGroup>) => void;
onDeleteGroup: (groupId: string) => void; onDeleteGroup: (groupId: string) => void;
@ -1265,7 +1247,6 @@ function GroupsSection({
{editingGroup ? ( {editingGroup ? (
<GroupEditorModal <GroupEditorModal
group={editingGroup} group={editingGroup}
me={me}
users={data.memberships users={data.memberships
.filter((membership) => membership.clientId === clientId) .filter((membership) => membership.clientId === clientId)
.map((membership) => getUser(data, membership.userId))} .map((membership) => getUser(data, membership.userId))}
@ -2693,14 +2674,12 @@ function UserEditorModal({
function GroupEditorModal({ function GroupEditorModal({
group, group,
me,
users, users,
onClose, onClose,
onSave, onSave,
onDelete, onDelete,
}: { }: {
group: ClientGroup; group: ClientGroup;
me: MeResponse;
users: LauncherUser[]; users: LauncherUser[];
onClose: () => void; onClose: () => void;
onSave: (patch: Partial<ClientGroup>) => void; onSave: (patch: Partial<ClientGroup>) => void;
@ -2739,23 +2718,17 @@ function GroupEditorModal({
<div className="service-content-field service-content-field--wide"> <div className="service-content-field service-content-field--wide">
<span>Участники</span> <span>Участники</span>
<div className="admin-token-grid"> <div className="admin-token-grid">
{users.map((user) => { {users.map((user) => (
const lockedProtectedUser = isProtectedFromCurrentActor(me, user.id); <button
key={user.id}
return ( className="admin-token"
<button data-active={draft.memberIds.includes(user.id)}
key={user.id} type="button"
className="admin-token" onClick={() => toggleUser(user.id)}
data-active={draft.memberIds.includes(user.id)} >
type="button" {user.name}
disabled={lockedProtectedUser} </button>
title={lockedProtectedUser ? "Защищённого пользователя может менять в группах только он сам" : undefined} ))}
onClick={() => toggleUser(user.id)}
>
{user.name}
</button>
);
})}
</div> </div>
</div> </div>
</div> </div>
@ -2953,7 +2926,6 @@ function mediaKindFromUrl(value: string): MediaKind | null {
function AccessSection({ function AccessSection({
data, data,
me,
matrix, matrix,
selectedCell, selectedCell,
onSelectCell, onSelectCell,
@ -2970,7 +2942,6 @@ function AccessSection({
onSetServiceModuleEntitlement, onSetServiceModuleEntitlement,
}: { }: {
data: LauncherData; data: LauncherData;
me: MeResponse;
matrix: ReturnType<typeof buildAccessMatrix>; matrix: ReturnType<typeof buildAccessMatrix>;
selectedCell: AccessMatrixCell | null; selectedCell: AccessMatrixCell | null;
onSelectCell: (cell: AccessMatrixCell) => void; onSelectCell: (cell: AccessMatrixCell) => void;
@ -3040,9 +3011,7 @@ function AccessSection({
const membership = data.memberships.find((item) => item.clientId === matrix.client.id && item.userId === user.id); const membership = data.memberships.find((item) => item.clientId === matrix.client.id && item.userId === user.id);
if (!membership) return null; if (!membership) return null;
const protectedUser = isProtectedLauncherUser(user.id); const protectedUser = user.id === "user_root";
const protectedFromActor = isProtectedFromCurrentActor(me, user.id);
const protectedSelfManage = canSelfManageProtectedUser(me, user.id);
const inviterMeta = getMembershipInviterMeta(data, membership); const inviterMeta = getMembershipInviterMeta(data, membership);
const pendingKey = `${matrix.client.id}:${user.id}:${primaryTaskManagerWorkspace?.slug ?? "primary"}`; const pendingKey = `${matrix.client.id}:${user.id}:${primaryTaskManagerWorkspace?.slug ?? "primary"}`;
const pendingTaskerAssignment = Boolean(pendingTaskManagerMemberships[pendingKey]); const pendingTaskerAssignment = Boolean(pendingTaskManagerMemberships[pendingKey]);
@ -3069,7 +3038,7 @@ function AccessSection({
<div className="access-grid-cell" role="cell"> <div className="access-grid-cell" role="cell">
<MainRoleControl <MainRoleControl
value={membership.role} value={membership.role}
protectedUser={protectedFromActor} protectedUser={protectedUser}
onChange={(role) => onUpdateMembership(membership.id, { role })} onChange={(role) => onUpdateMembership(membership.id, { role })}
/> />
</div> </div>
@ -3087,14 +3056,13 @@ function AccessSection({
pendingValue={pendingAccessAssignments[accessCellKey(user.id, service.id)]} pendingValue={pendingAccessAssignments[accessCellKey(user.id, service.id)]}
busy={!usePublicTaskerAccess && isTaskManagerService && pendingTaskerAssignment} busy={!usePublicTaskerAccess && isTaskManagerService && pendingTaskerAssignment}
publicSelfService={usePublicTaskerAccess} publicSelfService={usePublicTaskerAccess}
readOnly={protectedFromActor}
onSelectCell={onSelectCell} onSelectCell={onSelectCell}
onSetAccess={(value) => { onSetAccess={(value) => {
const nextValue = value; const nextValue = value;
onSetUserServiceAccess({ userId: user.id, serviceId: service.id, value: nextValue }); onSetUserServiceAccess({ userId: user.id, serviceId: service.id, value: nextValue });
if (usePublicTaskerAccess) return; if (usePublicTaskerAccess) return;
if (!isTaskManagerService || !primaryTaskManagerWorkspace || protectedFromActor || (forcedTaskManagerAdmin && !protectedSelfManage)) return; if (!isTaskManagerService || !primaryTaskManagerWorkspace || protectedUser || forcedTaskManagerAdmin) return;
onSetTaskManagerWorkspaceMemberRole({ onSetTaskManagerWorkspaceMemberRole({
clientId: matrix.client.id, clientId: matrix.client.id,
@ -3118,7 +3086,6 @@ function AccessSection({
{detailsCell && detailsService ? ( {detailsCell && detailsService ? (
<OperationalCoreAccessModal <OperationalCoreAccessModal
data={data} data={data}
me={me}
client={matrix.client} client={matrix.client}
user={getUser(data, detailsCell.userId)} user={getUser(data, detailsCell.userId)}
service={detailsService} service={detailsService}
@ -3143,7 +3110,6 @@ function AccessSection({
function PublicAccessUsersPanel({ function PublicAccessUsersPanel({
data, data,
me,
matrix, matrix,
selectedCell, selectedCell,
onSelectCell, onSelectCell,
@ -3155,7 +3121,6 @@ function PublicAccessUsersPanel({
onSetServiceModuleEntitlement, onSetServiceModuleEntitlement,
}: { }: {
data: LauncherData; data: LauncherData;
me: MeResponse;
matrix: ReturnType<typeof buildAccessMatrix>; matrix: ReturnType<typeof buildAccessMatrix>;
selectedCell: AccessMatrixCell | null; selectedCell: AccessMatrixCell | null;
onSelectCell: (cell: AccessMatrixCell) => void; onSelectCell: (cell: AccessMatrixCell) => void;
@ -3196,8 +3161,7 @@ function PublicAccessUsersPanel({
const membership = data.memberships.find((item) => item.clientId === matrix.client.id && item.userId === user.id); const membership = data.memberships.find((item) => item.clientId === matrix.client.id && item.userId === user.id);
if (!membership) return null; if (!membership) return null;
const protectedUser = isProtectedLauncherUser(user.id); const protectedUser = user.id === "user_root";
const protectedFromActor = isProtectedFromCurrentActor(me, user.id);
const inviterMeta = getMembershipInviterMeta(data, membership); const inviterMeta = getMembershipInviterMeta(data, membership);
const operationalCoreCell = operationalCoreService const operationalCoreCell = operationalCoreService
? matrix.cells.find((cell) => cell.userId === user.id && cell.serviceId === operationalCoreService.id) ?? null ? matrix.cells.find((cell) => cell.userId === user.id && cell.serviceId === operationalCoreService.id) ?? null
@ -3232,7 +3196,7 @@ function PublicAccessUsersPanel({
)} )}
</td> </td>
<td> <td>
{protectedFromActor ? ( {protectedUser ? (
<AdminStaticPill>{membershipRoleLabel(membership.role)}</AdminStaticPill> <AdminStaticPill>{membershipRoleLabel(membership.role)}</AdminStaticPill>
) : ( ) : (
<NodeDcSelect <NodeDcSelect
@ -3289,7 +3253,6 @@ function PublicAccessUsersPanel({
{detailsCell && detailsService ? ( {detailsCell && detailsService ? (
<OperationalCoreAccessModal <OperationalCoreAccessModal
data={data} data={data}
me={me}
client={matrix.client} client={matrix.client}
user={getUser(data, detailsCell.userId)} user={getUser(data, detailsCell.userId)}
service={detailsService} service={detailsService}
@ -3397,7 +3360,6 @@ function MainRoleControl({
function OperationalCoreAccessModal({ function OperationalCoreAccessModal({
data, data,
me,
client, client,
user, user,
service, service,
@ -3416,7 +3378,6 @@ function OperationalCoreAccessModal({
onSetServiceModuleEntitlement, onSetServiceModuleEntitlement,
}: { }: {
data: LauncherData; data: LauncherData;
me: MeResponse;
client: Client; client: Client;
user: LauncherUser; user: LauncherUser;
service: Service; service: Service;
@ -3435,8 +3396,7 @@ function OperationalCoreAccessModal({
onSetServiceModuleEntitlement: (command: SetServiceModuleEntitlementCommand) => void; onSetServiceModuleEntitlement: (command: SetServiceModuleEntitlementCommand) => void;
}) { }) {
const membership = data.memberships.find((item) => item.clientId === client.id && item.userId === user.id); const membership = data.memberships.find((item) => item.clientId === client.id && item.userId === user.id);
const protectedSelfManage = canSelfManageProtectedUser(me, user.id); const protectedUser = user.id === "user_root" || membership?.role === "client_owner";
const protectedUser = isProtectedFromCurrentActor(me, user.id) || (membership?.role === "client_owner" && !protectedSelfManage);
const basePendingValue = pendingAccessAssignments[accessCellKey(user.id, service.id)]; const basePendingValue = pendingAccessAssignments[accessCellKey(user.id, service.id)];
const baseAssignmentValue = basePendingValue ?? accessAssignmentValue(cell); const baseAssignmentValue = basePendingValue ?? accessAssignmentValue(cell);
const baseSelectOptions = publicSelfService ? publicOperationalCoreAccessOptions : accessAssignmentOptions; const baseSelectOptions = publicSelfService ? publicOperationalCoreAccessOptions : accessAssignmentOptions;
@ -3668,7 +3628,6 @@ function AccessCellControl({
pendingValue, pendingValue,
busy = false, busy = false,
publicSelfService = false, publicSelfService = false,
readOnly = false,
onSelectCell, onSelectCell,
onSetAccess, onSetAccess,
onOpenDetails, onOpenDetails,
@ -3678,7 +3637,6 @@ function AccessCellControl({
pendingValue?: AccessAssignmentValue; pendingValue?: AccessAssignmentValue;
busy?: boolean; busy?: boolean;
publicSelfService?: boolean; publicSelfService?: boolean;
readOnly?: boolean;
onSelectCell: (cell: AccessMatrixCell) => void; onSelectCell: (cell: AccessMatrixCell) => void;
onSetAccess: (value: AccessAssignmentValue) => void; onSetAccess: (value: AccessAssignmentValue) => void;
onOpenDetails?: () => void; onOpenDetails?: () => void;
@ -3705,7 +3663,6 @@ function AccessCellControl({
cell.effectiveAccess.allowed && "access-cell--allowed", cell.effectiveAccess.allowed && "access-cell--allowed",
!cell.effectiveAccess.allowed && "access-cell--denied", !cell.effectiveAccess.allowed && "access-cell--denied",
cell.effectiveAccess.source === "exception" && !publicSelfService && "access-cell--exception", cell.effectiveAccess.source === "exception" && !publicSelfService && "access-cell--exception",
readOnly && "access-cell--readonly",
isPending && "access-cell--pending", isPending && "access-cell--pending",
active && "access-cell--active" active && "access-cell--active"
); );
@ -3728,15 +3685,6 @@ function AccessCellControl({
); );
} }
if (readOnly) {
return (
<span className={cellClassName}>
<strong>{displayTitle}</strong>
<span>{displaySource}</span>
</span>
);
}
return ( return (
<NodeDcSelect <NodeDcSelect
value={selectValue} value={selectValue}