mirror of
https://github.com/wekan/wekan.git
synced 2026-11-07 11:02:33 -05:00
Board import no longer asks for member mapping up front. Every imported member is
brought in as a virtual (placeholder) user carrying its avatar, username and full
name (the existing createPlaceholderUsers path), added to the board inactive with
no permissions. The client import flow is now a single step that imports
immediately with an empty mapping -- no map-members screen, nothing to get stuck
on.
Mapping a virtual member to an existing user is a deliberate, later action done by
a board admin from the board sidebar member-avatar popup ("Map to existing user"),
and CANNOT be used to gain privileges: the new board-scoped method
mapImportedBoardMember only maps a virtual member onto an existing ACTIVE, REAL
member of the SAME board and never changes that member's role (so no new
membership is created and nothing is escalated); authorization and this guarantee
live in the pure planBoardMemberMapping helper. The mapping reassigns the virtual
member's cards/comments/activities ON THAT BOARD to the target and removes the
placeholder from the board.
Automatic hang mitigation so an import can never spin forever: a client watchdog
surfaces an import-timeout and clears the spinner if importBoard does not answer in
WEKAN_IMPORT_TIMEOUT_MS (default 2 min), and the server bounds creator.create with
the same deadline via a new generic withDeadline() wrapper.
Covered by tests/boardMemberMapPlan.test.cjs (no-escalation + auth matrix),
tests/withDeadline.test.cjs (deadline wrapper) and tests/importVirtualMembers.test.cjs
(wiring).
Thanks to xet7 !
66 lines
2.7 KiB
JavaScript
66 lines
2.7 KiB
JavaScript
// Pure decision helper for mapping an imported VIRTUAL (placeholder) board member to an
|
|
// existing REAL user on the same board, from the board sidebar member-avatar popup.
|
|
//
|
|
// Security model — mapping can NEVER be used to gain privileges:
|
|
// - Only a board admin (or a site admin) may map.
|
|
// - The target must be an existing, ACTIVE, real (non-imported) member of THIS board.
|
|
// Mapping never adds a new member and never changes the target's role, so the target
|
|
// keeps exactly the role they already had — nothing is escalated. To give a real
|
|
// user a board role you use the normal add-member / change-permissions flow first
|
|
// (which is itself privilege-checked), then map the virtual member onto them.
|
|
// - The source must be an imported placeholder member of this board.
|
|
//
|
|
// Returns { ok:true, newMembers, placeholderId, targetId } (newMembers = members with the
|
|
// placeholder entry removed) or { ok:false, error } with a stable error key. Pure and
|
|
// dependency-free so it is unit-tested without a database.
|
|
|
|
function planBoardMemberMapping({
|
|
members,
|
|
placeholderId,
|
|
targetId,
|
|
callerId,
|
|
placeholderIsImported,
|
|
targetIsImported,
|
|
callerIsSiteAdmin = false,
|
|
} = {}) {
|
|
const list = Array.isArray(members) ? members : [];
|
|
const find = id => list.find(m => m && m.userId === id);
|
|
|
|
if (!placeholderId || !targetId || placeholderId === targetId) {
|
|
return { ok: false, error: 'bad-map' };
|
|
}
|
|
|
|
// Authorization: board admin of THIS board, or a site admin.
|
|
const caller = find(callerId);
|
|
const callerIsBoardAdmin = !!(caller && caller.isActive !== false && caller.isAdmin);
|
|
if (!callerIsSiteAdmin && !callerIsBoardAdmin) {
|
|
return { ok: false, error: 'not-authorized' };
|
|
}
|
|
|
|
// Source must be an imported placeholder that is a member of this board.
|
|
if (!placeholderIsImported) {
|
|
return { ok: false, error: 'not-a-placeholder' };
|
|
}
|
|
if (!find(placeholderId)) {
|
|
return { ok: false, error: 'placeholder-not-member' };
|
|
}
|
|
|
|
// Target must be a REAL (non-imported), ACTIVE member of this board — so no new
|
|
// membership is created and no role is granted by the mapping itself.
|
|
if (targetIsImported) {
|
|
return { ok: false, error: 'target-not-real' };
|
|
}
|
|
const targetMember = find(targetId);
|
|
if (!targetMember || targetMember.isActive === false) {
|
|
return { ok: false, error: 'target-not-board-member' };
|
|
}
|
|
|
|
// Resulting membership: drop the placeholder entry, leave the target's entry (and role)
|
|
// exactly as it was. No entry is added, no flag is raised.
|
|
const newMembers = list.filter(m => m && m.userId !== placeholderId);
|
|
|
|
return { ok: true, newMembers, placeholderId, targetId };
|
|
}
|
|
|
|
module.exports = { planBoardMemberMapping };
|