mirror of
https://github.com/HeyPuter/puter.git
synced 2026-09-23 21:56:57 +00:00
fix: stop pushing file metadata to revoked recipients (PUT-1611)
The realtime fan-out resolved its audience straight from the `share` index, which has no live-grant check. `/auth/revoke-user-user` deletes the permission and leaves the index row, so a revoked recipient's socket kept receiving name, size, masked path and mtime for every write and move under the folder, with no expiry. The service already solves this elsewhere — `#reachingHolders` returns exactly the holder/entry pairs whose grant still stands, and `listSharedWithMe` was moved onto it for the same reason. The realtime path never got the same treatment; it does now. Free on the unshared path: with no share rows reaching the entry there are no holders to check, so the write path every user takes is unchanged. Pinned by a test that counts permission reads.
This commit is contained in:
@@ -2035,6 +2035,69 @@ describe('ShareService', () => {
|
||||
expect(lookups).toBeLessThanOrEqual(2);
|
||||
});
|
||||
|
||||
it('stays quiet for a recipient whose grant was revoked', async () => {
|
||||
const owner = await makeUser();
|
||||
const recipient = await makeUser();
|
||||
const { dir } = await makeDirWithFile(owner.user);
|
||||
|
||||
await share(owner.actor, {
|
||||
uid: dir.uuid,
|
||||
recipient: { email: recipient.email },
|
||||
mode: 'read',
|
||||
});
|
||||
|
||||
// As `/auth/revoke-user-user` does; the share row survives.
|
||||
await server.services.permission.revokeUserUserPermission(
|
||||
owner.actor,
|
||||
recipient.user.username!,
|
||||
`fs:${dir.uuid}:read`,
|
||||
);
|
||||
|
||||
const seen: unknown[] = [];
|
||||
const listener = (_key: string, data: unknown) => {
|
||||
const payload = data as { user_id_list?: number[] };
|
||||
if (!payload.user_id_list?.includes(recipient.user.id)) return;
|
||||
seen.push(payload);
|
||||
};
|
||||
server.clients.event.on('outer.gui.item.added', listener);
|
||||
server.clients.event.on('outer.gui.item.updated', listener);
|
||||
|
||||
try {
|
||||
await server.services.fs.touch(owner.user.id, {
|
||||
path: `${dir.path}/after-revoke.txt`,
|
||||
});
|
||||
await new Promise((resolve) => setTimeout(resolve, 150));
|
||||
} finally {
|
||||
server.clients.event.off('outer.gui.item.added', listener);
|
||||
server.clients.event.off('outer.gui.item.updated', listener);
|
||||
}
|
||||
|
||||
expect(seen).toEqual([]);
|
||||
});
|
||||
|
||||
it('checks no grants when nothing is shared', async () => {
|
||||
const owner = await makeUser();
|
||||
const { dir } = await makeDirWithFile(owner.user);
|
||||
|
||||
// This guards every write on the server, shared or not.
|
||||
const linked = vi.spyOn(
|
||||
server.stores.permission,
|
||||
'readLinkedUserUserPerms',
|
||||
);
|
||||
let reads = -1;
|
||||
try {
|
||||
await server.services.fs.touch(owner.user.id, {
|
||||
path: `${dir.path}/unshared.txt`,
|
||||
});
|
||||
await new Promise((resolve) => setTimeout(resolve, 150));
|
||||
reads = linked.mock.calls.length;
|
||||
} finally {
|
||||
linked.mockRestore();
|
||||
}
|
||||
|
||||
expect(reads).toBe(0);
|
||||
});
|
||||
|
||||
it('stays quiet for an event another node already handled', async () => {
|
||||
const owner = await makeUser();
|
||||
const recipient = await makeUser();
|
||||
|
||||
@@ -689,6 +689,10 @@ export class ShareService extends PuterService {
|
||||
realPath: string = entry.path,
|
||||
): Promise<Array<{ root: FSEntry; holders: number[] }>> {
|
||||
const { rows, nodesById } = await this.#sharesReaching(entry, realPath);
|
||||
// An index row outlives the grant it records, so a recipient revoked
|
||||
// through the ACL alone would keep receiving pushes. Free when nothing
|
||||
// is shared, which is the path every write takes.
|
||||
const live = await this.#reachingHolders(rows, nodesById);
|
||||
|
||||
// Deepest root wins, so a holder with nested shares is told once.
|
||||
const rootByHolder = new Map<number, FSEntry>();
|
||||
@@ -697,6 +701,7 @@ export class ShareService extends PuterService {
|
||||
const root = nodesById.get(Number(row.fsentry_id));
|
||||
if (!root || !Number.isFinite(holderId)) continue;
|
||||
if (holderId === entry.userId) continue;
|
||||
if (!live.has(`${holderId}:${root.id}`)) continue;
|
||||
const current = rootByHolder.get(holderId);
|
||||
if (!current || root.path.length > current.path.length) {
|
||||
rootByHolder.set(holderId, root);
|
||||
@@ -729,7 +734,7 @@ export class ShareService extends PuterService {
|
||||
entry: FSEntry,
|
||||
realPath: string = entry.path,
|
||||
): Promise<{
|
||||
rows: Array<{ holder_user_id: number; fsentry_id: number }>;
|
||||
rows: ShareIndexRow[];
|
||||
nodesById: Map<number, FSEntry>;
|
||||
}> {
|
||||
const ancestorPaths: string[] = [];
|
||||
|
||||
Reference in New Issue
Block a user