diff --git a/.changeset/atomic-model-operations.md b/.changeset/atomic-model-operations.md new file mode 100644 index 0000000000000..4e05f12719360 --- /dev/null +++ b/.changeset/atomic-model-operations.md @@ -0,0 +1,8 @@ +--- +'@rocket.chat/meteor': patch +'@rocket.chat/core-typings': patch +'@rocket.chat/model-typings': patch +'@rocket.chat/models': patch +--- + +Fixes race conditions in several check-then-write database flows by collapsing them into single atomic operations: CAS login tokens can no longer be consumed by two concurrent logins, revoking a room invite no longer emits duplicate removal notifications, and deleting an integration now enforces the creator-only permission scope in the delete itself diff --git a/apps/meteor/app/integrations/server/methods/incoming/deleteIncomingIntegration.ts b/apps/meteor/app/integrations/server/methods/incoming/deleteIncomingIntegration.ts index 3258aeed8aebc..8d37888f37589 100644 --- a/apps/meteor/app/integrations/server/methods/incoming/deleteIncomingIntegration.ts +++ b/apps/meteor/app/integrations/server/methods/incoming/deleteIncomingIntegration.ts @@ -4,7 +4,7 @@ import { Meteor } from 'meteor/meteor'; import { hasPermissionAsync } from '../../../../authorization/server/functions/hasPermission'; import { methodDeprecationLogger } from '../../../../lib/server/lib/deprecationWarningLogger'; -import { notifyOnIntegrationChangedById } from '../../../../lib/server/lib/notifyListener'; +import { notifyOnIntegrationChanged } from '../../../../lib/server/lib/notifyListener'; declare module '@rocket.chat/ddp-client' { // eslint-disable-next-line @typescript-eslint/naming-convention @@ -14,29 +14,28 @@ declare module '@rocket.chat/ddp-client' { } export const deleteIncomingIntegration = async (integrationId: string, userId: string): Promise => { - let integration; - - if (userId && (await hasPermissionAsync(userId, 'manage-incoming-integrations'))) { - integration = Integrations.findOneById(integrationId); - } else if (userId && (await hasPermissionAsync(userId, 'manage-own-incoming-integrations'))) { - integration = Integrations.findOne({ - '_id': integrationId, - '_createdBy._id': userId, - }); - } else { + const canManageAllIntegrations = !!userId && (await hasPermissionAsync(userId, 'manage-incoming-integrations')); + const canManageOwnIntegrations = + !canManageAllIntegrations && !!userId && (await hasPermissionAsync(userId, 'manage-own-incoming-integrations')); + + if (!canManageAllIntegrations && !canManageOwnIntegrations) { throw new Meteor.Error('not_authorized', 'Unauthorized', { method: 'deleteIncomingIntegration', }); } - if (!(await integration)) { + const integration = await Integrations.removeByIdAndCreatedByIfExists({ + _id: integrationId, + ...(canManageOwnIntegrations && { createdBy: userId }), + }); + + if (!integration) { throw new Meteor.Error('error-invalid-integration', 'Invalid integration', { method: 'deleteIncomingIntegration', }); } - await Integrations.removeById(integrationId); - void notifyOnIntegrationChangedById(integrationId, 'removed'); + void notifyOnIntegrationChanged(integration, 'removed'); }; Meteor.methods({ diff --git a/apps/meteor/app/integrations/server/methods/outgoing/deleteOutgoingIntegration.ts b/apps/meteor/app/integrations/server/methods/outgoing/deleteOutgoingIntegration.ts index b9e44d7d5adbd..2c9e894d6f910 100644 --- a/apps/meteor/app/integrations/server/methods/outgoing/deleteOutgoingIntegration.ts +++ b/apps/meteor/app/integrations/server/methods/outgoing/deleteOutgoingIntegration.ts @@ -4,7 +4,7 @@ import { Meteor } from 'meteor/meteor'; import { hasPermissionAsync } from '../../../../authorization/server/functions/hasPermission'; import { methodDeprecationLogger } from '../../../../lib/server/lib/deprecationWarningLogger'; -import { notifyOnIntegrationChangedById } from '../../../../lib/server/lib/notifyListener'; +import { notifyOnIntegrationChanged } from '../../../../lib/server/lib/notifyListener'; declare module '@rocket.chat/ddp-client' { // eslint-disable-next-line @typescript-eslint/naming-convention @@ -14,37 +14,35 @@ declare module '@rocket.chat/ddp-client' { } export const deleteOutgoingIntegration = async (integrationId: string, userId: string): Promise => { - let integration; - if (!userId) { throw new Meteor.Error('not_authorized', 'Unauthorized', { method: 'deleteOutgoingIntegration', }); } - if (await hasPermissionAsync(userId, 'manage-outgoing-integrations')) { - integration = Integrations.findOneById(integrationId); - } else if (await hasPermissionAsync(userId, 'manage-own-outgoing-integrations')) { - integration = Integrations.findOne({ - '_id': integrationId, - '_createdBy._id': userId, - }); - } else { + const canManageAllIntegrations = await hasPermissionAsync(userId, 'manage-outgoing-integrations'); + const canManageOwnIntegrations = !canManageAllIntegrations && (await hasPermissionAsync(userId, 'manage-own-outgoing-integrations')); + + if (!canManageAllIntegrations && !canManageOwnIntegrations) { throw new Meteor.Error('not_authorized', 'Unauthorized', { method: 'deleteOutgoingIntegration', }); } - if (!(await integration)) { + const integration = await Integrations.removeByIdAndCreatedByIfExists({ + _id: integrationId, + ...(canManageOwnIntegrations && { createdBy: userId }), + }); + + if (!integration) { throw new Meteor.Error('error-invalid-integration', 'Invalid integration', { method: 'deleteOutgoingIntegration', }); } - await Integrations.removeById(integrationId); // Don't sending to IntegrationHistory listener since it don't waits for 'removed' events. await IntegrationHistory.removeByIntegrationId(integrationId); - void notifyOnIntegrationChangedById(integrationId, 'removed'); + void notifyOnIntegrationChanged(integration, 'removed'); }; Meteor.methods({ diff --git a/apps/meteor/app/invites/server/functions/removeInvite.ts b/apps/meteor/app/invites/server/functions/removeInvite.ts index c754d82d071cc..266d4f7f2fc81 100644 --- a/apps/meteor/app/invites/server/functions/removeInvite.ts +++ b/apps/meteor/app/invites/server/functions/removeInvite.ts @@ -20,16 +20,13 @@ export const removeInvite = async (userId: string, invite: Pick) }); } - // Before anything, let's check if there's an existing invite - const existing = await Invites.findOneById(invite._id); + const { deletedCount } = await Invites.removeById(invite._id); - if (!existing) { + if (!deletedCount) { throw new Meteor.Error('invalid-invitation-id', 'Invalid Invitation _id', { method: 'removeInvite', }); } - await Invites.removeById(invite._id); - return true; }; diff --git a/apps/meteor/app/meteor-accounts-saml/server/lib/settings.ts b/apps/meteor/app/meteor-accounts-saml/server/lib/settings.ts index 0316045acb5c8..7231d6fa59ad8 100644 --- a/apps/meteor/app/meteor-accounts-saml/server/lib/settings.ts +++ b/apps/meteor/app/meteor-accounts-saml/server/lib/settings.ts @@ -150,13 +150,8 @@ export const loadSamlServiceProviders = async function (): Promise { SAMLUtils.logger?.warn({ msg: 'SAML Provider not loaded due to invalid configuration', key }); } - const service = await LoginServiceConfiguration.findOneByService(serviceName, { projection: { _id: 1 } }); - if (!service?._id) { - return false; - } - - const { deletedCount } = await LoginServiceConfiguration.removeService(service._id); - if (!deletedCount) { + const service = await LoginServiceConfiguration.removeByService(serviceName); + if (!service) { return false; } diff --git a/apps/meteor/app/oauth2-server-config/server/admin/methods/deleteOAuthApp.ts b/apps/meteor/app/oauth2-server-config/server/admin/methods/deleteOAuthApp.ts index ea86151253ec4..bff067a10526e 100644 --- a/apps/meteor/app/oauth2-server-config/server/admin/methods/deleteOAuthApp.ts +++ b/apps/meteor/app/oauth2-server-config/server/admin/methods/deleteOAuthApp.ts @@ -18,15 +18,13 @@ export const deleteOAuthApp = async (userId: string, applicationId: IOAuthApps[' throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'deleteOAuthApp' }); } - const application = await OAuthApps.findOneById(applicationId); + const application = await OAuthApps.findOneAndDeleteById(applicationId, { projection: { clientId: 1 } }); if (!application) { throw new Meteor.Error('error-application-not-found', 'Application not found', { method: 'deleteOAuthApp', }); } - await OAuthApps.deleteOne({ _id: applicationId }); - await OAuthAccessTokens.deleteMany({ clientId: application.clientId }); await OAuthAuthCodes.deleteMany({ clientId: application.clientId }); diff --git a/apps/meteor/app/oauth2-server-config/server/admin/methods/updateOAuthApp.ts b/apps/meteor/app/oauth2-server-config/server/admin/methods/updateOAuthApp.ts index a5adfbaea4904..cf7c866acf3b8 100644 --- a/apps/meteor/app/oauth2-server-config/server/admin/methods/updateOAuthApp.ts +++ b/apps/meteor/app/oauth2-server-config/server/admin/methods/updateOAuthApp.ts @@ -42,13 +42,6 @@ export const updateOAuthApp = async ( }); } - const currentApplication = await OAuthApps.findOneById(applicationId); - if (currentApplication == null) { - throw new Meteor.Error('error-application-not-found', 'Application not found', { - method: 'updateOAuthApp', - }); - } - const redirectUri = parseUriList(application.redirectUri); if (redirectUri.length === 0) { @@ -57,23 +50,24 @@ export const updateOAuthApp = async ( }); } - await OAuthApps.updateOne( - { _id: applicationId }, - { - $set: { - name: application.name, - active: application.active, - redirectUri, - _updatedAt: new Date(), - _updatedBy: await Users.findOneById(userId, { - projection: { - username: 1, - }, - }), + const updatedApplication = await OAuthApps.updateById(applicationId, { + name: application.name, + active: application.active, + redirectUri, + _updatedBy: await Users.findOneById(userId, { + projection: { + username: 1, }, - }, - ); - return OAuthApps.findOneById(applicationId); + }), + }); + + if (updatedApplication == null) { + throw new Meteor.Error('error-application-not-found', 'Application not found', { + method: 'updateOAuthApp', + }); + } + + return updatedApplication; }; Meteor.methods({ diff --git a/apps/meteor/app/user-status/server/methods/deleteCustomUserStatus.ts b/apps/meteor/app/user-status/server/methods/deleteCustomUserStatus.ts index 7592e99413d30..260220eea3f15 100644 --- a/apps/meteor/app/user-status/server/methods/deleteCustomUserStatus.ts +++ b/apps/meteor/app/user-status/server/methods/deleteCustomUserStatus.ts @@ -18,12 +18,11 @@ export const deleteCustomUserStatus = async (userId: string, userStatusID: strin throw new Meteor.Error('not_authorized'); } - const userStatus = await CustomUserStatus.findOneById(userStatusID); + const userStatus = await CustomUserStatus.findOneAndDeleteById(userStatusID); if (userStatus == null) { throw new Meteor.Error('Custom_User_Status_Error_Invalid_User_Status', 'Invalid user status', { method: 'deleteCustomUserStatus' }); } - await CustomUserStatus.removeById(userStatusID); void api.broadcast('user.deleteCustomStatus', userStatus); return true; diff --git a/apps/meteor/server/lib/cas/findExistingCASUser.ts b/apps/meteor/server/lib/cas/findExistingCASUser.ts index 2c65ab77546dc..9d6354b81cf77 100644 --- a/apps/meteor/server/lib/cas/findExistingCASUser.ts +++ b/apps/meteor/server/lib/cas/findExistingCASUser.ts @@ -16,12 +16,6 @@ export const findExistingCASUser = async (username: string): Promise { beforeEach(() => { - findOneNotExpiredById.reset(); - removeById.reset(); + removeNotExpiredById.reset(); + removeNotExpiredById.resolves(null); findExistingCASUser.reset(); settingsGet.reset(); settingsGet.returns(true); @@ -44,13 +43,18 @@ describe('loginHandlerCAS', () => { expect(await handler({ cas: { credentialToken: ['a'] } })).to.be.undefined; expect(await handler({ cas: { credentialToken: null } })).to.be.undefined; - expect(findOneNotExpiredById.called).to.be.false; + expect(removeNotExpiredById.called).to.be.false; + }); + + it('should consume the credential token and reject when no matching login attempt is found', async () => { + await expect(handler({ cas: { credentialToken: 'valid-token' } })).to.be.rejected; + expect(removeNotExpiredById.calledOnceWith('valid-token')).to.be.true; }); it('should return undefined when CAS is disabled', async () => { settingsGet.returns(false); expect(await handler({ cas: { credentialToken: 'valid-token' } })).to.be.undefined; - expect(findOneNotExpiredById.called).to.be.false; + expect(removeNotExpiredById.called).to.be.false; }); }); diff --git a/apps/meteor/server/lib/cas/loginHandler.ts b/apps/meteor/server/lib/cas/loginHandler.ts index 070a6834335bd..c8093b5a722f5 100644 --- a/apps/meteor/server/lib/cas/loginHandler.ts +++ b/apps/meteor/server/lib/cas/loginHandler.ts @@ -14,14 +14,11 @@ export const loginHandlerCAS = async (options: any): Promise('CAS_Sync_User_Data_FieldMap').trim(); const casVersion = parseFloat(settings.get('CAS_version') ?? '1.0'); diff --git a/apps/meteor/server/lib/oauth/updateOAuthServices.ts b/apps/meteor/server/lib/oauth/updateOAuthServices.ts index 784628cae44a9..393474fe4b24e 100644 --- a/apps/meteor/server/lib/oauth/updateOAuthServices.ts +++ b/apps/meteor/server/lib/oauth/updateOAuthServices.ts @@ -119,12 +119,9 @@ export async function updateOAuthServices(): Promise { await LoginServiceConfiguration.createOrUpdateService(serviceKey, data); void notifyOnLoginServiceConfigurationChangedByService(serviceKey); } else { - const service = await LoginServiceConfiguration.findOneByService(serviceName, { projection: { _id: 1 } }); - if (service?._id) { - const { deletedCount } = await LoginServiceConfiguration.removeService(service._id); - if (deletedCount > 0) { - void notifyOnLoginServiceConfigurationChanged({ _id: service._id }, 'removed'); - } + const service = await LoginServiceConfiguration.removeByService(serviceName); + if (service) { + void notifyOnLoginServiceConfigurationChanged({ _id: service._id }, 'removed'); } } } diff --git a/apps/meteor/server/services/room/service.ts b/apps/meteor/server/services/room/service.ts index 5f417e530a084..606f979eb7d4b 100644 --- a/apps/meteor/server/services/room/service.ts +++ b/apps/meteor/server/services/room/service.ts @@ -153,12 +153,11 @@ export class RoomService extends ServiceClassInternal implements IRoomService { } async revokeInvite(room: IRoom, user: IUser): Promise { - const subscription = await Subscriptions.findOneByRoomIdAndUserId(room._id, user._id); - if (subscription?.status !== 'INVITED') { + const subscription = await Subscriptions.removeInvitedByRoomIdAndUserId(room._id, user._id); + if (!subscription) { return; } - await Subscriptions.removeById(subscription._id); void notifyOnSubscriptionChanged(subscription, 'removed'); } diff --git a/apps/meteor/server/services/team/service.ts b/apps/meteor/server/services/team/service.ts index 9c3c7c8b746fa..5da5df9380d03 100644 --- a/apps/meteor/server/services/team/service.ts +++ b/apps/meteor/server/services/team/service.ts @@ -936,40 +936,21 @@ export class TeamService extends ServiceClassInternal implements ITeamService { } async addRolesToMember(teamId: string, userId: string, roles: Array): Promise { - const isMember = await TeamMember.findOneByUserIdAndTeamId(userId, teamId, { - projection: { _id: 1 }, - }); - - if (!isMember) { - // TODO should this throw an error instead? - return false; - } + const { matchedCount } = await TeamMember.updateRolesByTeamIdAndUserId(teamId, userId, roles); - return !!(await TeamMember.updateRolesByTeamIdAndUserId(teamId, userId, roles)); + return matchedCount > 0; } async addRolesToSubscription(roomId: string, userId: string, roles: Array): Promise { - const subscription = await Subscriptions.findOneByRoomIdAndUserId(roomId, userId); + const { matchedCount } = await Subscriptions.addRolesByUserId(userId, roles, roomId); - if (!subscription) { - // TODO should this throw an error instead? - return false; - } - - return !!(await Subscriptions.addRolesByUserId(userId, roles, roomId)); + return matchedCount > 0; } async removeRolesFromMember(teamId: string, userId: string, roles: Array): Promise { - const isMember = await TeamMember.findOneByUserIdAndTeamId(userId, teamId, { - projection: { _id: 1 }, - }); - - if (!isMember) { - // TODO should this throw an error instead? - return false; - } + const { matchedCount } = await TeamMember.removeRolesByTeamIdAndUserId(teamId, userId, roles); - return !!(await TeamMember.removeRolesByTeamIdAndUserId(teamId, userId, roles)); + return matchedCount > 0; } async getInfoByName(teamName: string): Promise | null> { diff --git a/packages/core-typings/src/IOAuthApps.ts b/packages/core-typings/src/IOAuthApps.ts index 8003170233aa7..2c92fb6e78e53 100644 --- a/packages/core-typings/src/IOAuthApps.ts +++ b/packages/core-typings/src/IOAuthApps.ts @@ -11,5 +11,9 @@ export interface IOAuthApps extends IRocketChatRecord { _id: string; username: string; }; + _updatedBy?: { + _id: string; + username?: string; + } | null; appId?: string; } diff --git a/packages/model-typings/src/models/IBaseModel.ts b/packages/model-typings/src/models/IBaseModel.ts index e293dfa52d750..93a513d17cbfb 100644 --- a/packages/model-typings/src/models/IBaseModel.ts +++ b/packages/model-typings/src/models/IBaseModel.ts @@ -55,6 +55,7 @@ export interface IBaseModel< updateFromUpdater(query: Filter, updater: Updater, options?: UpdateOptions): Promise; findOneAndDelete(filter: Filter, options?: FindOneAndDeleteOptions): Promise>; + findOneAndDeleteById(_id: T['_id'], options?: FindOneAndDeleteOptions): Promise>; findOneAndUpdate(query: Filter, update: UpdateFilter | T, options?: FindOneAndUpdateOptions): Promise>; findOneById(_id: T['_id'], options?: FindOptions | undefined): Promise; diff --git a/packages/model-typings/src/models/ICredentialTokensModel.ts b/packages/model-typings/src/models/ICredentialTokensModel.ts index 600d2aac82502..6ffb3fc04cfa8 100644 --- a/packages/model-typings/src/models/ICredentialTokensModel.ts +++ b/packages/model-typings/src/models/ICredentialTokensModel.ts @@ -5,4 +5,5 @@ import type { IBaseModel } from './IBaseModel'; export interface ICredentialTokensModel extends IBaseModel { create(_id: string, userInfo: ICredentialToken['userInfo']): Promise; findOneNotExpiredById(_id: string): Promise; + removeNotExpiredById(_id: string): Promise; } diff --git a/packages/model-typings/src/models/IIntegrationsModel.ts b/packages/model-typings/src/models/IIntegrationsModel.ts index fd7b4cc692b5b..8eb4888e16b06 100644 --- a/packages/model-typings/src/models/IIntegrationsModel.ts +++ b/packages/model-typings/src/models/IIntegrationsModel.ts @@ -8,6 +8,7 @@ export interface IIntegrationsModel extends IBaseModel { findByChannels(channels: IIntegration['channel']): FindCursor; findByUserId(userId: IIntegration['userId']): FindCursor>; findOneByIdAndCreatedByIfExists(params: { _id: IIntegration['_id']; createdBy?: IUser['_id'] }): Promise; + removeByIdAndCreatedByIfExists(params: { _id: IIntegration['_id']; createdBy?: IUser['_id'] }): Promise; findOneByUrl(url: string): Promise; updateRoomName(oldRoomName: string, newRoomName: string): ReturnType['updateMany']>; findOneByIdAndToken

( diff --git a/packages/model-typings/src/models/ILoginServiceConfigurationModel.ts b/packages/model-typings/src/models/ILoginServiceConfigurationModel.ts index ce71854b70abf..6a1a564c45d9f 100644 --- a/packages/model-typings/src/models/ILoginServiceConfigurationModel.ts +++ b/packages/model-typings/src/models/ILoginServiceConfigurationModel.ts @@ -1,5 +1,5 @@ import type { LoginServiceConfiguration } from '@rocket.chat/core-typings'; -import type { DeleteResult, Document, FindOptions } from 'mongodb'; +import type { Document, FindOptions } from 'mongodb'; import type { IBaseModel } from './IBaseModel'; @@ -8,7 +8,7 @@ export interface ILoginServiceConfigurationModel extends IBaseModel, ): Promise; - removeService(_id: LoginServiceConfiguration['_id']): Promise; + removeByService(serviceName: LoginServiceConfiguration['service']): Promise | null>; findOneByService

( serviceName: LoginServiceConfiguration['service'], options?: FindOptions

, diff --git a/packages/model-typings/src/models/IOAuthAppsModel.ts b/packages/model-typings/src/models/IOAuthAppsModel.ts index 859c972e45979..3a696465d2ca3 100644 --- a/packages/model-typings/src/models/IOAuthAppsModel.ts +++ b/packages/model-typings/src/models/IOAuthAppsModel.ts @@ -16,6 +16,11 @@ export interface IOAuthAppsModel extends IBaseModel { findOneActiveByClientId(clientId: string, options?: FindOptions): Promise; + updateById( + _id: IOAuthApps['_id'], + data: Partial>, + ): Promise; + findOneActiveByClientIdAndClientSecret( clientId: string, clientSecret: string, diff --git a/packages/model-typings/src/models/ISubscriptionsModel.ts b/packages/model-typings/src/models/ISubscriptionsModel.ts index 73d2a2892aef9..79342e45ca4c9 100644 --- a/packages/model-typings/src/models/ISubscriptionsModel.ts +++ b/packages/model-typings/src/models/ISubscriptionsModel.ts @@ -315,6 +315,7 @@ export interface ISubscriptionsModel extends IBaseModel { ): Promise>; removeByRoomIdsAndUserId(rids: string[], userId: string): Promise; removeByRoomIdAndUserId(roomId: string, userId: string): Promise; + removeInvitedByRoomIdAndUserId(roomId: string, userId: string): Promise; removeByRoomIds(rids: string[], options?: { onTrash: (doc: ISubscription) => void }): Promise; diff --git a/packages/model-typings/src/models/IUsersModel.ts b/packages/model-typings/src/models/IUsersModel.ts index 6e748b49fa2d4..0b34a297ef2a0 100644 --- a/packages/model-typings/src/models/IUsersModel.ts +++ b/packages/model-typings/src/models/IUsersModel.ts @@ -169,6 +169,7 @@ export interface IUsersModel extends IBaseModel { setAbacAttributesById(userId: IUser['_id'], attributes: NonNullable): Promise; unsetAbacAttributesById(userId: IUser['_id']): Promise; findActiveByRoomIds(roomIds: IRoom['_id'][], options?: FindOptions): FindCursor; + setCasExternalIdByUsername(username: string): Promise; updateStatusText(_id: IUser['_id'], statusText: string, options?: UpdateOptions): Promise; diff --git a/packages/models/src/dummy/BaseDummy.ts b/packages/models/src/dummy/BaseDummy.ts index c161b23584b65..5b4363a93b475 100644 --- a/packages/models/src/dummy/BaseDummy.ts +++ b/packages/models/src/dummy/BaseDummy.ts @@ -57,6 +57,10 @@ export class BaseDummy< return null; } + async findOneAndDeleteById(_id: T['_id']): Promise | null> { + return null; + } + async findOneAndUpdate(): Promise | null> { return null; } diff --git a/packages/models/src/models/BaseRaw.ts b/packages/models/src/models/BaseRaw.ts index 3c389e9cad9c1..6c572b9fa70d2 100644 --- a/packages/models/src/models/BaseRaw.ts +++ b/packages/models/src/models/BaseRaw.ts @@ -366,6 +366,10 @@ export abstract class BaseRaw< return doc as WithId; } + findOneAndDeleteById(_id: T['_id'], options?: FindOneAndDeleteOptions): Promise | null> { + return this.findOneAndDelete({ _id } as Filter, options); + } + async deleteMany(filter: Filter, options?: DeleteOptions & { onTrash?: (record: ResultFields) => void }): Promise { if (!this.trash) { if (options) { diff --git a/packages/models/src/models/CredentialTokens.ts b/packages/models/src/models/CredentialTokens.ts index cdeb1d0f59c7e..dd78b9108f3c2 100644 --- a/packages/models/src/models/CredentialTokens.ts +++ b/packages/models/src/models/CredentialTokens.ts @@ -32,4 +32,13 @@ export class CredentialTokensRaw extends BaseRaw implements IC return this.findOne(query); } + + removeNotExpiredById(_id: string): Promise { + const query = { + _id, + expireAt: { $gt: new Date() }, + }; + + return this.findOneAndDelete(query); + } } diff --git a/packages/models/src/models/Integrations.ts b/packages/models/src/models/Integrations.ts index 9acb2496e3805..8ae0c87037b5d 100644 --- a/packages/models/src/models/Integrations.ts +++ b/packages/models/src/models/Integrations.ts @@ -46,6 +46,13 @@ export class IntegrationsRaw extends BaseRaw implements IIntegrati }); } + removeByIdAndCreatedByIfExists({ _id, createdBy }: { _id: IIntegration['_id']; createdBy?: IUser['_id'] }): Promise { + return this.findOneAndDelete({ + _id, + ...(createdBy && { '_createdBy._id': createdBy }), + }); + } + disableByUserId(userId: IIntegration['userId']): ReturnType['updateMany']> { return this.updateMany({ userId }, { $set: { enabled: false } }); } diff --git a/packages/models/src/models/LoginServiceConfiguration.ts b/packages/models/src/models/LoginServiceConfiguration.ts index 06d0c5753f910..721d63ff21b40 100644 --- a/packages/models/src/models/LoginServiceConfiguration.ts +++ b/packages/models/src/models/LoginServiceConfiguration.ts @@ -1,6 +1,6 @@ import type { LoginServiceConfiguration, RocketChatRecordDeleted } from '@rocket.chat/core-typings'; import type { ILoginServiceConfigurationModel } from '@rocket.chat/model-typings'; -import type { Collection, Db, DeleteResult, Document, FindOptions } from 'mongodb'; +import type { Collection, Db, Document, FindOptions } from 'mongodb'; import { BaseRaw } from './BaseRaw'; @@ -44,8 +44,8 @@ export class LoginServiceConfigurationRaw extends BaseRaw { - return this.deleteOne({ _id }); + removeByService(serviceName: LoginServiceConfiguration['service']): Promise | null> { + return this.findOneAndDelete({ service: serviceName.toLowerCase() }, { projection: { _id: 1 } }); } async findOneByService

( diff --git a/packages/models/src/models/OAuthApps.ts b/packages/models/src/models/OAuthApps.ts index ccac7c15eee16..b6d63688331e0 100644 --- a/packages/models/src/models/OAuthApps.ts +++ b/packages/models/src/models/OAuthApps.ts @@ -40,6 +40,13 @@ export class OAuthAppsRaw extends BaseRaw implements IOAuthAppsModel ); } + updateById( + _id: IOAuthApps['_id'], + data: Partial>, + ): Promise { + return this.findOneAndUpdate({ _id }, { $set: data }, { returnDocument: 'after' }); + } + findOneActiveByClientIdAndClientSecret( clientId: string, clientSecret: string, diff --git a/packages/models/src/models/Subscriptions.ts b/packages/models/src/models/Subscriptions.ts index c4533e58bcbb0..dc3bccd9811bc 100644 --- a/packages/models/src/models/Subscriptions.ts +++ b/packages/models/src/models/Subscriptions.ts @@ -1984,6 +1984,16 @@ export class SubscriptionsRaw extends BaseRaw implements ISubscri return doc; } + removeInvitedByRoomIdAndUserId(roomId: string, userId: string): Promise { + const query = { + 'rid': roomId, + 'u._id': userId, + 'status': 'INVITED' as const, + }; + + return this.findOneAndDelete(query); + } + async removeByRoomIds(rids: string[], options?: { onTrash: (doc: ISubscription) => void }): Promise { const result = await this.deleteMany({ rid: { $in: rids } }, options); diff --git a/packages/models/src/models/Users.ts b/packages/models/src/models/Users.ts index 8c18f2dbdf8f5..cd46792cc9a6e 100644 --- a/packages/models/src/models/Users.ts +++ b/packages/models/src/models/Users.ts @@ -33,6 +33,8 @@ import { Rooms, Subscriptions } from '../index'; import { BaseRaw } from './BaseRaw'; import { queryAvailableAgentsForSelection, queryStatusAgentOnline } from '../helpers'; +const usersDefaultFields = { __rooms: 0 } as const; + export class UsersRaw extends BaseRaw> implements IUsersModel { constructor(db: Db, trash?: Collection>) { super(db, 'users', trash, { @@ -41,9 +43,7 @@ export class UsersRaw extends BaseRaw> implements IU }, }); - this.defaultFields = { - __rooms: 0, - }; + this.defaultFields = usersDefaultFields; } // Move index from constructor to here @@ -148,6 +148,16 @@ export class UsersRaw extends BaseRaw> implements IU return this.find({ active: true, __rooms: { $in: roomIds } }, options); } + setCasExternalIdByUsername(username: string): Promise { + // #TODO: Remove regex based search + const regex = new RegExp(`^${escapeRegExp(username)}$`, 'i'); + return this.findOneAndUpdate( + { username: regex }, + { $set: { 'services.cas.external_id': username } }, + { returnDocument: 'after', projection: usersDefaultFields }, + ); + } + /** * @param {string} uid * @param {IRole['_id'][]} roles list of role ids