Skip to content
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
72 changes: 72 additions & 0 deletions packages/room/src/authorizartion-rules/rules.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1143,4 +1143,76 @@ describe('authorization rules', () => {
).not.toThrow();
});
// TODO: restricted rooms

it('should tolerate missing power level event for membership change', async () => {
const { create, join, joinRules, powerLevel } = getInitialEvents({
joinRule: 'public',
});

store.events.delete(powerLevel.eventId);

const state = getStateMap([create, join, joinRules]);

// try to join
const bob = '@bob:example.com';

const joinBob = new FakeStateEventCreator()
.asRoomMember()
.withRoomId(roomId)
.withSender(bob)
.withContent({ membership: 'join' })
.withStateKey(bob)
.build();

expect(() => checkEventAuthWithState(joinBob, state, store)).not.toThrow();
});

it('should tolerate missing power level event for power level change', async () => {
const { create, join, joinRules, powerLevel } = getInitialEvents({
joinRule: 'public',
});

store.events.delete(powerLevel.eventId);

const state = getStateMap([create, join, joinRules]);

// try to join
const pl2 = new FakeStateEventCreator()
.asPowerLevel()
.withRoomId(roomId)
.withSender(creator)
.withContent({
events: {},
users: {
[creator]: 100,
},
users_default: 0,
state_default: 50,
})
.build();

// should allow since no existing is there
expect(() => checkEventAuthWithState(pl2, state, store)).not.toThrow();

const state2 = getStateMap([create, join, joinRules, pl2]);

store.events.set(pl2.eventId, pl2);

const pl3 = new FakeStateEventCreator()
.asPowerLevel()
.withRoomId(roomId)
.withSender(creator)
.withContent({
events: {},
users: {
[creator]: 100,
},
users_default: 0,
state_default: 100, // trying to increase
})
.build();

// should still allow
expect(() => checkEventAuthWithState(pl3, state2, store)).not.toThrow();
});
});
35 changes: 22 additions & 13 deletions packages/room/src/authorizartion-rules/rules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,9 +157,13 @@ async function isMembershipChangeAllowed(
? joinRuleEvent.getJoinRule()
: undefined;

const powerLevelEvent = PowerLevelEvent.fromEvent(
getStateByMapKey(authEventStateMap, { type: 'm.room.power_levels' }),
);
const powerLevelEventInAuthStateMap = getStateByMapKey(authEventStateMap, {
type: 'm.room.power_levels',
});

const powerLevelEvent = powerLevelEventInAuthStateMap?.isPowerLevelEvent()
? PowerLevelEvent.fromEvent(powerLevelEventInAuthStateMap)
: PowerLevelEvent.fromDefault();

const roomCreateEvent = getStateByMapKey(authEventStateMap, {
type: 'm.room.create',
Expand Down Expand Up @@ -354,17 +358,18 @@ export function validatePowerLevelEvent(
) {
// If the users property in content is not an object with keys that are valid user IDs with values that are integers (or a string that is an integer), reject.
// If there is no previous m.room.power_levels event in the room, allow.
const existingPowerLevel = PowerLevelEvent.fromEvent(
getStateByMapKey(authEventMap, { type: 'm.room.power_levels' }),
);

const newPowerLevel = powerLevelEvent;

if (!existingPowerLevel.exists()) {
const existinPowerLevelEvent = getStateByMapKey(authEventMap, {
type: 'm.room.power_levels',
});
if (!existinPowerLevelEvent?.isPowerLevelEvent()) {
// allow if no previous power level event
return;
}

const existingPowerLevel = PowerLevelEvent.fromEvent(existinPowerLevelEvent);

const newPowerLevel = powerLevelEvent;

const senderCurrentPowerLevel = existingPowerLevel.getPowerLevelForUser(
newPowerLevel.sender,
roomCreateEvent,
Expand Down Expand Up @@ -767,9 +772,13 @@ export async function checkEventAuthWithState(
);
}

const powerLevelEvent = PowerLevelEvent.fromEvent(
getStateByMapKey(state, { type: 'm.room.power_levels' }),
);
const existingPowerLevelEvent = getStateByMapKey(state, {
type: 'm.room.power_levels',
});

const powerLevelEvent = existingPowerLevelEvent?.isPowerLevelEvent()
? PowerLevelEvent.fromEvent(existingPowerLevelEvent)
: PowerLevelEvent.fromDefault();

// If the event type’s required power level is greater than the sender’s power level, reject.
const eventRequiredPowerLevel = powerLevelEvent.getRequiredPowerLevelForEvent(
Expand Down
6 changes: 3 additions & 3 deletions packages/room/src/manager/event-wrapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,8 @@ export abstract class PersistentEventBase<
return this.rawEvent.hashes.sha256;
}

get type() {
return this.rawEvent.type;
get type(): Type {
return this.rawEvent.type as Type;
}

get roomId() {
Expand Down Expand Up @@ -135,7 +135,7 @@ export abstract class PersistentEventBase<

toPowerLevelEvent() {
if (this.isPowerLevelEvent()) {
return new PowerLevelEvent(this);
return PowerLevelEvent.fromEvent(this);
}

throw new Error('Event is not a power level event');
Expand Down
27 changes: 15 additions & 12 deletions packages/room/src/manager/power-level-event-wrapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,22 +10,29 @@ import { RoomVersion } from './type';
// whether there is an event or not
// all defaults and transformations according to diff versions of pdus

class PowerLevelEvent {
class PowerLevelEvent<
PowerLevelEventType extends
| PersistentEventBase<RoomVersion, 'm.room.power_levels'>
| undefined = PersistentEventBase<RoomVersion, 'm.room.power_levels'>,
> {
private readonly _content?: PduPowerLevelsEventContent;

static fromEvent(event?: PersistentEventBase) {
static fromEvent(
event: PersistentEventBase<RoomVersion, 'm.room.power_levels'>,
) {
return new PowerLevelEvent(event);
}

constructor(private readonly event?: PersistentEventBase) {
static fromDefault() {
return new PowerLevelEvent(undefined);
}

private constructor(private readonly event: PowerLevelEventType) {
this._content = event?.getContent();
}

toEventBase(): PersistentEventBase<RoomVersion, 'm.room.power_levels'> {
return this.event as PersistentEventBase<
RoomVersion,
'm.room.power_levels'
>;
toEventBase() {
return this.event;
}

// power level event accessors
Expand Down Expand Up @@ -177,10 +184,6 @@ class PowerLevelEvent {
return this._content.users?.[userId];
}

exists() {
return !!this._content;
}

get sender() {
if (!this.event) {
throw new Error('PowerLevelEvent does not exist can not access sender');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
type EventStore,
_kahnsOrder,
getAuthChainDifference,
mainlineOrdering,
} from '../definitions';

import { resolveStateV2Plus } from './v2';
Expand Down Expand Up @@ -526,6 +527,68 @@ describe('Definitions', () => {
);
});

it('successful mainline sort with no existing power level event', async () => {
const createEvent = new FakeEvent('CREATE2', ALICE, 'm.room.create', '', {
creator: ALICE,
}).toEvent([], []);

const aliceMemberEvent = new FakeEvent(
'IMA2',
ALICE,
'm.room.member',
ALICE,
MEMBERSHIP_CONTENT_JOIN,
).toEvent([createEvent.eventId], [createEvent.eventId]);

const joinRuleEvent = new FakeEvent(
'IJR2',
ALICE,
'm.room.join_rules',
'',
{ join_rule: 'public' },
).toEvent(
[createEvent.eventId, aliceMemberEvent.eventId],
[aliceMemberEvent.eventId],
);

const bobJoinEvent = new FakeEvent(
'IMB2',
BOB,
'm.room.member',
BOB,
MEMBERSHIP_CONTENT_JOIN,
).toEvent(
[createEvent.eventId, joinRuleEvent.eventId],
[joinRuleEvent.eventId],
);

const events = [bobJoinEvent, joinRuleEvent, aliceMemberEvent, createEvent];

for (const event of events) {
eventStore.events.push(event);
}

const sortedEvents = events
.sort((e1, e2) => {
if (e1.originServerTs !== e2.originServerTs) {
return e1.originServerTs - e2.originServerTs;
}

return e1.eventId.localeCompare(e2.eventId);
})
.map((e) => {
return e.eventId;
});

const mainlineSorted = (await mainlineOrdering(events, eventStore)).map(
(e) => {
return e.eventId;
},
);

expect(mainlineSorted).toEqual(sortedEvents);
});

it('kahns', () => {
/*
graph: Dict[str, Set[str]] = {
Expand Down
10 changes: 4 additions & 6 deletions packages/room/src/state_resolution/definitions/algorithm/v2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -266,10 +266,9 @@ export async function resolveStateV2Plus(
// ^^ non power events, since we should have power events figured out already, i.e. having single resolved power level event, single resolved join rules event, etc.
// we can validate if the rest of the events are "allowed" or not

const powerLevelEvent =
getStateByMapKey(partiallyResolvedState, {
type: 'm.room.power_levels',
}) ?? new PowerLevelEvent().toEventBase();
const powerLevelEvent = getStateByMapKey(partiallyResolvedState, {
type: 'm.room.power_levels',
});

// mainline ordering essentially sorts the rest of the events
// by their place in the history of the room's power levels.
Expand All @@ -285,9 +284,8 @@ export async function resolveStateV2Plus(

const orderedRemainingEvents = await mainlineOrdering(
sanitizedRemainingEvents,
powerLevelEvent,
initialState,
wrappedStore,
powerLevelEvent,
);

// 4. Apply the iterative auth checks algorithm on the partial resolved state and the list of events from the previous step.
Expand Down
Loading