Split PowerLevelsMirror into PowerLevelsEventMirror (#1098)
Docker Hub - Develop / docker-latest (push) Failing after 33s
GHCR - Development Branches / ghcr-publish (push) Failing after 38s
Tests / Build & Lint (push) Failing after 6m54s
Tests / Integration tests (push) Failing after 23s
Tests / Application Service Integration tests (push) Failing after 15s
Tests / Unit tests (push) Successful in 7m44s

This is to accept the createEvent in the PowerLevelsMirror and makes it harder to forget privileged creators.
Closes https://github.com/the-draupnir-project/planning/issues/122.
This commit is contained in:
Gnuxie
2026-04-17 18:09:14 +01:00
committed by GitHub
parent be4fc0871b
commit ecfcc1c2c3
7 changed files with 140 additions and 25 deletions
+7
View File
@@ -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.
@@ -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;
}
@@ -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
@@ -119,9 +119,19 @@ export class RoomStateManagerFactory {
if (isError(roomStateIssuer)) {
return roomStateIssuer;
}
const createEvent =
roomStateIssuer.ok.currentRevision.getStateEvent<RoomCreateEvent>(
"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);
@@ -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
@@ -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
);
}
@@ -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