Fix Launcher Engine access role handling
This commit is contained in:
parent
03f4044e35
commit
de446c1879
|
|
@ -1212,6 +1212,7 @@ export function createControlPlaneStore({ projectRoot }) {
|
|||
}
|
||||
|
||||
const targetUser = findById(data.users, request.targetUserId, "user");
|
||||
const membership = ensureEngineWorkflowPublicPoolMembership(data, targetUser, request, actor, now);
|
||||
const grant = ensureEngineWorkflowServiceAccess(data, targetUser, request.role, now);
|
||||
|
||||
request.status = "approved";
|
||||
|
|
@ -1230,7 +1231,7 @@ export function createControlPlaneStore({ projectRoot }) {
|
|||
});
|
||||
|
||||
await writeData(data);
|
||||
return { engineWorkflowAccessRequest: request, grant, targetUser, data };
|
||||
return { engineWorkflowAccessRequest: request, grant, membership, targetUser, data };
|
||||
}
|
||||
|
||||
async function rejectEngineWorkflowAccessRequest(engineWorkflowAccessRequestId, payload, identity) {
|
||||
|
|
@ -1647,6 +1648,11 @@ export function createControlPlaneStore({ projectRoot }) {
|
|||
const value = requireString(payload?.value, "value");
|
||||
const user = findById(data.users, userId, "user");
|
||||
const service = findById(data.services, serviceId, "service");
|
||||
const engineService = isEngineService(service);
|
||||
|
||||
if (engineService && (value === "admin" || value === "owner")) {
|
||||
throw new Error("Для Engine доступны только роли guest, member или блокировка");
|
||||
}
|
||||
|
||||
const directGrant = data.grants.find(
|
||||
(grant) => grant.serviceId === serviceId && grant.targetType === "user" && grant.targetId === userId
|
||||
|
|
@ -1677,6 +1683,9 @@ export function createControlPlaneStore({ projectRoot }) {
|
|||
createdAt: directGrant?.createdAt ?? now,
|
||||
updatedAt: now,
|
||||
});
|
||||
if (engineService) {
|
||||
ensurePublicPoolMembershipActiveForServiceAccess(data, user, actor, now);
|
||||
}
|
||||
} else if (value !== "unset") {
|
||||
throw new Error(`Unsupported access value: ${value}`);
|
||||
}
|
||||
|
|
@ -2662,13 +2671,78 @@ function ensureEngineWorkflowServiceAccess(data, user, workflowRole, now) {
|
|||
return grant;
|
||||
}
|
||||
|
||||
function ensureEngineWorkflowPublicPoolMembership(data, user, request, actor, now) {
|
||||
const invitedByUserId = request.requesterUserId || actor?.id || null;
|
||||
let membership = data.memberships.find(
|
||||
(candidate) => candidate.clientId === publicPoolClientId && candidate.userId === user.id
|
||||
);
|
||||
|
||||
if (membership) {
|
||||
const wasActive = membership.status === "active";
|
||||
membership.status = "active";
|
||||
if (!wasActive) {
|
||||
membership.role = "member";
|
||||
membership.source = "engine_workflow_access_request";
|
||||
membership.sourceEngineWorkflowAccessRequestId = request.id;
|
||||
} else {
|
||||
membership.sourceEngineWorkflowAccessRequestId =
|
||||
membership.sourceEngineWorkflowAccessRequestId ?? request.id;
|
||||
}
|
||||
membership.invitedByUserId = membership.invitedByUserId ?? invitedByUserId;
|
||||
membership.updatedAt = now;
|
||||
markPendingSync(data, user, "user", user.email);
|
||||
return membership;
|
||||
}
|
||||
|
||||
membership = {
|
||||
id: uniqueId(data.memberships, "mem", `${publicPoolClientId}-${user.email}`),
|
||||
clientId: publicPoolClientId,
|
||||
userId: user.id,
|
||||
role: "member",
|
||||
status: "active",
|
||||
invitedByUserId,
|
||||
inviteId: null,
|
||||
source: "engine_workflow_access_request",
|
||||
sourceTaskerInviteRequestId: null,
|
||||
sourceEngineWorkflowAccessRequestId: request.id,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
};
|
||||
data.memberships.push(membership);
|
||||
markPendingSync(data, user, "user", user.email);
|
||||
return membership;
|
||||
}
|
||||
|
||||
function ensurePublicPoolMembershipActiveForServiceAccess(data, user, actor, now) {
|
||||
const membership = data.memberships.find(
|
||||
(candidate) => candidate.clientId === publicPoolClientId && candidate.userId === user.id
|
||||
);
|
||||
if (!membership || membership.status === "active") {
|
||||
return null;
|
||||
}
|
||||
|
||||
membership.role = "member";
|
||||
membership.status = "active";
|
||||
membership.invitedByUserId = membership.invitedByUserId ?? actor?.id ?? null;
|
||||
membership.source = membership.source ?? "launcher";
|
||||
membership.updatedAt = now;
|
||||
markPendingSync(data, user, "user", user.email);
|
||||
return membership;
|
||||
}
|
||||
|
||||
function findEngineService(data) {
|
||||
return data.services.find(
|
||||
(candidate) =>
|
||||
candidate.id === "service_nodedc" ||
|
||||
candidate.slug === "nodedc" ||
|
||||
candidate.slug === "engine" ||
|
||||
candidate.authentikApplicationSlug === "nodedc-engine"
|
||||
return data.services.find(isEngineService);
|
||||
}
|
||||
|
||||
function isEngineService(candidate) {
|
||||
return Boolean(
|
||||
candidate &&
|
||||
(
|
||||
candidate.id === "service_nodedc" ||
|
||||
candidate.slug === "nodedc" ||
|
||||
candidate.slug === "engine" ||
|
||||
candidate.authentikApplicationSlug === "nodedc-engine"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1527,7 +1527,6 @@ function buildLauncherNotifications(data: LauncherData, me: ReturnType<typeof bu
|
|||
for (const request of data.accessRequests) {
|
||||
const isOwnRequest = request.email.toLowerCase() === currentEmail;
|
||||
if (!canModerate && !isOwnRequest) continue;
|
||||
if (canModerate && request.status !== "new") continue;
|
||||
|
||||
const applicantName = [request.lastName, request.firstName].filter(Boolean).join(" ") || request.email;
|
||||
items.push({
|
||||
|
|
@ -1547,7 +1546,6 @@ function buildLauncherNotifications(data: LauncherData, me: ReturnType<typeof bu
|
|||
request.inviterEmail.toLowerCase() === currentEmail ||
|
||||
request.inviteeEmail.toLowerCase() === currentEmail;
|
||||
if (!canModerate && !isRelated) continue;
|
||||
if (canModerate && request.status !== "new") continue;
|
||||
|
||||
items.push({
|
||||
id: `tasker:${request.id}`,
|
||||
|
|
@ -1567,7 +1565,6 @@ function buildLauncherNotifications(data: LauncherData, me: ReturnType<typeof bu
|
|||
request.requesterEmail.toLowerCase() === currentEmail ||
|
||||
request.targetEmail.toLowerCase() === currentEmail;
|
||||
if (!canModerate && !isRelated) continue;
|
||||
if (canModerate && request.status !== "new") continue;
|
||||
|
||||
items.push({
|
||||
id: `engine:${request.id}`,
|
||||
|
|
|
|||
|
|
@ -32,8 +32,9 @@ export interface ClientMembership {
|
|||
status: ClientMembershipStatus;
|
||||
invitedByUserId?: string | null;
|
||||
inviteId?: string | null;
|
||||
source?: "launcher" | "access_request" | "tasker_workspace_invite" | null;
|
||||
source?: "launcher" | "access_request" | "tasker_workspace_invite" | "engine_workflow_access_request" | null;
|
||||
sourceTaskerInviteRequestId?: string | null;
|
||||
sourceEngineWorkflowAccessRequestId?: string | null;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -178,6 +178,15 @@ function isProtectedFromCurrentActor(me: MeResponse, userId: string): boolean {
|
|||
return isProtectedLauncherUser(userId) && !canSelfManageProtectedUser(me, userId);
|
||||
}
|
||||
|
||||
function isEngineServiceForAccessControl(service: Service): boolean {
|
||||
return (
|
||||
service.id === "service_nodedc" ||
|
||||
service.slug === "nodedc" ||
|
||||
service.slug === "engine" ||
|
||||
service.authentikApplicationSlug === "nodedc-engine"
|
||||
);
|
||||
}
|
||||
|
||||
export function AdminOverlay({
|
||||
data,
|
||||
me,
|
||||
|
|
@ -1399,6 +1408,14 @@ const accessAssignmentOptions: Array<NodeDcSelectOption<AccessAssignmentValue>>
|
|||
{ value: "deny", label: "Заблокирован", description: "Запрет доступа", tone: "red" },
|
||||
];
|
||||
|
||||
const engineAccessAssignmentOptions: Array<NodeDcSelectOption<AccessAssignmentValue>> = [
|
||||
{ value: "unset", label: "—", description: "Не назначен" },
|
||||
{ value: "viewer", label: "Гость", description: "Только явно открытые workflow", tone: "green" },
|
||||
{ value: "member", label: "Участник", description: "Рабочий доступ", tone: "green" },
|
||||
{ value: "admin", label: "Админ", description: "Существующее значение", tone: "green", hidden: true },
|
||||
{ value: "deny", label: "Заблокирован", description: "Запрет доступа", tone: "red" },
|
||||
];
|
||||
|
||||
const publicOperationalCoreAccessOptions: Array<NodeDcSelectOption<AccessAssignmentValue>> = [
|
||||
{ value: "unset", label: "—", description: "Не назначен", hidden: true },
|
||||
{ value: "viewer", label: "Workspace Guest", description: "Выдаётся только через Tasker", tone: "green", hidden: true },
|
||||
|
|
@ -3103,6 +3120,7 @@ function AccessSection({
|
|||
<div key={service.id} className="access-grid-cell" role="cell">
|
||||
<AccessCellControl
|
||||
cell={cell}
|
||||
service={service}
|
||||
active={active}
|
||||
pendingValue={pendingAccessAssignments[accessCellKey(user.id, service.id)]}
|
||||
busy={!usePublicTaskerAccess && isTaskManagerService && pendingTaskerAssignment}
|
||||
|
|
@ -3287,9 +3305,10 @@ function PublicAccessUsersPanel({
|
|||
)}
|
||||
</td>
|
||||
<td>
|
||||
{operationalCoreCell ? (
|
||||
{operationalCoreCell && operationalCoreService ? (
|
||||
<AccessCellControl
|
||||
cell={operationalCoreCell}
|
||||
service={operationalCoreService}
|
||||
active={selectedCell?.userId === user.id && selectedCell.serviceId === operationalCoreCell.serviceId}
|
||||
pendingValue={pendingAccessAssignments[accessCellKey(user.id, operationalCoreCell.serviceId)]}
|
||||
publicSelfService
|
||||
|
|
@ -3684,6 +3703,7 @@ function OperationalCoreAccessModal({
|
|||
|
||||
function AccessCellControl({
|
||||
cell,
|
||||
service,
|
||||
active,
|
||||
pendingValue,
|
||||
busy = false,
|
||||
|
|
@ -3694,6 +3714,7 @@ function AccessCellControl({
|
|||
onOpenDetails,
|
||||
}: {
|
||||
cell: AccessMatrixCell;
|
||||
service: Service;
|
||||
active: boolean;
|
||||
pendingValue?: AccessAssignmentValue;
|
||||
busy?: boolean;
|
||||
|
|
@ -3706,7 +3727,11 @@ function AccessCellControl({
|
|||
const isPending = pendingValue !== undefined || busy;
|
||||
const assignmentValue = pendingValue ?? accessAssignmentValue(cell);
|
||||
const selectValue = publicSelfService ? publicOperationalCoreSelectValue(assignmentValue) : assignmentValue;
|
||||
const selectOptions = publicSelfService ? publicOperationalCoreAccessOptions : accessAssignmentOptions;
|
||||
const selectOptions = publicSelfService
|
||||
? publicOperationalCoreAccessOptions
|
||||
: isEngineServiceForAccessControl(service)
|
||||
? engineAccessAssignmentOptions
|
||||
: accessAssignmentOptions;
|
||||
const displayTitle = isPending
|
||||
? publicSelfService
|
||||
? publicAccessAssignmentLabel(assignmentValue)
|
||||
|
|
@ -4551,6 +4576,9 @@ function getMembershipInviterMeta(data: LauncherData, membership: ClientMembersh
|
|||
const taskerRequest = membership.sourceTaskerInviteRequestId
|
||||
? data.taskerInviteRequests.find((request) => request.id === membership.sourceTaskerInviteRequestId)
|
||||
: null;
|
||||
const engineRequest = membership.sourceEngineWorkflowAccessRequestId
|
||||
? data.engineWorkflowAccessRequests.find((request) => request.id === membership.sourceEngineWorkflowAccessRequestId)
|
||||
: null;
|
||||
|
||||
if (membership.source === "tasker_workspace_invite") {
|
||||
const title = inviter?.name ?? taskerRequest?.inviterName ?? "Operational Core";
|
||||
|
|
@ -4574,6 +4602,15 @@ function getMembershipInviterMeta(data: LauncherData, membership: ClientMembersh
|
|||
};
|
||||
}
|
||||
|
||||
if (membership.source === "engine_workflow_access_request") {
|
||||
return {
|
||||
title: inviter?.name ?? engineRequest?.requesterName ?? "Engine",
|
||||
subtitle: inviter?.email ?? engineRequest?.requesterEmail ?? "Workflow share request",
|
||||
sourceLabel: engineRequest?.workflowName ? `Engine · ${engineRequest.workflowName}` : "Engine workflow",
|
||||
showInAccessMatrix: true,
|
||||
};
|
||||
}
|
||||
|
||||
if (membership.inviteId) {
|
||||
return {
|
||||
title: inviter?.name ?? "NODE.DC",
|
||||
|
|
@ -4612,6 +4649,13 @@ function getPlatformUserOrigin(data: LauncherData, user: LauncherUser, membershi
|
|||
};
|
||||
}
|
||||
|
||||
if (publicMembership.source === "engine_workflow_access_request") {
|
||||
return {
|
||||
label: "Открытый контур · Engine workflow",
|
||||
detail: `${inviterMeta.title} · ${inviterMeta.sourceLabel}`,
|
||||
};
|
||||
}
|
||||
|
||||
if (publicMembership.inviteId) {
|
||||
return {
|
||||
label: "Открытый контур · ручной инвайт",
|
||||
|
|
|
|||
|
|
@ -110,8 +110,8 @@ export function TopBar({
|
|||
<div className="nodedc-notifications-list">
|
||||
{visibleNotifications.length === 0 ? (
|
||||
<div className="nodedc-notifications-empty">
|
||||
<strong>Новых входящих нет</strong>
|
||||
<span>Заявки NODE.DC, Operational Core и Engine появятся здесь.</span>
|
||||
<strong>Уведомлений нет</strong>
|
||||
<span>История заявок NODE.DC, Operational Core и Engine появится здесь.</span>
|
||||
</div>
|
||||
) : (
|
||||
visibleNotifications.map((notification) => (
|
||||
|
|
|
|||
Loading…
Reference in New Issue