From b5e1a434de623d613154b8a8d4717761fb4b8445 Mon Sep 17 00:00:00 2001 From: Pierre Lehnen Date: Tue, 19 Sep 2023 16:32:00 -0300 Subject: [PATCH] avoid triggering "invalidate" events when an active license is replaced with another valid license. --- ee/packages/license/src/actionBlockers.ts | 8 ++ ee/packages/license/src/events/emitter.ts | 33 ++++++-- ee/packages/license/src/license.ts | 81 ++++++++++++------- ee/packages/license/src/modules.ts | 20 +++++ ee/packages/license/src/pendingLicense.ts | 30 +++++++ ee/packages/license/src/tags.ts | 5 +- .../getCurrentValueForLicenseLimit.ts | 3 + .../src/validation/getModulesToDisable.ts | 2 +- .../src/validation/validateLicenseLimits.ts | 2 +- ee/packages/license/src/workspaceUrl.ts | 6 +- 10 files changed, 151 insertions(+), 39 deletions(-) create mode 100644 ee/packages/license/src/pendingLicense.ts diff --git a/ee/packages/license/src/actionBlockers.ts b/ee/packages/license/src/actionBlockers.ts index 6876512a147e..ad170ad29250 100644 --- a/ee/packages/license/src/actionBlockers.ts +++ b/ee/packages/license/src/actionBlockers.ts @@ -8,3 +8,11 @@ export const preventNewPrivateApps = async (appCount = 1) => shouldPreventAction export const preventNewMarketplaceApps = async (appCount = 1) => shouldPreventAction('marketplaceApps', {}, appCount); export const preventNewGuestSubscriptions = async (guest: IUser['_id'], roomCount = 1) => shouldPreventAction('roomsPerGuest', { userId: guest }, roomCount); +export const preventNewActiveContacts = async (contactCount = 1) => shouldPreventAction('monthlyActiveContacts', {}, contactCount); + +export const userLimitReached = async () => preventNewUsers(0); +export const guestLimitReached = async () => preventNewGuests(0); +export const privateAppLimitReached = async () => preventNewPrivateApps(0); +export const marketplaceAppLimitReached = async () => preventNewMarketplaceApps(0); +export const guestSubscriptionLimitReached = async (guest: IUser['_id']) => preventNewGuestSubscriptions(guest, 0); +export const macLimitReached = async () => preventNewActiveContacts(0); diff --git a/ee/packages/license/src/events/emitter.ts b/ee/packages/license/src/events/emitter.ts index d9ce189df008..8b16e5527121 100644 --- a/ee/packages/license/src/events/emitter.ts +++ b/ee/packages/license/src/events/emitter.ts @@ -1,19 +1,40 @@ import { EventEmitter } from 'events'; import type { LicenseModule } from '../definition/LicenseModule'; +import { logger } from '../logger'; export const EnterpriseLicenses = new EventEmitter(); -export const licenseValidated = () => EnterpriseLicenses.emit('validate'); +export const licenseValidated = () => { + try { + EnterpriseLicenses.emit('validate'); + } catch (error) { + logger.error({ msg: 'Error running license validated event', error }); + } +}; -export const licenseRemoved = () => EnterpriseLicenses.emit('invalidate'); +export const licenseRemoved = () => { + try { + EnterpriseLicenses.emit('invalidate'); + } catch (error) { + logger.error({ msg: 'Error running license invalidated event', error }); + } +}; export const moduleValidated = (module: LicenseModule) => { - EnterpriseLicenses.emit('module', { module, valid: true }); - EnterpriseLicenses.emit(`valid:${module}`); + try { + EnterpriseLicenses.emit('module', { module, valid: true }); + EnterpriseLicenses.emit(`valid:${module}`); + } catch (error) { + logger.error({ msg: 'Error running module added event', error }); + } }; export const moduleRemoved = (module: LicenseModule) => { - EnterpriseLicenses.emit('module', { module, valid: false }); - EnterpriseLicenses.emit(`invalid:${module}`); + try { + EnterpriseLicenses.emit('module', { module, valid: false }); + EnterpriseLicenses.emit(`invalid:${module}`); + } catch (error) { + logger.error({ msg: 'Error running module removed event', error }); + } }; diff --git a/ee/packages/license/src/license.ts b/ee/packages/license/src/license.ts index 8141ec42f482..64f75eb501ee 100644 --- a/ee/packages/license/src/license.ts +++ b/ee/packages/license/src/license.ts @@ -5,36 +5,28 @@ import type { BehaviorWithContext } from './definition/LicenseBehavior'; import { isLicenseDuplicate, lockLicense } from './encryptedLicense'; import { licenseRemoved, licenseValidated } from './events/emitter'; import { logger } from './logger'; -import { getModules, invalidateAll, notifyValidatedModules } from './modules'; +import { getModules, invalidateAll, replaceModules } from './modules'; +import { clearPendingLicense, hasPendingLicense, isPendingLicense, setPendingLicense } from './pendingLicense'; import { showLicense } from './showLicense'; -import { addTags } from './tags'; +import { replaceTags } from './tags'; import { convertToV3 } from './v2/convertToV3'; import { getModulesToDisable } from './validation/getModulesToDisable'; import { isBehaviorsInResult } from './validation/isBehaviorsInResult'; import { runValidation } from './validation/runValidation'; +import { validateFormat } from './validation/validateFormat'; +import { getWorkspaceUrl } from './workspaceUrl'; let unmodifiedLicense: ILicenseV2 | ILicenseV3 | undefined; let license: ILicenseV3 | undefined; let valid: boolean | undefined; let inFairPolicy: boolean | undefined; -const removeCurrentLicense = () => { - const oldLicense = license; - const wasValid = valid; - +const clearLicenseData = () => { license = undefined; unmodifiedLicense = undefined; valid = undefined; inFairPolicy = undefined; - - if (!oldLicense || !wasValid) { - return; - } - valid = false; - - licenseRemoved(); - invalidateAll(); }; const processValidationResult = (result: BehaviorWithContext[]) => { @@ -46,13 +38,13 @@ const processValidationResult = (result: BehaviorWithContext[]) => { inFairPolicy = isBehaviorsInResult(result, ['start_fair_policy']); if (license.information.tags) { - addTags(license.information.tags); + replaceTags(license.information.tags); } const disabledModules = getModulesToDisable(result); const modulesToEnable = license.grantedModules.filter(({ module }) => !disabledModules.includes(module)); - notifyValidatedModules(modulesToEnable.map(({ module }) => module)); + replaceModules(modulesToEnable.map(({ module }) => module)); logger.log({ msg: 'License validated', modules: modulesToEnable }); licenseValidated(); @@ -60,7 +52,7 @@ const processValidationResult = (result: BehaviorWithContext[]) => { }; export const validateLicense = async () => { - if (!license) { + if (!license || !getWorkspaceUrl()) { return; } @@ -74,18 +66,54 @@ export const validateLicense = async () => { processValidationResult(validationResult); }; -const setLicenseV3 = async (newLicense: ILicenseV3, originalLicense?: ILicenseV2 | ILicenseV3) => { - removeCurrentLicense(); - unmodifiedLicense = originalLicense || newLicense; - license = newLicense; +const setLicenseV3 = async (newLicense: ILicenseV3, encryptedLicense: string, originalLicense?: ILicenseV2 | ILicenseV3) => { + const hadValidLicense = isEnterprise(); + clearLicenseData(); - await validateLicense(); + try { + unmodifiedLicense = originalLicense || newLicense; + license = newLicense; + clearPendingLicense(); + + await validateLicense(); + lockLicense(encryptedLicense); + } finally { + if (hadValidLicense && !isEnterprise()) { + licenseRemoved(); + invalidateAll(); + } + } }; -const setLicenseV2 = async (newLicense: ILicenseV2) => setLicenseV3(convertToV3(newLicense), newLicense); +const setLicenseV2 = async (newLicense: ILicenseV2, encryptedLicense: string) => + setLicenseV3(convertToV3(newLicense), encryptedLicense, newLicense); + +// Can only validate licenses once the workspace URL is set +export const isReadyForValidation = () => Boolean(getWorkspaceUrl()); + +export const setLicense = async (encryptedLicense: string, forceSet = false): Promise => { + if (!encryptedLicense || String(encryptedLicense).trim() === '') { + return false; + } + + if (isLicenseDuplicate(encryptedLicense)) { + // If there is a pending license but the user is trying to revert to the license that is currently active + if (hasPendingLicense() && !isPendingLicense(encryptedLicense)) { + // simply remove the pending license + clearPendingLicense(); + return true; + } + + return false; + } + + if (!isReadyForValidation() && !forceSet) { + // If we can't validate the license data yet, but is a valid license string, store it to validate when we can + if (validateFormat(encryptedLicense)) { + setPendingLicense(encryptedLicense); + return true; + } -export const setLicense = async (encryptedLicense: string): Promise => { - if (!encryptedLicense || String(encryptedLicense).trim() === '' || isLicenseDuplicate(encryptedLicense)) { return false; } @@ -101,8 +129,7 @@ export const setLicense = async (encryptedLicense: string): Promise => } // #TODO: Check license version and call setLicenseV2 or setLicenseV3 - await setLicenseV2(JSON.parse(decrypted)); - lockLicense(encryptedLicense); + await setLicenseV2(JSON.parse(decrypted), encryptedLicense); return true; } catch (e) { diff --git a/ee/packages/license/src/modules.ts b/ee/packages/license/src/modules.ts index d61552dbec4b..bfdb2bbc996b 100644 --- a/ee/packages/license/src/modules.ts +++ b/ee/packages/license/src/modules.ts @@ -25,3 +25,23 @@ export const invalidateAll = () => { export const getModules = () => [...modules]; export const hasModule = (module: LicenseModule) => modules.has(module); + +export const replaceModules = (newModules: LicenseModule[]) => { + for (const moduleName of newModules) { + if (modules.has(moduleName)) { + continue; + } + + modules.add(moduleName); + moduleValidated(moduleName); + } + + for (const moduleName of modules) { + if (newModules.includes(moduleName)) { + continue; + } + + moduleRemoved(moduleName); + modules.delete(moduleName); + } +}; diff --git a/ee/packages/license/src/pendingLicense.ts b/ee/packages/license/src/pendingLicense.ts new file mode 100644 index 000000000000..582a52f93736 --- /dev/null +++ b/ee/packages/license/src/pendingLicense.ts @@ -0,0 +1,30 @@ +import { setLicense } from './license'; +import { logger } from './logger'; + +let pendingLicense: string; + +export const setPendingLicense = (encryptedLicense: string) => { + pendingLicense = encryptedLicense; + if (pendingLicense) { + logger.info('Storing license as pending validation.'); + } +}; + +export const applyPendingLicense = async () => { + if (pendingLicense) { + logger.info('Applying pending license.'); + await setLicense(pendingLicense, true); + } +}; + +export const hasPendingLicense = () => Boolean(pendingLicense); + +export const isPendingLicense = (encryptedLicense: string) => !!pendingLicense && pendingLicense === encryptedLicense; + +export const clearPendingLicense = () => { + if (pendingLicense) { + logger.info('Removing pending license.'); + } + + pendingLicense = ''; +}; diff --git a/ee/packages/license/src/tags.ts b/ee/packages/license/src/tags.ts index 207b716d2274..b115b164cd64 100644 --- a/ee/packages/license/src/tags.ts +++ b/ee/packages/license/src/tags.ts @@ -13,8 +13,9 @@ export const addTag = (tag: ILicenseTag) => { tags.add(tag); }; -export const addTags = (tags: ILicenseTag[]) => { - for (const tag of tags) { +export const replaceTags = (newTags: ILicenseTag[]) => { + tags.clear(); + for (const tag of newTags) { addTag(tag); } }; diff --git a/ee/packages/license/src/validation/getCurrentValueForLicenseLimit.ts b/ee/packages/license/src/validation/getCurrentValueForLicenseLimit.ts index bbe89516f449..cb86c5ff3dfe 100644 --- a/ee/packages/license/src/validation/getCurrentValueForLicenseLimit.ts +++ b/ee/packages/license/src/validation/getCurrentValueForLicenseLimit.ts @@ -2,6 +2,7 @@ import type { IUser } from '@rocket.chat/core-typings'; import { Subscriptions, Users } from '@rocket.chat/models'; import type { LicenseLimitKind } from '../definition/ILicenseV3'; +import { logger } from '../logger'; type LimitContext = T extends 'roomsPerGuest' ? { userId: IUser['_id'] } : Record; @@ -23,5 +24,7 @@ export const getCurrentValueForLicenseLimit = async return dataCounters.get(limitKey)?.(context as LimitContext | undefined) ?? 0; } + logger.error({ msg: 'Unable to validate license limit due to missing data counter.', limitKey }); + return 0; }; diff --git a/ee/packages/license/src/validation/getModulesToDisable.ts b/ee/packages/license/src/validation/getModulesToDisable.ts index 62b625374976..d42426e8af26 100644 --- a/ee/packages/license/src/validation/getModulesToDisable.ts +++ b/ee/packages/license/src/validation/getModulesToDisable.ts @@ -9,7 +9,7 @@ export const getModulesToDisable = (validationResult: BehaviorWithContext[]): Li ...new Set([ ...filterValidationResult(validationResult, 'disable_modules') .map(({ modules }) => modules || []) - .reduce((prev, curr) => [...prev, ...curr], []), + .flat(), ]), ]; }; diff --git a/ee/packages/license/src/validation/validateLicenseLimits.ts b/ee/packages/license/src/validation/validateLicenseLimits.ts index 2bfa4a2d1b75..32b0a1229d9a 100644 --- a/ee/packages/license/src/validation/validateLicenseLimits.ts +++ b/ee/packages/license/src/validation/validateLicenseLimits.ts @@ -33,5 +33,5 @@ export const validateLicenseLimits = async ( }); }), ) - ).reduce((prev, curr) => [...prev, ...curr], []); + ).flat(); }; diff --git a/ee/packages/license/src/workspaceUrl.ts b/ee/packages/license/src/workspaceUrl.ts index f8edb62bf5ee..8d384b2d453d 100644 --- a/ee/packages/license/src/workspaceUrl.ts +++ b/ee/packages/license/src/workspaceUrl.ts @@ -1,11 +1,13 @@ -import { validateLicense } from './license'; +import { applyPendingLicense, hasPendingLicense } from './pendingLicense'; let workspaceUrl: string | undefined; export const setWorkspaceUrl = async (url: string) => { workspaceUrl = url.replace(/\/$/, '').replace(/^https?:\/\/(.*)$/, '$1'); - await validateLicense(); + if (hasPendingLicense()) { + await applyPendingLicense(); + } }; export const getWorkspaceUrl = () => workspaceUrl;