fix: allow protected hub self access management
This commit is contained in:
parent
55ab952ae8
commit
2c6efdd116
|
|
@ -817,7 +817,7 @@ app.post("/api/admin/task-manager/workspace-memberships/ensure", requireLauncher
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!assertAdminCanManageClient(req, res, client.id) || !assertAdminCanManageUser(req, res, user.id)) {
|
if (!assertAdminCanManageClient(req, res, client.id) || !assertAdminCanManageAccessForUser(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) || !assertAdminCanManageUser(req, res, user.id)) {
|
if (!assertAdminCanManageClient(req, res, client.id) || !assertAdminCanManageAccessForUser(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) || !assertAdminCanManageUser(req, res, user.id)) {
|
if (!assertAdminCanManageClient(req, res, client.id) || !assertAdminCanManageAccessForUser(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) || !assertAdminCanManageUser(req, res, user.id)) {
|
if (!assertAdminCanManageClient(req, res, client.id) || !assertAdminCanManageAccessForUser(req, res, user.id)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1190,7 +1190,15 @@ app.patch("/api/admin/memberships/:membershipId", requireLauncherAdmin, asyncRou
|
||||||
return;
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1369,6 +1377,10 @@ 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);
|
||||||
|
|
@ -1389,6 +1401,11 @@ 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,
|
||||||
|
|
@ -1412,6 +1429,10 @@ 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);
|
||||||
|
|
@ -1445,7 +1466,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)) {
|
if (!assertAdminCanManageGrantTarget(req, res, snapshot.data, req.body?.targetType, req.body?.targetId, { allowProtectedSelf: true })) {
|
||||||
return;
|
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) => {
|
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;
|
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) => {
|
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;
|
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) => {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -3183,8 +3204,16 @@ 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 canAdminManageUser(req, userId) {
|
function isProtectedLauncherUser(userId) {
|
||||||
if (protectedLauncherUserIds.has(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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -3206,8 +3235,8 @@ function assertAdminCanManageClient(req, res, clientId) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function assertAdminCanManageUser(req, res, userId) {
|
function assertAdminCanManageUser(req, res, userId, options = {}) {
|
||||||
if (canAdminManageUser(req, userId)) {
|
if (canAdminManageUser(req, userId, options)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -3215,19 +3244,50 @@ function assertAdminCanManageUser(req, res, userId) {
|
||||||
return false;
|
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)) {
|
if (!assertAdminCanManageClient(req, res, membership.clientId)) {
|
||||||
return false;
|
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) {
|
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") {
|
||||||
return assertAdminCanManageClient(req, res, targetId);
|
return assertAdminCanManageClient(req, res, targetId);
|
||||||
}
|
}
|
||||||
|
|
@ -3240,11 +3300,21 @@ function assertAdminCanManageGrantTarget(req, res, data, targetType, targetId) {
|
||||||
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);
|
return assertAdminCanManageUser(req, res, targetId, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
res.status(403).json({ error: "Недостаточно прав для управления этим доступом" });
|
res.status(403).json({ error: "Недостаточно прав для управления этим доступом" });
|
||||||
|
|
|
||||||
|
|
@ -163,6 +163,20 @@ 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,
|
||||||
|
|
@ -499,6 +513,7 @@ 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}
|
||||||
|
|
@ -517,6 +532,7 @@ 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 })}
|
||||||
|
|
@ -1174,12 +1190,14 @@ 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;
|
||||||
|
|
@ -1247,6 +1265,7 @@ 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))}
|
||||||
|
|
@ -2674,12 +2693,14 @@ 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;
|
||||||
|
|
@ -2718,17 +2739,23 @@ 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);
|
||||||
|
|
||||||
|
return (
|
||||||
<button
|
<button
|
||||||
key={user.id}
|
key={user.id}
|
||||||
className="admin-token"
|
className="admin-token"
|
||||||
data-active={draft.memberIds.includes(user.id)}
|
data-active={draft.memberIds.includes(user.id)}
|
||||||
type="button"
|
type="button"
|
||||||
|
disabled={lockedProtectedUser}
|
||||||
|
title={lockedProtectedUser ? "Защищённого пользователя может менять в группах только он сам" : undefined}
|
||||||
onClick={() => toggleUser(user.id)}
|
onClick={() => toggleUser(user.id)}
|
||||||
>
|
>
|
||||||
{user.name}
|
{user.name}
|
||||||
</button>
|
</button>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -2926,6 +2953,7 @@ function mediaKindFromUrl(value: string): MediaKind | null {
|
||||||
|
|
||||||
function AccessSection({
|
function AccessSection({
|
||||||
data,
|
data,
|
||||||
|
me,
|
||||||
matrix,
|
matrix,
|
||||||
selectedCell,
|
selectedCell,
|
||||||
onSelectCell,
|
onSelectCell,
|
||||||
|
|
@ -2942,6 +2970,7 @@ 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;
|
||||||
|
|
@ -3011,7 +3040,9 @@ 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 = user.id === "user_root";
|
const protectedUser = isProtectedLauncherUser(user.id);
|
||||||
|
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]);
|
||||||
|
|
@ -3038,7 +3069,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={protectedUser}
|
protectedUser={protectedFromActor}
|
||||||
onChange={(role) => onUpdateMembership(membership.id, { role })}
|
onChange={(role) => onUpdateMembership(membership.id, { role })}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -3056,13 +3087,14 @@ 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 || protectedUser || forcedTaskManagerAdmin) return;
|
if (!isTaskManagerService || !primaryTaskManagerWorkspace || protectedFromActor || (forcedTaskManagerAdmin && !protectedSelfManage)) return;
|
||||||
|
|
||||||
onSetTaskManagerWorkspaceMemberRole({
|
onSetTaskManagerWorkspaceMemberRole({
|
||||||
clientId: matrix.client.id,
|
clientId: matrix.client.id,
|
||||||
|
|
@ -3086,6 +3118,7 @@ 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}
|
||||||
|
|
@ -3110,6 +3143,7 @@ function AccessSection({
|
||||||
|
|
||||||
function PublicAccessUsersPanel({
|
function PublicAccessUsersPanel({
|
||||||
data,
|
data,
|
||||||
|
me,
|
||||||
matrix,
|
matrix,
|
||||||
selectedCell,
|
selectedCell,
|
||||||
onSelectCell,
|
onSelectCell,
|
||||||
|
|
@ -3121,6 +3155,7 @@ 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;
|
||||||
|
|
@ -3161,7 +3196,8 @@ 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 = user.id === "user_root";
|
const protectedUser = isProtectedLauncherUser(user.id);
|
||||||
|
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
|
||||||
|
|
@ -3196,7 +3232,7 @@ function PublicAccessUsersPanel({
|
||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{protectedUser ? (
|
{protectedFromActor ? (
|
||||||
<AdminStaticPill>{membershipRoleLabel(membership.role)}</AdminStaticPill>
|
<AdminStaticPill>{membershipRoleLabel(membership.role)}</AdminStaticPill>
|
||||||
) : (
|
) : (
|
||||||
<NodeDcSelect
|
<NodeDcSelect
|
||||||
|
|
@ -3253,6 +3289,7 @@ 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}
|
||||||
|
|
@ -3360,6 +3397,7 @@ function MainRoleControl({
|
||||||
|
|
||||||
function OperationalCoreAccessModal({
|
function OperationalCoreAccessModal({
|
||||||
data,
|
data,
|
||||||
|
me,
|
||||||
client,
|
client,
|
||||||
user,
|
user,
|
||||||
service,
|
service,
|
||||||
|
|
@ -3378,6 +3416,7 @@ function OperationalCoreAccessModal({
|
||||||
onSetServiceModuleEntitlement,
|
onSetServiceModuleEntitlement,
|
||||||
}: {
|
}: {
|
||||||
data: LauncherData;
|
data: LauncherData;
|
||||||
|
me: MeResponse;
|
||||||
client: Client;
|
client: Client;
|
||||||
user: LauncherUser;
|
user: LauncherUser;
|
||||||
service: Service;
|
service: Service;
|
||||||
|
|
@ -3396,7 +3435,8 @@ 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 protectedUser = user.id === "user_root" || membership?.role === "client_owner";
|
const protectedSelfManage = canSelfManageProtectedUser(me, user.id);
|
||||||
|
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;
|
||||||
|
|
@ -3628,6 +3668,7 @@ function AccessCellControl({
|
||||||
pendingValue,
|
pendingValue,
|
||||||
busy = false,
|
busy = false,
|
||||||
publicSelfService = false,
|
publicSelfService = false,
|
||||||
|
readOnly = false,
|
||||||
onSelectCell,
|
onSelectCell,
|
||||||
onSetAccess,
|
onSetAccess,
|
||||||
onOpenDetails,
|
onOpenDetails,
|
||||||
|
|
@ -3637,6 +3678,7 @@ 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;
|
||||||
|
|
@ -3663,6 +3705,7 @@ 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"
|
||||||
);
|
);
|
||||||
|
|
@ -3685,6 +3728,15 @@ function AccessCellControl({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (readOnly) {
|
||||||
|
return (
|
||||||
|
<span className={cellClassName}>
|
||||||
|
<strong>{displayTitle}</strong>
|
||||||
|
<span>{displaySource}</span>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NodeDcSelect
|
<NodeDcSelect
|
||||||
value={selectValue}
|
value={selectValue}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue