Skip to content

Commit

Permalink
feat!: Implement Cloud communication reliability (#32856)
Browse files Browse the repository at this point in the history
  • Loading branch information
Gustrb authored Oct 17, 2024
1 parent f63d8e2 commit b338807
Show file tree
Hide file tree
Showing 16 changed files with 190 additions and 78 deletions.
9 changes: 9 additions & 0 deletions .changeset/plenty-hairs-camp.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"@rocket.chat/meteor": major
"@rocket.chat/core-typings": major
"@rocket.chat/model-typings": major
"@rocket.chat/models": major
---

Adds a new collection to store all the workspace cloud tokens to defer the race condition management to MongoDB instead of having to handle it within the settings cache.
Removes the Cloud_Workspace_Access_Token & Cloud_Workspace_Access_Token_Expires_At settings since they are not going to be used anymore.
5 changes: 2 additions & 3 deletions apps/meteor/app/api/server/v1/misc.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import crypto from 'crypto';

import { isOAuthUser, type IUser } from '@rocket.chat/core-typings';
import { Settings, Users } from '@rocket.chat/models';
import { Settings, Users, WorkspaceCredentials } from '@rocket.chat/models';
import {
isShieldSvgProps,
isSpotlightProps,
Expand Down Expand Up @@ -664,6 +664,7 @@ API.v1.addRoute(
const settingsIds: string[] = [];

if (this.bodyParams.setDeploymentAs === 'new-workspace') {
await WorkspaceCredentials.unsetCredentialByScope();
settingsIds.push(
'Cloud_Service_Agree_PrivacyTerms',
'Cloud_Workspace_Id',
Expand All @@ -675,9 +676,7 @@ API.v1.addRoute(
'Cloud_Workspace_PublicKey',
'Cloud_Workspace_License',
'Cloud_Workspace_Had_Trial',
'Cloud_Workspace_Access_Token',
'uniqueID',
'Cloud_Workspace_Access_Token_Expires_At',
);
}

Expand Down
43 changes: 23 additions & 20 deletions apps/meteor/app/cloud/server/functions/getWorkspaceAccessToken.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,21 @@
import { Settings } from '@rocket.chat/models';
import type { IWorkspaceCredentials } from '@rocket.chat/core-typings';
import { WorkspaceCredentials } from '@rocket.chat/models';

import { notifyOnSettingChangedById } from '../../../lib/server/lib/notifyListener';
import { settings } from '../../../settings/server';
import { getWorkspaceAccessTokenWithScope } from './getWorkspaceAccessTokenWithScope';
import { retrieveRegistrationStatus } from './retrieveRegistrationStatus';

const hasWorkspaceAccessTokenExpired = (credentials: IWorkspaceCredentials): boolean => new Date() >= credentials.expirationDate;

/**
* @param {boolean} forceNew
* @param {string} scope
* @param {boolean} save
* @returns string
* Returns the access token for the workspace, if it is expired or forceNew is true, it will get a new one
* and save it to the database, therefore if this function does not throw an error, it will always return a valid token.
*
* @param {boolean} forceNew - If true, it will get a new token even if the current one is not expired
* @param {string} scope - The scope of the token to get
* @param {boolean} save - If true, it will save the new token to the database
* @throws {CloudWorkspaceAccessTokenError} If the workspace is not registered (no credentials in the database)
*
* @returns string - A valid access token for the workspace
*/
export async function getWorkspaceAccessToken(forceNew = false, scope = '', save = true, throwOnError = false): Promise<string> {
const { workspaceRegistered } = await retrieveRegistrationStatus();
Expand All @@ -18,26 +24,23 @@ export async function getWorkspaceAccessToken(forceNew = false, scope = '', save
return '';
}

const expires = await Settings.findOneById('Cloud_Workspace_Access_Token_Expires_At');

if (expires === null) {
throw new Error('Cloud_Workspace_Access_Token_Expires_At is not set');
const workspaceCredentials = await WorkspaceCredentials.getCredentialByScope(scope);
if (!workspaceCredentials) {
throw new CloudWorkspaceAccessTokenError();
}

const now = new Date();

if (expires.value && now < expires.value && !forceNew) {
return settings.get<string>('Cloud_Workspace_Access_Token');
if (!hasWorkspaceAccessTokenExpired(workspaceCredentials) && !forceNew) {
return workspaceCredentials.accessToken;
}

const accessToken = await getWorkspaceAccessTokenWithScope(scope, throwOnError);

if (save) {
(await Settings.updateValueById('Cloud_Workspace_Access_Token', accessToken.token)).modifiedCount &&
void notifyOnSettingChangedById('Cloud_Workspace_Access_Token');

(await Settings.updateValueById('Cloud_Workspace_Access_Token_Expires_At', accessToken.expiresAt)).modifiedCount &&
void notifyOnSettingChangedById('Cloud_Workspace_Access_Token_Expires_At');
await WorkspaceCredentials.updateCredentialByScope({
scope,
accessToken: accessToken.token,
expirationDate: accessToken.expiresAt,
});
}

return accessToken.token;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Settings } from '@rocket.chat/models';
import { Settings, WorkspaceCredentials } from '@rocket.chat/models';

import { notifyOnSettingChangedById } from '../../../lib/server/lib/notifyListener';
import { retrieveRegistrationStatus } from './retrieveRegistrationStatus';
Expand All @@ -9,6 +9,8 @@ export async function removeWorkspaceRegistrationInfo() {
return true;
}

await WorkspaceCredentials.removeAllCredentials();

const settingsIds = [
'Cloud_Workspace_Id',
'Cloud_Workspace_Name',
Expand Down
55 changes: 23 additions & 32 deletions apps/meteor/app/cloud/server/functions/saveRegistrationData.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,22 @@
import { applyLicense } from '@rocket.chat/license';
import { Settings } from '@rocket.chat/models';
import { Settings, WorkspaceCredentials } from '@rocket.chat/models';

import { notifyOnSettingChangedById } from '../../../lib/server/lib/notifyListener';
import { settings } from '../../../settings/server';
import { syncCloudData } from './syncWorkspace/syncCloudData';

type SaveRegistrationDataDTO = {
workspaceId: string;
client_name: string;
client_id: string;
client_secret: string;
client_secret_expires_at: number;
publicKey: string;
registration_client_uri: string;
};

type ManualSaveRegistrationDataDTO = SaveRegistrationDataDTO & { licenseData: { license: string } };

export async function saveRegistrationData({
workspaceId,
client_name,
Expand All @@ -13,15 +25,7 @@ export async function saveRegistrationData({
client_secret_expires_at,
publicKey,
registration_client_uri,
}: {
workspaceId: string;
client_name: string;
client_id: string;
client_secret: string;
client_secret_expires_at: number;
publicKey: string;
registration_client_uri: string;
}) {
}: SaveRegistrationDataDTO) {
await saveRegistrationDataBase({
workspaceId,
client_name,
Expand All @@ -43,15 +47,7 @@ async function saveRegistrationDataBase({
client_secret_expires_at,
publicKey,
registration_client_uri,
}: {
workspaceId: string;
client_name: string;
client_id: string;
client_secret: string;
client_secret_expires_at: number;
publicKey: string;
registration_client_uri: string;
}) {
}: SaveRegistrationDataDTO) {
const settingsData = [
{ _id: 'Register_Server', value: true },
{ _id: 'Cloud_Workspace_Id', value: workspaceId },
Expand All @@ -63,7 +59,13 @@ async function saveRegistrationDataBase({
{ _id: 'Cloud_Workspace_Registration_Client_Uri', value: registration_client_uri },
];

const promises = settingsData.map(({ _id, value }) => Settings.updateValueById(_id, value));
await WorkspaceCredentials.updateCredentialByScope({
scope: '',
accessToken: '',
expirationDate: new Date(0),
});

const promises = [...settingsData.map(({ _id, value }) => Settings.updateValueById(_id, value))];

(await Promise.all(promises)).forEach((value, index) => {
if (value?.modifiedCount) {
Expand Down Expand Up @@ -104,18 +106,7 @@ export async function saveRegistrationDataManual({
publicKey,
registration_client_uri,
licenseData,
}: {
workspaceId: string;
client_name: string;
client_id: string;
client_secret: string;
client_secret_expires_at: number;
publicKey: string;
registration_client_uri: string;
licenseData: {
license: string;
};
}) {
}: ManualSaveRegistrationDataDTO) {
await saveRegistrationDataBase({
workspaceId,
client_name,
Expand Down
6 changes: 6 additions & 0 deletions apps/meteor/ee/server/models/WorkspaceCredentials.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { registerModel } from '@rocket.chat/models';

import { db } from '../../../server/database/utils';
import { WorkspaceCredentialsRaw } from './raw/WorkspaceCredentials';

registerModel('IWorkspaceCredentialsModel', new WorkspaceCredentialsRaw(db));
68 changes: 68 additions & 0 deletions apps/meteor/ee/server/models/raw/WorkspaceCredentials.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import type { IWorkspaceCredentials } from '@rocket.chat/core-typings';
import type { IWorkspaceCredentialsModel } from '@rocket.chat/model-typings';
import type { Db, DeleteResult, Filter, IndexDescription, UpdateResult } from 'mongodb';

import { BaseRaw } from '../../../../server/models/raw/BaseRaw';

export class WorkspaceCredentialsRaw extends BaseRaw<IWorkspaceCredentials> implements IWorkspaceCredentialsModel {
constructor(db: Db) {
super(db, 'workspace_credentials');
}

protected modelIndexes(): IndexDescription[] {
return [{ key: { scopes: 1, expirationDate: 1, accessToken: 1 }, unique: true }];
}

getCredentialByScope(scope = ''): Promise<IWorkspaceCredentials | null> {
const query: Filter<IWorkspaceCredentials> = {
scopes: {
$all: [scope],
$size: 1,
},
};

return this.findOne(query);
}

unsetCredentialByScope(scope = ''): Promise<DeleteResult> {
const query: Filter<IWorkspaceCredentials> = {
scopes: {
$all: [scope],
$size: 1,
},
};

return this.deleteOne(query);
}

updateCredentialByScope({
scope,
accessToken,
expirationDate,
}: {
scope: string;
accessToken: string;
expirationDate: Date;
}): Promise<UpdateResult> {
const record = {
$set: {
scopes: [scope],
accessToken,
expirationDate,
},
};

const query: Filter<IWorkspaceCredentials> = {
scopes: {
$all: [scope],
$size: 1,
},
};

return this.updateOne(query, record, { upsert: true });
}

removeAllCredentials(): Promise<DeleteResult> {
return this.col.deleteMany({});
}
}
1 change: 1 addition & 0 deletions apps/meteor/ee/server/models/startup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import('./LivechatPriority');
import('./OmnichannelServiceLevelAgreements');
import('./AuditLog');
import('./ReadReceipts');
import('./WorkspaceCredentials');

void License.onLicense('livechat-enterprise', () => {
import('./CannedResponse');
Expand Down
22 changes: 0 additions & 22 deletions apps/meteor/server/settings/setup-wizard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1322,28 +1322,6 @@ export const createSetupWSettings = () =>
secret: true,
});

await this.add('Cloud_Workspace_Access_Token', '', {
type: 'string',
hidden: true,
readonly: true,
enableQuery: {
_id: 'Register_Server',
value: true,
},
secret: true,
});

await this.add('Cloud_Workspace_Access_Token_Expires_At', new Date(0), {
type: 'date',
hidden: true,
readonly: true,
enableQuery: {
_id: 'Register_Server',
value: true,
},
secret: true,
});

await this.add('Cloud_Workspace_Registration_State', '', {
type: 'string',
hidden: true,
Expand Down
1 change: 1 addition & 0 deletions apps/meteor/server/startup/migrations/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,5 +48,6 @@ import './v312';
import './v313';
import './v314';
import './v315';
import './v316';

export * from './xrun';
31 changes: 31 additions & 0 deletions apps/meteor/server/startup/migrations/v316.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { Settings, WorkspaceCredentials } from '@rocket.chat/models';

import { addMigration } from '../../lib/migrations';

addMigration({
version: 316,
name: 'Remove Cloud_Workspace_Access_Token and Cloud_Workspace_Access_Token_Expires_At from the settings collection and add to the WorkspaceCredentials collection',
async up() {
const workspaceCredentials = await WorkspaceCredentials.getCredentialByScope();
if (workspaceCredentials) {
return;
}

const accessToken = ((await Settings.getValueById('Cloud_Workspace_Access_Token')) as string) || '';
const expirationDate = ((await Settings.getValueById('Cloud_Workspace_Access_Token_Expires_At')) as Date) || new Date(0);

if (accessToken) {
await Settings.removeById('Cloud_Workspace_Access_Token');
}

if (expirationDate) {
await Settings.removeById('Cloud_Workspace_Access_Token_Expires_At');
}

await WorkspaceCredentials.updateCredentialByScope({
scope: '',
accessToken,
expirationDate,
});
},
});
8 changes: 8 additions & 0 deletions packages/core-typings/src/ee/IWorkspaceCredentials.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import type { IRocketChatRecord } from '../IRocketChatRecord';

export interface IWorkspaceCredentials extends IRocketChatRecord {
_id: string;
scopes: string[];
expirationDate: Date;
accessToken: string;
}
1 change: 1 addition & 0 deletions packages/core-typings/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export * from './IUserStatus';
export * from './IUser';

export * from './ee/IAuditLog';
export * from './ee/IWorkspaceCredentials';

export * from './import';
export * from './IIncomingMessage';
Expand Down
1 change: 1 addition & 0 deletions packages/model-typings/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,3 +81,4 @@ export * from './models/ICronHistoryModel';
export * from './models/IMigrationsModel';
export * from './models/IModerationReportsModel';
export * from './updater';
export * from './models/IWorkspaceCredentialsModel';
11 changes: 11 additions & 0 deletions packages/model-typings/src/models/IWorkspaceCredentialsModel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import type { IWorkspaceCredentials } from '@rocket.chat/core-typings';
import type { DeleteResult, UpdateResult } from 'mongodb';

import type { IBaseModel } from './IBaseModel';

export interface IWorkspaceCredentialsModel extends IBaseModel<IWorkspaceCredentials> {
getCredentialByScope(scope?: string): Promise<IWorkspaceCredentials | null>;
unsetCredentialByScope(scope?: string): Promise<DeleteResult>;
updateCredentialByScope(credentials: { scope: string; accessToken: string; expirationDate: Date }): Promise<UpdateResult>;
removeAllCredentials(): Promise<DeleteResult>;
}
Loading

0 comments on commit b338807

Please sign in to comment.