Skip to content

Commit

Permalink
[NEW] Ability to set roles on federated rooms (#27633)
Browse files Browse the repository at this point in the history
* feat: adding the ability to set roles on federated rooms (WIP)

* feat: changing room roles logic (WIP)

* feat: changing room roles logic (WIP)

* refactor: remove all logs

* refactor: remove dead code

* refactor: small improvements

* test: add unit tests(WIP)

* test: add more unit tests

* refactor: code improvement

* fix: change hardcoded error to i18n

* test: fix test

* fix: DM issue

* fix: suggestions from review
  • Loading branch information
MarcosSpessatto committed Feb 13, 2023
1 parent ca6190f commit bf0a815
Show file tree
Hide file tree
Showing 73 changed files with 3,680 additions and 308 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ async function validateRoomMessagePermissionsAsync(
throw new Error('error-not-allowed');
}

if (roomCoordinator.getRoomDirectives(room.t)?.allowMemberAction(room, RoomMemberActions.BLOCK)) {
if (roomCoordinator.getRoomDirectives(room.t)?.allowMemberAction(room, RoomMemberActions.BLOCK, uid)) {
const subscription = await Subscriptions.findOneByRoomIdAndUserId(room._id, uid, subscriptionOptions);
if (subscription && (subscription.blocked || subscription.blocker)) {
throw new Error('room_is_blocked');
Expand Down
20 changes: 11 additions & 9 deletions apps/meteor/app/authorization/server/methods/removeUserFromRole.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,17 +73,19 @@ Meteor.methods({
}

const remove = await Roles.removeUserRoles(user._id, [role._id], scope);
const event = {
type: 'removed',
_id: role._id,
u: {
_id: user._id,
username,
},
scope,
};
if (settings.get('UI_DisplayRoles')) {
api.broadcast('user.roleUpdate', {
type: 'removed',
_id: role._id,
u: {
_id: user._id,
username,
},
scope,
});
api.broadcast('user.roleUpdate', event);
}
api.broadcast('federation.userRoleChanged', { ...event, givenByUserId: userId });

return remove;
},
Expand Down
29 changes: 25 additions & 4 deletions apps/meteor/app/federation-v2/server/Federation.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,40 @@
import type { IRoom, ValueOf } from '@rocket.chat/core-typings';
import { isDirectMessageRoom } from '@rocket.chat/core-typings';
import type { IRoom, IUser, ValueOf } from '@rocket.chat/core-typings';
import { isRoomFederated, isDirectMessageRoom } from '@rocket.chat/core-typings';
import { Subscriptions } from '@rocket.chat/models';

import { RoomMemberActions } from '../../../definition/IRoomTypeConfig';
import { escapeExternalFederationEventId, unescapeExternalFederationEventId } from './infrastructure/rocket-chat/adapters/MessageConverter';

const allowedActionsInFederatedRooms: ValueOf<typeof RoomMemberActions>[] = [
RoomMemberActions.REMOVE_USER,
RoomMemberActions.SET_AS_OWNER,
RoomMemberActions.SET_AS_MODERATOR,
RoomMemberActions.INVITE,
RoomMemberActions.JOIN,
RoomMemberActions.LEAVE,
];

export class Federation {
public static actionAllowed(room: IRoom, action: ValueOf<typeof RoomMemberActions>): boolean {
return isDirectMessageRoom(room) && action === RoomMemberActions.REMOVE_USER ? false : allowedActionsInFederatedRooms.includes(action);
public static actionAllowed(room: IRoom, action: ValueOf<typeof RoomMemberActions>, userId?: IUser['_id']): boolean {
if (!isRoomFederated(room)) {
return false;
}
if (isDirectMessageRoom(room)) {
return false;
}
if (!userId) {
return true;
}

const userSubscription = Promise.await(Subscriptions.findOneByRoomIdAndUserId(room._id, userId));
if (!userSubscription) {
return true;
}

return Boolean(
(userSubscription.roles?.includes('owner') || userSubscription.roles?.includes('moderator')) &&
allowedActionsInFederatedRooms.includes(action),
);
}

public static isAFederatedUsername(username: string): boolean {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import type {
FederationRoomReceiveExternalFileMessageDto,
FederationRoomRedactEventDto,
FederationRoomEditExternalMessageDto,
FederationRoomRoomChangePowerLevelsEventDto,
} from './input/RoomReceiverDto';
import { FederationService } from '../AbstractFederationService';
import type { RocketChatFileAdapter } from '../../infrastructure/rocket-chat/adapters/File';
Expand Down Expand Up @@ -521,4 +522,40 @@ export class FederationRoomServiceListener extends FederationService {
}
await handler.handle();
}

public async onChangeRoomPowerLevels(roomPowerLevelsInput: FederationRoomRoomChangePowerLevelsEventDto): Promise<void> {
const { externalRoomId, roleChangesToApply = {}, externalSenderId } = roomPowerLevelsInput;

const federatedRoom = await this.internalRoomAdapter.getFederatedRoomByExternalId(externalRoomId);
if (!federatedRoom) {
return;
}

const federatedUserWhoChangedThePermission = await this.internalUserAdapter.getFederatedUserByExternalId(externalSenderId);
if (!federatedUserWhoChangedThePermission) {
return;
}

const federatedUsers = await this.internalUserAdapter.getFederatedUsersByExternalIds(Object.keys(roleChangesToApply));

await Promise.all(
federatedUsers.map((targetFederatedUser) => {
const changes = roleChangesToApply[targetFederatedUser.getExternalId()];
if (!changes) {
return;
}
const rolesToRemove = changes.filter((change) => change.action === 'remove').map((change) => change.role);
const rolesToAdd = changes.filter((change) => change.action === 'add').map((change) => change.role);

return this.internalRoomAdapter.applyRoomRolesToUser({
federatedRoom,
targetFederatedUser,
fromUser: federatedUserWhoChangedThePermission,
rolesToAdd,
rolesToRemove,
notifyChannel: true,
});
}),
);
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { RoomType } from '@rocket.chat/apps-engine/definition/rooms';

import type { EVENT_ORIGIN } from '../../../domain/IFederationBridge';
import type { ROCKET_CHAT_FEDERATION_ROLES } from '../../../infrastructure/rocket-chat/definitions/InternalFederatedRoomRoles';

interface IFederationBaseInputDto {
externalEventId: string;
Expand Down Expand Up @@ -68,6 +69,10 @@ export interface IFederationRoomRedactEventInputDto extends IFederationReceiverB
redactsEvent: string;
externalSenderId: string;
}
export interface IFederationRoomChangePowerLevelsInputDto extends IFederationReceiverBaseRoomInputDto {
roleChangesToApply: IExternalRolesChangesToApplyInputDto;
externalSenderId: string;
}

export interface IFederationSendInternalMessageBaseInputDto extends IFederationReceiverBaseRoomInputDto {
externalSenderId: string;
Expand Down Expand Up @@ -376,3 +381,24 @@ export class FederationRoomRedactEventDto extends FederationBaseRoomInputDto {

externalSenderId: string;
}

export interface IExternalRolesChangesToApplyInputDto {
[key: string]: { action: string; role: ROCKET_CHAT_FEDERATION_ROLES }[];
}
export class FederationRoomRoomChangePowerLevelsEventDto extends FederationBaseRoomInputDto {
constructor({
externalRoomId,
normalizedRoomId,
externalEventId,
roleChangesToApply,
externalSenderId,
}: IFederationRoomChangePowerLevelsInputDto) {
super({ externalRoomId, normalizedRoomId, externalEventId });
this.roleChangesToApply = roleChangesToApply;
this.externalSenderId = externalSenderId;
}

roleChangesToApply: IExternalRolesChangesToApplyInputDto;

externalSenderId: string;
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { IMessage, MessageQuoteAttachment } from '@rocket.chat/core-typings';
import { isDeletedMessage, isEditedMessage, isMessageFromMatrixFederation, isQuoteAttachment } from '@rocket.chat/core-typings';

import type { FederatedRoom } from '../../domain/FederatedRoom';
import { DirectMessageFederatedRoom } from '../../domain/FederatedRoom';
import { FederatedUser } from '../../domain/FederatedUser';
import type { IFederationBridge } from '../../domain/IFederationBridge';
Expand All @@ -18,6 +19,8 @@ import type {
FederationRoomSendExternalMessageDto,
} from './input/RoomSenderDto';
import { getExternalMessageSender } from './MessageSenders';
import { MATRIX_POWER_LEVELS } from '../../infrastructure/matrix/definitions/MatrixPowerLevels';
import { ROCKET_CHAT_FEDERATION_ROLES } from '../../infrastructure/rocket-chat/definitions/InternalFederatedRoomRoles';

export class FederationRoomServiceSender extends FederationService {
constructor(
Expand Down Expand Up @@ -263,4 +266,199 @@ export class FederationRoomServiceSender extends FederationService {
internalMessage.msg,
);
}

public async onRoomOwnerAdded(internalUserId: string, internalTargetUserId: string, internalRoomId: string): Promise<void> {
const federatedRoom = await this.internalRoomAdapter.getFederatedRoomByInternalId(internalRoomId);
if (!federatedRoom) {
return;
}

const federatedUser = await this.internalUserAdapter.getFederatedUserByInternalId(internalUserId);
if (!federatedUser) {
return;
}

const federatedTargetUser = await this.internalUserAdapter.getFederatedUserByInternalId(internalTargetUserId);
if (!federatedTargetUser) {
return;
}

const userRoomRoles = await this.internalRoomAdapter.getInternalRoomRolesByUserId(internalRoomId, internalUserId);
const myself = federatedUser.getInternalId() === federatedTargetUser.getInternalId();
if (!userRoomRoles?.includes(ROCKET_CHAT_FEDERATION_ROLES.OWNER) && !myself) {
throw new Error('Federation_Matrix_not_allowed_to_change_owner');
}

const isUserFromTheSameHomeServer = FederatedUser.isOriginalFromTheProxyServer(
this.bridge.extractHomeserverOrigin(federatedUser.getExternalId()),
this.internalSettingsAdapter.getHomeServerDomain(),
);
if (!isUserFromTheSameHomeServer) {
return;
}
try {
await this.bridge.setRoomPowerLevels(
federatedRoom.getExternalId(),
federatedUser.getExternalId(),
federatedTargetUser.getExternalId(),
MATRIX_POWER_LEVELS.ADMIN,
);
} catch (e) {
await this.rollbackRoomRoles(federatedRoom, federatedTargetUser, federatedUser, [], [ROCKET_CHAT_FEDERATION_ROLES.OWNER]);
}
}

public async onRoomOwnerRemoved(internalUserId: string, internalTargetUserId: string, internalRoomId: string): Promise<void> {
const federatedRoom = await this.internalRoomAdapter.getFederatedRoomByInternalId(internalRoomId);
if (!federatedRoom) {
return;
}

const federatedUser = await this.internalUserAdapter.getFederatedUserByInternalId(internalUserId);
if (!federatedUser) {
return;
}

const federatedTargetUser = await this.internalUserAdapter.getFederatedUserByInternalId(internalTargetUserId);
if (!federatedTargetUser) {
return;
}

const userRoomRoles = await this.internalRoomAdapter.getInternalRoomRolesByUserId(internalRoomId, internalUserId);
const myself = federatedUser.getInternalId() === federatedTargetUser.getInternalId();
if (!userRoomRoles?.includes(ROCKET_CHAT_FEDERATION_ROLES.OWNER) && !myself) {
throw new Error('Federation_Matrix_not_allowed_to_change_owner');
}

const isUserFromTheSameHomeServer = FederatedUser.isOriginalFromTheProxyServer(
this.bridge.extractHomeserverOrigin(federatedUser.getExternalId()),
this.internalSettingsAdapter.getHomeServerDomain(),
);
if (!isUserFromTheSameHomeServer) {
return;
}
try {
await this.bridge.setRoomPowerLevels(
federatedRoom.getExternalId(),
federatedUser.getExternalId(),
federatedTargetUser.getExternalId(),
MATRIX_POWER_LEVELS.USER,
);
} catch (e) {
await this.rollbackRoomRoles(federatedRoom, federatedTargetUser, federatedUser, [ROCKET_CHAT_FEDERATION_ROLES.OWNER], []);
}
}

public async onRoomModeratorAdded(internalUserId: string, internalTargetUserId: string, internalRoomId: string): Promise<void> {
const federatedRoom = await this.internalRoomAdapter.getFederatedRoomByInternalId(internalRoomId);
if (!federatedRoom) {
return;
}

const federatedUser = await this.internalUserAdapter.getFederatedUserByInternalId(internalUserId);
if (!federatedUser) {
return;
}
const federatedTargetUser = await this.internalUserAdapter.getFederatedUserByInternalId(internalTargetUserId);
if (!federatedTargetUser) {
return;
}

const userRoomRoles = await this.internalRoomAdapter.getInternalRoomRolesByUserId(internalRoomId, internalUserId);
const myself = federatedUser.getInternalId() === federatedTargetUser.getInternalId();
if (
!userRoomRoles?.includes(ROCKET_CHAT_FEDERATION_ROLES.OWNER) &&
!userRoomRoles?.includes(ROCKET_CHAT_FEDERATION_ROLES.MODERATOR) &&
!myself
) {
throw new Error('Federation_Matrix_not_allowed_to_change_moderator');
}

const isUserFromTheSameHomeServer = FederatedUser.isOriginalFromTheProxyServer(
this.bridge.extractHomeserverOrigin(federatedUser.getExternalId()),
this.internalSettingsAdapter.getHomeServerDomain(),
);
if (!isUserFromTheSameHomeServer) {
return;
}

try {
await this.bridge.setRoomPowerLevels(
federatedRoom.getExternalId(),
federatedUser.getExternalId(),
federatedTargetUser.getExternalId(),
MATRIX_POWER_LEVELS.MODERATOR,
);
} catch (e) {
await this.rollbackRoomRoles(federatedRoom, federatedTargetUser, federatedUser, [], [ROCKET_CHAT_FEDERATION_ROLES.MODERATOR]);
}
}

public async onRoomModeratorRemoved(internalUserId: string, internalTargetUserId: string, internalRoomId: string): Promise<void> {
const federatedRoom = await this.internalRoomAdapter.getFederatedRoomByInternalId(internalRoomId);
if (!federatedRoom) {
return;
}

const federatedUser = await this.internalUserAdapter.getFederatedUserByInternalId(internalUserId);
if (!federatedUser) {
return;
}

const federatedTargetUser = await this.internalUserAdapter.getFederatedUserByInternalId(internalTargetUserId);
if (!federatedTargetUser) {
return;
}

const userRoomRoles = await this.internalRoomAdapter.getInternalRoomRolesByUserId(internalRoomId, internalUserId);
const myself = federatedUser.getInternalId() === federatedTargetUser.getInternalId();
if (
!userRoomRoles?.includes(ROCKET_CHAT_FEDERATION_ROLES.OWNER) &&
!userRoomRoles?.includes(ROCKET_CHAT_FEDERATION_ROLES.MODERATOR) &&
!myself
) {
throw new Error('Federation_Matrix_not_allowed_to_change_moderator');
}

const isUserFromTheSameHomeServer = FederatedUser.isOriginalFromTheProxyServer(
this.bridge.extractHomeserverOrigin(federatedUser.getExternalId()),
this.internalSettingsAdapter.getHomeServerDomain(),
);
if (!isUserFromTheSameHomeServer) {
return;
}

try {
await this.bridge.setRoomPowerLevels(
federatedRoom.getExternalId(),
federatedUser.getExternalId(),
federatedTargetUser.getExternalId(),
MATRIX_POWER_LEVELS.USER,
);
} catch (e) {
await this.rollbackRoomRoles(federatedRoom, federatedTargetUser, federatedUser, [ROCKET_CHAT_FEDERATION_ROLES.MODERATOR], []);
}
}

private async rollbackRoomRoles(
federatedRoom: FederatedRoom,
targetFederatedUser: FederatedUser,
fromUser: FederatedUser,
rolesToAdd: ROCKET_CHAT_FEDERATION_ROLES[],
rolesToRemove: ROCKET_CHAT_FEDERATION_ROLES[],
): Promise<void> {
await this.internalRoomAdapter.applyRoomRolesToUser({
federatedRoom,
targetFederatedUser,
fromUser,
rolesToAdd,
rolesToRemove,
notifyChannel: false,
});
this.internalNotificationAdapter.notifyWithEphemeralMessage(
'Federation_Matrix_error_applying_room_roles',
fromUser.getInternalId(),
federatedRoom.getInternalId(),
);
}
}

0 comments on commit bf0a815

Please sign in to comment.