diff --git a/packages/federation-sdk/src/services/invite.service.ts b/packages/federation-sdk/src/services/invite.service.ts index c7615c2ba..844134147 100644 --- a/packages/federation-sdk/src/services/invite.service.ts +++ b/packages/federation-sdk/src/services/invite.service.ts @@ -1,7 +1,6 @@ import { EventBase, createLogger } from '@rocket.chat/federation-core'; import { PduForType, - PersistentEventBase, PersistentEventFactory, RoomVersion, } from '@rocket.chat/federation-room'; @@ -86,7 +85,7 @@ export class InviteService { await stateService.persistStateEvent(inviteEvent); if (inviteEvent.rejected) { - throw new Error(inviteEvent.rejectedReason); + throw new Error(inviteEvent.rejectReason); } // let all servers know of this state change @@ -157,7 +156,7 @@ export class InviteService { await this.stateService.persistStateEvent(inviteEvent); if (inviteEvent.rejected) { - throw new Error(inviteEvent.rejectedReason); + throw new Error(inviteEvent.rejectReason); } // we do not send transaction here @@ -174,7 +173,7 @@ export class InviteService { // if we have the state we try to persist the invite event await this.stateService.persistStateEvent(inviteEvent); if (inviteEvent.rejected) { - throw new Error(inviteEvent.rejectedReason); + throw new Error(inviteEvent.rejectReason); } } catch (e) { // don't have state copy yet diff --git a/packages/federation-sdk/src/services/message.service.ts b/packages/federation-sdk/src/services/message.service.ts index 3a8e4c3b1..c239b2cf2 100644 --- a/packages/federation-sdk/src/services/message.service.ts +++ b/packages/federation-sdk/src/services/message.service.ts @@ -1,23 +1,8 @@ -import { - type MessageAuthEvents, - type RoomMessageEvent, - roomMessageEvent, -} from '@rocket.chat/federation-core'; -import { type SignedEvent } from '@rocket.chat/federation-core'; - import { ForbiddenError } from '@rocket.chat/federation-core'; -import { - type RedactionAuthEvents, - type RedactionEvent, - redactionEvent, -} from '@rocket.chat/federation-core'; import { createLogger } from '@rocket.chat/federation-core'; -import { signEvent } from '@rocket.chat/federation-core'; import { type EventID, type PersistentEventBase, - PersistentEventFactory, - type RoomVersion, } from '@rocket.chat/federation-room'; import { singleton } from 'tsyringe'; import { EventRepository } from '../repositories/event.repository'; @@ -97,7 +82,7 @@ export class MessageService { await this.stateService.persistTimelineEvent(event); if (event.rejected) { - throw new Error(event.rejectedReason); + throw new Error(event.rejectReason); } void this.federationService.sendEventToAllServersInRoom(event); @@ -145,7 +130,7 @@ export class MessageService { await this.stateService.persistTimelineEvent(event); if (event.rejected) { - throw new Error(event.rejectedReason); + throw new Error(event.rejectReason); } void this.federationService.sendEventToAllServersInRoom(event); @@ -181,7 +166,7 @@ export class MessageService { await this.stateService.persistTimelineEvent(event); if (event.rejected) { - throw new Error(event.rejectedReason); + throw new Error(event.rejectReason); } void this.federationService.sendEventToAllServersInRoom(event); @@ -237,7 +222,7 @@ export class MessageService { await this.stateService.persistTimelineEvent(event); if (event.rejected) { - throw new Error(event.rejectedReason); + throw new Error(event.rejectReason); } void this.federationService.sendEventToAllServersInRoom(event); @@ -288,7 +273,7 @@ export class MessageService { await this.stateService.persistTimelineEvent(event); if (event.rejected) { - throw new Error(event.rejectedReason); + throw new Error(event.rejectReason); } void this.federationService.sendEventToAllServersInRoom(event); diff --git a/packages/federation-sdk/src/services/room.service.ts b/packages/federation-sdk/src/services/room.service.ts index 222e87132..f35d06142 100644 --- a/packages/federation-sdk/src/services/room.service.ts +++ b/packages/federation-sdk/src/services/room.service.ts @@ -1,17 +1,10 @@ import { EventBase, EventStore, - RoomNameAuthEvents, RoomPowerLevelsEvent, - RoomTombstoneEvent, SignedEvent, TombstoneAuthEvents, - generateId, - isRoomPowerLevelsEvent, - roomNameEvent, roomPowerLevelsEvent, - roomTombstoneEvent, - signEvent, } from '@rocket.chat/federation-core'; import { singleton } from 'tsyringe'; import { FederationService } from './federation.service'; @@ -21,18 +14,15 @@ import { HttpException, HttpStatus, } from '@rocket.chat/federation-core'; -import { type SigningKey } from '@rocket.chat/federation-core'; import { logger } from '@rocket.chat/federation-core'; import { type EventID, - PduCreateEventContent, PduForType, PduJoinRuleEventContent, PduType, PersistentEventBase, PersistentEventFactory, - RoomVersion, } from '@rocket.chat/federation-room'; import { EventRepository } from '../repositories/event.repository'; import { RoomRepository } from '../repositories/room.repository'; @@ -810,7 +800,7 @@ export class RoomService { await stateService.persistStateEvent(membershipEvent); if (membershipEvent.rejected) { - throw new Error(membershipEvent.rejectedReason); + throw new Error(membershipEvent.rejectReason); } void federationService.sendEventToAllServersInRoom(membershipEvent); @@ -993,7 +983,7 @@ export class RoomService { ); if (joinEventFinal.rejected) { - throw new Error(joinEventFinal.rejectedReason); + throw new Error(joinEventFinal.rejectReason); } return joinEventFinal.eventId; diff --git a/packages/federation-sdk/src/services/send-join.service.ts b/packages/federation-sdk/src/services/send-join.service.ts index 1f5c0fdf1..ef6a3dd06 100644 --- a/packages/federation-sdk/src/services/send-join.service.ts +++ b/packages/federation-sdk/src/services/send-join.service.ts @@ -50,7 +50,7 @@ export class SendJoinService { await stateService.persistStateEvent(joinEvent); if (joinEvent.rejected) { - throw new Error(joinEvent.rejectedReason); + throw new Error(joinEvent.rejectReason); } const configService = this.configService; diff --git a/packages/federation-sdk/src/services/state.service.ts b/packages/federation-sdk/src/services/state.service.ts index ca45182c8..505c8c456 100644 --- a/packages/federation-sdk/src/services/state.service.ts +++ b/packages/federation-sdk/src/services/state.service.ts @@ -18,6 +18,7 @@ import type { RoomVersion } from '@rocket.chat/federation-room'; import { resolveStateV2Plus } from '@rocket.chat/federation-room'; import type { PduCreateEventContent } from '@rocket.chat/federation-room'; import { checkEventAuthWithState } from '@rocket.chat/federation-room'; +import { RejectCodes } from '@rocket.chat/federation-room'; import { singleton } from 'tsyringe'; import { EventRepository } from '../repositories/event.repository'; import { StateRepository, StateStore } from '../repositories/state.repository'; @@ -53,12 +54,12 @@ export class StateService { if (pdu.isState()) { await this.persistStateEvent(pdu); if (pdu.rejected) { - throw new Error(pdu.rejectedReason); + throw new Error(pdu.rejectReason); } } else { await this.persistTimelineEvent(pdu); if (pdu.rejected) { - throw new Error(pdu.rejectedReason); + throw new Error(pdu.rejectReason); } } } @@ -506,7 +507,7 @@ export class StateService { if (!hasConflict) { await checkEventAuthWithState(event, state, this._getStore(roomVersion)); if (event.rejected) { - throw new Error(event.rejectedReason); + throw new Error(event.rejectReason); } // save the state mapping @@ -750,7 +751,7 @@ export class StateService { // we need the auth events required to validate this event from our state const requiredAuthEventsWeHaveSeenMap = new Map< - string, + EventID, PersistentEventBase >(); for (const auth of event.getAuthEventStateKeys()) { @@ -777,24 +778,27 @@ export class StateService { if (requiredAuthEventsWeHaveSeenMap.size !== authEventsReferencedMap.size) { // incorrect length may mean either redacted event still referenced or event in state that wasn't referenced, both cases, reject the event event.reject( + RejectCodes.AuthError, `Auth events referenced in message do not match, expected ${requiredAuthEventsWeHaveSeenMap.size} but got ${authEventsReferencedMap.size}`, ); - throw new Error(event.rejectedReason); + throw new Error(event.rejectReason); } for (const [eventId] of requiredAuthEventsWeHaveSeenMap) { if (!authEventsReferencedMap.has(eventId)) { event.reject( + RejectCodes.AuthError, `wrong auth event in message, expected ${eventId} but not found in event`, + eventId, ); - throw new Error(event.rejectedReason); + throw new Error(event.rejectReason); } } // now we validate against auth rules await checkEventAuthWithState(event, room, store); if (event.rejected) { - throw new Error(event.rejectedReason); + throw new Error(event.rejectReason); } // TODO: save event still but with mark diff --git a/packages/homeserver/src/controllers/internal/invite.controller.ts b/packages/homeserver/src/controllers/internal/invite.controller.ts index 9824e0557..0d45fadf7 100644 --- a/packages/homeserver/src/controllers/internal/invite.controller.ts +++ b/packages/homeserver/src/controllers/internal/invite.controller.ts @@ -61,7 +61,7 @@ export const internalInvitePlugin = (app: Elysia) => { await stateService.persistStateEvent(membershipEvent); if (membershipEvent.rejected) { - throw new Error(membershipEvent.rejectedReason); + throw new Error(membershipEvent.rejectReason); } return { diff --git a/packages/room/src/authorizartion-rules/errors.ts b/packages/room/src/authorizartion-rules/errors.ts index d3603fb37..e6e57b2ef 100644 --- a/packages/room/src/authorizartion-rules/errors.ts +++ b/packages/room/src/authorizartion-rules/errors.ts @@ -1,23 +1,47 @@ import type { PersistentEventBase } from '../manager/event-wrapper'; +import { type EventID } from '../types/_common'; + +export const RejectCodes = { + AuthError: 'auth_error', + ValidationError: 'validation_error', + NotImplemented: 'not_implemented', +} as const; + +export type RejectCode = (typeof RejectCodes)[keyof typeof RejectCodes]; class StateResolverAuthorizationError extends Error { name = 'StateResolverAuthorizationError'; + reason: string; + + rejectedBy?: EventID; + constructor( - message: string, + public code: RejectCode, { - eventFailed, + rejectedEvent, reason, + rejectedBy, }: { - eventFailed: PersistentEventBase; - reason?: PersistentEventBase; + rejectedEvent: PersistentEventBase; + reason: string; + rejectedBy?: PersistentEventBase; }, ) { - let error = `${message} for event ${eventFailed.eventId} in room ${eventFailed.roomId} type ${eventFailed.type} state_key ${eventFailed.stateKey}`; - if (reason) { - error += `, reason: ${reason.eventId} in room ${reason.roomId} type ${reason.type} state_key ${reason.stateKey}`; + // build the message + let message = `${code}: ${rejectedEvent.toStrippedJson()} failed authorization check`; + + if (rejectedBy) { + message += ` against auth event ${rejectedBy.toStrippedJson()}`; } - super(error); + + message += `: ${reason}`; + + super(message); + + this.reason = reason; + + this.rejectedBy = rejectedBy?.eventId; } } diff --git a/packages/room/src/authorizartion-rules/rules.ts b/packages/room/src/authorizartion-rules/rules.ts index 67f6b07df..d365788d6 100644 --- a/packages/room/src/authorizartion-rules/rules.ts +++ b/packages/room/src/authorizartion-rules/rules.ts @@ -9,7 +9,7 @@ import { getStateByMapKey, } from '../state_resolution/definitions/definitions'; import { type StateMapKey } from '../types/_common'; -import { StateResolverAuthorizationError } from './errors'; +import { RejectCodes, StateResolverAuthorizationError } from './errors'; // https://spec.matrix.org/v1.12/rooms/v1/#authorization-rules // skip if not any of the specified type of events @@ -27,30 +27,23 @@ function extractDomain(identifier: string) { return identifier.split(':').pop(); } -function isCreateAllowed(createEvent: PersistentEventBase) { - if (!createEvent.isCreateEvent()) { - throw new StateResolverAuthorizationError('m.room.create event not found', { - eventFailed: createEvent, - }); - } +function isCreateAllowed( + createEvent: PersistentEventBase, +) { // If it has any prev_events, reject. if (createEvent.event.prev_events.length > 0) { - throw new StateResolverAuthorizationError( - 'm.room.create event has prev_events', - { - eventFailed: createEvent, - }, - ); + throw new StateResolverAuthorizationError(RejectCodes.AuthError, { + rejectedEvent: createEvent, + reason: 'm.room.create event has prev_events', + }); } // If the domain of the room_id does not match the domain of the sender, reject. if (extractDomain(createEvent.roomId) !== extractDomain(createEvent.sender)) { - throw new StateResolverAuthorizationError( - 'm.room.create event sender domain does not match room_id domain', - { - eventFailed: createEvent, - }, - ); + throw new StateResolverAuthorizationError(RejectCodes.AuthError, { + rejectedEvent: createEvent, + reason: 'm.room.create event sender domain does not match room_id domain', + }); } const content = createEvent.getContent(); @@ -62,52 +55,47 @@ function isCreateAllowed(createEvent: PersistentEventBase) { content.room_version, ) ) { - throw new StateResolverAuthorizationError( - 'm.room.create event content.room_version is not a recognised version', - { - eventFailed: createEvent, - }, - ); + throw new StateResolverAuthorizationError(RejectCodes.AuthError, { + rejectedEvent: createEvent, + reason: `m.room.create event content.room_version is not a recognised version ${content.room_version}`, + }); } // If content has no creator property, reject. if (!content.creator) { - throw new StateResolverAuthorizationError( - 'm.room.create event content has no creator property', - { - eventFailed: createEvent, - }, - ); + throw new StateResolverAuthorizationError(RejectCodes.AuthError, { + rejectedEvent: createEvent, + reason: 'm.room.create event content has no creator property', + }); } } // TODO: better typing for alias event -function isRoomAliasAllowed(roomAliasEvent: PersistentEventBase): void { +function isRoomAliasAllowed( + roomAliasEvent: PersistentEventBase, +): void { // If event has no state_key, reject. if (!roomAliasEvent.stateKey) { - throw new StateResolverAuthorizationError( - 'm.room.canonical_alias event has no state_key', - { - eventFailed: roomAliasEvent, - }, - ); + throw new StateResolverAuthorizationError(RejectCodes.AuthError, { + rejectedEvent: roomAliasEvent, + reason: 'm.room.canonical_alias event has no state_key', + }); } // If sender’s domain doesn’t matches state_key, reject. if (roomAliasEvent.origin !== roomAliasEvent.stateKey) { - throw new StateResolverAuthorizationError( - 'm.room.canonical_alias event sender domain does not match state_key', - { - eventFailed: roomAliasEvent, - }, - ); + throw new StateResolverAuthorizationError(RejectCodes.AuthError, { + rejectedEvent: roomAliasEvent, + reason: + 'm.room.canonical_alias event sender domain does not match state_key', + }); } return; } async function isMembershipChangeAllowed( - membershipEventToCheck: PersistentEventBase, + membershipEventToCheck: PersistentEventBase, authEventStateMap: Map, store: EventStore, ): Promise { @@ -116,12 +104,10 @@ async function isMembershipChangeAllowed( !membershipEventToCheck.stateKey || !membershipEventToCheck.isMembershipEvent() ) { - throw new StateResolverAuthorizationError( - 'm.room.member event has no state_key or membership property', - { - eventFailed: membershipEventToCheck, - }, - ); + throw new StateResolverAuthorizationError(RejectCodes.AuthError, { + rejectedEvent: membershipEventToCheck, + reason: 'm.room.member event has no state_key or membership property', + }); } // sender -> who asked for the change @@ -198,12 +184,19 @@ async function isMembershipChangeAllowed( // If the sender does not match state_key, reject. if (sender !== invitee) { - throw new Error('state_key does not match the sender'); + throw new StateResolverAuthorizationError(RejectCodes.AuthError, { + rejectedEvent: membershipEventToCheck, + reason: 'state_key does not match the sender', + }); } // If the sender is banned, reject. if (senderMembership === 'ban') { - throw new Error('sender is banned'); + throw new StateResolverAuthorizationError(RejectCodes.AuthError, { + rejectedEvent: membershipEventToCheck, + reason: 'sender is banned', + rejectedBy: senderMembershipEvent, + }); } // If the join_rule is public, allow. @@ -217,13 +210,19 @@ async function isMembershipChangeAllowed( return; } - throw new Error( - 'join_rule is invite but membership is not invite or join', - ); + throw new StateResolverAuthorizationError(RejectCodes.AuthError, { + rejectedEvent: membershipEventToCheck, + rejectedBy: joinRuleEvent, + reason: 'join_rule is invite but membership is not invite or join', + }); } // otherwise reject - throw new Error('join_rule is not public or invite'); + throw new StateResolverAuthorizationError(RejectCodes.AuthError, { + rejectedEvent: membershipEventToCheck, + rejectedBy: joinRuleEvent, + reason: 'join_rule is not public or invite', + }); } case 'invite': { @@ -251,17 +250,28 @@ async function isMembershipChangeAllowed( // // If there is no m.room.third_party_invite event in the current room state with state_key matching token, reject. - throw new Error('third_party_invite not implemented'); + throw new StateResolverAuthorizationError(RejectCodes.NotImplemented, { + rejectedEvent: membershipEventToCheck, + reason: 'third_party_invite not implemented', + }); } // If the sender’s current membership state is not join, reject. if (senderMembership !== 'join') { - throw new Error('sender is not part of the room'); + throw new StateResolverAuthorizationError(RejectCodes.AuthError, { + rejectedEvent: membershipEventToCheck, + reason: 'sender is not part of the room', + rejectedBy: senderMembershipEvent, + }); } // If target user’s current membership state is join or ban, reject. if (inviteeMembership === 'join' || inviteeMembership === 'ban') { - throw new Error('invitee is not join or ban'); + throw new StateResolverAuthorizationError(RejectCodes.AuthError, { + rejectedEvent: membershipEventToCheck, + reason: 'invitee is already join or ban', + rejectedBy: inviteeMembershipEvent, + }); } // If the sender’s power level is greater than or equal to the invite level, allow. @@ -277,7 +287,11 @@ async function isMembershipChangeAllowed( return; } - throw new Error('sender power level is less than invite level'); + throw new StateResolverAuthorizationError(RejectCodes.AuthError, { + rejectedEvent: membershipEventToCheck, + reason: `sender power level is less than invite level (${senderPowerLevel} < ${inviteLevel})`, + rejectedBy: powerLevelEvent.toEventBase(), + }); } // If the sender does not match state_key, case 'leave': { @@ -291,7 +305,11 @@ async function isMembershipChangeAllowed( // If the sender’s current membership state is not join, reject. if (senderMembership !== 'join') { - throw new Error('sender is not join'); + throw new StateResolverAuthorizationError(RejectCodes.AuthError, { + rejectedEvent: membershipEventToCheck, + reason: 'sender is not join', + rejectedBy: senderMembershipEvent, + }); } // If the target user’s current membership state is ban, and the sender’s power level is less than the ban level, reject. @@ -303,7 +321,11 @@ async function isMembershipChangeAllowed( const banLevel = powerLevelEvent.getRequiredPowerForBan(); if (inviteeMembership === 'ban' && senderPowerLevel < banLevel) { - throw new Error('sender power level is less than ban level'); + throw new StateResolverAuthorizationError(RejectCodes.AuthError, { + rejectedEvent: membershipEventToCheck, + reason: 'sender power level is less than ban level', + rejectedBy: powerLevelEvent.toEventBase(), + }); } // If the sender’s power level is greater than or equal to the kick level, and the target user’s power level is less than the sender’s power level, allow. @@ -316,13 +338,21 @@ async function isMembershipChangeAllowed( return; } - throw new Error('sender power level is less than kick level'); + throw new StateResolverAuthorizationError(RejectCodes.AuthError, { + rejectedEvent: membershipEventToCheck, + reason: 'sender power level is less than kick level', + rejectedBy: powerLevelEvent.toEventBase(), + }); } case 'ban': { // If the sender’s current membership state is not join, reject. if (senderMembership !== 'join') { - throw new Error('sender is not join'); + throw new StateResolverAuthorizationError(RejectCodes.AuthError, { + rejectedEvent: membershipEventToCheck, + reason: 'sender is not join', + rejectedBy: senderMembershipEvent, + }); } // If the sender’s power level is greater than or equal to the ban level, and the target user’s power level is less than the sender’s power level, allow. @@ -340,12 +370,19 @@ async function isMembershipChangeAllowed( return; } - throw new Error('sender power level is less than ban level'); + throw new StateResolverAuthorizationError(RejectCodes.AuthError, { + rejectedEvent: membershipEventToCheck, + reason: 'sender power level is less than ban level', + rejectedBy: powerLevelEvent.toEventBase(), + }); } default: // unknown - throw new Error('unknown membership state'); + throw new StateResolverAuthorizationError(RejectCodes.AuthError, { + rejectedEvent: membershipEventToCheck, + reason: `unknown membership state ${content.membership}`, + }); } } @@ -389,18 +426,24 @@ export function validatePowerLevelEvent( newUserDefaultPowerLevel && newUserDefaultPowerLevel > senderCurrentPowerLevel ) { - throw new Error( - 'new power level value is greater than sender power level', - ); + throw new StateResolverAuthorizationError(RejectCodes.AuthError, { + rejectedEvent: powerLevelEvent.toEventBase()!, + reason: + 'new user_default power level is greater than sender power level', + rejectedBy: existingPowerLevel.toEventBase(), + }); } if ( existingUserDefaultPowerLevel && existingUserDefaultPowerLevel > senderCurrentPowerLevel ) { - throw new Error( - 'existing power level value is greater than sender power level', - ); + throw new StateResolverAuthorizationError(RejectCodes.AuthError, { + rejectedEvent: powerLevelEvent.toEventBase()!, + reason: + 'existing user_default power level is greater than sender power level', + rejectedBy: existingPowerLevel.toEventBase(), + }); } } @@ -413,18 +456,24 @@ export function validatePowerLevelEvent( newEventsDefaultValue && newEventsDefaultValue > senderCurrentPowerLevel ) { - throw new Error( - 'new power level value is greater than sender power level', - ); + throw new StateResolverAuthorizationError(RejectCodes.AuthError, { + rejectedEvent: powerLevelEvent.toEventBase()!, + reason: + 'new events_default power level is greater than sender power level', + rejectedBy: existingPowerLevel.toEventBase(), + }); } if ( existingEventsDefaultValue && existingEventsDefaultValue > senderCurrentPowerLevel ) { - throw new Error( - 'existing power level value is greater than sender power level', - ); + throw new StateResolverAuthorizationError(RejectCodes.AuthError, { + rejectedEvent: powerLevelEvent.toEventBase()!, + reason: + 'existing events_default power level is greater than sender power level', + rejectedBy: existingPowerLevel.toEventBase(), + }); } } @@ -437,18 +486,24 @@ export function validatePowerLevelEvent( newStateDefaultValue && newStateDefaultValue > senderCurrentPowerLevel ) { - throw new Error( - 'new power level value is greater than sender power level', - ); + throw new StateResolverAuthorizationError(RejectCodes.AuthError, { + rejectedEvent: powerLevelEvent.toEventBase()!, + reason: + 'new state_default power level is greater than sender power level', + rejectedBy: existingPowerLevel.toEventBase(), + }); } if ( existingStateDefaultValue && existingStateDefaultValue > senderCurrentPowerLevel ) { - throw new Error( - 'existing power level value is greater than sender power level', - ); + throw new StateResolverAuthorizationError(RejectCodes.AuthError, { + rejectedEvent: powerLevelEvent.toEventBase()!, + reason: + 'existing state_default power level is greater than sender power level', + rejectedBy: existingPowerLevel.toEventBase(), + }); } } @@ -458,15 +513,19 @@ export function validatePowerLevelEvent( // for ban if (existingBanValue !== newBanValue) { if (newBanValue && newBanValue > senderCurrentPowerLevel) { - throw new Error( - 'new power level value is greater than sender power level', - ); + throw new StateResolverAuthorizationError(RejectCodes.AuthError, { + rejectedEvent: powerLevelEvent.toEventBase()!, + reason: 'new power level value is greater than sender power level', + rejectedBy: existingPowerLevel.toEventBase(), + }); } if (existingBanValue && existingBanValue > senderCurrentPowerLevel) { - throw new Error( - 'existing power level value is greater than sender power level', - ); + throw new StateResolverAuthorizationError(RejectCodes.AuthError, { + rejectedEvent: powerLevelEvent.toEventBase()!, + reason: 'existing power level value is greater than sender power level', + rejectedBy: existingPowerLevel.toEventBase(), + }); } } @@ -476,15 +535,19 @@ export function validatePowerLevelEvent( // for kick if (existingKickValue !== newKickValue) { if (newKickValue && newKickValue > senderCurrentPowerLevel) { - throw new Error( - 'new power level value is greater than sender power level', - ); + throw new StateResolverAuthorizationError(RejectCodes.AuthError, { + rejectedEvent: powerLevelEvent.toEventBase()!, + reason: 'new power level value is greater than sender power level', + rejectedBy: existingPowerLevel.toEventBase(), + }); } if (existingKickValue && existingKickValue > senderCurrentPowerLevel) { - throw new Error( - 'existing power level value is greater than sender power level', - ); + throw new StateResolverAuthorizationError(RejectCodes.AuthError, { + rejectedEvent: powerLevelEvent.toEventBase()!, + reason: 'existing power level value is greater than sender power level', + rejectedBy: existingPowerLevel.toEventBase(), + }); } } @@ -494,15 +557,19 @@ export function validatePowerLevelEvent( if (existingRedactValue !== newRedactValue) { if (newRedactValue && newRedactValue > senderCurrentPowerLevel) { - throw new Error( - 'new power level value is greater than sender power level', - ); + throw new StateResolverAuthorizationError(RejectCodes.AuthError, { + rejectedEvent: powerLevelEvent.toEventBase()!, + reason: 'new power level value is greater than sender power level', + rejectedBy: existingPowerLevel.toEventBase(), + }); } if (existingRedactValue && existingRedactValue > senderCurrentPowerLevel) { - throw new Error( - 'existing power level value is greater than sender power level', - ); + throw new StateResolverAuthorizationError(RejectCodes.AuthError, { + rejectedEvent: powerLevelEvent.toEventBase()!, + reason: 'existing power level value is greater than sender power level', + rejectedBy: existingPowerLevel.toEventBase(), + }); } } @@ -512,15 +579,19 @@ export function validatePowerLevelEvent( if (existingInviteValue !== newInviteValue) { if (newInviteValue && newInviteValue > senderCurrentPowerLevel) { - throw new Error( - 'new power level value is greater than sender power level', - ); + throw new StateResolverAuthorizationError(RejectCodes.AuthError, { + rejectedEvent: powerLevelEvent.toEventBase()!, + reason: 'new power level value is greater than sender power level', + rejectedBy: existingPowerLevel.toEventBase(), + }); } if (existingInviteValue && existingInviteValue > senderCurrentPowerLevel) { - throw new Error( - 'existing power level value is greater than sender power level', - ); + throw new StateResolverAuthorizationError(RejectCodes.AuthError, { + rejectedEvent: powerLevelEvent.toEventBase()!, + reason: 'existing power level value is greater than sender power level', + rejectedBy: existingPowerLevel.toEventBase(), + }); } } @@ -543,9 +614,12 @@ export function validatePowerLevelEvent( existingPowerLevelValue && existingPowerLevelValue > senderCurrentPowerLevel ) { - throw new Error( - 'existing power level value is greater than sender power level', - ); + throw new StateResolverAuthorizationError(RejectCodes.AuthError, { + rejectedEvent: powerLevelEvent.toEventBase()!, + reason: + 'existing power level value is greater than sender power level', + rejectedBy: existingPowerLevel.toEventBase(), + }); } } } @@ -563,9 +637,11 @@ export function validatePowerLevelEvent( // changed or added // If the new value is greater than the sender’s current power level, reject. if (newPowerLevelValue && newPowerLevelValue > senderCurrentPowerLevel) { - throw new Error( - 'new power level value is greater than sender power level', - ); + throw new StateResolverAuthorizationError(RejectCodes.AuthError, { + rejectedEvent: powerLevelEvent.toEventBase()!, + reason: 'new power level value is greater than sender power level', + rejectedBy: existingPowerLevel.toEventBase(), + }); } } } @@ -587,9 +663,12 @@ export function validatePowerLevelEvent( existingPowerLevelValue && existingPowerLevelValue > senderCurrentPowerLevel ) { - throw new Error( - 'existing power level value is greater than sender power level', - ); + throw new StateResolverAuthorizationError(RejectCodes.AuthError, { + rejectedEvent: powerLevelEvent.toEventBase()!, + reason: + 'existing power level value is greater than sender power level', + rejectedBy: existingPowerLevel.toEventBase(), + }); } } } @@ -607,9 +686,11 @@ export function validatePowerLevelEvent( // changed or added // If the new value is greater than the sender’s current power level, reject. if (newPowerLevelValue && newPowerLevelValue > senderCurrentPowerLevel) { - throw new Error( - 'new power level value is greater than sender power level', - ); + throw new StateResolverAuthorizationError(RejectCodes.AuthError, { + rejectedEvent: powerLevelEvent.toEventBase()!, + reason: 'new power level value is greater than sender power level', + rejectedBy: existingPowerLevel.toEventBase(), + }); } } } @@ -621,12 +702,10 @@ export function checkEventAuthWithoutState( ) { if (event.isCreateEvent()) { if (authEvents.length > 0) { - throw new StateResolverAuthorizationError( - 'm.room.create event has auth_events', - { - eventFailed: event, - }, - ); + throw new StateResolverAuthorizationError(RejectCodes.AuthError, { + rejectedEvent: event, + reason: 'm.room.create event has auth_events', + }); } return isCreateAllowed(event); @@ -651,36 +730,30 @@ export function checkEventAuthWithoutState( for (const authEvent of authEvents) { // if rejected, reject this event if (authEvent.rejected) { - throw new StateResolverAuthorizationError( - `auth event ${authEvent.eventId} rejected`, - { - eventFailed: event, - reason: authEvent, - }, - ); + throw new StateResolverAuthorizationError(RejectCodes.AuthError, { + rejectedEvent: event, + reason: 'auth event required to authorize this event was rejected', + rejectedBy: authEvent, + }); } const stateKey = authEvent.getUniqueStateIdentifier(); // if this is not neeede, throw if (!stateKeysNeeded.has(stateKey)) { - throw new StateResolverAuthorizationError( - `excess auth event ${authEvent.eventId} with type ${authEvent.type} state_key ${authEvent.stateKey}`, - { - eventFailed: event, - reason: authEvent, - }, - ); + throw new StateResolverAuthorizationError(RejectCodes.AuthError, { + rejectedEvent: event, + reason: 'excess auth event', + rejectedBy: authEvent, + }); } if (authEventStateMap.has(stateKey)) { - throw new StateResolverAuthorizationError( - `duplicate auth event ${authEvent.eventId} with type ${authEvent.type} state_key ${authEvent.stateKey}`, - { - eventFailed: event, - reason: authEvent, - }, - ); + throw new StateResolverAuthorizationError(RejectCodes.AuthError, { + rejectedEvent: event, + rejectedBy: authEvent, + reason: 'duplicate auth event', + }); } authEventStateMap.set(stateKey, authEvent); @@ -691,8 +764,9 @@ export function checkEventAuthWithoutState( }); if (!roomCreateEvent) { - throw new StateResolverAuthorizationError('missing m.room.create event', { - eventFailed: event, + throw new StateResolverAuthorizationError(RejectCodes.AuthError, { + rejectedEvent: event, + reason: 'missing m.room.create event', }); } @@ -716,8 +790,9 @@ export async function checkEventAuthWithState( assert(roomCreateEvent, 'missing m.room.create event'); if (!roomCreateEvent.isCreateEvent()) { - throw new StateResolverAuthorizationError('m.room.create event not found', { - eventFailed: event, + throw new StateResolverAuthorizationError(RejectCodes.AuthError, { + rejectedEvent: event, + reason: 'm.room.create event not found', }); } @@ -726,13 +801,11 @@ export async function checkEventAuthWithState( roomCreateEvent.getContent()['m.federate'] === false && event.origin !== roomCreateEvent.origin ) { - throw new StateResolverAuthorizationError( - 'm.federate is false and sender domain does not match', - { - eventFailed: event, - reason: roomCreateEvent, - }, - ); + throw new StateResolverAuthorizationError(RejectCodes.AuthError, { + rejectedEvent: event, + rejectedBy: roomCreateEvent, + reason: 'm.federate is false and sender domain does not match', + }); } if (event.isAliasEvent()) { @@ -749,25 +822,21 @@ export async function checkEventAuthWithState( state_key: event.sender, }); if (senderMembership?.getMembership() !== 'join') { - throw new StateResolverAuthorizationError( - "sender's membership is not join", - { - eventFailed: event, - reason: senderMembership, - }, - ); + throw new StateResolverAuthorizationError(RejectCodes.AuthError, { + rejectedEvent: event, + reason: "sender's membership is not join", + rejectedBy: senderMembership, + }); } // If type is m.room.third_party_invite: // @ts-ignore the pdu union doesn't have this type TODO: add if (event.type === 'm.room.third_party_invite') { console.warn('third_party_invite not implemented'); - throw new StateResolverAuthorizationError( - 'third_party_invite not implemented', - { - eventFailed: event, - }, - ); + throw new StateResolverAuthorizationError(RejectCodes.NotImplemented, { + rejectedEvent: event, + reason: 'third_party_invite not implemented', + }); } const existingPowerLevelEvent = getStateByMapKey(state, { @@ -789,24 +858,19 @@ export async function checkEventAuthWithState( ); if (userPowerLevel < eventRequiredPowerLevel) { - throw new StateResolverAuthorizationError( - 'user power level is less than event required power level', - { - eventFailed: event, - reason: powerLevelEvent.toEventBase(), - }, - ); + throw new StateResolverAuthorizationError(RejectCodes.AuthError, { + rejectedEvent: event, + rejectedBy: powerLevelEvent.toEventBase(), + reason: `user power level ${userPowerLevel} is less than event required power level ${eventRequiredPowerLevel}`, + }); } // If the event has a state_key that starts with an @ and does not match the sender, reject. if (event.stateKey?.startsWith('@') && event.stateKey !== event.sender) { - throw new StateResolverAuthorizationError( - 'event state key does not match sender', - { - eventFailed: event, - reason: event, - }, - ); + throw new StateResolverAuthorizationError(RejectCodes.AuthError, { + rejectedEvent: event, + reason: 'event state_key does not match sender', + }); } // If type is m.room.power_levels: diff --git a/packages/room/src/index.ts b/packages/room/src/index.ts index 677296fe2..3ce4a334e 100644 --- a/packages/room/src/index.ts +++ b/packages/room/src/index.ts @@ -2,6 +2,7 @@ export { checkEventAuthWithState, checkEventAuthWithoutState, } from './authorizartion-rules/rules'; +export * from './authorizartion-rules/errors'; export { resolveStateV2Plus } from './state_resolution/definitions/algorithm/v2'; export * from './manager/factory'; export * from './manager/event-wrapper'; diff --git a/packages/room/src/manager/event-wrapper.ts b/packages/room/src/manager/event-wrapper.ts index 904fc070c..191d11116 100644 --- a/packages/room/src/manager/event-wrapper.ts +++ b/packages/room/src/manager/event-wrapper.ts @@ -3,6 +3,7 @@ import { encodeCanonicalJson, toUnpaddedBase64, } from '@rocket.chat/federation-crypto'; +import type { RejectCode } from '../authorizartion-rules/errors'; import { type EventStore, getStateMapKey, @@ -44,7 +45,11 @@ export abstract class PersistentEventBase< Version extends RoomVersion = RoomVersion, Type extends PduType = PduType, > { - private _rejectedReason?: string; + public rejectCode = ''; + + public rejectReason = ''; + + public rejectedBy = '' as EventID; private signatures: Signature = {}; @@ -409,15 +414,13 @@ export abstract class PersistentEventBase< } get rejected() { - return this._rejectedReason !== undefined; - } - - reject(reason: string) { - this._rejectedReason = reason; + return this.rejectCode !== ''; } - get rejectedReason() { - return this._rejectedReason; + reject(code: RejectCode, reason: string, rejectedBy?: EventID) { + this.rejectCode = code; + this.rejectReason = reason; + if (rejectedBy) this.rejectedBy = rejectedBy; } addPrevEvents(events: PersistentEventBase[]) { @@ -440,5 +443,15 @@ export abstract class PersistentEventBase< return this; } + + toStrippedJson() { + return encodeCanonicalJson({ + eventId: this.eventId, + type: this.type, + roomId: this.roomId, + sender: this.sender, + stateKey: this.stateKey, + }); + } } export type { EventStore }; diff --git a/packages/room/src/manager/factory.ts b/packages/room/src/manager/factory.ts index a8a2f1d0c..973a390f3 100644 --- a/packages/room/src/manager/factory.ts +++ b/packages/room/src/manager/factory.ts @@ -52,7 +52,7 @@ export class PersistentEventFactory { static createFromRawEvent( event: PduWithHashesAndSignaturesOptional, - roomVersion: string, + roomVersion: RoomVersion, ): PersistentEventBase { if (!PersistentEventFactory.isSupportedRoomVersion(roomVersion)) { throw new Error(`Room version ${roomVersion} is not supported`); diff --git a/packages/room/src/state_resolution/definitions/definitions.ts b/packages/room/src/state_resolution/definitions/definitions.ts index 9abc14a3b..085942107 100644 --- a/packages/room/src/state_resolution/definitions/definitions.ts +++ b/packages/room/src/state_resolution/definitions/definitions.ts @@ -3,8 +3,9 @@ import type { EventID, State, StateMapKey } from '../../types/_common'; import { type PduType } from '../../types/v3-11'; import assert from 'node:assert'; +import { StateResolverAuthorizationError } from '../../authorizartion-rules/errors'; import { checkEventAuthWithState } from '../../authorizartion-rules/rules'; -import type { PersistentEventBase } from '../../manager/event-wrapper'; +import { PersistentEventBase } from '../../manager/event-wrapper'; import { PowerLevelEvent } from '../../manager/power-level-event-wrapper'; import { RoomVersion } from '../../manager/type'; @@ -591,10 +592,15 @@ export async function iterativeAuthChecks( try { await checkEventAuthWithState(event, authEventStateMap, store); - } catch (e) { - console.warn('event not allowed', event.eventId, e); - event.reject((e as Error).message); - continue; + } catch (error) { + console.warn('event not allowed', error); + if (error instanceof StateResolverAuthorizationError) { + event.reject(error.code, error.reason, error.rejectedBy); + continue; + } + + // if unknown error we halt building new state + throw error; } newState.set(event.getUniqueStateIdentifier(), event);