fix: let files in a shared folder be renamed and deleted on Stalwart 1.0, which sends rights for folders only

This commit is contained in:
Linus Rath
2026-10-29 02:11:28 +01:00
parent 87472ae937
commit 7c495d03ab
2 changed files with 26 additions and 4 deletions
+9
View File
@@ -32,6 +32,15 @@ describe('fileNodeAccess', () => {
expect(fileNodeAccess(nodes.at(-1)!, nodes)).toEqual({ rename: true, delete: true, move: false, duplicate: true }); expect(fileNodeAccess(nodes.at(-1)!, nodes)).toEqual({ rename: true, delete: true, move: false, duplicate: true });
}); });
it('takes the folder\'s rights for a file that names none (Stalwart 1.0)', () => {
const writable = shared(WRITE, WRITE);
const nodes = [...writable.slice(0, 2), { ...writable[2], myRights: undefined }];
expect(fileNodeAccess(nodes.at(-1)!, nodes)).toEqual({ rename: true, delete: true, move: false, duplicate: true });
const readOnly = shared(NONE, NONE);
const ro = [...readOnly.slice(0, 2), { ...readOnly[2], myRights: undefined }];
expect(fileNodeAccess(ro.at(-1)!, ro)).toEqual({ rename: false, delete: false, move: false, duplicate: false });
});
it('offers nothing inside a folder shared read-only', () => { it('offers nothing inside a folder shared read-only', () => {
const nodes = shared(NONE, NONE); const nodes = shared(NONE, NONE);
expect(fileNodeAccess(nodes.at(-1)!, nodes)).toEqual({ rename: false, delete: false, move: false, duplicate: false }); expect(fileNodeAccess(nodes.at(-1)!, nodes)).toEqual({ rename: false, delete: false, move: false, duplicate: false });
+17 -4
View File
@@ -27,24 +27,37 @@ function sharedParent(node: FileNode, nodes: FileNode[]): FileNode | undefined {
return nodes.find((n) => n.id === node.parentId && !n.discoverableOnly); return nodes.find((n) => n.id === node.parentId && !n.discoverableOnly);
} }
// Stalwart 1.0 sends `myRights` on folders only; a file has the rights of
// the folder it is in. Walk up to the nearest node that names its rights.
function effectiveRights(node: FileNode, nodes: FileNode[]): FileNode['myRights'] {
const seen = new Set<string>();
let current: FileNode | undefined = node;
while (current && !seen.has(current.id)) {
if (current.myRights) return current.myRights;
seen.add(current.id);
const parentId: string | null | undefined = current.parentId;
current = parentId == null ? undefined : nodes.find((n) => n.id === parentId);
}
return undefined;
}
export function fileNodeAccess(node: FileNode, nodes: FileNode[]): FileNodeAccess { export function fileNodeAccess(node: FileNode, nodes: FileNode[]): FileNodeAccess {
if (!node.isShared) return ALL; if (!node.isShared) return ALL;
const parent = sharedParent(node, nodes); const parent = sharedParent(node, nodes);
if (!parent) return { rename: false, delete: false, move: false, duplicate: false }; if (!parent) return { rename: false, delete: false, move: false, duplicate: false };
const rights = node.myRights; const rights = effectiveRights(node, nodes);
return { return {
rename: !!rights?.mayRename, rename: !!rights?.mayRename,
delete: !!rights?.mayDelete, delete: !!rights?.mayDelete,
move: false, move: false,
duplicate: !!parent.myRights?.mayAddChildren, duplicate: !!effectiveRights(parent, nodes)?.mayAddChildren,
}; };
} }
/** /**
* Whether new folders and uploads may go into the folder `folderId` (null: * Whether new folders and uploads may go into the folder `folderId` (null:
* the user's own drive root). Inside a shared folder only with its * the user's own drive root). Inside a shared folder only with its
* `mayAddChildren`; Stalwart 0.16 reports a child of a shared folder with * `mayAddChildren`.
* every right false, so there it is offered in the shared folder itself.
*/ */
export function mayCreateIn(folderId: string | null, nodes: FileNode[]): boolean { export function mayCreateIn(folderId: string | null, nodes: FileNode[]): boolean {
if (folderId === null) return true; if (folderId === null) return true;