Skip to content
This repository has been archived by the owner on Sep 11, 2024. It is now read-only.

Respect m.space.parent relations if they hold valid permissions #6746

Merged
merged 2 commits into from
Sep 8, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 29 additions & 6 deletions src/stores/SpaceStore.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -366,16 +366,22 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
}

public getParents(roomId: string, canonicalOnly = false): Room[] {
const userId = this.matrixClient?.getUserId();
const room = this.matrixClient?.getRoom(roomId);
return room?.currentState.getStateEvents(EventType.SpaceParent)
.filter(ev => {
.map(ev => {
const content = ev.getContent();
if (!content?.via?.length) return false;
// TODO apply permissions check to verify that the parent mapping is valid
if (canonicalOnly && !content?.canonical) return false;
return true;
if (Array.isArray(content?.via) && (!canonicalOnly || content?.canonical)) {
const parent = this.matrixClient.getRoom(ev.getStateKey());
// only respect the relationship if the sender has sufficient permissions in the parent to set
// child relations, as per MSC1772.
// https://github.com/matrix-org/matrix-doc/blob/main/proposals/1772-groups-as-rooms.md#relationship-between-rooms-and-spaces
if (parent?.currentState.maySendStateEvent(EventType.SpaceChild, userId)) {
return parent;
}
}
// else implicit undefined which causes this element to be filtered out
})
.map(ev => this.matrixClient.getRoom(ev.getStateKey()))
.filter(Boolean) || [];
}

Expand Down Expand Up @@ -530,6 +536,14 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
});
}

const hiddenChildren = new EnhancedMap<string, Set<string>>();
t3chguy marked this conversation as resolved.
Show resolved Hide resolved
visibleRooms.forEach(room => {
if (room.getMyMembership() !== "join") return;
this.getParents(room.roomId).forEach(parent => {
hiddenChildren.getOrCreate(parent.roomId, new Set()).add(room.roomId);
});
});

this.rootSpaces.forEach(s => {
// traverse each space tree in DFS to build up the supersets as you go up,
// reusing results from like subtrees.
Expand Down Expand Up @@ -559,6 +573,9 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
roomIds.add(roomId);
});
});
hiddenChildren.get(spaceId)?.forEach(roomId => {
roomIds.add(roomId);
});
this.spaceFilteredRooms.set(spaceId, roomIds);
return roomIds;
};
Expand Down Expand Up @@ -690,6 +707,12 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
}
this.emit(room.roomId);
break;

case EventType.RoomPowerLevels:
if (room.isSpaceRoom()) {
this.onRoomsUpdate();
}
break;
}
};

Expand Down
54 changes: 53 additions & 1 deletion test/stores/SpaceStore-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -276,10 +276,12 @@ describe("SpaceStore", () => {

describe("test fixture 1", () => {
beforeEach(async () => {
[fav1, fav2, fav3, dm1, dm2, dm3, orphan1, orphan2, invite1, invite2, room1].forEach(mkRoom);
[fav1, fav2, fav3, dm1, dm2, dm3, orphan1, orphan2, invite1, invite2, room1, room2, room3]
.forEach(mkRoom);
mkSpace(space1, [fav1, room1]);
mkSpace(space2, [fav1, fav2, fav3, room1]);
mkSpace(space3, [invite2]);
// client.getRoom.mockImplementation(roomId => rooms.find(room => room.roomId === roomId));

[fav1, fav2, fav3].forEach(roomId => {
client.getRoom(roomId).tags = {
Expand Down Expand Up @@ -329,6 +331,48 @@ describe("SpaceStore", () => {
]);
// dmPartner3 is not in any common spaces with you

// room 2 claims to be a child of space2 and is so via a valid m.space.parent
const cliRoom2 = client.getRoom(room2);
cliRoom2.currentState.getStateEvents.mockImplementation(testUtils.mockStateEventImplementation([
mkEvent({
event: true,
type: EventType.SpaceParent,
room: room2,
user: client.getUserId(),
skey: space2,
content: { via: [], canonical: true },
ts: Date.now(),
}),
]));
const cliSpace2 = client.getRoom(space2);
cliSpace2.currentState.maySendStateEvent.mockImplementation((evType: string, userId: string) => {
if (evType === EventType.SpaceChild) {
return userId === client.getUserId();
}
return true;
});

// room 3 claims to be a child of space3 but is not due to invalid m.space.parent (permissions)
const cliRoom3 = client.getRoom(room3);
cliRoom3.currentState.getStateEvents.mockImplementation(testUtils.mockStateEventImplementation([
mkEvent({
event: true,
type: EventType.SpaceParent,
room: room3,
user: client.getUserId(),
skey: space3,
content: { via: [], canonical: true },
ts: Date.now(),
}),
]));
const cliSpace3 = client.getRoom(space3);
cliSpace3.currentState.maySendStateEvent.mockImplementation((evType: string, userId: string) => {
if (evType === EventType.SpaceChild) {
return false;
}
return true;
});

await run();
});

Expand Down Expand Up @@ -445,6 +489,14 @@ describe("SpaceStore", () => {
expect(store.getNotificationState(space2).rooms.map(r => r.roomId).includes(room1)).toBeTruthy();
expect(store.getNotificationState(space3).rooms.map(r => r.roomId).includes(room1)).toBeFalsy();
});

it("honours m.space.parent if sender has permission in parent space", () => {
expect(store.getSpaceFilteredRoomIds(client.getRoom(space2)).has(room2)).toBeTruthy();
});

it("does not honour m.space.parent if sender does not have permission in parent space", () => {
expect(store.getSpaceFilteredRoomIds(client.getRoom(space3)).has(room3)).toBeFalsy();
});
});
});

Expand Down