From 41f8fdf89b2894a378c325775e8b7a50ba941295 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Wed, 11 Feb 2026 15:49:34 -0300 Subject: [PATCH 1/4] refactor: Update authorization service to accept IUser type for user parameters Modified the Authorization class methods to accept IUser type in place of string for userId parameters. Updated the IAuthorization interface to reflect these changes, marking the previous string-based methods as deprecated. --- .../server/services/authorization/service.ts | 27 ++++++++++--------- .../core-services/src/types/IAuthorization.ts | 6 +++++ 2 files changed, 21 insertions(+), 12 deletions(-) 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..e6b8a2b3249cf 100644 --- a/packages/core-services/src/types/IAuthorization.ts +++ b/packages/core-services/src/types/IAuthorization.ts @@ -7,8 +7,14 @@ export type RoomAccessValidator = ( ) => 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; From edeaf6d3d5998e7fa586a190c6dc90f0269d0573 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Wed, 11 Feb 2026 16:33:47 -0300 Subject: [PATCH 2/4] refactor: Update message permission validation to accept IUser type Refactored the message permission validation functions to accept IUser type directly, enhancing type safety and consistency. Adjusted related functions to streamline user parameter handling and removed deprecated uid and username options. Updated the IAuthorization interface to reflect these changes. --- .../server/functions/canSendMessage.ts | 24 +++++++------ .../services/authorization/canAccessRoom.ts | 36 +++++++++++++++---- .../core-services/src/types/IAuthorization.ts | 2 +- 3 files changed, 44 insertions(+), 18 deletions(-) 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/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/packages/core-services/src/types/IAuthorization.ts b/packages/core-services/src/types/IAuthorization.ts index e6b8a2b3249cf..d87b2a873e68f 100644 --- a/packages/core-services/src/types/IAuthorization.ts +++ b/packages/core-services/src/types/IAuthorization.ts @@ -2,7 +2,7 @@ import type { IRoom, IUser, IRole } from '@rocket.chat/core-typings'; export type RoomAccessValidator = ( room?: Pick, - user?: Pick, + user?: IUser | Pick, extraData?: Record, ) => Promise; From 6d429de8144be751ea18a505f87448932242f5c6 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Wed, 11 Feb 2026 16:51:08 -0300 Subject: [PATCH 3/4] refactor: Update executeSendMessage to accept IUser type directly Refactored the executeSendMessage function to accept IUser type instead of IUser['_id'], improving type safety. Adjusted related method calls in the chat API to utilize the updated user parameter handling. --- apps/meteor/app/api/server/v1/chat.ts | 2 +- apps/meteor/app/lib/server/methods/sendMessage.ts | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) 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/lib/server/methods/sendMessage.ts b/apps/meteor/app/lib/server/methods/sendMessage.ts index 895ccc27a5b87..775a5db7d6f5f 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') { @@ -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, { From b885b050b8816e720e908e4d78d0ba82657abb40 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Wed, 11 Feb 2026 17:14:21 -0300 Subject: [PATCH 4/4] refactor: Update broadcast call in executeSendMessage to use user._id Modified the broadcast call in the executeSendMessage function to utilize user._id instead of uid, enhancing clarity and consistency in user parameter handling. --- apps/meteor/app/lib/server/methods/sendMessage.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/meteor/app/lib/server/methods/sendMessage.ts b/apps/meteor/app/lib/server/methods/sendMessage.ts index 775a5db7d6f5f..a1208543d6841 100644 --- a/apps/meteor/app/lib/server/methods/sendMessage.ts +++ b/apps/meteor/app/lib/server/methods/sendMessage.ts @@ -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 }), });