diff --git a/apps/meteor/app/api/server/v1/roles.ts b/apps/meteor/app/api/server/v1/roles.ts index a0fff9683b80..6727f4f970cb 100644 --- a/apps/meteor/app/api/server/v1/roles.ts +++ b/apps/meteor/app/api/server/v1/roles.ts @@ -9,6 +9,7 @@ import { getUsersInRolePaginated } from '../../../authorization/server/functions import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; import { hasRoleAsync, hasAnyRoleAsync } from '../../../authorization/server/functions/hasRole'; import { apiDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger'; +import { notifyListenerOnRoleChanges } from '../../../lib/server/lib/notifyListenerOnRoleChanges'; import { settings } from '../../../settings/server/index'; import { API } from '../api'; import { getPaginationItems } from '../helpers/getPaginationItems'; @@ -179,6 +180,8 @@ API.v1.addRoute( await Roles.removeById(role._id); + void notifyListenerOnRoleChanges(role._id, 'removed', role); + return API.v1.success(); }, }, diff --git a/apps/meteor/app/lib/server/lib/notifyListenerOnRoleChanges.ts b/apps/meteor/app/lib/server/lib/notifyListenerOnRoleChanges.ts new file mode 100644 index 000000000000..b05e54af3625 --- /dev/null +++ b/apps/meteor/app/lib/server/lib/notifyListenerOnRoleChanges.ts @@ -0,0 +1,24 @@ +import { api, dbWatchersDisabled } from '@rocket.chat/core-services'; +import type { IRole } from '@rocket.chat/core-typings'; +import { Roles } from '@rocket.chat/models'; + +type ClientAction = 'inserted' | 'updated' | 'removed'; + +export async function notifyListenerOnRoleChanges( + rid: IRole['_id'], + clientAction: ClientAction = 'updated', + existingRoleData?: IRole, +): Promise { + if (!dbWatchersDisabled) { + return; + } + + const role = existingRoleData || (await Roles.findOneById(rid)); + + if (role) { + void api.broadcast('watch.roles', { + clientAction, + role, + }); + } +} diff --git a/apps/meteor/ee/server/api/roles.ts b/apps/meteor/ee/server/api/roles.ts index 7a71613b3548..7e8048387ec3 100644 --- a/apps/meteor/ee/server/api/roles.ts +++ b/apps/meteor/ee/server/api/roles.ts @@ -130,9 +130,7 @@ API.v1.addRoute( const role = await insertRoleAsync(roleData, options); - return API.v1.success({ - role, - }); + return API.v1.success({ role }); }, }, ); @@ -172,9 +170,7 @@ API.v1.addRoute( const updatedRole = await updateRole(roleId, roleData, options); - return API.v1.success({ - role: updatedRole, - }); + return API.v1.success({ role: updatedRole }); }, }, ); diff --git a/apps/meteor/ee/server/lib/roles/insertRole.ts b/apps/meteor/ee/server/lib/roles/insertRole.ts index 80aa4e57385e..23cb394a913c 100644 --- a/apps/meteor/ee/server/lib/roles/insertRole.ts +++ b/apps/meteor/ee/server/lib/roles/insertRole.ts @@ -2,6 +2,7 @@ import { api, MeteorError } from '@rocket.chat/core-services'; import type { IRole } from '@rocket.chat/core-typings'; import { Roles } from '@rocket.chat/models'; +import { notifyListenerOnRoleChanges } from '../../../../app/lib/server/lib/notifyListenerOnRoleChanges'; import { isValidRoleScope } from '../../../../lib/roles/isValidRoleScope'; type InsertRoleOptions = { @@ -19,21 +20,16 @@ export const insertRoleAsync = async (roleData: Omit, options: Ins throw new MeteorError('error-invalid-scope', 'Invalid scope'); } - const result = await Roles.createWithRandomId(name, scope, description, false, mandatory2fa); + const role = await Roles.createWithRandomId(name, scope, description, false, mandatory2fa); - const roleId = result.insertedId; + void notifyListenerOnRoleChanges(role._id, 'inserted', role); if (options.broadcastUpdate) { void api.broadcast('user.roleUpdate', { type: 'changed', - _id: roleId, + _id: role._id, }); } - const newRole = await Roles.findOneById(roleId); - if (!newRole) { - throw new MeteorError('error-role-not-found', 'Role not found'); - } - - return newRole; + return role; }; diff --git a/apps/meteor/ee/server/lib/roles/updateRole.ts b/apps/meteor/ee/server/lib/roles/updateRole.ts index e3a9a7063526..976abbd8921f 100644 --- a/apps/meteor/ee/server/lib/roles/updateRole.ts +++ b/apps/meteor/ee/server/lib/roles/updateRole.ts @@ -2,6 +2,7 @@ import { api, MeteorError } from '@rocket.chat/core-services'; import type { IRole } from '@rocket.chat/core-typings'; import { Roles } from '@rocket.chat/models'; +import { notifyListenerOnRoleChanges } from '../../../../app/lib/server/lib/notifyListenerOnRoleChanges'; import { isValidRoleScope } from '../../../../lib/roles/isValidRoleScope'; type UpdateRoleOptions = { @@ -38,6 +39,8 @@ export const updateRole = async (roleId: IRole['_id'], roleData: Omit 0) { diff --git a/apps/meteor/server/lib/roles/createOrUpdateProtectedRole.ts b/apps/meteor/server/lib/roles/createOrUpdateProtectedRole.ts index 9361689d2b5f..487c74c0f76b 100644 --- a/apps/meteor/server/lib/roles/createOrUpdateProtectedRole.ts +++ b/apps/meteor/server/lib/roles/createOrUpdateProtectedRole.ts @@ -1,6 +1,8 @@ import type { IRole, AtLeast } from '@rocket.chat/core-typings'; import { Roles } from '@rocket.chat/models'; +import { notifyListenerOnRoleChanges } from '../../../app/lib/server/lib/notifyListenerOnRoleChanges'; + export const createOrUpdateProtectedRoleAsync = async ( roleId: string, roleData: AtLeast, 'name'>, @@ -8,8 +10,9 @@ export const createOrUpdateProtectedRoleAsync = async ( const role = await Roles.findOneById>(roleId, { projection: { name: 1, scope: 1, description: 1, mandatory2fa: 1 }, }); + if (role) { - await Roles.updateById( + const updatedRole = await Roles.updateById( roleId, roleData.name || role.name, roleData.scope || role.scope, @@ -17,10 +20,12 @@ export const createOrUpdateProtectedRoleAsync = async ( roleData.mandatory2fa || role.mandatory2fa, ); + void notifyListenerOnRoleChanges(roleId, 'updated', updatedRole); + return; } - await Roles.insertOne({ + const insertedRole = await Roles.insertOne({ _id: roleId, scope: 'Users', description: '', @@ -28,4 +33,6 @@ export const createOrUpdateProtectedRoleAsync = async ( ...roleData, protected: true, }); + + void notifyListenerOnRoleChanges(insertedRole.insertedId, 'inserted'); }; diff --git a/apps/meteor/server/models/raw/Roles.ts b/apps/meteor/server/models/raw/Roles.ts index 355faae256e5..84a5b088ea30 100644 --- a/apps/meteor/server/models/raw/Roles.ts +++ b/apps/meteor/server/models/raw/Roles.ts @@ -1,7 +1,7 @@ import type { IRole, IRoom, IUser, RocketChatRecordDeleted } from '@rocket.chat/core-typings'; import type { IRolesModel } from '@rocket.chat/model-typings'; import { Subscriptions, Users } from '@rocket.chat/models'; -import type { Collection, FindCursor, Db, Filter, FindOptions, InsertOneResult, UpdateResult, WithId, Document } from 'mongodb'; +import type { Collection, FindCursor, Db, Filter, FindOptions, Document } from 'mongodb'; import { BaseRaw } from './BaseRaw'; @@ -190,21 +190,31 @@ export class RolesRaw extends BaseRaw implements IRolesModel { return this.find(query, options || {}); } - updateById( + async updateById( _id: IRole['_id'], name: IRole['name'], scope: IRole['scope'], description: IRole['description'] = '', mandatory2fa: IRole['mandatory2fa'] = false, - ): Promise { - const queryData = { - name, - scope, - description, - mandatory2fa, - }; + ): Promise { + const response = await this.findOneAndUpdate( + { _id }, + { + $set: { + name, + scope, + description, + mandatory2fa, + }, + }, + { upsert: true, returnDocument: 'after' }, + ); + + if (!response.value) { + throw new Error('Role not found'); + } - return this.updateOne({ _id }, { $set: queryData }, { upsert: true }); + return response.value; } findUsersInRole(roleId: IRole['_id'], scope?: IRoom['_id']): Promise>; @@ -242,13 +252,13 @@ export class RolesRaw extends BaseRaw implements IRolesModel { } } - createWithRandomId( + async createWithRandomId( name: IRole['name'], scope: IRole['scope'] = 'Users', description = '', protectedRole = true, mandatory2fa = false, - ): Promise>> { + ): Promise { const role = { name, scope, @@ -257,7 +267,12 @@ export class RolesRaw extends BaseRaw implements IRolesModel { mandatory2fa, }; - return this.insertOne(role); + const res = await this.insertOne(role); + + return { + _id: res.insertedId, + ...role, + }; } async canAddUserToRole(uid: IUser['_id'], roleId: IRole['_id'], scope?: IRoom['_id']): Promise { diff --git a/apps/meteor/server/modules/listeners/listeners.module.ts b/apps/meteor/server/modules/listeners/listeners.module.ts index 29d92d2f6a72..af85d4a29a5f 100644 --- a/apps/meteor/server/modules/listeners/listeners.module.ts +++ b/apps/meteor/server/modules/listeners/listeners.module.ts @@ -3,7 +3,7 @@ import type { ISetting as AppsSetting } from '@rocket.chat/apps-engine/definitio import type { IServiceClass } from '@rocket.chat/core-services'; import { EnterpriseSettings } from '@rocket.chat/core-services'; import { isSettingColor, isSettingEnterprise, UserStatus } from '@rocket.chat/core-typings'; -import type { IUser, IRoom, VideoConference, ISetting, IOmnichannelRoom, IMessage, IOTRMessage } from '@rocket.chat/core-typings'; +import type { IUser, IRoom, IRole, VideoConference, ISetting, IOmnichannelRoom, IMessage, IOTRMessage } from '@rocket.chat/core-typings'; import { Logger } from '@rocket.chat/logger'; import { parse } from '@rocket.chat/message-parser'; @@ -205,11 +205,10 @@ export class ListenersModule { }); service.onEvent('watch.roles', ({ clientAction, role }): void => { - const payload = { + notifications.streamRoles.emitWithoutBroadcast('roles', { type: clientAction, - ...role, - }; - notifications.streamRoles.emitWithoutBroadcast('roles', payload as any); + ...(role as IRole), + }); }); service.onEvent('watch.inquiries', async ({ clientAction, inquiry, diff }): Promise => { diff --git a/packages/model-typings/src/models/IRolesModel.ts b/packages/model-typings/src/models/IRolesModel.ts index 43ff1fc2f9e6..a6e8354a0bc0 100644 --- a/packages/model-typings/src/models/IRolesModel.ts +++ b/packages/model-typings/src/models/IRolesModel.ts @@ -1,5 +1,5 @@ import type { IRole, IUser, IRoom } from '@rocket.chat/core-typings'; -import type { FindCursor, FindOptions, InsertOneResult, UpdateResult, WithId } from 'mongodb'; +import type { FindCursor, FindOptions } from 'mongodb'; import type { IBaseModel } from './IBaseModel'; @@ -32,7 +32,7 @@ export interface IRolesModel extends IBaseModel { scope: IRole['scope'], description?: IRole['description'], mandatory2fa?: IRole['mandatory2fa'], - ): Promise; + ): Promise; findUsersInRole(roleId: IRole['_id'], scope?: IRoom['_id']): Promise>; findUsersInRole(roleId: IRole['_id'], scope: IRoom['_id'] | undefined, options: FindOptions): Promise>; @@ -58,7 +58,7 @@ export interface IRolesModel extends IBaseModel { description?: string, protectedRole?: boolean, mandatory2fa?: boolean, - ): Promise>>; + ): Promise; canAddUserToRole(uid: IUser['_id'], roleId: IRole['_id'], scope?: IRoom['_id']): Promise; }