diff --git a/.changeset/slick-plants-jump.md b/.changeset/slick-plants-jump.md new file mode 100644 index 00000000..efcc4934 --- /dev/null +++ b/.changeset/slick-plants-jump.md @@ -0,0 +1,7 @@ +--- +"@the-draupnir-project/matrix-protection-suite": minor +--- + +The PowerLevelsMirror has split in two into a PowerLevelsEventMirror and accept +the createEvent in the PowerLevelsMirror. This makes it harder to forget +privileged creators. diff --git a/apps/draupnir/src/managementroom/ManagementRoomDetail.ts b/apps/draupnir/src/managementroom/ManagementRoomDetail.ts index e78acd6c..3f062e3f 100644 --- a/apps/draupnir/src/managementroom/ManagementRoomDetail.ts +++ b/apps/draupnir/src/managementroom/ManagementRoomDetail.ts @@ -16,7 +16,6 @@ import { RoomCreateEvent, RoomMembershipRevisionIssuer, RoomStateRevisionIssuer, - RoomVersionMirror, } from "matrix-protection-suite"; export interface ManagementRoomDetail { @@ -78,9 +77,9 @@ export class StandardManagementRoomDetail implements ManagementRoomDetail { PowerLevelsMirror.isUserAbleToUse( draupnirUserID, PowerLevelPermission.StateDefault, + createEvent, powerLevelEvent.content - ) || - RoomVersionMirror.isUserAPrivilegedCreator(draupnirUserID, createEvent) + ) ) { return true; } diff --git a/apps/draupnir/src/protections/ProtectedRooms/WatchReplacementPolicyRooms.tsx b/apps/draupnir/src/protections/ProtectedRooms/WatchReplacementPolicyRooms.tsx index cd57b411..f9f09ed1 100644 --- a/apps/draupnir/src/protections/ProtectedRooms/WatchReplacementPolicyRooms.tsx +++ b/apps/draupnir/src/protections/ProtectedRooms/WatchReplacementPolicyRooms.tsx @@ -8,7 +8,7 @@ import { PolicyRoomManager, PolicyRuleType, PowerLevelsEvent, - PowerLevelsMirror, + PowerLevelsEventMirror, RoomCreateEvent, RoomEvent, RoomJoiner, @@ -74,7 +74,7 @@ function renderPrivilegedUsers(revision: RoomStateRevision): DocumentNode { const poweredUsers = powerLevels.content.users ? Object.entries(powerLevels.content.users) .filter(([userID]) => - PowerLevelsMirror.isUserAbleToSendEvent( + PowerLevelsEventMirror.isUserAbleToSendEvent( userID as StringUserID, PolicyRuleType.User, powerLevels.content diff --git a/packages/matrix-protection-suite-for-matrix-bot-sdk/src/ClientManagement/RoomStateManagerFactory.ts b/packages/matrix-protection-suite-for-matrix-bot-sdk/src/ClientManagement/RoomStateManagerFactory.ts index ac20f8df..12d64652 100644 --- a/packages/matrix-protection-suite-for-matrix-bot-sdk/src/ClientManagement/RoomStateManagerFactory.ts +++ b/packages/matrix-protection-suite-for-matrix-bot-sdk/src/ClientManagement/RoomStateManagerFactory.ts @@ -119,9 +119,19 @@ export class RoomStateManagerFactory { if (isError(roomStateIssuer)) { return roomStateIssuer; } + const createEvent = + roomStateIssuer.ok.currentRevision.getStateEvent( + "m.room.create", + "" + ); + if (createEvent === undefined) { + return ActionError.Result( + `Unable to find create event for ${room.toRoomIDOrAlias()} while creating policy room revision issuer` + ); + } const issuer = new RoomStatePolicyRoomRevisionIssuer( room, - StandardPolicyRoomRevision.blankRevision(room), + StandardPolicyRoomRevision.blankRevision(room, createEvent), roomStateIssuer.ok ); this.sha256Reverser?.addPolicyRoomRevisionIssuer(issuer); diff --git a/packages/matrix-protection-suite/src/Client/PowerLevelsMirror.ts b/packages/matrix-protection-suite/src/Client/PowerLevelsMirror.ts index 587e80a2..ae0d19eb 100644 --- a/packages/matrix-protection-suite/src/Client/PowerLevelsMirror.ts +++ b/packages/matrix-protection-suite/src/Client/PowerLevelsMirror.ts @@ -15,15 +15,31 @@ export enum PowerLevelPermission { StateDefault = "state_default", } -export type MissingPermissionsChange = { - missingStatePermissions: string[]; - missingPermissions: PowerLevelPermission[]; - missingEventPermissions: string[]; - isPrivilidgedInNextPowerLevels: boolean; - isPrivilidgedInPriorPowerLevels: boolean; -}; +export function isPowerLevelPermission( + permission: string +): permission is PowerLevelPermission { + switch (permission) { + case PowerLevelPermission.Ban: + case PowerLevelPermission.Invite: + case PowerLevelPermission.Kick: + case PowerLevelPermission.Redact: + case PowerLevelPermission.EventsDefault: + case PowerLevelPermission.StateDefault: + return true; + default: + return false; + } +} -export const PowerLevelsMirror = Object.freeze({ +/** + * Used for directly introspecting on the power levels event. + * Do not use to introspect on the room power levels because you need + * to also consider the room create event and room version. + * + * FIXME: Guh technically the default behaviours are specific to the room version + * too and the entire mirror. + */ +export const PowerLevelsEventMirror = Object.freeze({ getUserPowerLevel( who: StringUserID, content?: PowerLevelsEventContent @@ -42,6 +58,14 @@ export const PowerLevelsMirror = Object.freeze({ ): number { return content?.events?.[eventType] ?? content?.events_default ?? 0; }, + getPermissionPowerLevel( + permission: PowerLevelPermission, + content?: PowerLevelsEventContent + ): number { + const defaultPermissionLevel = + permission === PowerLevelPermission.Invite ? 0 : 50; + return content?.[permission] ?? defaultPermissionLevel; + }, isUserAbleToSendState( who: StringUserID, eventType: string, @@ -58,9 +82,7 @@ export const PowerLevelsMirror = Object.freeze({ content?: PowerLevelsEventContent ): boolean { const userLevel = this.getUserPowerLevel(who, content); - const defaultPermissionLevel = - permission === PowerLevelPermission.Invite ? 0 : 50; - const permissionLevel = content?.[permission] ?? defaultPermissionLevel; + const permissionLevel = this.getPermissionPowerLevel(permission, content); return userLevel >= permissionLevel; }, isUserAbleToSendEvent( @@ -73,6 +95,59 @@ export const PowerLevelsMirror = Object.freeze({ this.getEventPowerLevel(eventType, content) ); }, +}); + +export type MissingPermissionsChange = { + missingStatePermissions: string[]; + missingPermissions: PowerLevelPermission[]; + missingEventPermissions: string[]; + isPrivilidgedInNextPowerLevels: boolean; + isPrivilidgedInPriorPowerLevels: boolean; +}; + +export const PowerLevelsMirror = Object.freeze({ + isUserAbleToSendState( + who: StringUserID, + eventType: string, + createEvent: RoomCreateEvent, + powerLevelsContent?: PowerLevelsEventContent + ): boolean { + return ( + PowerLevelsEventMirror.isUserAbleToSendState( + who, + eventType, + powerLevelsContent + ) || RoomVersionMirror.isUserAPrivilegedCreator(who, createEvent) + ); + }, + isUserAbleToUse( + who: StringUserID, + permission: PowerLevelPermission, + createEvent: RoomCreateEvent, + powerLevelsContent?: PowerLevelsEventContent + ): boolean { + return ( + PowerLevelsEventMirror.isUserAbleToUse( + who, + permission, + powerLevelsContent + ) || RoomVersionMirror.isUserAPrivilegedCreator(who, createEvent) + ); + }, + isUserAbleToSendEvent( + who: StringUserID, + eventType: string, + createEvent: RoomCreateEvent, + powerLevelsContent?: PowerLevelsEventContent + ): boolean { + return ( + PowerLevelsEventMirror.isUserAbleToSendEvent( + who, + eventType, + powerLevelsContent + ) || RoomVersionMirror.isUserAPrivilegedCreator(who, createEvent) + ); + }, missingPermissions( clientUserID: StringUserID, requiredPermissions: PowerLevelPermission[], @@ -81,7 +156,7 @@ export const PowerLevelsMirror = Object.freeze({ const missingPermissions: PowerLevelPermission[] = []; for (const permission of requiredPermissions) { if ( - !PowerLevelsMirror.isUserAbleToUse( + !PowerLevelsEventMirror.isUserAbleToUse( clientUserID, permission, powerLevelsContent @@ -100,7 +175,7 @@ export const PowerLevelsMirror = Object.freeze({ const missingPermissions: string[] = []; for (const permission of requiredStatePermissions) { if ( - !PowerLevelsMirror.isUserAbleToSendState( + !PowerLevelsEventMirror.isUserAbleToSendState( clientUserID, permission, powerLevelsContent @@ -119,7 +194,7 @@ export const PowerLevelsMirror = Object.freeze({ const missingPermissions: string[] = []; for (const permission of requiredEventPermissions) { if ( - !PowerLevelsMirror.isUserAbleToSendEvent( + !PowerLevelsEventMirror.isUserAbleToSendEvent( clientUserID, permission, powerLevelsContent diff --git a/packages/matrix-protection-suite/src/PolicyList/StandardPolicyRoomRevision.ts b/packages/matrix-protection-suite/src/PolicyList/StandardPolicyRoomRevision.ts index 38cde909..297210c1 100644 --- a/packages/matrix-protection-suite/src/PolicyList/StandardPolicyRoomRevision.ts +++ b/packages/matrix-protection-suite/src/PolicyList/StandardPolicyRoomRevision.ts @@ -46,6 +46,7 @@ import { import { isError } from "@gnuxie/typescript-result"; import { SHA256 } from "crypto-js"; import Base64 from "crypto-js/enc-base64"; +import { RoomCreateEvent } from "../MatrixTypes/CreateRoom"; const log = new Logger("StandardPolicyRoomRevision"); @@ -91,13 +92,17 @@ export class StandardPolicyRoomRevision implements PolicyRoomRevision { */ private readonly policyRuleByEventId: PolicyRuleByEventIDMap, private readonly policyRuleBySHA256: PolicyRuleByHashMap, + private readonly createEvent: RoomCreateEvent, private readonly powerLevelsEvent: PowerLevelsEvent | undefined ) {} /** * @returns An empty revision. */ - public static blankRevision(room: MatrixRoomID): StandardPolicyRoomRevision { + public static blankRevision( + room: MatrixRoomID, + createEvent: RoomCreateEvent + ): StandardPolicyRoomRevision { return new StandardPolicyRoomRevision( room, new Revision(), @@ -105,6 +110,7 @@ export class StandardPolicyRoomRevision implements PolicyRoomRevision { PersistentMap(), PersistentMap(), PersistentMap(), + createEvent, undefined ); } @@ -338,6 +344,7 @@ export class StandardPolicyRoomRevision implements PolicyRoomRevision { nextPolicyRules, nextPolicyRulesByEventID, nextPolicyRulesBySHA256, + this.createEvent, this.powerLevelsEvent ); } @@ -506,6 +513,7 @@ export class StandardPolicyRoomRevision implements PolicyRoomRevision { return PowerLevelsMirror.isUserAbleToSendState( who, policy, + this.createEvent, powerLevelsContent ); } @@ -520,6 +528,7 @@ export class StandardPolicyRoomRevision implements PolicyRoomRevision { this.policyRules, this.policyRuleByEventId, this.policyRuleBySHA256, + this.createEvent, powerLevels ); } @@ -531,6 +540,7 @@ export class StandardPolicyRoomRevision implements PolicyRoomRevision { this.policyRules, this.policyRuleByEventId, this.policyRuleBySHA256, + this.createEvent, this.powerLevelsEvent ); } diff --git a/packages/matrix-protection-suite/src/StateTracking/DeclareRoomState.ts b/packages/matrix-protection-suite/src/StateTracking/DeclareRoomState.ts index db2be37b..83540edd 100644 --- a/packages/matrix-protection-suite/src/StateTracking/DeclareRoomState.ts +++ b/packages/matrix-protection-suite/src/StateTracking/DeclareRoomState.ts @@ -42,6 +42,7 @@ import { MjolnirPolicyRoomsConfig } from "../Protection/PolicyListConfig/Mjolnir import { StandardWatchedPolicyRooms } from "../Protection/WatchedPolicyRooms/StandardWatchedPolicyRooms"; import { DefaultEventDecoder } from "../MatrixTypes/DefaultEventDecoder"; import { DefaultMixinExtractor } from "../SafeMatrixEvents/MatrixEventMixinDescriptions/DefaultMixinExtractor"; +import { RoomCreateEvent } from "../MatrixTypes/CreateRoom"; const log = new Logger("DeclareRoomState"); @@ -219,16 +220,29 @@ export function describeRoom({ membershipDescriptions, policyDescriptions, }); + // if a create event isn't provided, make one. + const providedCreateEvent = stateEvents.find( + (event) => event.type === "m.room.create" + ); + const createEvent = + (providedCreateEvent as RoomCreateEvent | undefined) ?? + (describeStateEvent({ + sender: randomUserID(), + room_id: room.toRoomIDOrAlias(), + type: "m.room.create", + state_key: "", + content: {}, + }) as RoomCreateEvent); const stateRevision = StandardRoomStateRevision.blankRevision(room).reviseFromState(stateEvents); const membershipRevision = StandardRoomMembershipRevision.blankRevision(room).reviseFromMembership( membershipEvents ); - const policyRevision = - StandardPolicyRoomRevision.blankRevision(room).reviseFromState( - policyEvents - ); + const policyRevision = StandardPolicyRoomRevision.blankRevision( + room, + createEvent + ).reviseFromState(policyEvents); const stateRevisionIssuer = new FakeRoomStateRevisionIssuer( stateRevision, room