diff --git a/packages/fake/src/room.ts b/packages/fake/src/room.ts index 7f206603..302060e3 100644 --- a/packages/fake/src/room.ts +++ b/packages/fake/src/room.ts @@ -137,7 +137,7 @@ export const fakeEndpoints = new Elysia({ prefix: "/fake" }) } const lastEventId = events[events.length - 1]._id; - const lastEvent = events[events.length - 1].event as any; //TODO: fix typing + const lastEvent = events[events.length - 1].event; const inviteEvent = await signEvent( roomMemberEvent({ diff --git a/packages/homeserver/src/fixtures/ContextBuilder.ts b/packages/homeserver/src/fixtures/ContextBuilder.ts index 743dd6c4..d19081cc 100644 --- a/packages/homeserver/src/fixtures/ContextBuilder.ts +++ b/packages/homeserver/src/fixtures/ContextBuilder.ts @@ -34,9 +34,9 @@ export function createMediaId(length: number) { } class MockedRoom { - private events: EventStore[] = []; + public events: EventStore[] = []; constructor( - private roomId: string, + public roomId: string, events: EventStore[], ) { for (const event of events) { @@ -191,7 +191,10 @@ export class ContextBuilder { makeRequest, createRoom: async (sender: string, ...members: string[]) => { const { roomId, events } = await createRoom( - [sender, ...members], + [ + `@${sender}:${config.name}`, + ...members.map((member) => `@${member}:${config.name}`), + ], createSignedEvent(config.signingKey[0], config.name), `!${createMediaId(18)}:${config.name}`, ); diff --git a/packages/homeserver/src/procedures/autorization/ensureAuthorizationRules.ts b/packages/homeserver/src/procedures/autorization/ensureAuthorizationRules.ts index 286fb28b..c2d509bb 100644 --- a/packages/homeserver/src/procedures/autorization/ensureAuthorizationRules.ts +++ b/packages/homeserver/src/procedures/autorization/ensureAuthorizationRules.ts @@ -20,6 +20,17 @@ const getMissingEvents = (a: string[]) => []; // https://spec.matrix.org/v1.2/rooms/v9/#authorization-rules +export const ensureAuthorizationRulesBatch = function* ( + events: EventBase[], + roomId: string, + size = 100, +) { + const array = [...events]; + while (array.length) { + yield ensureAuthorizationRules(array.splice(0, size), roomId); + } +}; + export const ensureAuthorizationRules = async ( events: EventBase[], roomId: string, @@ -60,15 +71,22 @@ export const ensureAuthorizationRules = async ( } } + const eventsToBeStored = new Set(); for await (const event of sortedAuthEvents) { - console.log("event ->", event); - console.log("event.auth_events ->", event.auth_events); - console.log("authMap ->", authMap); + try { + if (await prep(event, authMap)) { + eventsToBeStored.add(generateId(event)); + } + } catch (e) { + console.log("error", e); + } } + + return eventsToBeStored; }; async function prep(event: EventBase, authMap: Map) { - const auth: EventBase[] = []; + const authEvents = new Map(); for (const authEventId of event.auth_events) { const ae = authMap.get(authEventId); if (!ae) { @@ -80,7 +98,7 @@ async function prep(event: EventBase, authMap: Map) { ); return; } - auth.push(ae); + authEvents.set(authEventId, ae); } // We're not bothering about room state, so flag the event as an outlier. // event.internalMetadata.outlier = true; @@ -89,12 +107,12 @@ async function prep(event: EventBase, authMap: Map) { // validateEventForRoomVersion(event); switch (true) { case isRoomCreateEvent(event): { - await validateRoomCreateEvent(event, authMap); - break; + await validateRoomCreateEvent(event, authEvents); + return true; } case isRoomMemberEvent(event): { - await validateRoomMemberEvent(event, authMap); - break; + await validateRoomMemberEvent(event, authEvents); + return true; } } @@ -107,9 +125,9 @@ async function prep(event: EventBase, authMap: Map) { const callerInRoom = caller?.content.membership === "join"; - const callerPowerLevel = getUserPowerLevel(event.sender, authMap); + const callerPowerLevel = getUserPowerLevel(event.sender, authEvents); - const inviteLevel = getNamedPowerLevel("invite", authMap) ?? 0; + const inviteLevel = getNamedPowerLevel("invite", authEvents) ?? 0; // 5 If the sender’s current membership state is not join, reject. if (!callerInRoom) { @@ -138,7 +156,7 @@ async function prep(event: EventBase, authMap: Map) { // 9 If type is m.room.power_levels if (isRoomPowerLevelsEvent(event)) { - await validateRoomPowerLevelsEvent(event, authMap); + await validateRoomPowerLevelsEvent(event, authEvents); } // 10 otherwise, allow. return true; diff --git a/packages/homeserver/src/procedures/autorization/ensureAutorizationRules.spec.ts b/packages/homeserver/src/procedures/autorization/ensureAutorizationRules.spec.ts new file mode 100644 index 00000000..e9459da6 --- /dev/null +++ b/packages/homeserver/src/procedures/autorization/ensureAutorizationRules.spec.ts @@ -0,0 +1,167 @@ +import { describe, expect, test } from "bun:test"; +import { ensureAuthorizationRules } from "./ensureAuthorizationRules"; +import { hs1 } from "../../fixtures/ContextBuilder"; + +describe("ensureAuthorizationRules", () => { + test("it should pass - real case from synapse", async () => { + const promise = ensureAuthorizationRules( + [ + { + auth_events: [], + prev_events: [], + type: "m.room.create", + room_id: "!YrzBTuXYuWmkPFNPvP:hs1", + sender: "@admin:hs1", + content: { + room_version: "10", + creator: "@admin:hs1", + }, + depth: 1, + state_key: "", + origin: "hs1", + origin_server_ts: 1734749094984, + // @ts-expect-error + hashes: { + sha256: "4eNVJv9TsqtwoOObcFcVlBzPngedlhHFMJzC6XlXO48", + }, + signatures: { + hs1: { + "ed25519:a_HDhg": + "bU3jArtCF6n3j1cJ6uvybisyX8vC9m9pTphN6VG1ju5MhpiVjtBlgHoVOM2ofGZ6JK01kuE6By6PwaV7qwj/Dg", + }, + }, + unsigned: { + age: 1193, + }, + }, + { + auth_events: [ + "$CUQnskjHihUMzzxepn2fvkAybKdmUglnbUyV4sw4UnE", + "$xuZGslwbSTKYjCDAqlDsdvRy963ijx5lBIia2c2BOQw", + ], + prev_events: ["$xuZGslwbSTKYjCDAqlDsdvRy963ijx5lBIia2c2BOQw"], + type: "m.room.power_levels", + room_id: "!YrzBTuXYuWmkPFNPvP:hs1", + sender: "@admin:hs1", + content: { + users: { + "@admin:hs1": 100, + "@g21:rc1": 100, + }, + users_default: 0, + events: { + "m.room.name": 50, + "m.room.power_levels": 100, + "m.room.history_visibility": 100, + "m.room.canonical_alias": 50, + "m.room.avatar": 50, + "m.room.tombstone": 100, + "m.room.server_acl": 100, + "m.room.encryption": 100, + }, + events_default: 0, + state_default: 50, + ban: 50, + kick: 50, + redact: 50, + invite: 0, + historical: 100, + }, + depth: 3, + state_key: "", + origin: "hs1", + origin_server_ts: 1734749095033, + // @ts-expect-error + hashes: { + sha256: "caoUhtqs/bob6I4duim8flOAf9/qQP5t/aZ0g2ZJn2U", + }, + signatures: { + hs1: { + "ed25519:a_HDhg": + "dwH6+IoS1N+3c+PdMi8K21G6R4ndMJ7oK4ds3Ny9gUlh5i9qJpflE5YgSmY7tXJJaFxgu/ahHESMsmUZ1clJBQ", + }, + }, + unsigned: { + age: 1144, + }, + }, + { + auth_events: ["$CUQnskjHihUMzzxepn2fvkAybKdmUglnbUyV4sw4UnE"], + prev_events: ["$CUQnskjHihUMzzxepn2fvkAybKdmUglnbUyV4sw4UnE"], + type: "m.room.member", + room_id: "!YrzBTuXYuWmkPFNPvP:hs1", + sender: "@admin:hs1", + content: { + displayname: "admin", + membership: "join", + }, + depth: 2, + state_key: "@admin:hs1", + origin: "hs1", + origin_server_ts: 1734749095014, + // @ts-expect-error + hashes: { + sha256: "HseVbe6ngHdu2bJRVG9TfIy3HyQ+ZftmgYettfV0Kwc", + }, + signatures: { + hs1: { + "ed25519:a_HDhg": + "hwRDAopG7oSlEDt+V00hzGstHQixwdm86vexS9yT9eG13qXpDo7IQUmFxBVtGEPKnHs19yBCYMdBcrqBPK9eAA", + }, + }, + unsigned: { + age: 1163, + }, + }, + { + auth_events: [ + "$CUQnskjHihUMzzxepn2fvkAybKdmUglnbUyV4sw4UnE", + "$xuZGslwbSTKYjCDAqlDsdvRy963ijx5lBIia2c2BOQw", + "$pLzcU_5Kn3ir4HpYYee1XHVUTO3IvIqPmR2xDLxAL8I", + ], + prev_events: ["$pLzcU_5Kn3ir4HpYYee1XHVUTO3IvIqPmR2xDLxAL8I"], + type: "m.room.join_rules", + room_id: "!YrzBTuXYuWmkPFNPvP:hs1", + sender: "@admin:hs1", + content: { + join_rule: "invite", + }, + depth: 4, + state_key: "", + origin: "hs1", + origin_server_ts: 1734749095039, + // @ts-expect-error + hashes: { + sha256: "A5zAjIbuy4uoPN8wasdasukqViu8Ox7WjTmUSjXgVFPTTsEY", + }, + signatures: { + hs1: { + "ed25519:a_HDhg": + "zwScOvaLrjAYAalWDHit6IB7O00xJ1zNy5/Q39H8aDcUt85pwzvEntNPxtDcIr3bq91p3wca6FXV9egjZ//sDQ", + }, + }, + unsigned: { + age: 1138, + }, + }, + ], + "roomId", + ); + expect(async () => promise).not.toThrow(); + const data = await promise; + expect(data.size).toEqual(4); + }); + + test("it should pass - fake scenario made using createRoom", async () => { + const hs1Context = await hs1.build(); + + const room = await hs1Context.createRoom("@g21:hs1"); + + const result = await ensureAuthorizationRules( + room.events.map((event) => event.event), + room.roomId, + ); + + expect(result.size).toEqual(6); + }); +}); diff --git a/packages/homeserver/src/procedures/autorization/validateRoomPowerLevelsEvent.ts b/packages/homeserver/src/procedures/autorization/validateRoomPowerLevelsEvent.ts index 2eee3d03..f16480b2 100644 --- a/packages/homeserver/src/procedures/autorization/validateRoomPowerLevelsEvent.ts +++ b/packages/homeserver/src/procedures/autorization/validateRoomPowerLevelsEvent.ts @@ -6,6 +6,29 @@ import { import { getUserPowerLevel } from "./ensureAuthorizationRules"; import { env } from "bun"; +const isValidPowerLevel = ( + obj: unknown, +): obj is { + [key: string]: number; +} => { + if (typeof obj !== "object" || obj === null) { + return false; + } + + for (const key of Object.keys(obj)) { + if (typeof key !== "string") { + return false; + } + + const powerLevel = key in obj && obj[key as keyof typeof obj]; + + if (typeof powerLevel !== "number") { + return false; + } + } + + return true; +}; const isValidUserPowerLevel = ( obj: unknown, ): obj is { @@ -20,6 +43,7 @@ const isValidUserPowerLevel = ( return false; } const [user, server] = key.split(":"); + if (user.length === 0 || server.length === 0) { return false; } @@ -41,6 +65,7 @@ export const validateRoomPowerLevelsEvent = async ( // 9.1 If users key in content is not a dictionary with keys that are valid user IDs with values that are integers (or a string that is an integer), reject. const users = event.content.users ?? {}; + if (!isValidUserPowerLevel(users)) { throw new Error("Invalid users"); } @@ -65,7 +90,7 @@ export const validateRoomPowerLevelsEvent = async ( if ( typeof value !== "object" || value === null || - !isValidUserPowerLevel(value) + !isValidPowerLevel(value) ) { throw new Error("Invalid keys"); }