Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add supported versions + minimum clients versions to the info endpoint #30178

Merged
merged 20 commits into from
Sep 29, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion apps/meteor/app/api/server/default/info.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ API.default.addRoute(
{
async get() {
const user = await getLoggedInUser(this.request);

return API.v1.success(await getServerInfo(user?._id));
},
},
Expand Down
52 changes: 52 additions & 0 deletions apps/meteor/app/api/server/lib/getServerInfo.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { expect } from 'chai';
import { describe, it } from 'mocha';
import proxyquire from 'proxyquire';
import sinon from 'sinon';

const hasAllPermissionAsyncMock = sinon.stub();
const getCachedSupportedVersionsTokenMock = sinon.stub();

const { getServerInfo } = proxyquire.noCallThru().load('./getServerInfo', {
'../../../utils/rocketchat.info': {
Info: {
version: '3.0.1',
},
},
'../../../authorization/server/functions/hasPermission': {
hasPermissionAsync: hasAllPermissionAsyncMock,
},
'../../../cloud/server/functions/supportedVersionsToken/supportedVersionsToken': {
getCachedSupportedVersionsToken: getCachedSupportedVersionsTokenMock,
},
'../../../settings/server': {
settings: new Map(),
},
});
describe('#getServerInfo()', () => {
beforeEach(() => {
hasAllPermissionAsyncMock.reset();
getCachedSupportedVersionsTokenMock.reset();
});

it('should return only the version (without the patch info) when the user is not present', async () => {
expect(await getServerInfo(undefined)).to.be.eql({ version: '3.0' });
});

it('should return only the version (without the patch info) when the user present but they dont have permission', async () => {
hasAllPermissionAsyncMock.resolves(false);
expect(await getServerInfo('userId')).to.be.eql({ version: '3.0' });
});

it('should return the info object + the supportedVersions from the cloud when the request to the cloud was a success', async () => {
const signedJwt = 'signedJwt';
hasAllPermissionAsyncMock.resolves(true);
getCachedSupportedVersionsTokenMock.resolves(signedJwt);
expect(await getServerInfo('userId')).to.be.eql({ info: { version: '3.0.1', supportedVersions: signedJwt } });
});

it('should return the info object ONLY from the cloud when the request to the cloud was NOT a success', async () => {
hasAllPermissionAsyncMock.resolves(true);
getCachedSupportedVersionsTokenMock.rejects();
expect(await getServerInfo('userId')).to.be.eql({ info: { version: '3.0.1' } });
});
});
40 changes: 27 additions & 13 deletions apps/meteor/app/api/server/lib/getServerInfo.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,37 @@
import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission';
import { Info } from '../../../utils/rocketchat.info';
import {
getCachedSupportedVersionsToken,
wrapPromise,
} from '../../../cloud/server/functions/supportedVersionsToken/supportedVersionsToken';
import { Info, minimumClientVersions } from '../../../utils/rocketchat.info';

type ServerInfo =
| {
info: typeof Info;
}
| {
version: string | undefined;
};
type ServerInfo = {
info?: typeof Info;
supportedVersions?: { signed: string };
minimumClientVersions: typeof minimumClientVersions;
version: string;
};

const removePatchInfo = (version: string): string => version.replace(/(\d+\.\d+).*/, '$1');

export async function getServerInfo(userId?: string): Promise<ServerInfo> {
if (userId && (await hasPermissionAsync(userId, 'get-server-info'))) {
return {
info: Info,
};
}
const hasPermissionToViewStatistics = userId && (await hasPermissionAsync(userId, 'view-statistics'));
const supportedVersionsToken = await wrapPromise(getCachedSupportedVersionsToken());

return {
version: removePatchInfo(Info.version),

...(hasPermissionToViewStatistics && {
info: {
...Info,
},
version: Info.version,
}),

minimumClientVersions,
...(supportedVersionsToken.success &&
supportedVersionsToken.result && {
supportedVersions: { signed: supportedVersionsToken.result },
}),
};
}
113 changes: 0 additions & 113 deletions apps/meteor/app/cloud/server/functions/getUserCloudAccessToken.ts

This file was deleted.

48 changes: 46 additions & 2 deletions apps/meteor/app/cloud/server/functions/getWorkspaceAccessToken.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { retrieveRegistrationStatus } from './retrieveRegistrationStatus';
* @param {boolean} save
* @returns string
*/
export async function getWorkspaceAccessToken(forceNew = false, scope = '', save = true) {
export async function getWorkspaceAccessToken(forceNew = false, scope = '', save = true): Promise<string> {
const { workspaceRegistered } = await retrieveRegistrationStatus();

if (!workspaceRegistered) {
Expand All @@ -22,10 +22,11 @@ export async function getWorkspaceAccessToken(forceNew = false, scope = '', save
if (expires === null) {
throw new Error('Cloud_Workspace_Access_Token_Expires_At is not set');
}

const now = new Date();

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

const accessToken = await getWorkspaceAccessTokenWithScope(scope);
Expand All @@ -39,3 +40,46 @@ export async function getWorkspaceAccessToken(forceNew = false, scope = '', save

return accessToken.token;
}

export class CloudWorkspaceAccessTokenError extends Error {
constructor() {
super('Could not get workspace access token');
}
}

export async function getWorkspaceAccessTokenOrThrow(forceNew = false, scope = '', save = true): Promise<string> {
const token = await getWorkspaceAccessToken(forceNew, scope, save);

if (!token) {
throw new CloudWorkspaceAccessTokenError();
}

return token;
}

export const generateWorkspaceBearerHttpHeaderOrThrow = async (
forceNew = false,
scope = '',
save = true,
): Promise<{ Authorization: string }> => {
const token = await getWorkspaceAccessTokenOrThrow(forceNew, scope, save);
return {
Authorization: `Bearer ${token}`,
};
};

export const generateWorkspaceBearerHttpHeader = async (
forceNew = false,
scope = '',
save = true,
): Promise<{ Authorization: string } | undefined> => {
const token = await getWorkspaceAccessToken(forceNew, scope, save);

if (!token) {
return undefined;
}

return {
Authorization: `Bearer ${token}`,
};
};
60 changes: 24 additions & 36 deletions apps/meteor/app/cloud/server/functions/getWorkspaceLicense.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,64 +5,52 @@ import { callbacks } from '../../../../lib/callbacks';
import { SystemLogger } from '../../../../server/lib/logger/system';
import { settings } from '../../../settings/server';
import { LICENSE_VERSION } from '../license';
import { getWorkspaceAccessToken } from './getWorkspaceAccessToken';
import { generateWorkspaceBearerHttpHeaderOrThrow } from './getWorkspaceAccessToken';
import { handleResponse } from './supportedVersionsToken/supportedVersionsToken';

export async function getWorkspaceLicense(): Promise<{ updated: boolean; license: string }> {
const currentLicense = await Settings.findOne('Cloud_Workspace_License');

const cachedLicenseReturn = async () => {
const license = currentLicense?.value as string;
if (license) {
await callbacks.run('workspaceLicenseChanged', license);
}
export async function getWorkspaceLicense() {
const token = await generateWorkspaceBearerHttpHeaderOrThrow();

return { updated: false, license };
};
const currentLicense = await Settings.findOne('Cloud_Workspace_License');

const token = await getWorkspaceAccessToken();
if (!token) {
return cachedLicenseReturn();
// TODO: check if this is the correct way to handle this
// If there is no license, in theory, it should be a new workspace non registered
// in this case the `generateWorkspaceBearerHttpHeaderOrThrow` show throw an error before
// so in theory, this should never happen
if (!currentLicense?._updatedAt) {
throw new Error('Failed to retrieve current license');
}

let licenseResult;
try {
const request = await fetch(`${settings.get('Cloud_Workspace_Registration_Client_Uri')}/license`, {
const request = await handleResponse(
fetch(`${settings.get('Cloud_Workspace_Registration_Client_Uri')}/license`, {
headers: {
Authorization: `Bearer ${token}`,
...token,
},
params: {
version: LICENSE_VERSION,
},
});

if (!request.ok) {
throw new Error((await request.json()).error);
}
}),
);

licenseResult = await request.json();
} catch (err: any) {
if (!request.success) {
SystemLogger.error({
msg: 'Failed to update license from Rocket.Chat Cloud',
url: '/license',
err,
err: request.error,
});

return cachedLicenseReturn();
if (currentLicense.value) {
return callbacks.run('workspaceLicenseChanged', currentLicense.value);
}
return;
}

const remoteLicense = licenseResult;

if (!currentLicense || !currentLicense._updatedAt) {
throw new Error('Failed to retrieve current license');
}
const remoteLicense = request.result as any;

if (remoteLicense.updatedAt <= currentLicense._updatedAt) {
return cachedLicenseReturn();
return callbacks.run('workspaceLicenseChanged', currentLicense.value);
}

await Settings.updateValueById('Cloud_Workspace_License', remoteLicense.license);

await callbacks.run('workspaceLicenseChanged', remoteLicense.license);

return { updated: true, license: remoteLicense.license };
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export async function reconnectWorkspace() {

await Settings.updateValueById('Register_Server', true);

await syncWorkspace(true);
await syncWorkspace();

return true;
}
Loading
Loading