Skip to content
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
32 changes: 8 additions & 24 deletions apps/meteor/ee/server/lib/license/startup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,24 +12,6 @@ import { callbacks } from '../../../../server/lib/callbacks';
import { syncWorkspace } from '../../../../server/lib/cloud/syncWorkspace';
import { SystemLogger } from '../../../../server/lib/logger/system';

const logOfflineLicense = (() => {
let logged = false;
return () => {
if (!License.hasOfflineLicense()) {
logged = false;
return;
}

if (!logged) {
// startup level so it is visible at the default Log_Level, like 'License installed'
SystemLogger.startup(
'Offline license detected: outbound connections to Rocket.Chat Cloud services and the Rocket.Chat Push Gateway are disabled',
);
logged = true;
}
};
})();

export const startLicense = async () => {
settings.watch<string>('Site_Url', (value) => {
if (value) {
Expand Down Expand Up @@ -144,12 +126,14 @@ export const startLicense = async () => {
}
}

logOfflineLicense();
License.onValidateLicense(logOfflineLicense);
// Also run on invalidate/remove so the once-per-activation flag resets and a
// subsequently re-applied offline license is logged again.
License.onInvalidateLicense(logOfflineLicense);
License.onRemoveLicense(logOfflineLicense);
License.onInstall(() => {
if (License.hasOfflineLicense()) {
// startup level so it is visible at the default Log_Level, like 'License installed'
SystemLogger.startup(
'Offline license detected: outbound connections to Rocket.Chat Cloud services and the Rocket.Chat Push Gateway are disabled',
);
}
});

// After the current license is already loaded, watch the setting value to react to new licenses being applied.
settings.change<string>('Enterprise_License', (license) => applyLicenseOrRemove(license, true));
Expand Down
4 changes: 2 additions & 2 deletions apps/meteor/server/lib/cloud/getWorkspaceAccessToken.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import type { IWorkspaceCredentials } from '@rocket.chat/core-typings';
import { License } from '@rocket.chat/license';
import { WorkspaceCredentials } from '@rocket.chat/models';

import { getWorkspaceAccessTokenWithScope } from './getWorkspaceAccessTokenWithScope';
import { workspaceScopes } from './oauthScopes';
import { hasOfflineLicense } from './offlineLicense';
import { retrieveRegistrationStatus } from './retrieveRegistrationStatus';
import { SystemLogger } from '../logger/system';

Expand All @@ -27,7 +27,7 @@ export async function getWorkspaceAccessToken(forceNew = false, scope = '', save
return '';
}

if (hasOfflineLicense()) {
if (License.hasOfflineLicense()) {
return '';
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { License } from '@rocket.chat/license';
import { serverFetch as fetch } from '@rocket.chat/server-fetch';

import { getRedirectUri } from './getRedirectUri';
import { CloudWorkspaceAccessTokenError } from './getWorkspaceAccessToken';
import { workspaceScopes } from './oauthScopes';
import { hasOfflineLicense } from './offlineLicense';
import { removeWorkspaceRegistrationInfo } from './removeWorkspaceRegistrationInfo';
import { retrieveRegistrationStatus } from './retrieveRegistrationStatus';
import { settings } from '../../../app/settings/server';
Expand Down Expand Up @@ -32,7 +32,7 @@ export async function getWorkspaceAccessTokenWithScope({
return tokenResponse;
}

if (hasOfflineLicense()) {
if (License.hasOfflineLicense()) {
return tokenResponse;
}

Expand Down
16 changes: 2 additions & 14 deletions apps/meteor/server/lib/cloud/offlineLicense.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,12 @@ import { License } from '@rocket.chat/license';

import { CloudOfflineLicenseError } from '../../../lib/errors/CloudOfflineLicenseError';

/**
* Whether the applied license was issued for offline (air-gapped) workspaces.
*
* When this returns true the workspace must never initiate outbound connections
* to Rocket.Chat Cloud services (registration, sync, marketplace, telemetry) or
* to the Rocket.Chat Push Gateway. Calls must be suppressed at the source — not
* by relying on the requests failing.
*/
export function hasOfflineLicense(): boolean {
return License.hasOfflineLicense();
}

/**
* Guard for interactive cloud flows (registration, OAuth, billing). Background
* jobs should instead skip silently via {@link hasOfflineLicense}.
* jobs should instead skip silently by checking {@link License.hasOfflineLicense}.
*/
export function assertNotOfflineLicense(): void {
if (hasOfflineLicense()) {
if (License.hasOfflineLicense()) {
throw new CloudOfflineLicenseError('Cloud connectivity is disabled by the offline license applied to this workspace');
}
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import type { IUser } from '@rocket.chat/core-typings';
import { License } from '@rocket.chat/license';
import { Users } from '@rocket.chat/models';
import { serverFetch as fetch } from '@rocket.chat/server-fetch';

import { buildWorkspaceRegistrationData } from './buildRegistrationData';
import { hasOfflineLicense } from './offlineLicense';
import { settings } from '../../../app/settings/server';
import { SystemLogger } from '../logger/system';

export async function registerPreIntentWorkspaceWizard(): Promise<boolean> {
if (hasOfflineLicense()) {
if (License.hasOfflineLicense()) {
return false;
}

Expand All @@ -23,7 +23,7 @@ export async function registerPreIntentWorkspaceWizard(): Promise<boolean> {

// Re-validated at dispatch time: an offline license applied while the
// registration data was being built must still suppress the request.
if (hasOfflineLicense()) {
if (License.hasOfflineLicense()) {
return false;
}

Expand Down
5 changes: 3 additions & 2 deletions apps/meteor/server/lib/cloud/syncWorkspace/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { License } from '@rocket.chat/license';

import { CloudWorkspaceRegistrationError } from '../../../../lib/errors/CloudWorkspaceRegistrationError';
import { SystemLogger } from '../../logger/system';
import { CloudWorkspaceAccessTokenEmptyError, CloudWorkspaceAccessTokenError, isAbortError } from '../getWorkspaceAccessToken';
import { hasOfflineLicense } from '../offlineLicense';
import { announcementSync } from './announcementSync';
import { legacySyncWorkspace } from './legacySyncWorkspace';
import { syncCloudData } from './syncCloudData';
Expand All @@ -13,7 +14,7 @@ import { getCachedSupportedVersionsToken } from '../supportedVersionsToken/suppo
* @throws {Error} - If there is an unexpected error during sync like a network error
*/
export async function syncWorkspace() {
if (hasOfflineLicense()) {
if (License.hasOfflineLicense()) {
SystemLogger.debug({ msg: 'Skipping cloud sync: workspace has an offline license', function: 'syncWorkspace' });
await getCachedSupportedVersionsToken.reset();
return;
Expand Down
4 changes: 2 additions & 2 deletions apps/meteor/server/lib/cloud/userLogout.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { License } from '@rocket.chat/license';
import { Users } from '@rocket.chat/models';
import { serverFetch as fetch } from '@rocket.chat/server-fetch';

import { hasOfflineLicense } from './offlineLicense';
import { retrieveRegistrationStatus } from './retrieveRegistrationStatus';
import { userLoggedOut } from './userLoggedOut';
import { settings } from '../../../app/settings/server';
Expand All @@ -20,7 +20,7 @@ export async function userLogout(userId: string): Promise<string | boolean> {

// Offline (air-gapped) licenses forbid the outbound token-revocation call, but
// the local cloud credentials must still be destroyed on logout.
if (hasOfflineLicense()) {
if (License.hasOfflineLicense()) {
return userLoggedOut(userId);
}

Expand Down
4 changes: 2 additions & 2 deletions apps/meteor/server/lib/users/getAvatarSuggestionForUser.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import type { IUser } from '@rocket.chat/core-typings';
import { License } from '@rocket.chat/license';
import { serverFetch as fetch } from '@rocket.chat/server-fetch';
import Gravatar from 'gravatar';
import { check } from 'meteor/check';
import { ServiceConfiguration } from 'meteor/service-configuration';

import { settings } from '../../../app/settings/server';
import { hasOfflineLicense } from '../cloud/offlineLicense';

const avatarProviders = {
facebook(user: IUser) {
Expand Down Expand Up @@ -107,7 +107,7 @@ const avatarProviders = {
// Offline (air-gapped) licenses suppress Gravatar lookups: every suggested
// URL is fetched server-side below, and gravatar.com is not admin-configured
// infrastructure (unlike OAuth provider avatars, which keep working).
if (hasOfflineLicense()) {
if (License.hasOfflineLicense()) {
return avatars;
}

Expand Down
4 changes: 2 additions & 2 deletions apps/meteor/server/lib/users/saveUser/saveNewUser.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import type { IUser } from '@rocket.chat/core-typings';
import { License } from '@rocket.chat/license';
import { Users } from '@rocket.chat/models';
import Gravatar from 'gravatar';
import { Accounts } from 'meteor/accounts-base';

import { notifyOnUserChangeById } from '../../../../app/lib/server/lib/notifyListener';
import { validateEmailDomain } from '../../../../app/lib/server/lib/validateEmailDomain';
import { hasOfflineLicense } from '../../cloud/offlineLicense';
import { setUserAvatar } from '../setUserAvatar';
import { handleBio } from './handleBio';
import { handleNickname } from './handleNickname';
Expand Down Expand Up @@ -73,7 +73,7 @@ export const saveNewUser = async function (userData: SaveUserData, sendPassword:

// Offline (air-gapped) licenses suppress the default Gravatar fetch — a
// default-on outbound call the workspace must never initiate on its own.
if (settings.get('Accounts_SetDefaultAvatar') === true && userData.email && !hasOfflineLicense()) {
if (settings.get('Accounts_SetDefaultAvatar') === true && userData.email && !License.hasOfflineLicense()) {
const gravatarUrl = Gravatar.url(userData.email, {
default: '404',
size: '200',
Expand Down
3 changes: 0 additions & 3 deletions apps/meteor/server/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,6 @@ await Promise.all([configureServer(settings), registerServices(), startup()]);

await startRocketChat();

// Cron jobs start only after startRocketChat() has applied the license, so jobs
// that contact Rocket.Chat Cloud on boot (e.g. the usage report) respect the
// offline license flag from their very first run.
setImmediate(() => {
startCronJobs().catch((err) => {
SystemLogger.error({ msg: 'Failed to start cron jobs', err });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@ const buildRegistrationDataStub = sinon.stub();
const { registerPreIntentWorkspaceWizard } = proxyquire
.noCallThru()
.load('../../../../../server/lib/cloud/registerPreIntentWorkspaceWizard.ts', {
'@rocket.chat/license': { License: { hasOfflineLicense: hasOfflineLicenseStub } },
'@rocket.chat/models': { Users: { getOldest: getOldestStub } },
'@rocket.chat/server-fetch': { serverFetch: fetchStub },
'./buildRegistrationData': { buildWorkspaceRegistrationData: buildRegistrationDataStub },
'./offlineLicense': { hasOfflineLicense: hasOfflineLicenseStub },
'../../../app/settings/server': { settings: { get: sinon.stub().returns('https://cloud.rocket.chat') } },
'../logger/system': { SystemLogger: { error: sinon.stub() } },
});
Expand Down
2 changes: 1 addition & 1 deletion apps/meteor/tests/unit/server/lib/cloud/userLogout.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ const findOneByIdStub = sinon.stub();
const settingsGetStub = sinon.stub();

const { userLogout } = proxyquire.noCallThru().load('../../../../../server/lib/cloud/userLogout.ts', {
'@rocket.chat/license': { License: { hasOfflineLicense: hasOfflineLicenseStub } },
'@rocket.chat/models': { Users: { findOneById: findOneByIdStub } },
'@rocket.chat/server-fetch': { serverFetch: fetchStub },
'./offlineLicense': { hasOfflineLicense: hasOfflineLicenseStub },
'./retrieveRegistrationStatus': { retrieveRegistrationStatus: retrieveRegistrationStatusStub },
'./userLoggedOut': { userLoggedOut: userLoggedOutStub },
'../../../app/settings/server': { settings: { get: settingsGetStub } },
Expand Down
Loading