Skip to content

Commit

Permalink
feat: License trigger cloud sync after reach limits (#30603)
Browse files Browse the repository at this point in the history
Co-authored-by: Guilherme Gazzo <[email protected]>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
  • Loading branch information
3 people authored and debdutdeb committed Oct 26, 2023
1 parent 5744c8f commit d02da09
Show file tree
Hide file tree
Showing 3 changed files with 108 additions and 54 deletions.
49 changes: 7 additions & 42 deletions apps/meteor/ee/app/license/server/settings.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import { License } from '@rocket.chat/license';
import { Settings } from '@rocket.chat/models';
import { Meteor } from 'meteor/meteor';

import { settings, settingsRegistry } from '../../../../app/settings/server';
import { settingsRegistry } from '../../../../app/settings/server';

Meteor.startup(async () => {
await settingsRegistry.addGroup('Enterprise', async function () {
Expand All @@ -11,6 +9,12 @@ Meteor.startup(async () => {
type: 'string',
i18nLabel: 'Enterprise_License',
});
await this.add('Enterprise_License_Data', '', {
type: 'string',
hidden: true,
blocked: true,
public: false,
});
await this.add('Enterprise_License_Status', '', {
readonly: true,
type: 'string',
Expand All @@ -19,42 +23,3 @@ Meteor.startup(async () => {
});
});
});

settings.watch<string>('Enterprise_License', async (license) => {
if (!license || String(license).trim() === '') {
return;
}

if (license === process.env.ROCKETCHAT_LICENSE) {
return;
}

try {
if (!(await License.setLicense(license))) {
await Settings.updateValueById('Enterprise_License_Status', 'Invalid');
return;
}
} catch (_error) {
// do nothing
}

await Settings.updateValueById('Enterprise_License_Status', 'Valid');
});

if (process.env.ROCKETCHAT_LICENSE) {
try {
await License.setLicense(process.env.ROCKETCHAT_LICENSE);
} catch (_error) {
// do nothing
}

Meteor.startup(async () => {
if (settings.get('Enterprise_License')) {
console.warn(
'Rocket.Chat Enterprise: The license from your environment variable was ignored, please use only the admin setting from now on.',
);
return;
}
await Settings.updateValueById('Enterprise_License', process.env.ROCKETCHAT_LICENSE);
});
}
87 changes: 82 additions & 5 deletions apps/meteor/ee/app/license/server/startup.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { License } from '@rocket.chat/license';
import { Subscriptions, Users } from '@rocket.chat/models';
import { Subscriptions, Users, Settings } from '@rocket.chat/models';
import { wrapExceptions } from '@rocket.chat/tools';

import { syncWorkspace } from '../../../../app/cloud/server/functions/syncWorkspace';
import { settings } from '../../../../app/settings/server';
import { callbacks } from '../../../../lib/callbacks';
import { getAppCount } from './lib/getAppCount';
Expand All @@ -11,12 +13,87 @@ settings.watch<string>('Site_Url', (value) => {
}
});

callbacks.add('workspaceLicenseChanged', async (updatedLicense) => {
License.onValidateLicense(async () => {
await Settings.updateValueById('Enterprise_License', License.encryptedLicense);
await Settings.updateValueById('Enterprise_License_Status', 'Valid');
});

License.onInvalidateLicense(async () => {
await Settings.updateValueById('Enterprise_License_Status', 'Invalid');
});

const applyLicense = async (license: string, isNewLicense: boolean): Promise<boolean> => {
const enterpriseLicense = (license ?? '').trim();
if (!enterpriseLicense) {
return false;
}

if (enterpriseLicense === License.encryptedLicense) {
return false;
}

try {
await License.setLicense(updatedLicense);
} catch (_error) {
// Ignore
return License.setLicense(enterpriseLicense, isNewLicense);
} catch {
return false;
}
};

const syncByTrigger = async (context: string) => {
if (!License.encryptedLicense) {
return;
}

const existingData = wrapExceptions(() => JSON.parse(settings.get<string>('Enterprise_License_Data'))).catch(() => ({})) ?? {};

const date = new Date();

const day = date.getDate();
const month = date.getMonth() + 1;
const year = date.getFullYear();

const period = `${year}-${month}-${day}`;

const [, , signed] = License.encryptedLicense.split('.');

// Check if this sync has already been done. Based on License, behavior.
if (existingData.signed === signed && existingData[context] === period) {
return;
}

await Settings.updateValueById(
'Enterprise_License_Data',
JSON.stringify({
...(existingData.signed === signed && existingData),
...existingData,
[context]: period,
signed,
}),
);

await syncWorkspace();
};

// When settings are loaded, apply the current license if there is one.
settings.onReady(async () => {
if (!(await applyLicense(settings.get<string>('Enterprise_License') ?? '', false))) {
// License from the envvar is always treated as new, because it would have been saved on the setting if it was already in use.
if (process.env.ROCKETCHAT_LICENSE && !License.hasValidLicense()) {
await applyLicense(process.env.ROCKETCHAT_LICENSE, true);
}
}

// After the current license is already loaded, watch the setting value to react to new licenses being applied.
settings.watch<string>('Enterprise_License', async (license) => applyLicense(license, true));

callbacks.add('workspaceLicenseChanged', async (updatedLicense) => applyLicense(updatedLicense, true));

License.onBehaviorTriggered('prevent_action', (context) => syncByTrigger(`prevent_action_${context.limit}`));

License.onBehaviorTriggered('start_fair_policy', async (context) => syncByTrigger(`start_fair_policy_${context.limit}`));

License.onBehaviorTriggered('disable_modules', async (context) => syncByTrigger(`disable_modules_${context.limit}`));

});

License.setLicenseLimitCounter('activeUsers', () => Users.getActiveLocalUserCount());
Expand Down
26 changes: 19 additions & 7 deletions ee/packages/license/src/license.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,14 @@ export class LicenseManager extends Emitter<LicenseEvents> {
return this._valid;
}

public get encryptedLicense(): string | undefined {
if (!this.hasValidLicense()) {
return undefined;
}

return this._lockedLicense;
}

public async setWorkspaceUrl(url: string) {
this.workspaceUrl = url.replace(/\/$/, '').replace(/^https?:\/\/(.*)$/, '$1');

Expand Down Expand Up @@ -106,15 +114,19 @@ export class LicenseManager extends Emitter<LicenseEvents> {
invalidateAll.call(this);
}

private async setLicenseV3(newLicense: ILicenseV3, encryptedLicense: string, originalLicense?: ILicenseV2 | ILicenseV3): Promise<void> {
private async setLicenseV3(
newLicense: ILicenseV3,
encryptedLicense: string,
originalLicense?: ILicenseV2 | ILicenseV3,
isNewLicense?: boolean,
): Promise<void> {
const hadValidLicense = this.hasValidLicense();
this.clearLicenseData();

try {
this._unmodifiedLicense = originalLicense || newLicense;
this._license = newLicense;

const isNewLicense = encryptedLicense !== this._lockedLicense;
this._lockedLicense = encryptedLicense;

await this.validateLicense({ isNewLicense });
Expand All @@ -127,8 +139,8 @@ export class LicenseManager extends Emitter<LicenseEvents> {
}
}

private async setLicenseV2(newLicense: ILicenseV2, encryptedLicense: string): Promise<void> {
return this.setLicenseV3(convertToV3(newLicense), encryptedLicense, newLicense);
private async setLicenseV2(newLicense: ILicenseV2, encryptedLicense: string, isNewLicense?: boolean): Promise<void> {
return this.setLicenseV3(convertToV3(newLicense), encryptedLicense, newLicense, isNewLicense);
}

private isLicenseDuplicated(encryptedLicense: string): boolean {
Expand Down Expand Up @@ -180,7 +192,7 @@ export class LicenseManager extends Emitter<LicenseEvents> {
licenseValidated.call(this);
}

public async setLicense(encryptedLicense: string): Promise<boolean> {
public async setLicense(encryptedLicense: string, isNewLicense = true): Promise<boolean> {
if (!(await validateFormat(encryptedLicense))) {
throw new InvalidLicenseError();
}
Expand Down Expand Up @@ -209,10 +221,10 @@ export class LicenseManager extends Emitter<LicenseEvents> {
logger.debug({ msg: 'license', decrypted });

if (!encryptedLicense.startsWith('RCV3_')) {
await this.setLicenseV2(decrypted, encryptedLicense);
await this.setLicenseV2(decrypted, encryptedLicense, isNewLicense);
return true;
}
await this.setLicenseV3(decrypted, encryptedLicense);
await this.setLicenseV3(decrypted, encryptedLicense, decrypted, isNewLicense);

return true;
} catch (e) {
Expand Down

0 comments on commit d02da09

Please sign in to comment.