diff --git a/apps/meteor/app/api/server/v1/chat.ts b/apps/meteor/app/api/server/v1/chat.ts index f5a9250fe29b6..c9fc2ce6925ec 100644 --- a/apps/meteor/app/api/server/v1/chat.ts +++ b/apps/meteor/app/api/server/v1/chat.ts @@ -434,7 +434,7 @@ API.v1.addRoute( } const sent = await applyAirGappedRestrictionsValidation(() => - executeSendMessage(this.userId, this.bodyParams.message as Pick, { previewUrls: this.bodyParams.previewUrls }), + executeSendMessage(this.user, this.bodyParams.message as Pick, { previewUrls: this.bodyParams.previewUrls }), ); const [message] = await normalizeMessagesForUser([sent], this.userId); diff --git a/apps/meteor/app/authorization/server/functions/canSendMessage.ts b/apps/meteor/app/authorization/server/functions/canSendMessage.ts index b9d6b740c2ddd..5f5c97fda453d 100644 --- a/apps/meteor/app/authorization/server/functions/canSendMessage.ts +++ b/apps/meteor/app/authorization/server/functions/canSendMessage.ts @@ -13,9 +13,10 @@ const subscriptionOptions = { }, }; +// TODO: remove option uid and username and type export async function validateRoomMessagePermissionsAsync( room: IRoom | null, - { uid, username, type }: { uid: IUser['_id']; username: IUser['username']; type: IUser['type'] }, + args: { uid: IUser['_id']; username: IUser['username']; type: IUser['type'] } | IUser, extraData?: Record, ): Promise { if (!room) { @@ -25,33 +26,34 @@ export async function validateRoomMessagePermissionsAsync( if (room.archived) { throw new Error('room_is_archived'); } - - if (type !== 'app' && !(await canAccessRoomAsync(room, { _id: uid }, extraData))) { + if (args.type !== 'app' && !(await canAccessRoomAsync(room, 'uid' in args ? { _id: args.uid } : args, extraData))) { throw new Error('error-not-allowed'); } - if (await roomCoordinator.getRoomDirectives(room.t).allowMemberAction(room, RoomMemberActions.BLOCK, uid)) { - const subscription = await Subscriptions.findOneByRoomIdAndUserId(room._id, uid, subscriptionOptions); + if ( + await roomCoordinator.getRoomDirectives(room.t).allowMemberAction(room, RoomMemberActions.BLOCK, 'uid' in args ? args.uid : args._id) + ) { + const subscription = await Subscriptions.findOneByRoomIdAndUserId(room._id, 'uid' in args ? args.uid : args._id, subscriptionOptions); if (subscription && (subscription.blocked || subscription.blocker)) { throw new Error('room_is_blocked'); } } - if (room.ro === true && !(await hasPermissionAsync(uid, 'post-readonly', room._id))) { + if (room.ro === true && !(await hasPermissionAsync('uid' in args ? args.uid : args._id, 'post-readonly', room._id))) { // Unless the user was manually unmuted - if (username && !(room.unmuted || []).includes(username)) { + if (args.username && !(room.unmuted || []).includes(args.username)) { throw new Error("You can't send messages because the room is readonly."); } } - if (username && room?.muted?.includes(username)) { + if (args.username && room?.muted?.includes(args.username)) { throw new Error('You_have_been_muted'); } } - +// TODO: remove option uid and username and type export async function canSendMessageAsync( rid: IRoom['_id'], - { uid, username, type }: { uid: IUser['_id']; username: IUser['username']; type: IUser['type'] }, + user: { uid: IUser['_id']; username: IUser['username']; type: IUser['type'] } | IUser, extraData?: Record, ): Promise { const room = await Rooms.findOneById(rid); @@ -59,6 +61,6 @@ export async function canSendMessageAsync( throw new Error('error-invalid-room'); } - await validateRoomMessagePermissionsAsync(room, { uid, username, type }, extraData); + await validateRoomMessagePermissionsAsync(room, user, extraData); return room; } diff --git a/apps/meteor/app/lib/server/methods/sendMessage.ts b/apps/meteor/app/lib/server/methods/sendMessage.ts index 895ccc27a5b87..a1208543d6841 100644 --- a/apps/meteor/app/lib/server/methods/sendMessage.ts +++ b/apps/meteor/app/lib/server/methods/sendMessage.ts @@ -30,7 +30,7 @@ import { RateLimiter } from '../lib'; * @returns */ export async function executeSendMessage( - uid: IUser['_id'], + uid: IUser['_id'] | IUser, message: AtLeast, extraInfo?: { ts?: Date; previewUrls?: string[] }, ) { @@ -71,7 +71,7 @@ export async function executeSendMessage( } } - const user = await Users.findOneById(uid); + const user = typeof uid === 'string' ? await Users.findOneById(uid) : uid; if (!user?.username) { throw new Meteor.Error('error-invalid-user', 'Invalid user'); } @@ -95,7 +95,7 @@ export async function executeSendMessage( check(rid, String); try { - const room = await canSendMessageAsync(rid, { uid, username: user.username, type: user.type }); + const room = await canSendMessageAsync(rid, user); if (room.encrypted && settings.get('E2E_Enable') && !settings.get('E2E_Allow_Unencrypted_Messages')) { if (message.t !== 'e2e') { @@ -112,7 +112,7 @@ export async function executeSendMessage( const errorMessage: RocketchatI18nKeys = typeof err === 'string' ? err : err.error || err.message; const errorContext: TOptions = err.details ?? {}; - void api.broadcast('notify.ephemeralMessage', uid, message.rid, { + void api.broadcast('notify.ephemeralMessage', user._id, message.rid, { msg: i18n.t(errorMessage, { ...errorContext, lng: user.language }), }); @@ -151,8 +151,8 @@ Meteor.methods({ sentByEmail: Match.Maybe(Boolean), }); - const uid = Meteor.userId(); - if (!uid) { + const user = (await Meteor.userAsync()) as IUser; + if (!user) { throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'sendMessage', }); @@ -163,7 +163,7 @@ Meteor.methods({ } try { - return await applyAirGappedRestrictionsValidation(() => executeSendMessage(uid, message, { previewUrls })); + return await applyAirGappedRestrictionsValidation(() => executeSendMessage(user, message, { previewUrls })); } catch (error: any) { if (['error-not-allowed', 'restricted-workspace'].includes(error.error || error.message)) { throw new Meteor.Error(error.error || error.message, error.reason, { diff --git a/apps/meteor/server/services/authorization/canAccessRoom.ts b/apps/meteor/server/services/authorization/canAccessRoom.ts index 675336df84bc5..684d52ac477a6 100644 --- a/apps/meteor/server/services/authorization/canAccessRoom.ts +++ b/apps/meteor/server/services/authorization/canAccessRoom.ts @@ -1,8 +1,8 @@ import { Authorization, License, Abac, Settings } from '@rocket.chat/core-services'; import type { RoomAccessValidator } from '@rocket.chat/core-services'; import { TeamType, AbacAccessOperation, AbacObjectType } from '@rocket.chat/core-typings'; -import type { IUser, ITeam } from '@rocket.chat/core-typings'; -import { Subscriptions, Rooms, TeamMember, Team } from '@rocket.chat/models'; +import type { IUser, ITeam, IRoom } from '@rocket.chat/core-typings'; +import { Subscriptions, Rooms, TeamMember, Team, Users } from '@rocket.chat/models'; import { canAccessRoomLivechat } from './canAccessRoomLivechat'; @@ -15,7 +15,13 @@ async function canAccessPublicRoom(user?: Partial): Promise { return Authorization.hasPermission(user._id, 'view-c-room'); } -const roomAccessValidators: RoomAccessValidator[] = [ +type RoomAccessValidatorConverted = ( + room?: Pick, + user?: IUser, + extraData?: Record, +) => Promise; + +const roomAccessValidators: RoomAccessValidatorConverted[] = [ async function _validateAccessToPublicRoomsInTeams(room, user): Promise { if (!room) { return false; @@ -56,8 +62,8 @@ const roomAccessValidators: RoomAccessValidator[] = [ } const [canViewJoined, canViewT] = await Promise.all([ - Authorization.hasPermission(user._id, 'view-joined-room'), - Authorization.hasPermission(user._id, `view-${room.t}-room`), + Authorization.hasPermission(user, 'view-joined-room'), + Authorization.hasPermission(user, `view-${room.t}-room`), ]); // When there's no ABAC setting, license or values on the room, fallback to previous behavior @@ -89,14 +95,32 @@ const roomAccessValidators: RoomAccessValidator[] = [ canAccessRoomLivechat, ]; +const isPartialUser = (user: IUser | Pick | undefined): user is Pick => { + return Boolean(user && Object.keys(user).length === 1 && '_id' in user); +}; + export const canAccessRoom: RoomAccessValidator = async (room, user, extraData): Promise => { // TODO livechat can send both as null, so they we need to validate nevertheless // if (!room || !user) { // return false; // } + // TODO: remove this after migrations + // if user only contains _id, convert it to a full IUser object + + if (isPartialUser(user)) { + user = (await Users.findOneById(user._id)) || undefined; + if (!user) { + throw new Error('User not found'); + } + + if (process.env.NODE_ENV === 'development') { + console.log('User converted to full IUser object'); + } + } + for await (const roomAccessValidator of roomAccessValidators) { - if (await roomAccessValidator(room, user, extraData)) { + if (await roomAccessValidator(room, user as IUser, extraData)) { return true; } } diff --git a/apps/meteor/server/services/authorization/service.ts b/apps/meteor/server/services/authorization/service.ts index 3c1858b1305e4..a8984dd79c241 100644 --- a/apps/meteor/server/services/authorization/service.ts +++ b/apps/meteor/server/services/authorization/service.ts @@ -56,21 +56,21 @@ export class Authorization extends ServiceClass implements IAuthorization { } } - async hasAllPermission(userId: string, permissions: string[], scope?: string): Promise { + async hasAllPermission(userId: string | IUser, permissions: string[], scope?: string): Promise { if (!userId) { return false; } return this.all(userId, permissions, scope); } - async hasPermission(userId: string, permissionId: string, scope?: string): Promise { + async hasPermission(userId: string | IUser, permissionId: string, scope?: string): Promise { if (!userId) { return false; } return this.all(userId, [permissionId], scope); } - async hasAtLeastOnePermission(userId: string, permissions: string[], scope?: string): Promise { + async hasAtLeastOnePermission(userId: string | IUser, permissions: string[], scope?: string): Promise { if (!userId) { return false; } @@ -85,7 +85,7 @@ export class Authorization extends ServiceClass implements IAuthorization { return canReadRoom(...args); } - async canAccessRoomId(rid: IRoom['_id'], uid: IUser['_id']): Promise { + async canAccessRoomId(rid: IRoom['_id'], user: IUser['_id']): Promise { const room = await Rooms.findOneById>(rid, { projection: { _id: 1, @@ -100,7 +100,7 @@ export class Authorization extends ServiceClass implements IAuthorization { return false; } - return this.canAccessRoom(room, { _id: uid }); + return this.canAccessRoom(room, { _id: user }); } async addRoleRestrictions(role: IRole['_id'], permissions: string[]): Promise { @@ -160,17 +160,20 @@ export class Authorization extends ServiceClass implements IAuthorization { return !!result; } - private async getRoles(uid: string, scope?: IRoom['_id']): Promise { - const { roles: userRoles = [] } = (await Users.findOneById(uid, { projection: { roles: 1 } })) || {}; + private async getRoles(user: string | IUser, scope?: IRoom['_id']): Promise { + const { roles: userRoles = [] } = typeof user === 'string' ? (await Users.findOneById(user, { projection: { roles: 1 } })) || {} : user; const { roles: subscriptionsRoles = [] } = (scope && - (await Subscriptions.findOne>({ 'rid': scope, 'u._id': uid }, { projection: { roles: 1 } }))) || + (await Subscriptions.findOne>( + { 'rid': scope, 'u._id': typeof user === 'string' ? user : user._id }, + { projection: { roles: 1 } }, + ))) || {}; return [...userRoles, ...subscriptionsRoles].sort((a, b) => a.localeCompare(b)); } - private async atLeastOne(uid: string, permissions: string[] = [], scope?: string): Promise { - const sortedRoles = await this.getRolesCached(uid, scope); + private async atLeastOne(user: string | IUser, permissions: string[] = [], scope?: string): Promise { + const sortedRoles = await this.getRolesCached(user, scope); for await (const permission of permissions) { if (await this.rolesHasPermissionCached(permission, sortedRoles)) { return true; @@ -180,8 +183,8 @@ export class Authorization extends ServiceClass implements IAuthorization { return false; } - private async all(uid: string, permissions: string[] = [], scope?: string): Promise { - const sortedRoles = await this.getRolesCached(uid, scope); + private async all(user: string | IUser, permissions: string[] = [], scope?: string): Promise { + const sortedRoles = await this.getRolesCached(user, scope); for await (const permission of permissions) { if (!(await this.rolesHasPermissionCached(permission, sortedRoles))) { return false; diff --git a/packages/core-services/src/types/IAuthorization.ts b/packages/core-services/src/types/IAuthorization.ts index d4bb1c1c67d4f..d87b2a873e68f 100644 --- a/packages/core-services/src/types/IAuthorization.ts +++ b/packages/core-services/src/types/IAuthorization.ts @@ -2,13 +2,19 @@ import type { IRoom, IUser, IRole } from '@rocket.chat/core-typings'; export type RoomAccessValidator = ( room?: Pick, - user?: Pick, + user?: IUser | Pick, extraData?: Record, ) => Promise; export interface IAuthorization { + hasAllPermission(user: IUser, permissions: string[], scope?: string): Promise; + // @deprecated hasAllPermission(userId: string, permissions: string[], scope?: string): Promise; + hasPermission(user: IUser, permissionId: string, scope?: string): Promise; + // @deprecated hasPermission(userId: string, permissionId: string, scope?: string): Promise; + hasAtLeastOnePermission(user: IUser, permissions: string[], scope?: string): Promise; + // @deprecated hasAtLeastOnePermission(userId: string, permissions: string[], scope?: string): Promise; canAccessRoom: RoomAccessValidator; canReadRoom: RoomAccessValidator;