From e8c28a4509e705cdaaf5e1bd2b668ceee21c92c0 Mon Sep 17 00:00:00 2001 From: Pierre Lehnen Date: Tue, 3 Oct 2023 20:50:38 -0300 Subject: [PATCH 01/11] feat: License v3 validations --- apps/meteor/ee/app/license/server/startup.ts | 17 ++ .../license/src/definition/LicenseBehavior.ts | 8 +- ee/packages/license/src/events/emitter.ts | 26 ++- ee/packages/license/src/events/listeners.ts | 16 +- ee/packages/license/src/index.ts | 17 +- ee/packages/license/src/license.ts | 157 ++++++++++++++---- ee/packages/license/src/modules.ts | 7 +- .../getCurrentValueForLicenseLimit.ts | 9 +- .../license/src/validation/runValidation.ts | 16 +- .../src/validation/validateLicenseLimits.ts | 5 +- 10 files changed, 226 insertions(+), 52 deletions(-) diff --git a/apps/meteor/ee/app/license/server/startup.ts b/apps/meteor/ee/app/license/server/startup.ts index d3523282d1e8..990a381334af 100644 --- a/apps/meteor/ee/app/license/server/startup.ts +++ b/apps/meteor/ee/app/license/server/startup.ts @@ -1,3 +1,4 @@ +import { cronJobs } from '@rocket.chat/cron'; import { License } from '@rocket.chat/license'; import { Subscriptions, Users } from '@rocket.chat/models'; @@ -26,3 +27,19 @@ License.setLicenseLimitCounter('privateApps', () => getAppCount('private')); License.setLicenseLimitCounter('marketplaceApps', () => getAppCount('marketplace')); // #TODO: Get real value License.setLicenseLimitCounter('monthlyActiveContacts', async () => 0); + +const updateCronJob = async () => { + if (License.hasValidLicense() === (await cronJobs.has('licenseLimitChecker'))) { + return; + } + + if (License.hasValidLicense()) { + await cronJobs.add('licenseLimitChecker', '*/30 * * * *', () => License.revalidateLimits()); + } else { + await cronJobs.remove('licenseLimitChecker'); + } +}; + +License.onValidateLicense(async () => updateCronJob()); +License.onInvalidateLicense(async () => updateCronJob()); +void updateCronJob(); diff --git a/ee/packages/license/src/definition/LicenseBehavior.ts b/ee/packages/license/src/definition/LicenseBehavior.ts index b6d52bbfa8c5..776c2ae34074 100644 --- a/ee/packages/license/src/definition/LicenseBehavior.ts +++ b/ee/packages/license/src/definition/LicenseBehavior.ts @@ -1,6 +1,12 @@ import type { LicenseModule } from './LicenseModule'; -export type LicenseBehavior = 'invalidate_license' | 'start_fair_policy' | 'prevent_action' | 'prevent_installation' | 'disable_modules'; +export type LicenseBehavior = + | 'invalidate_license' + | 'start_fair_policy' + | 'prevent_action' + | 'prevent_installation' + | 'disable_modules' + | 'custom'; export type BehaviorWithContext = { behavior: LicenseBehavior; diff --git a/ee/packages/license/src/events/emitter.ts b/ee/packages/license/src/events/emitter.ts index 9d4025e4bce3..deba9aabb715 100644 --- a/ee/packages/license/src/events/emitter.ts +++ b/ee/packages/license/src/events/emitter.ts @@ -1,4 +1,5 @@ import type { LicenseLimitKind } from '../definition/ILicenseV3'; +import type { LicenseBehavior } from '../definition/LicenseBehavior'; import type { LicenseModule } from '../definition/LicenseModule'; import type { LicenseManager } from '../license'; import { logger } from '../logger'; @@ -21,10 +22,31 @@ export function moduleRemoved(this: LicenseManager, module: LicenseModule) { } } -export function limitReached(this: LicenseManager, limitKind: LicenseLimitKind) { +export function limitReached( + this: LicenseManager, + limitKind: Exclude, + limitBehavior: Exclude, +) { try { - this.emit(`limitReached:${limitKind}`); + // This will never be emitted for limits that fallback to "not reached" when missing context params (eg: roomsPerGuest) + this.emit(`limitReached:${limitKind}:${limitBehavior}`); } catch (error) { logger.error({ msg: 'Error running limit reached event', error }); } } + +export function licenseValidated(this: LicenseManager) { + try { + this.emit('validate'); + } catch (error) { + logger.error({ msg: 'Error running license validated event', error }); + } +} + +export function licenseInvalidated(this: LicenseManager) { + try { + this.emit('invalidate'); + } catch (error) { + logger.error({ msg: 'Error running license invalidated event', error }); + } +} diff --git a/ee/packages/license/src/events/listeners.ts b/ee/packages/license/src/events/listeners.ts index d6e9fb016f2c..c371e782cb43 100644 --- a/ee/packages/license/src/events/listeners.ts +++ b/ee/packages/license/src/events/listeners.ts @@ -1,4 +1,5 @@ import type { LicenseLimitKind } from '../definition/ILicenseV3'; +import type { LicenseBehavior } from '../definition/LicenseBehavior'; import type { LicenseModule } from '../definition/LicenseModule'; import type { LicenseManager } from '../license'; import { hasModule } from '../modules'; @@ -58,18 +59,23 @@ export function onToggledFeature( }; } -export function onModule(this: LicenseManager, cb: (...args: any[]) => void) { +export function onModule(this: LicenseManager, cb: (data: { module: LicenseModule; valid: boolean }) => void) { this.on('module', cb); } -export function onValidateLicense(this: LicenseManager, cb: (...args: any[]) => void) { +export function onValidateLicense(this: LicenseManager, cb: () => void) { this.on('validate', cb); } -export function onInvalidateLicense(this: LicenseManager, cb: (...args: any[]) => void) { +export function onInvalidateLicense(this: LicenseManager, cb: () => void) { this.on('invalidate', cb); } -export function onLimitReached(this: LicenseManager, limitKind: LicenseLimitKind, cb: (...args: any[]) => void) { - this.on(`limitReached:${limitKind}`, cb); +export function onLimitReached( + this: LicenseManager, + limitKind: Exclude, + cb: () => void, + limitBehavior: Exclude = 'custom', +) { + this.on(`limitReached:${limitKind}:${limitBehavior}`, cb); } diff --git a/ee/packages/license/src/index.ts b/ee/packages/license/src/index.ts index c5dbd9f9496f..bc32513a9c90 100644 --- a/ee/packages/license/src/index.ts +++ b/ee/packages/license/src/index.ts @@ -1,4 +1,5 @@ import type { ILicenseV3, LicenseLimitKind } from './definition/ILicenseV3'; +import type { LicenseBehavior } from './definition/LicenseBehavior'; import type { LicenseModule } from './definition/LicenseModule'; import type { LimitContext } from './definition/LimitContext'; import { getAppsConfig, getMaxActiveUsers, getUnmodifiedLicenseAndModules } from './deprecated'; @@ -37,7 +38,11 @@ interface License { overwriteClassOnLicense: typeof overwriteClassOnLicense; setLicenseLimitCounter: typeof setLicenseLimitCounter; getCurrentValueForLicenseLimit: typeof getCurrentValueForLicenseLimit; - isLimitReached: (action: T, context?: Partial>) => Promise; + isLimitReached: ( + action: T, + behaviors: LicenseBehavior[], + context?: Partial>, + ) => Promise; onValidFeature: typeof onValidFeature; onInvalidFeature: typeof onInvalidFeature; onToggledFeature: typeof onToggledFeature; @@ -45,6 +50,8 @@ interface License { onValidateLicense: typeof onValidateLicense; onInvalidateLicense: typeof onInvalidateLicense; onLimitReached: typeof onLimitReached; + revalidateLicense: () => Promise; + revalidateLimits: () => Promise; getInfo: (loadCurrentValues: boolean) => Promise<{ license: ILicenseV3 | undefined; @@ -78,8 +85,12 @@ export class LicenseImp extends LicenseManager implements License { getCurrentValueForLicenseLimit = getCurrentValueForLicenseLimit; - public async isLimitReached(action: T, context?: Partial>) { - return this.shouldPreventAction(action, context, 0); + public async isLimitReached( + action: T, + behaviors: LicenseBehavior[], + context?: Partial>, + ): Promise { + return super.isLimitReached(action, behaviors, context); } onValidFeature = onValidFeature; diff --git a/ee/packages/license/src/license.ts b/ee/packages/license/src/license.ts index a420eb2b0d57..2fba74e87364 100644 --- a/ee/packages/license/src/license.ts +++ b/ee/packages/license/src/license.ts @@ -2,12 +2,13 @@ import { Emitter } from '@rocket.chat/emitter'; import type { ILicenseV2 } from './definition/ILicenseV2'; import type { ILicenseV3, LicenseLimitKind } from './definition/ILicenseV3'; -import type { BehaviorWithContext } from './definition/LicenseBehavior'; +import type { BehaviorWithContext, LicenseBehavior } from './definition/LicenseBehavior'; import type { LicenseModule } from './definition/LicenseModule'; import type { LimitContext } from './definition/LimitContext'; import { DuplicatedLicenseError } from './errors/DuplicatedLicenseError'; import { InvalidLicenseError } from './errors/InvalidLicenseError'; import { NotReadyForValidation } from './errors/NotReadyForValidation'; +import { licenseInvalidated, licenseValidated, limitReached } from './events/emitter'; import { logger } from './logger'; import { getModules, invalidateAll, replaceModules } from './modules'; import { applyPendingLicense, clearPendingLicense, hasPendingLicense, isPendingLicense, setPendingLicense } from './pendingLicense'; @@ -22,15 +23,23 @@ import { isReadyForValidation } from './validation/isReadyForValidation'; import { runValidation } from './validation/runValidation'; import { validateFormat } from './validation/validateFormat'; +const invalidLicenseBehaviors: LicenseBehavior[] = ['invalidate_license', 'prevent_installation']; +const generalValidationBehaviors: LicenseBehavior[] = ['start_fair_policy', 'disable_modules']; +const behaviorsWithLimitEvents = ['invalidate_license', 'start_fair_policy', 'disable_modules', 'custom', 'prevent_action'] as const; +const globalLimitKinds: LicenseLimitKind[] = ['activeUsers', 'guestUsers', 'privateApps', 'marketplaceApps', 'monthlyActiveContacts']; + export class LicenseManager extends Emitter< - Record<`limitReached:${LicenseLimitKind}` | `${'invalid' | 'valid'}:${LicenseModule}`, undefined> & { - validate: undefined; - invalidate: undefined; - module: { module: LicenseModule; valid: boolean }; - } + Record<`limitReached:${Exclude}:${Exclude}`, undefined> & + Record<`${'invalid' | 'valid'}:${LicenseModule}`, undefined> & { + validate: undefined; + invalidate: undefined; + module: { module: LicenseModule; valid: boolean }; + } > { dataCounters = new Map) => Promise>(); + countersCache = new Map(); + pendingLicense = ''; modules = new Set(); @@ -47,6 +56,8 @@ export class LicenseManager extends Emitter< private _lockedLicense: string | undefined; + private _accessedLimits = new Set>(); + public get license(): ILicenseV3 | undefined { return this._license; } @@ -75,15 +86,61 @@ export class LicenseManager extends Emitter< return this.workspaceUrl; } + public async revalidateLimits(): Promise { + this.countersCache.clear(); + await this.triggerLimitEvents(); + } + + private async triggerLimitEvents(): Promise { + const license = this.getLicense(); + if (!license) { + return; + } + + const limits = [...this._accessedLimits]; + this._accessedLimits.clear(); + + for await (const limit of limits) { + for await (const behavior of behaviorsWithLimitEvents) { + if (this.has(`limitReached:${limit}:${behavior}`) && (await this.isLimitReached(limit, [behavior], undefined, 0, false))) { + limitReached.call(this, limit, behavior); + } + } + } + } + + public async revalidateLicense(): Promise { + if (!this.hasValidLicense()) { + return; + } + + try { + this.countersCache.clear(); + await this.validateLicense(false); + } finally { + this.maybeInvalidateLicense(); + } + } + private clearLicenseData(): void { this._license = undefined; this._unmodifiedLicense = undefined; this._inFairPolicy = undefined; this._valid = false; this._lockedLicense = undefined; + this.countersCache.clear(); clearPendingLicense.call(this); } + private maybeInvalidateLicense(): void { + if (this.hasValidLicense()) { + return; + } + + licenseInvalidated.call(this); + invalidateAll.call(this); + } + private async setLicenseV3(newLicense: ILicenseV3, encryptedLicense: string, originalLicense?: ILicenseV2 | ILicenseV3): Promise { const hadValidLicense = this.hasValidLicense(); this.clearLicenseData(); @@ -92,13 +149,16 @@ export class LicenseManager extends Emitter< this._unmodifiedLicense = originalLicense || newLicense; this._license = newLicense; - await this.validateLicense(); - + await this.validateLicense(encryptedLicense !== this._lockedLicense); this._lockedLicense = encryptedLicense; + + if (this.valid) { + licenseValidated.call(this); + showLicense.call(this, this._license, this._valid); + } } finally { - if (hadValidLicense && !this.hasValidLicense()) { - this.emit('invalidate'); - invalidateAll.call(this); + if (hadValidLicense) { + this.maybeInvalidateLicense(); } } } @@ -111,7 +171,7 @@ export class LicenseManager extends Emitter< return Boolean(this._lockedLicense && this._lockedLicense === encryptedLicense); } - private async validateLicense(): Promise { + private async validateLicenseBehaviors(behaviorsToConsider: LicenseBehavior[]): Promise { if (!this._license) { throw new InvalidLicenseError(); } @@ -120,15 +180,34 @@ export class LicenseManager extends Emitter< throw new NotReadyForValidation(); } - // #TODO: Only include 'prevent_installation' here if this is actually the initial installation of the license - const validationResult = await runValidation.call(this, this._license, [ + // Run the `invalidate_license` behavior first and skip everything else if it's already invalid. + const validationResult = await runValidation.call( + this, + this._license, + behaviorsToConsider.filter((behavior) => invalidLicenseBehaviors.includes(behavior)), + ); + + if (isBehaviorsInResult(validationResult, invalidLicenseBehaviors)) { + this._valid = false; + return; + } + + const generalResult = await runValidation.call( + this, + this._license, + behaviorsToConsider.filter((behavior) => generalValidationBehaviors.includes(behavior)), + ); + + this.processValidationResult(generalResult, behaviorsToConsider.includes('prevent_installation')); + } + + private async validateLicense(isNewLicense: boolean): Promise { + return this.validateLicenseBehaviors([ 'invalidate_license', - 'prevent_installation', 'start_fair_policy', 'disable_modules', + ...(isNewLicense ? ['prevent_installation' as LicenseBehavior] : []), ]); - - this.processValidationResult(validationResult); } public async setLicense(encryptedLicense: string): Promise { @@ -175,11 +254,14 @@ export class LicenseManager extends Emitter< } } - private processValidationResult(result: BehaviorWithContext[]): void { - if (!this._license || isBehaviorsInResult(result, ['invalidate_license', 'prevent_installation'])) { + private processValidationResult(result: BehaviorWithContext[], isNewLicense: boolean): void { + if (!this._license || isBehaviorsInResult(result, invalidLicenseBehaviors)) { + this._valid = false; return; } + const shouldLogModules = !this._valid || isNewLicense; + this._valid = true; this._inFairPolicy = isBehaviorsInResult(result, ['start_fair_policy']); @@ -190,14 +272,14 @@ export class LicenseManager extends Emitter< const disabledModules = getModulesToDisable(result); const modulesToEnable = this._license.grantedModules.filter(({ module }) => !disabledModules.includes(module)); - replaceModules.call( + const modulesChanged = replaceModules.call( this, modulesToEnable.map(({ module }) => module), ); - logger.log({ msg: 'License validated', modules: modulesToEnable }); - this.emit('validate'); - showLicense.call(this, this._license, this._valid); + if (shouldLogModules || modulesChanged) { + logger.log({ msg: 'License validated', modules: modulesToEnable }); + } } public hasValidLicense(): boolean { @@ -214,18 +296,35 @@ export class LicenseManager extends Emitter< action: T, context?: Partial>, newCount = 1, + ): Promise { + return this.isLimitReached(action, ['prevent_action'], context, newCount); + } + + protected async isLimitReached( + action: T, + behaviorsToConsider?: LicenseBehavior[], + context?: Partial>, + extraCount = 0, + flagAsAccessed = true, ): Promise { const license = this.getLicense(); if (!license) { return false; } - const currentValue = (await getCurrentValueForLicenseLimit.call(this, action, context)) + newCount; - return Boolean( - license.limits[action] - ?.filter(({ behavior, max }) => behavior === 'prevent_action' && max >= 0) - .some(({ max }) => max < currentValue), + if (action !== 'roomsPerGuest' && flagAsAccessed) { + this._accessedLimits.add(action); + } + + const filteredLimits = license.limits[action]?.filter( + ({ behavior, max }) => max >= 0 && (!behaviorsToConsider || behaviorsToConsider.includes(behavior)), ); + if (!filteredLimits?.length) { + return false; + } + + const currentValue = (await getCurrentValueForLicenseLimit.call(this, action, context)) + extraCount; + return Boolean(filteredLimits.some(({ max }) => max < currentValue)); } public async getInfo(loadCurrentValues = false): Promise<{ @@ -241,7 +340,7 @@ export class LicenseManager extends Emitter< const limits = ( (license && (await Promise.all( - (['activeUsers', 'guestUsers', 'privateApps', 'marketplaceApps', 'monthlyActiveContacts'] as LicenseLimitKind[]) + globalLimitKinds .map((limitKey) => ({ limitKey, max: Math.max(-1, Math.min(...Array.from(license.limits[limitKey as LicenseLimitKind] || [])?.map(({ max }) => max))), diff --git a/ee/packages/license/src/modules.ts b/ee/packages/license/src/modules.ts index 7570ec525fc7..6931fb7a6a5d 100644 --- a/ee/packages/license/src/modules.ts +++ b/ee/packages/license/src/modules.ts @@ -29,7 +29,8 @@ export function hasModule(this: LicenseManager, module: LicenseModule) { return this.modules.has(module); } -export function replaceModules(this: LicenseManager, newModules: LicenseModule[]) { +export function replaceModules(this: LicenseManager, newModules: LicenseModule[]): boolean { + let anyChange = false; for (const moduleName of newModules) { if (this.modules.has(moduleName)) { continue; @@ -37,6 +38,7 @@ export function replaceModules(this: LicenseManager, newModules: LicenseModule[] this.modules.add(moduleName); moduleValidated.call(this, moduleName); + anyChange = true; } for (const moduleName of this.modules) { @@ -46,5 +48,8 @@ export function replaceModules(this: LicenseManager, newModules: LicenseModule[] moduleRemoved.call(this, moduleName); this.modules.delete(moduleName); + anyChange = true; } + + return anyChange; } diff --git a/ee/packages/license/src/validation/getCurrentValueForLicenseLimit.ts b/ee/packages/license/src/validation/getCurrentValueForLicenseLimit.ts index 88cedc6c7bc9..be22c845ada1 100644 --- a/ee/packages/license/src/validation/getCurrentValueForLicenseLimit.ts +++ b/ee/packages/license/src/validation/getCurrentValueForLicenseLimit.ts @@ -30,7 +30,14 @@ export async function getCurrentValueForLicenseLimit throw new Error('Unable to validate license limit due to missing data counter.'); } - return counterFn(context as LimitContext | undefined); + if (this.countersCache.has(limitKey)) { + return this.countersCache.get(limitKey) as number; + } + + const count = await counterFn(context as LimitContext | undefined); + this.countersCache.set(limitKey, count); + + return count; } export function hasAllDataCounters(this: LicenseManager) { diff --git a/ee/packages/license/src/validation/runValidation.ts b/ee/packages/license/src/validation/runValidation.ts index 9cb623b8eae0..50fb8337d483 100644 --- a/ee/packages/license/src/validation/runValidation.ts +++ b/ee/packages/license/src/validation/runValidation.ts @@ -1,4 +1,4 @@ -import type { ILicenseV3 } from '../definition/ILicenseV3'; +import type { ILicenseV3, LicenseLimitKind } from '../definition/ILicenseV3'; import type { LicenseBehavior, BehaviorWithContext } from '../definition/LicenseBehavior'; import type { LicenseManager } from '../license'; import { validateLicenseLimits } from './validateLicenseLimits'; @@ -8,15 +8,15 @@ import { validateLicenseUrl } from './validateLicenseUrl'; export async function runValidation( this: LicenseManager, license: ILicenseV3, - behaviorsToValidate: LicenseBehavior[] = [], + behaviorsToValidate?: LicenseBehavior[], + limitsToValidate?: LicenseLimitKind[], ): Promise { - const shouldValidateBehavior = (behavior: LicenseBehavior) => !behaviorsToValidate.length || behaviorsToValidate.includes(behavior); + const shouldValidateBehavior = (behavior: LicenseBehavior) => !behaviorsToValidate || behaviorsToValidate.includes(behavior); + const shouldValidateLimit = (limit: LicenseLimitKind) => !limitsToValidate || limitsToValidate.includes(limit); return [ - ...new Set([ - ...validateLicenseUrl.call(this, license, shouldValidateBehavior), - ...validateLicensePeriods(license, shouldValidateBehavior), - ...(await validateLicenseLimits.call(this, license, shouldValidateBehavior)), - ]), + ...validateLicenseUrl.call(this, license, shouldValidateBehavior), + ...validateLicensePeriods(license, shouldValidateBehavior), + ...(await validateLicenseLimits.call(this, license, shouldValidateBehavior, shouldValidateLimit)), ]; } diff --git a/ee/packages/license/src/validation/validateLicenseLimits.ts b/ee/packages/license/src/validation/validateLicenseLimits.ts index 168effe6a250..0bc8223a74b0 100644 --- a/ee/packages/license/src/validation/validateLicenseLimits.ts +++ b/ee/packages/license/src/validation/validateLicenseLimits.ts @@ -1,4 +1,4 @@ -import type { ILicenseV3 } from '../definition/ILicenseV3'; +import type { ILicenseV3, LicenseLimitKind } from '../definition/ILicenseV3'; import type { BehaviorWithContext, LicenseBehavior } from '../definition/LicenseBehavior'; import type { LicenseManager } from '../license'; import { logger } from '../logger'; @@ -9,10 +9,11 @@ export async function validateLicenseLimits( this: LicenseManager, license: ILicenseV3, behaviorFilter: (behavior: LicenseBehavior) => boolean, + limitFilter: (limit: LicenseLimitKind) => boolean, ): Promise { const { limits } = license; - const limitKeys = Object.keys(limits) as (keyof ILicenseV3['limits'])[]; + const limitKeys = (Object.keys(limits) as LicenseLimitKind[]).filter(limitFilter || (() => true)); return ( await Promise.all( limitKeys.map(async (limitKey) => { From 874cc21de716f9bab311e0f4563176737969925e Mon Sep 17 00:00:00 2001 From: Pierre Lehnen Date: Wed, 4 Oct 2023 13:53:23 -0300 Subject: [PATCH 02/11] url validation --- ee/packages/license/package.json | 4 +++- ee/packages/license/src/v2/convertToV3.ts | 2 +- .../src/validation/validateLicenseUrl.ts | 21 ++++++++++++++----- yarn.lock | 2 ++ 4 files changed, 22 insertions(+), 7 deletions(-) diff --git a/ee/packages/license/package.json b/ee/packages/license/package.json index f6a1e7a2b7d5..6810f53e40dd 100644 --- a/ee/packages/license/package.json +++ b/ee/packages/license/package.json @@ -11,6 +11,7 @@ "@swc/jest": "^0.2.26", "@types/babel__core": "^7", "@types/babel__preset-env": "^7", + "@types/bcrypt": "^5.0.0", "@types/jest": "~29.5.3", "@types/ws": "^8.5.5", "babel-plugin-transform-inline-environment-variables": "^0.4.4", @@ -42,6 +43,7 @@ "dependencies": { "@rocket.chat/core-typings": "workspace:^", "@rocket.chat/jwt": "workspace:^", - "@rocket.chat/logger": "workspace:^" + "@rocket.chat/logger": "workspace:^", + "bcrypt": "^5.0.1" } } diff --git a/ee/packages/license/src/v2/convertToV3.ts b/ee/packages/license/src/v2/convertToV3.ts index 7586f54c8c54..10681cf04b47 100644 --- a/ee/packages/license/src/v2/convertToV3.ts +++ b/ee/packages/license/src/v2/convertToV3.ts @@ -36,7 +36,7 @@ export const convertToV3 = (v2: ILicenseV2): ILicenseV3 => { serverUrls: [ { value: v2.url, - type: 'url', + type: 'regex', }, ], validPeriods: [ diff --git a/ee/packages/license/src/validation/validateLicenseUrl.ts b/ee/packages/license/src/validation/validateLicenseUrl.ts index 55cd076c4378..210ddb04d80f 100644 --- a/ee/packages/license/src/validation/validateLicenseUrl.ts +++ b/ee/packages/license/src/validation/validateLicenseUrl.ts @@ -1,10 +1,14 @@ +import crypto from 'crypto'; + +import bcrypt from 'bcrypt'; + import type { ILicenseV3 } from '../definition/ILicenseV3'; import type { BehaviorWithContext, LicenseBehavior } from '../definition/LicenseBehavior'; import type { LicenseManager } from '../license'; import { logger } from '../logger'; import { getResultingBehavior } from './getResultingBehavior'; -export const validateUrl = (licenseURL: string, url: string) => { +const validateRegex = (licenseURL: string, url: string) => { licenseURL = licenseURL .replace(/\./g, '\\.') // convert dots to literal .replace(/\*/g, '.*'); // convert * to .* @@ -13,6 +17,15 @@ export const validateUrl = (licenseURL: string, url: string) => { return !!regex.exec(url); }; +const validateUrl = (licenseURL: string, url: string) => { + return licenseURL.toLowerCase() === url.toLowerCase(); +}; + +const validateHash = (licenseURL: string, url: string) => { + const value = crypto.createHash('sha256').update(url).digest('hex'); + return bcrypt.compareSync(value, licenseURL); +}; + export function validateLicenseUrl( this: LicenseManager, license: ILicenseV3, @@ -37,11 +50,9 @@ export function validateLicenseUrl( .filter((url) => { switch (url.type) { case 'regex': - // #TODO - break; + return !validateRegex(url.value, workspaceUrl); case 'hash': - // #TODO - break; + return !validateHash(url.value, workspaceUrl); case 'url': return !validateUrl(url.value, workspaceUrl); } diff --git a/yarn.lock b/yarn.lock index 4093777c8e33..7d75c388dc29 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8485,9 +8485,11 @@ __metadata: "@swc/jest": ^0.2.26 "@types/babel__core": ^7 "@types/babel__preset-env": ^7 + "@types/bcrypt": ^5.0.0 "@types/jest": ~29.5.3 "@types/ws": ^8.5.5 babel-plugin-transform-inline-environment-variables: ^0.4.4 + bcrypt: ^5.0.1 eslint: ~8.45.0 jest: ~29.6.1 jest-environment-jsdom: ~29.6.1 From 9405914dde9e4c8665f9226646df487629889581 Mon Sep 17 00:00:00 2001 From: Pierre Lehnen Date: Wed, 4 Oct 2023 14:02:19 -0300 Subject: [PATCH 03/11] fixed: isLimitReached suppressing arguments --- ee/packages/license/src/index.ts | 2 +- ee/packages/license/src/license.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/ee/packages/license/src/index.ts b/ee/packages/license/src/index.ts index bc32513a9c90..71b9f366d789 100644 --- a/ee/packages/license/src/index.ts +++ b/ee/packages/license/src/index.ts @@ -90,7 +90,7 @@ export class LicenseImp extends LicenseManager implements License { behaviors: LicenseBehavior[], context?: Partial>, ): Promise { - return super.isLimitReached(action, behaviors, context); + return this._isLimitReached(action, behaviors, context); } onValidFeature = onValidFeature; diff --git a/ee/packages/license/src/license.ts b/ee/packages/license/src/license.ts index 2fba74e87364..2bc2e471817e 100644 --- a/ee/packages/license/src/license.ts +++ b/ee/packages/license/src/license.ts @@ -297,10 +297,10 @@ export class LicenseManager extends Emitter< context?: Partial>, newCount = 1, ): Promise { - return this.isLimitReached(action, ['prevent_action'], context, newCount); + return this._isLimitReached(action, ['prevent_action'], context, newCount); } - protected async isLimitReached( + protected async _isLimitReached( action: T, behaviorsToConsider?: LicenseBehavior[], context?: Partial>, From c3b5792738b0e4544ea5f60b8dcf61c9da20f626 Mon Sep 17 00:00:00 2001 From: Pierre Lehnen Date: Thu, 5 Oct 2023 16:42:50 -0300 Subject: [PATCH 04/11] refactor --- apps/meteor/ee/app/license/server/startup.ts | 17 --- .../license/src/definition/LicenseBehavior.ts | 11 +- .../definition/LicenseValidationOptions.ts | 11 ++ .../license/src/definition/LimitContext.ts | 4 +- ee/packages/license/src/definition/events.ts | 18 +++ ee/packages/license/src/events/emitter.ts | 26 ++-- ee/packages/license/src/events/listeners.ts | 15 +- ee/packages/license/src/index.ts | 20 +-- ee/packages/license/src/isItemAllowed.ts | 12 ++ ee/packages/license/src/license.ts | 141 ++++++------------ .../getCurrentValueForLicenseLimit.ts | 11 +- .../src/validation/getResultingBehavior.ts | 10 +- .../src/validation/runValidation.spec.ts | 12 +- .../license/src/validation/runValidation.ts | 17 +-- .../src/validation/validateLicenseLimits.ts | 29 ++-- .../src/validation/validateLicensePeriods.ts | 26 ++-- .../src/validation/validateLicenseUrl.ts | 16 +- 17 files changed, 190 insertions(+), 206 deletions(-) create mode 100644 ee/packages/license/src/definition/LicenseValidationOptions.ts create mode 100644 ee/packages/license/src/definition/events.ts create mode 100644 ee/packages/license/src/isItemAllowed.ts diff --git a/apps/meteor/ee/app/license/server/startup.ts b/apps/meteor/ee/app/license/server/startup.ts index 990a381334af..d3523282d1e8 100644 --- a/apps/meteor/ee/app/license/server/startup.ts +++ b/apps/meteor/ee/app/license/server/startup.ts @@ -1,4 +1,3 @@ -import { cronJobs } from '@rocket.chat/cron'; import { License } from '@rocket.chat/license'; import { Subscriptions, Users } from '@rocket.chat/models'; @@ -27,19 +26,3 @@ License.setLicenseLimitCounter('privateApps', () => getAppCount('private')); License.setLicenseLimitCounter('marketplaceApps', () => getAppCount('marketplace')); // #TODO: Get real value License.setLicenseLimitCounter('monthlyActiveContacts', async () => 0); - -const updateCronJob = async () => { - if (License.hasValidLicense() === (await cronJobs.has('licenseLimitChecker'))) { - return; - } - - if (License.hasValidLicense()) { - await cronJobs.add('licenseLimitChecker', '*/30 * * * *', () => License.revalidateLimits()); - } else { - await cronJobs.remove('licenseLimitChecker'); - } -}; - -License.onValidateLicense(async () => updateCronJob()); -License.onInvalidateLicense(async () => updateCronJob()); -void updateCronJob(); diff --git a/ee/packages/license/src/definition/LicenseBehavior.ts b/ee/packages/license/src/definition/LicenseBehavior.ts index 776c2ae34074..0d9a7355294a 100644 --- a/ee/packages/license/src/definition/LicenseBehavior.ts +++ b/ee/packages/license/src/definition/LicenseBehavior.ts @@ -1,14 +1,11 @@ +import type { LicenseLimitKind } from './ILicenseV3'; import type { LicenseModule } from './LicenseModule'; -export type LicenseBehavior = - | 'invalidate_license' - | 'start_fair_policy' - | 'prevent_action' - | 'prevent_installation' - | 'disable_modules' - | 'custom'; +export type LicenseBehavior = 'invalidate_license' | 'start_fair_policy' | 'prevent_action' | 'prevent_installation' | 'disable_modules'; export type BehaviorWithContext = { behavior: LicenseBehavior; modules?: LicenseModule[]; + reason: 'limit' | 'period' | 'url'; + limit?: LicenseLimitKind; }; diff --git a/ee/packages/license/src/definition/LicenseValidationOptions.ts b/ee/packages/license/src/definition/LicenseValidationOptions.ts new file mode 100644 index 000000000000..6aa1e4213c62 --- /dev/null +++ b/ee/packages/license/src/definition/LicenseValidationOptions.ts @@ -0,0 +1,11 @@ +import type { LicenseLimitKind } from './ILicenseV3'; +import type { LicenseBehavior } from './LicenseBehavior'; +import type { LimitContext } from './LimitContext'; + +export type LicenseValidationOptions = { + behaviors?: LicenseBehavior[]; + limits?: LicenseLimitKind[]; + suppressLog?: boolean; + isNewLicense?: boolean; + context?: Partial<{ [K in LicenseLimitKind]: Partial> }>; +}; diff --git a/ee/packages/license/src/definition/LimitContext.ts b/ee/packages/license/src/definition/LimitContext.ts index a2c44744bd75..9dfc6d36be7f 100644 --- a/ee/packages/license/src/definition/LimitContext.ts +++ b/ee/packages/license/src/definition/LimitContext.ts @@ -2,4 +2,6 @@ import type { IUser } from '@rocket.chat/core-typings'; import type { LicenseLimitKind } from './ILicenseV3'; -export type LimitContext = T extends 'roomsPerGuest' ? { userId: IUser['_id'] } : Record; +export type LimitContext = { extraCount?: number } & (T extends 'roomsPerGuest' + ? { userId: IUser['_id'] } + : Record); diff --git a/ee/packages/license/src/definition/events.ts b/ee/packages/license/src/definition/events.ts new file mode 100644 index 000000000000..0e525ff36d25 --- /dev/null +++ b/ee/packages/license/src/definition/events.ts @@ -0,0 +1,18 @@ +import type { LicenseLimitKind } from './ILicenseV3'; +import type { BehaviorWithContext, LicenseBehavior } from './LicenseBehavior'; +import type { LicenseModule } from './LicenseModule'; + +type ModuleValidation = Record<`${'invalid' | 'valid'}:${LicenseModule}`, undefined>; +type BehaviorTriggered = Record< + `behavior:${Exclude}`, + { reason: BehaviorWithContext['reason']; limit?: LicenseLimitKind } +>; +type LimitReached = Record<`limitReached:${LicenseLimitKind}`, undefined>; + +export type LicenseEvents = ModuleValidation & + BehaviorTriggered & + LimitReached & { + validate: undefined; + invalidate: undefined; + module: { module: LicenseModule; valid: boolean }; + }; diff --git a/ee/packages/license/src/events/emitter.ts b/ee/packages/license/src/events/emitter.ts index deba9aabb715..71c6496f7390 100644 --- a/ee/packages/license/src/events/emitter.ts +++ b/ee/packages/license/src/events/emitter.ts @@ -1,5 +1,4 @@ -import type { LicenseLimitKind } from '../definition/ILicenseV3'; -import type { LicenseBehavior } from '../definition/LicenseBehavior'; +import type { BehaviorWithContext } from '../definition/LicenseBehavior'; import type { LicenseModule } from '../definition/LicenseModule'; import type { LicenseManager } from '../license'; import { logger } from '../logger'; @@ -22,14 +21,23 @@ export function moduleRemoved(this: LicenseManager, module: LicenseModule) { } } -export function limitReached( - this: LicenseManager, - limitKind: Exclude, - limitBehavior: Exclude, -) { +export function behaviorTriggered(this: LicenseManager, { behavior, reason, limit }: BehaviorWithContext) { + if (behavior === 'prevent_installation') { + return; + } + + try { + this.emit(`behavior:${behavior}`, { reason, limit }); + } catch (error) { + logger.error({ msg: 'Error running behavior triggered event', error }); + } + + if (behavior !== 'prevent_action' || reason !== 'limit' || !limit) { + return; + } + try { - // This will never be emitted for limits that fallback to "not reached" when missing context params (eg: roomsPerGuest) - this.emit(`limitReached:${limitKind}:${limitBehavior}`); + this.emit(`limitReached:${limit}`); } catch (error) { logger.error({ msg: 'Error running limit reached event', error }); } diff --git a/ee/packages/license/src/events/listeners.ts b/ee/packages/license/src/events/listeners.ts index c371e782cb43..ecabecb28c0f 100644 --- a/ee/packages/license/src/events/listeners.ts +++ b/ee/packages/license/src/events/listeners.ts @@ -1,5 +1,5 @@ import type { LicenseLimitKind } from '../definition/ILicenseV3'; -import type { LicenseBehavior } from '../definition/LicenseBehavior'; +import type { BehaviorWithContext, LicenseBehavior } from '../definition/LicenseBehavior'; import type { LicenseModule } from '../definition/LicenseModule'; import type { LicenseManager } from '../license'; import { hasModule } from '../modules'; @@ -71,11 +71,14 @@ export function onInvalidateLicense(this: LicenseManager, cb: () => void) { this.on('invalidate', cb); } -export function onLimitReached( +export function onBehaviorTriggered( this: LicenseManager, - limitKind: Exclude, - cb: () => void, - limitBehavior: Exclude = 'custom', + behavior: Exclude, + cb: (data: { reason: BehaviorWithContext['reason']; limit?: LicenseLimitKind }) => void, ) { - this.on(`limitReached:${limitKind}:${limitBehavior}`, cb); + this.on(`behavior:${behavior}`, cb); +} + +export function onLimitReached(this: LicenseManager, limitKind: LicenseLimitKind, cb: () => void) { + this.on(`limitReached:${limitKind}`, cb); } diff --git a/ee/packages/license/src/index.ts b/ee/packages/license/src/index.ts index 71b9f366d789..66dd014e790c 100644 --- a/ee/packages/license/src/index.ts +++ b/ee/packages/license/src/index.ts @@ -1,10 +1,10 @@ import type { ILicenseV3, LicenseLimitKind } from './definition/ILicenseV3'; -import type { LicenseBehavior } from './definition/LicenseBehavior'; import type { LicenseModule } from './definition/LicenseModule'; import type { LimitContext } from './definition/LimitContext'; import { getAppsConfig, getMaxActiveUsers, getUnmodifiedLicenseAndModules } from './deprecated'; import { onLicense } from './events/deprecated'; import { + onBehaviorTriggered, onInvalidFeature, onInvalidateLicense, onLimitReached, @@ -38,11 +38,7 @@ interface License { overwriteClassOnLicense: typeof overwriteClassOnLicense; setLicenseLimitCounter: typeof setLicenseLimitCounter; getCurrentValueForLicenseLimit: typeof getCurrentValueForLicenseLimit; - isLimitReached: ( - action: T, - behaviors: LicenseBehavior[], - context?: Partial>, - ) => Promise; + isLimitReached: (action: T, context?: Partial>) => Promise; onValidFeature: typeof onValidFeature; onInvalidFeature: typeof onInvalidFeature; onToggledFeature: typeof onToggledFeature; @@ -50,8 +46,8 @@ interface License { onValidateLicense: typeof onValidateLicense; onInvalidateLicense: typeof onInvalidateLicense; onLimitReached: typeof onLimitReached; + onBehaviorTriggered: typeof onBehaviorTriggered; revalidateLicense: () => Promise; - revalidateLimits: () => Promise; getInfo: (loadCurrentValues: boolean) => Promise<{ license: ILicenseV3 | undefined; @@ -85,12 +81,8 @@ export class LicenseImp extends LicenseManager implements License { getCurrentValueForLicenseLimit = getCurrentValueForLicenseLimit; - public async isLimitReached( - action: T, - behaviors: LicenseBehavior[], - context?: Partial>, - ): Promise { - return this._isLimitReached(action, behaviors, context); + public async isLimitReached(action: T, context?: Partial>): Promise { + return this.shouldPreventAction(action, context, 0); } onValidFeature = onValidFeature; @@ -107,6 +99,8 @@ export class LicenseImp extends LicenseManager implements License { onLimitReached = onLimitReached; + onBehaviorTriggered = onBehaviorTriggered; + // Deprecated: onLicense = onLicense; diff --git a/ee/packages/license/src/isItemAllowed.ts b/ee/packages/license/src/isItemAllowed.ts new file mode 100644 index 000000000000..16787cdf9c4d --- /dev/null +++ b/ee/packages/license/src/isItemAllowed.ts @@ -0,0 +1,12 @@ +import type { LicenseLimitKind } from './definition/ILicenseV3'; +import type { LicenseBehavior } from './definition/LicenseBehavior'; +import type { LicenseValidationOptions } from './definition/LicenseValidationOptions'; + +const isItemAllowed = (item: T, allowList?: T[]): boolean => { + return !allowList || allowList.includes(item); +}; + +export const isLimitAllowed = (item: LicenseLimitKind, options: LicenseValidationOptions): boolean => isItemAllowed(item, options.limits); + +export const isBehaviorAllowed = (item: LicenseBehavior, options: LicenseValidationOptions): boolean => + isItemAllowed(item, options.behaviors) && (options.isNewLicense || item !== 'prevent_installation'); diff --git a/ee/packages/license/src/license.ts b/ee/packages/license/src/license.ts index 2bc2e471817e..0468171aa1a9 100644 --- a/ee/packages/license/src/license.ts +++ b/ee/packages/license/src/license.ts @@ -2,13 +2,15 @@ import { Emitter } from '@rocket.chat/emitter'; import type { ILicenseV2 } from './definition/ILicenseV2'; import type { ILicenseV3, LicenseLimitKind } from './definition/ILicenseV3'; -import type { BehaviorWithContext, LicenseBehavior } from './definition/LicenseBehavior'; +import type { BehaviorWithContext } from './definition/LicenseBehavior'; import type { LicenseModule } from './definition/LicenseModule'; +import type { LicenseValidationOptions } from './definition/LicenseValidationOptions'; import type { LimitContext } from './definition/LimitContext'; +import type { LicenseEvents } from './definition/events'; import { DuplicatedLicenseError } from './errors/DuplicatedLicenseError'; import { InvalidLicenseError } from './errors/InvalidLicenseError'; import { NotReadyForValidation } from './errors/NotReadyForValidation'; -import { licenseInvalidated, licenseValidated, limitReached } from './events/emitter'; +import { behaviorTriggered, licenseInvalidated, licenseValidated } from './events/emitter'; import { logger } from './logger'; import { getModules, invalidateAll, replaceModules } from './modules'; import { applyPendingLicense, clearPendingLicense, hasPendingLicense, isPendingLicense, setPendingLicense } from './pendingLicense'; @@ -23,19 +25,9 @@ import { isReadyForValidation } from './validation/isReadyForValidation'; import { runValidation } from './validation/runValidation'; import { validateFormat } from './validation/validateFormat'; -const invalidLicenseBehaviors: LicenseBehavior[] = ['invalidate_license', 'prevent_installation']; -const generalValidationBehaviors: LicenseBehavior[] = ['start_fair_policy', 'disable_modules']; -const behaviorsWithLimitEvents = ['invalidate_license', 'start_fair_policy', 'disable_modules', 'custom', 'prevent_action'] as const; const globalLimitKinds: LicenseLimitKind[] = ['activeUsers', 'guestUsers', 'privateApps', 'marketplaceApps', 'monthlyActiveContacts']; -export class LicenseManager extends Emitter< - Record<`limitReached:${Exclude}:${Exclude}`, undefined> & - Record<`${'invalid' | 'valid'}:${LicenseModule}`, undefined> & { - validate: undefined; - invalidate: undefined; - module: { module: LicenseModule; valid: boolean }; - } -> { +export class LicenseManager extends Emitter { dataCounters = new Map) => Promise>(); countersCache = new Map(); @@ -52,12 +44,10 @@ export class LicenseManager extends Emitter< private _valid: boolean | undefined; - private _inFairPolicy: boolean | undefined; + private _inFairPolicy = false; private _lockedLicense: string | undefined; - private _accessedLimits = new Set>(); - public get license(): ILicenseV3 | undefined { return this._license; } @@ -71,7 +61,7 @@ export class LicenseManager extends Emitter< } public get inFairPolicy(): boolean { - return Boolean(this._inFairPolicy); + return this._inFairPolicy; } public async setWorkspaceUrl(url: string) { @@ -86,29 +76,6 @@ export class LicenseManager extends Emitter< return this.workspaceUrl; } - public async revalidateLimits(): Promise { - this.countersCache.clear(); - await this.triggerLimitEvents(); - } - - private async triggerLimitEvents(): Promise { - const license = this.getLicense(); - if (!license) { - return; - } - - const limits = [...this._accessedLimits]; - this._accessedLimits.clear(); - - for await (const limit of limits) { - for await (const behavior of behaviorsWithLimitEvents) { - if (this.has(`limitReached:${limit}:${behavior}`) && (await this.isLimitReached(limit, [behavior], undefined, 0, false))) { - limitReached.call(this, limit, behavior); - } - } - } - } - public async revalidateLicense(): Promise { if (!this.hasValidLicense()) { return; @@ -116,7 +83,7 @@ export class LicenseManager extends Emitter< try { this.countersCache.clear(); - await this.validateLicense(false); + await this.validateLicense({ isNewLicense: false }); } finally { this.maybeInvalidateLicense(); } @@ -125,7 +92,7 @@ export class LicenseManager extends Emitter< private clearLicenseData(): void { this._license = undefined; this._unmodifiedLicense = undefined; - this._inFairPolicy = undefined; + this._inFairPolicy = false; this._valid = false; this._lockedLicense = undefined; this.countersCache.clear(); @@ -149,7 +116,7 @@ export class LicenseManager extends Emitter< this._unmodifiedLicense = originalLicense || newLicense; this._license = newLicense; - await this.validateLicense(encryptedLicense !== this._lockedLicense); + await this.validateLicense({ isNewLicense: encryptedLicense !== this._lockedLicense }); this._lockedLicense = encryptedLicense; if (this.valid) { @@ -171,7 +138,7 @@ export class LicenseManager extends Emitter< return Boolean(this._lockedLicense && this._lockedLicense === encryptedLicense); } - private async validateLicenseBehaviors(behaviorsToConsider: LicenseBehavior[]): Promise { + private async validateLicense(options: LicenseValidationOptions = {}): Promise { if (!this._license) { throw new InvalidLicenseError(); } @@ -180,34 +147,9 @@ export class LicenseManager extends Emitter< throw new NotReadyForValidation(); } - // Run the `invalidate_license` behavior first and skip everything else if it's already invalid. - const validationResult = await runValidation.call( - this, - this._license, - behaviorsToConsider.filter((behavior) => invalidLicenseBehaviors.includes(behavior)), - ); - - if (isBehaviorsInResult(validationResult, invalidLicenseBehaviors)) { - this._valid = false; - return; - } - - const generalResult = await runValidation.call( - this, - this._license, - behaviorsToConsider.filter((behavior) => generalValidationBehaviors.includes(behavior)), - ); - - this.processValidationResult(generalResult, behaviorsToConsider.includes('prevent_installation')); - } - - private async validateLicense(isNewLicense: boolean): Promise { - return this.validateLicenseBehaviors([ - 'invalidate_license', - 'start_fair_policy', - 'disable_modules', - ...(isNewLicense ? ['prevent_installation' as LicenseBehavior] : []), - ]); + const validationResult = await runValidation.call(this, this._license, options); + this.processValidationResult(validationResult, options); + this.triggerBehaviorEvents(validationResult); } public async setLicense(encryptedLicense: string): Promise { @@ -254,16 +196,18 @@ export class LicenseManager extends Emitter< } } - private processValidationResult(result: BehaviorWithContext[], isNewLicense: boolean): void { - if (!this._license || isBehaviorsInResult(result, invalidLicenseBehaviors)) { + private processValidationResult(result: BehaviorWithContext[], options: LicenseValidationOptions): void { + if (!this._license || isBehaviorsInResult(result, ['invalidate_license', 'prevent_installation'])) { this._valid = false; return; } - const shouldLogModules = !this._valid || isNewLicense; + const shouldLogModules = !this._valid || options.isNewLicense; this._valid = true; - this._inFairPolicy = isBehaviorsInResult(result, ['start_fair_policy']); + if (isBehaviorsInResult(result, ['start_fair_policy'])) { + this._inFairPolicy = true; + } if (this._license.information.tags) { replaceTags(this._license.information.tags); @@ -282,6 +226,12 @@ export class LicenseManager extends Emitter< } } + private triggerBehaviorEvents(validationResult: BehaviorWithContext[]): void { + for (const { behavior, reason, limit } of validationResult) { + behaviorTriggered.call(this, { behavior, reason, limit }); + } + } + public hasValidLicense(): boolean { return Boolean(this.getLicense()); } @@ -294,37 +244,32 @@ export class LicenseManager extends Emitter< public async shouldPreventAction( action: T, - context?: Partial>, + context: Partial> = {}, newCount = 1, - ): Promise { - return this._isLimitReached(action, ['prevent_action'], context, newCount); - } - - protected async _isLimitReached( - action: T, - behaviorsToConsider?: LicenseBehavior[], - context?: Partial>, - extraCount = 0, - flagAsAccessed = true, + { suppressLog }: Pick = {}, ): Promise { const license = this.getLicense(); if (!license) { return false; } - if (action !== 'roomsPerGuest' && flagAsAccessed) { - this._accessedLimits.add(action); - } + const options: LicenseValidationOptions = { + behaviors: ['prevent_action'], + isNewLicense: false, + suppressLog: !!suppressLog, + context: { + [action]: { + extraCount: newCount, + ...context, + }, + }, + }; - const filteredLimits = license.limits[action]?.filter( - ({ behavior, max }) => max >= 0 && (!behaviorsToConsider || behaviorsToConsider.includes(behavior)), - ); - if (!filteredLimits?.length) { - return false; - } + this.countersCache.clear(); + const validationResult = await runValidation.call(this, license, options); + this.triggerBehaviorEvents(validationResult); - const currentValue = (await getCurrentValueForLicenseLimit.call(this, action, context)) + extraCount; - return Boolean(filteredLimits.some(({ max }) => max < currentValue)); + return isBehaviorsInResult(validationResult, ['prevent_action']); } public async getInfo(loadCurrentValues = false): Promise<{ diff --git a/ee/packages/license/src/validation/getCurrentValueForLicenseLimit.ts b/ee/packages/license/src/validation/getCurrentValueForLicenseLimit.ts index be22c845ada1..28b3b0fe93b3 100644 --- a/ee/packages/license/src/validation/getCurrentValueForLicenseLimit.ts +++ b/ee/packages/license/src/validation/getCurrentValueForLicenseLimit.ts @@ -1,12 +1,9 @@ -import type { IUser } from '@rocket.chat/core-typings'; - import type { LicenseLimitKind } from '../definition/ILicenseV3'; +import type { LimitContext } from '../definition/LimitContext'; import type { LicenseManager } from '../license'; import { logger } from '../logger'; import { applyPendingLicense, hasPendingLicense } from '../pendingLicense'; -type LimitContext = T extends 'roomsPerGuest' ? { userId: IUser['_id'] } : Record; - export function setLicenseLimitCounter( this: LicenseManager, limitKey: T, @@ -30,14 +27,16 @@ export async function getCurrentValueForLicenseLimit throw new Error('Unable to validate license limit due to missing data counter.'); } + const extraCount = context?.extraCount || 0; + if (this.countersCache.has(limitKey)) { - return this.countersCache.get(limitKey) as number; + return (this.countersCache.get(limitKey) as number) + extraCount; } const count = await counterFn(context as LimitContext | undefined); this.countersCache.set(limitKey, count); - return count; + return count + extraCount; } export function hasAllDataCounters(this: LicenseManager) { diff --git a/ee/packages/license/src/validation/getResultingBehavior.ts b/ee/packages/license/src/validation/getResultingBehavior.ts index 47e2d91b8b89..22ca02bfd220 100644 --- a/ee/packages/license/src/validation/getResultingBehavior.ts +++ b/ee/packages/license/src/validation/getResultingBehavior.ts @@ -1,8 +1,12 @@ +import type { LicenseLimitKind } from '../definition/ILicenseV3'; import type { BehaviorWithContext } from '../definition/LicenseBehavior'; import type { LicenseLimit } from '../definition/LicenseLimit'; import type { LicensePeriod } from '../definition/LicensePeriod'; -export const getResultingBehavior = (data: LicenseLimit | LicensePeriod | Partial): BehaviorWithContext => { +export const getResultingBehavior = ( + data: LicenseLimit | LicensePeriod | Partial>, + { reason, limit }: { reason: BehaviorWithContext['reason']; limit?: LicenseLimitKind }, +): BehaviorWithContext => { const behavior = 'invalidBehavior' in data ? data.invalidBehavior : data.behavior; switch (behavior) { @@ -10,11 +14,15 @@ export const getResultingBehavior = (data: LicenseLimit | LicensePeriod | Partia return { behavior, modules: ('modules' in data && data.modules) || [], + reason, + limit, }; default: return { behavior, + reason, + limit, } as BehaviorWithContext; } }; diff --git a/ee/packages/license/src/validation/runValidation.spec.ts b/ee/packages/license/src/validation/runValidation.spec.ts index 98797c86cd27..523090acd63a 100644 --- a/ee/packages/license/src/validation/runValidation.spec.ts +++ b/ee/packages/license/src/validation/runValidation.spec.ts @@ -22,16 +22,16 @@ describe('Validation behaviors', () => { }); await expect( - runValidation.call(licenseManager, await license.build(), [ - 'invalidate_license', - 'prevent_installation', - 'start_fair_policy', - 'disable_modules', - ]), + runValidation.call(licenseManager, await license.build(), { + behaviors: ['invalidate_license', 'prevent_installation', 'start_fair_policy', 'disable_modules'], + suppressLog: true, + }), ).resolves.toStrictEqual([ { behavior: 'disable_modules', + limit: undefined, modules: ['livechat-enterprise'], + reason: 'period', }, ]); }); diff --git a/ee/packages/license/src/validation/runValidation.ts b/ee/packages/license/src/validation/runValidation.ts index 50fb8337d483..922b4c49162e 100644 --- a/ee/packages/license/src/validation/runValidation.ts +++ b/ee/packages/license/src/validation/runValidation.ts @@ -1,5 +1,6 @@ -import type { ILicenseV3, LicenseLimitKind } from '../definition/ILicenseV3'; -import type { LicenseBehavior, BehaviorWithContext } from '../definition/LicenseBehavior'; +import type { ILicenseV3 } from '../definition/ILicenseV3'; +import type { BehaviorWithContext } from '../definition/LicenseBehavior'; +import type { LicenseValidationOptions } from '../definition/LicenseValidationOptions'; import type { LicenseManager } from '../license'; import { validateLicenseLimits } from './validateLicenseLimits'; import { validateLicensePeriods } from './validateLicensePeriods'; @@ -8,15 +9,11 @@ import { validateLicenseUrl } from './validateLicenseUrl'; export async function runValidation( this: LicenseManager, license: ILicenseV3, - behaviorsToValidate?: LicenseBehavior[], - limitsToValidate?: LicenseLimitKind[], + options: LicenseValidationOptions, ): Promise { - const shouldValidateBehavior = (behavior: LicenseBehavior) => !behaviorsToValidate || behaviorsToValidate.includes(behavior); - const shouldValidateLimit = (limit: LicenseLimitKind) => !limitsToValidate || limitsToValidate.includes(limit); - return [ - ...validateLicenseUrl.call(this, license, shouldValidateBehavior), - ...validateLicensePeriods(license, shouldValidateBehavior), - ...(await validateLicenseLimits.call(this, license, shouldValidateBehavior, shouldValidateLimit)), + ...validateLicenseUrl.call(this, license, options), + ...validateLicensePeriods(license, options), + ...(await validateLicenseLimits.call(this, license, options)), ]; } diff --git a/ee/packages/license/src/validation/validateLicenseLimits.ts b/ee/packages/license/src/validation/validateLicenseLimits.ts index 0bc8223a74b0..cd2674350946 100644 --- a/ee/packages/license/src/validation/validateLicenseLimits.ts +++ b/ee/packages/license/src/validation/validateLicenseLimits.ts @@ -1,5 +1,7 @@ import type { ILicenseV3, LicenseLimitKind } from '../definition/ILicenseV3'; -import type { BehaviorWithContext, LicenseBehavior } from '../definition/LicenseBehavior'; +import type { BehaviorWithContext } from '../definition/LicenseBehavior'; +import type { LicenseValidationOptions } from '../definition/LicenseValidationOptions'; +import { isLimitAllowed, isBehaviorAllowed } from '../isItemAllowed'; import type { LicenseManager } from '../license'; import { logger } from '../logger'; import { getCurrentValueForLicenseLimit } from './getCurrentValueForLicenseLimit'; @@ -8,31 +10,34 @@ import { getResultingBehavior } from './getResultingBehavior'; export async function validateLicenseLimits( this: LicenseManager, license: ILicenseV3, - behaviorFilter: (behavior: LicenseBehavior) => boolean, - limitFilter: (limit: LicenseLimitKind) => boolean, + options: LicenseValidationOptions, ): Promise { const { limits } = license; - const limitKeys = (Object.keys(limits) as LicenseLimitKind[]).filter(limitFilter || (() => true)); + const limitKeys = (Object.keys(limits) as LicenseLimitKind[]).filter((limit) => isLimitAllowed(limit, options)); return ( await Promise.all( limitKeys.map(async (limitKey) => { // Filter the limit list before running any query in the database so we don't end up loading some value we won't use. - const limitList = limits[limitKey]?.filter(({ behavior, max }) => max >= 0 && behaviorFilter(behavior)); + const limitList = limits[limitKey]?.filter(({ behavior, max }) => max >= 0 && isBehaviorAllowed(behavior, options)); if (!limitList?.length) { return []; } - const currentValue = await getCurrentValueForLicenseLimit.call(this, limitKey); + const currentValue = await getCurrentValueForLicenseLimit.call(this, limitKey, options.context?.[limitKey]); + return limitList .filter(({ max }) => max < currentValue) .map((limit) => { - logger.error({ - msg: 'Limit validation failed', - kind: limitKey, - limit, - }); - return getResultingBehavior(limit); + if (!options.suppressLog) { + logger.error({ + msg: 'Limit validation failed', + kind: limitKey, + limit, + }); + } + + return getResultingBehavior(limit, { reason: 'limit', limit: limitKey }); }); }), ) diff --git a/ee/packages/license/src/validation/validateLicensePeriods.ts b/ee/packages/license/src/validation/validateLicensePeriods.ts index 5b3fae433e38..fb27f72d0a8e 100644 --- a/ee/packages/license/src/validation/validateLicensePeriods.ts +++ b/ee/packages/license/src/validation/validateLicensePeriods.ts @@ -1,6 +1,8 @@ import type { ILicenseV3 } from '../definition/ILicenseV3'; -import type { BehaviorWithContext, LicenseBehavior } from '../definition/LicenseBehavior'; +import type { BehaviorWithContext } from '../definition/LicenseBehavior'; import type { Timestamp } from '../definition/LicensePeriod'; +import type { LicenseValidationOptions } from '../definition/LicenseValidationOptions'; +import { isBehaviorAllowed } from '../isItemAllowed'; import { logger } from '../logger'; import { getResultingBehavior } from './getResultingBehavior'; @@ -18,21 +20,23 @@ export const isPeriodInvalid = (from: Timestamp | undefined, until: Timestamp | return false; }; -export const validateLicensePeriods = ( - license: ILicenseV3, - behaviorFilter: (behavior: LicenseBehavior) => boolean, -): BehaviorWithContext[] => { +export const validateLicensePeriods = (license: ILicenseV3, options: LicenseValidationOptions): BehaviorWithContext[] => { const { validation: { validPeriods }, } = license; return validPeriods - .filter(({ validFrom, validUntil, invalidBehavior }) => behaviorFilter(invalidBehavior) && isPeriodInvalid(validFrom, validUntil)) + .filter( + ({ validFrom, validUntil, invalidBehavior }) => isBehaviorAllowed(invalidBehavior, options) && isPeriodInvalid(validFrom, validUntil), + ) .map((period) => { - logger.error({ - msg: 'Period validation failed', - period, - }); - return getResultingBehavior(period); + if (!options.suppressLog) { + logger.error({ + msg: 'Period validation failed', + period, + }); + } + + return getResultingBehavior(period, { reason: 'period' }); }); }; diff --git a/ee/packages/license/src/validation/validateLicenseUrl.ts b/ee/packages/license/src/validation/validateLicenseUrl.ts index 210ddb04d80f..69ecdb2e489f 100644 --- a/ee/packages/license/src/validation/validateLicenseUrl.ts +++ b/ee/packages/license/src/validation/validateLicenseUrl.ts @@ -3,7 +3,9 @@ import crypto from 'crypto'; import bcrypt from 'bcrypt'; import type { ILicenseV3 } from '../definition/ILicenseV3'; -import type { BehaviorWithContext, LicenseBehavior } from '../definition/LicenseBehavior'; +import type { BehaviorWithContext } from '../definition/LicenseBehavior'; +import type { LicenseValidationOptions } from '../definition/LicenseValidationOptions'; +import { isBehaviorAllowed } from '../isItemAllowed'; import type { LicenseManager } from '../license'; import { logger } from '../logger'; import { getResultingBehavior } from './getResultingBehavior'; @@ -26,12 +28,8 @@ const validateHash = (licenseURL: string, url: string) => { return bcrypt.compareSync(value, licenseURL); }; -export function validateLicenseUrl( - this: LicenseManager, - license: ILicenseV3, - behaviorFilter: (behavior: LicenseBehavior) => boolean, -): BehaviorWithContext[] { - if (!behaviorFilter('invalidate_license')) { +export function validateLicenseUrl(this: LicenseManager, license: ILicenseV3, options: LicenseValidationOptions): BehaviorWithContext[] { + if (!isBehaviorAllowed('invalidate_license', options)) { return []; } @@ -43,7 +41,7 @@ export function validateLicenseUrl( if (!workspaceUrl) { logger.error('Unable to validate license URL without knowing the workspace URL.'); - return [getResultingBehavior({ behavior: 'invalidate_license' })]; + return [getResultingBehavior({ behavior: 'invalidate_license' }, { reason: 'url' })]; } return serverUrls @@ -65,6 +63,6 @@ export function validateLicenseUrl( url, workspaceUrl, }); - return getResultingBehavior({ behavior: 'invalidate_license' }); + return getResultingBehavior({ behavior: 'invalidate_license' }, { reason: 'url' }); }); } From d6b72c941188fa886232e5faf6ff324585fd7dec Mon Sep 17 00:00:00 2001 From: Pierre Lehnen Date: Thu, 5 Oct 2023 17:25:40 -0300 Subject: [PATCH 05/11] skip triggering events when the license is added for the first time --- ee/packages/license/src/index.ts | 2 +- ee/packages/license/src/license.ts | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/ee/packages/license/src/index.ts b/ee/packages/license/src/index.ts index 66dd014e790c..fe5f3a84a12f 100644 --- a/ee/packages/license/src/index.ts +++ b/ee/packages/license/src/index.ts @@ -4,7 +4,7 @@ import type { LimitContext } from './definition/LimitContext'; import { getAppsConfig, getMaxActiveUsers, getUnmodifiedLicenseAndModules } from './deprecated'; import { onLicense } from './events/deprecated'; import { - onBehaviorTriggered, + onBehaviorTriggered, onInvalidFeature, onInvalidateLicense, onLimitReached, diff --git a/ee/packages/license/src/license.ts b/ee/packages/license/src/license.ts index 0468171aa1a9..a38b2273d07e 100644 --- a/ee/packages/license/src/license.ts +++ b/ee/packages/license/src/license.ts @@ -76,14 +76,14 @@ export class LicenseManager extends Emitter { return this.workspaceUrl; } - public async revalidateLicense(): Promise { + public async revalidateLicense(options: Omit = {}): Promise { if (!this.hasValidLicense()) { return; } try { this.countersCache.clear(); - await this.validateLicense({ isNewLicense: false }); + await this.validateLicense({ ...options, isNewLicense: false }); } finally { this.maybeInvalidateLicense(); } @@ -149,7 +149,9 @@ export class LicenseManager extends Emitter { const validationResult = await runValidation.call(this, this._license, options); this.processValidationResult(validationResult, options); - this.triggerBehaviorEvents(validationResult); + if (!options.isNewLicense) { + this.triggerBehaviorEvents(validationResult); + } } public async setLicense(encryptedLicense: string): Promise { From 3293503e0325feabec3001f357db8442d30553d9 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Thu, 5 Oct 2023 19:38:56 -0300 Subject: [PATCH 06/11] validate url unit tests --- .../src/validation/validateLicenseUrl.spec.ts | 130 ++++++++++++++++++ .../src/validation/validateLicenseUrl.ts | 5 +- 2 files changed, 131 insertions(+), 4 deletions(-) create mode 100644 ee/packages/license/src/validation/validateLicenseUrl.spec.ts diff --git a/ee/packages/license/src/validation/validateLicenseUrl.spec.ts b/ee/packages/license/src/validation/validateLicenseUrl.spec.ts new file mode 100644 index 000000000000..751dfcc2cc06 --- /dev/null +++ b/ee/packages/license/src/validation/validateLicenseUrl.spec.ts @@ -0,0 +1,130 @@ +/** + * @jest-environment node + */ + +import bcrypt from 'bcrypt'; + +import { MockedLicenseBuilder, getReadyLicenseManager } from '../../__tests__/MockedLicenseBuilder'; +import { validateLicenseUrl } from './validateLicenseUrl'; + +describe('Url Validation', () => { + describe('url method', () => { + it('should return a behavior if the license url is invalid', async () => { + const licenseManager = await getReadyLicenseManager(); + + const license = await new MockedLicenseBuilder().withServerUrls({ + value: 'localhost:3001', + type: 'url', + }); + + await expect( + validateLicenseUrl.call(licenseManager, await license.build(), { + behaviors: ['invalidate_license', 'prevent_installation', 'start_fair_policy', 'disable_modules'], + suppressLog: true, + }), + ).toStrictEqual([ + { + behavior: 'invalidate_license', + limit: undefined, + reason: 'url', + }, + ]); + }); + + it('should return an empty array if the license url is valid', async () => { + const licenseManager = await getReadyLicenseManager(); + + const license = await new MockedLicenseBuilder().withServerUrls({ + value: 'localhost:3000', + type: 'url', + }); + + await expect( + validateLicenseUrl.call(licenseManager, await license.build(), { + behaviors: ['invalidate_license', 'prevent_installation', 'start_fair_policy', 'disable_modules'], + suppressLog: true, + }), + ).toStrictEqual([]); + }); + }); + + describe('regex method', () => { + it('should return a behavior if the license does not match the regex', async () => { + const licenseManager = await getReadyLicenseManager(); + + const license = await new MockedLicenseBuilder().withServerUrls({ + value: 'unstable.rocket.*', + type: 'regex', + }); + + await expect( + validateLicenseUrl.call(licenseManager, await license.build(), { + behaviors: ['invalidate_license', 'prevent_installation', 'start_fair_policy', 'disable_modules'], + suppressLog: true, + }), + ).toStrictEqual([ + { + behavior: 'invalidate_license', + limit: undefined, + reason: 'url', + }, + ]); + }); + + it('should return an empty array if the license matches the regex', async () => { + const licenseManager = await getReadyLicenseManager(); + + const license = await new MockedLicenseBuilder().withServerUrls({ + value: 'localhost:300*', + type: 'regex', + }); + + await expect( + validateLicenseUrl.call(licenseManager, await license.build(), { + behaviors: ['invalidate_license', 'prevent_installation', 'start_fair_policy', 'disable_modules'], + suppressLog: true, + }), + ).toStrictEqual([]); + }); + }); + + describe('hash method', () => { + it('should return a behavior if the license does not match the hash', async () => { + const licenseManager = await getReadyLicenseManager(); + const hash = bcrypt.hashSync('localhost:3001', 10); + const license = await new MockedLicenseBuilder().withServerUrls({ + value: hash, + type: 'hash', + }); + + await expect( + validateLicenseUrl.call(licenseManager, await license.build(), { + behaviors: ['invalidate_license', 'prevent_installation', 'start_fair_policy', 'disable_modules'], + suppressLog: true, + }), + ).toStrictEqual([ + { + behavior: 'invalidate_license', + limit: undefined, + reason: 'url', + }, + ]); + }); + it('should return an empty array if the license matches the hash', async () => { + const licenseManager = await getReadyLicenseManager(); + + const hash = bcrypt.hashSync('localhost:3000', 10); + + const license = await new MockedLicenseBuilder().withServerUrls({ + value: hash, + type: 'hash', + }); + await expect( + validateLicenseUrl.call(licenseManager, await license.build(), { + behaviors: ['invalidate_license', 'prevent_installation', 'start_fair_policy', 'disable_modules'], + suppressLog: true, + }), + ).toStrictEqual([]); + }); + }); +}); diff --git a/ee/packages/license/src/validation/validateLicenseUrl.ts b/ee/packages/license/src/validation/validateLicenseUrl.ts index 69ecdb2e489f..b0710f55a51d 100644 --- a/ee/packages/license/src/validation/validateLicenseUrl.ts +++ b/ee/packages/license/src/validation/validateLicenseUrl.ts @@ -1,5 +1,3 @@ -import crypto from 'crypto'; - import bcrypt from 'bcrypt'; import type { ILicenseV3 } from '../definition/ILicenseV3'; @@ -24,8 +22,7 @@ const validateUrl = (licenseURL: string, url: string) => { }; const validateHash = (licenseURL: string, url: string) => { - const value = crypto.createHash('sha256').update(url).digest('hex'); - return bcrypt.compareSync(value, licenseURL); + return bcrypt.compareSync(url, licenseURL); }; export function validateLicenseUrl(this: LicenseManager, license: ILicenseV3, options: LicenseValidationOptions): BehaviorWithContext[] { From 02b3ab0ef8e382b9f4f0d5478ded8523ddb97796 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Thu, 5 Oct 2023 19:40:47 -0300 Subject: [PATCH 07/11] remove duplicated setWorkspaceUrl --- ee/packages/license/__tests__/MockedLicenseBuilder.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/ee/packages/license/__tests__/MockedLicenseBuilder.ts b/ee/packages/license/__tests__/MockedLicenseBuilder.ts index 316261744da5..4f2b49596be3 100644 --- a/ee/packages/license/__tests__/MockedLicenseBuilder.ts +++ b/ee/packages/license/__tests__/MockedLicenseBuilder.ts @@ -197,7 +197,6 @@ export class MockedLicenseBuilder { export const getReadyLicenseManager = async () => { const license = new LicenseImp(); await license.setWorkspaceUrl('http://localhost:3000'); - await license.setWorkspaceUrl('http://localhost:3000'); license.setLicenseLimitCounter('activeUsers', () => 0); license.setLicenseLimitCounter('guestUsers', () => 0); From 76258fcf4e86a74d1b4b15d0296a733948925742 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Thu, 5 Oct 2023 19:49:44 -0300 Subject: [PATCH 08/11] fix hash function --- .../src/validation/validateLicenseUrl.spec.ts | 8 ++++---- .../src/validation/validateLicenseUrl.ts | 17 ++++++++++------- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/ee/packages/license/src/validation/validateLicenseUrl.spec.ts b/ee/packages/license/src/validation/validateLicenseUrl.spec.ts index 751dfcc2cc06..9047876f8fbc 100644 --- a/ee/packages/license/src/validation/validateLicenseUrl.spec.ts +++ b/ee/packages/license/src/validation/validateLicenseUrl.spec.ts @@ -2,7 +2,7 @@ * @jest-environment node */ -import bcrypt from 'bcrypt'; +import crypto from 'crypto'; import { MockedLicenseBuilder, getReadyLicenseManager } from '../../__tests__/MockedLicenseBuilder'; import { validateLicenseUrl } from './validateLicenseUrl'; @@ -91,7 +91,8 @@ describe('Url Validation', () => { describe('hash method', () => { it('should return a behavior if the license does not match the hash', async () => { const licenseManager = await getReadyLicenseManager(); - const hash = bcrypt.hashSync('localhost:3001', 10); + + const hash = crypto.createHash('sha256').update('localhost:3001').digest('hex'); const license = await new MockedLicenseBuilder().withServerUrls({ value: hash, type: 'hash', @@ -113,8 +114,7 @@ describe('Url Validation', () => { it('should return an empty array if the license matches the hash', async () => { const licenseManager = await getReadyLicenseManager(); - const hash = bcrypt.hashSync('localhost:3000', 10); - + const hash = crypto.createHash('sha256').update('localhost:3000').digest('hex'); const license = await new MockedLicenseBuilder().withServerUrls({ value: hash, type: 'hash', diff --git a/ee/packages/license/src/validation/validateLicenseUrl.ts b/ee/packages/license/src/validation/validateLicenseUrl.ts index b0710f55a51d..416b107511cb 100644 --- a/ee/packages/license/src/validation/validateLicenseUrl.ts +++ b/ee/packages/license/src/validation/validateLicenseUrl.ts @@ -1,4 +1,4 @@ -import bcrypt from 'bcrypt'; +import crypto from 'crypto'; import type { ILicenseV3 } from '../definition/ILicenseV3'; import type { BehaviorWithContext } from '../definition/LicenseBehavior'; @@ -22,7 +22,8 @@ const validateUrl = (licenseURL: string, url: string) => { }; const validateHash = (licenseURL: string, url: string) => { - return bcrypt.compareSync(url, licenseURL); + const value = crypto.createHash('sha256').update(url).digest('hex'); + return licenseURL === value; }; export function validateLicenseUrl(this: LicenseManager, license: ILicenseV3, options: LicenseValidationOptions): BehaviorWithContext[] { @@ -55,11 +56,13 @@ export function validateLicenseUrl(this: LicenseManager, license: ILicenseV3, op return false; }) .map((url) => { - logger.error({ - msg: 'Url validation failed', - url, - workspaceUrl, - }); + if (!options.suppressLog) { + logger.error({ + msg: 'Url validation failed', + url, + workspaceUrl, + }); + } return getResultingBehavior({ behavior: 'invalidate_license' }, { reason: 'url' }); }); } From 6c77c22fa9ab7ab910e3fe9a02f58417798fb728 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Thu, 5 Oct 2023 20:11:24 -0300 Subject: [PATCH 09/11] test emit behaviors --- ee/packages/license/__tests__/emitter.spec.ts | 53 +++++++++++++++++++ .../license/src/definition/LicenseBehavior.ts | 18 ++++--- ee/packages/license/src/definition/events.ts | 5 +- ee/packages/license/src/events/emitter.ts | 20 ++++--- 4 files changed, 78 insertions(+), 18 deletions(-) diff --git a/ee/packages/license/__tests__/emitter.spec.ts b/ee/packages/license/__tests__/emitter.spec.ts index 4c7c5a8255d1..6147d12623bc 100644 --- a/ee/packages/license/__tests__/emitter.spec.ts +++ b/ee/packages/license/__tests__/emitter.spec.ts @@ -63,4 +63,57 @@ describe('Event License behaviors', () => { await expect(license.hasValidLicense()).toBe(true); await expect(fn).toBeCalledTimes(1); }); + + describe('behavior:prevent_action event', () => { + it('should emit `behavior:prevent_action` event when the limit is reached', async () => { + const licenseManager = await getReadyLicenseManager(); + const fn = jest.fn(); + + licenseManager.onBehaviorTriggered('prevent_action', fn); + + const license = await new MockedLicenseBuilder().withLimits('activeUsers', [ + { + max: 10, + behavior: 'prevent_action', + }, + ]); + + await expect(licenseManager.setLicense(await license.sign())).resolves.toBe(true); + + licenseManager.setLicenseLimitCounter('activeUsers', () => 10); + + await expect(licenseManager.shouldPreventAction('activeUsers')).resolves.toBe(true); + + await expect(fn).toBeCalledTimes(1); + + await expect(fn).toBeCalledWith({ + reason: 'limit', + limit: 'activeUsers', + }); + }); + + it('should emit `limitReached:activeUsers` event when the limit is reached', async () => { + const licenseManager = await getReadyLicenseManager(); + const fn = jest.fn(); + + licenseManager.onLimitReached('activeUsers', fn); + + const license = await new MockedLicenseBuilder().withLimits('activeUsers', [ + { + max: 10, + behavior: 'prevent_action', + }, + ]); + + await expect(licenseManager.setLicense(await license.sign())).resolves.toBe(true); + + licenseManager.setLicenseLimitCounter('activeUsers', () => 10); + + await expect(licenseManager.shouldPreventAction('activeUsers')).resolves.toBe(true); + + await expect(fn).toBeCalledTimes(1); + + await expect(fn).toBeCalledWith(undefined); + }); + }); }); diff --git a/ee/packages/license/src/definition/LicenseBehavior.ts b/ee/packages/license/src/definition/LicenseBehavior.ts index 0d9a7355294a..8b5af5f3c481 100644 --- a/ee/packages/license/src/definition/LicenseBehavior.ts +++ b/ee/packages/license/src/definition/LicenseBehavior.ts @@ -3,9 +3,15 @@ import type { LicenseModule } from './LicenseModule'; export type LicenseBehavior = 'invalidate_license' | 'start_fair_policy' | 'prevent_action' | 'prevent_installation' | 'disable_modules'; -export type BehaviorWithContext = { - behavior: LicenseBehavior; - modules?: LicenseModule[]; - reason: 'limit' | 'period' | 'url'; - limit?: LicenseLimitKind; -}; +export type BehaviorWithContext = + | { + behavior: LicenseBehavior; + modules?: LicenseModule[]; + reason: 'limit'; + limit?: LicenseLimitKind; + } + | { + behavior: LicenseBehavior; + modules?: LicenseModule[]; + reason: 'period' | 'url'; + }; diff --git a/ee/packages/license/src/definition/events.ts b/ee/packages/license/src/definition/events.ts index 0e525ff36d25..53f3afe846db 100644 --- a/ee/packages/license/src/definition/events.ts +++ b/ee/packages/license/src/definition/events.ts @@ -3,10 +3,7 @@ import type { BehaviorWithContext, LicenseBehavior } from './LicenseBehavior'; import type { LicenseModule } from './LicenseModule'; type ModuleValidation = Record<`${'invalid' | 'valid'}:${LicenseModule}`, undefined>; -type BehaviorTriggered = Record< - `behavior:${Exclude}`, - { reason: BehaviorWithContext['reason']; limit?: LicenseLimitKind } ->; +type BehaviorTriggered = Record<`behavior:${LicenseBehavior}`, { reason: BehaviorWithContext['reason']; limit?: LicenseLimitKind }>; type LimitReached = Record<`limitReached:${LicenseLimitKind}`, undefined>; export type LicenseEvents = ModuleValidation & diff --git a/ee/packages/license/src/events/emitter.ts b/ee/packages/license/src/events/emitter.ts index 71c6496f7390..9258fb29444c 100644 --- a/ee/packages/license/src/events/emitter.ts +++ b/ee/packages/license/src/events/emitter.ts @@ -21,23 +21,27 @@ export function moduleRemoved(this: LicenseManager, module: LicenseModule) { } } -export function behaviorTriggered(this: LicenseManager, { behavior, reason, limit }: BehaviorWithContext) { - if (behavior === 'prevent_installation') { - return; - } - +export function behaviorTriggered(this: LicenseManager, options: BehaviorWithContext) { + const { behavior, reason, modules: _, ...rest } = options; try { - this.emit(`behavior:${behavior}`, { reason, limit }); + this.emit(`behavior:${behavior}`, { + reason, + ...rest, + }); } catch (error) { logger.error({ msg: 'Error running behavior triggered event', error }); } - if (behavior !== 'prevent_action' || reason !== 'limit' || !limit) { + if (behavior !== 'prevent_action') { + return; + } + + if (reason !== 'limit' || !(`limit` in rest) || !rest.limit) { return; } try { - this.emit(`limitReached:${limit}`); + this.emit(`limitReached:${rest.limit}`); } catch (error) { logger.error({ msg: 'Error running limit reached event', error }); } From 34e1ff098b116e452ee392c7091fb3e077a48bcc Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Thu, 5 Oct 2023 20:15:35 -0300 Subject: [PATCH 10/11] remove counter cache --- ee/packages/license/src/license.ts | 9 ++------- .../src/validation/getCurrentValueForLicenseLimit.ts | 5 ----- 2 files changed, 2 insertions(+), 12 deletions(-) diff --git a/ee/packages/license/src/license.ts b/ee/packages/license/src/license.ts index a38b2273d07e..382f751603a5 100644 --- a/ee/packages/license/src/license.ts +++ b/ee/packages/license/src/license.ts @@ -30,8 +30,6 @@ const globalLimitKinds: LicenseLimitKind[] = ['activeUsers', 'guestUsers', 'priv export class LicenseManager extends Emitter { dataCounters = new Map) => Promise>(); - countersCache = new Map(); - pendingLicense = ''; modules = new Set(); @@ -82,7 +80,6 @@ export class LicenseManager extends Emitter { } try { - this.countersCache.clear(); await this.validateLicense({ ...options, isNewLicense: false }); } finally { this.maybeInvalidateLicense(); @@ -95,7 +92,6 @@ export class LicenseManager extends Emitter { this._inFairPolicy = false; this._valid = false; this._lockedLicense = undefined; - this.countersCache.clear(); clearPendingLicense.call(this); } @@ -229,8 +225,8 @@ export class LicenseManager extends Emitter { } private triggerBehaviorEvents(validationResult: BehaviorWithContext[]): void { - for (const { behavior, reason, limit } of validationResult) { - behaviorTriggered.call(this, { behavior, reason, limit }); + for (const { ...options } of validationResult) { + behaviorTriggered.call(this, { ...options }); } } @@ -267,7 +263,6 @@ export class LicenseManager extends Emitter { }, }; - this.countersCache.clear(); const validationResult = await runValidation.call(this, license, options); this.triggerBehaviorEvents(validationResult); diff --git a/ee/packages/license/src/validation/getCurrentValueForLicenseLimit.ts b/ee/packages/license/src/validation/getCurrentValueForLicenseLimit.ts index 28b3b0fe93b3..60443cc408bb 100644 --- a/ee/packages/license/src/validation/getCurrentValueForLicenseLimit.ts +++ b/ee/packages/license/src/validation/getCurrentValueForLicenseLimit.ts @@ -29,12 +29,7 @@ export async function getCurrentValueForLicenseLimit const extraCount = context?.extraCount || 0; - if (this.countersCache.has(limitKey)) { - return (this.countersCache.get(limitKey) as number) + extraCount; - } - const count = await counterFn(context as LimitContext | undefined); - this.countersCache.set(limitKey, count); return count + extraCount; } From f3f47060620e75ec84b882d7dd528b2596616548 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Thu, 5 Oct 2023 20:22:20 -0300 Subject: [PATCH 11/11] refactor: remove maybe invalidate license --- ee/packages/license/src/license.ts | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/ee/packages/license/src/license.ts b/ee/packages/license/src/license.ts index 382f751603a5..4121c70267da 100644 --- a/ee/packages/license/src/license.ts +++ b/ee/packages/license/src/license.ts @@ -82,7 +82,9 @@ export class LicenseManager extends Emitter { try { await this.validateLicense({ ...options, isNewLicense: false }); } finally { - this.maybeInvalidateLicense(); + if (!this.hasValidLicense()) { + this.invalidateLicense(); + } } } @@ -95,11 +97,7 @@ export class LicenseManager extends Emitter { clearPendingLicense.call(this); } - private maybeInvalidateLicense(): void { - if (this.hasValidLicense()) { - return; - } - + private invalidateLicense(): void { licenseInvalidated.call(this); invalidateAll.call(this); } @@ -120,8 +118,8 @@ export class LicenseManager extends Emitter { showLicense.call(this, this._license, this._valid); } } finally { - if (hadValidLicense) { - this.maybeInvalidateLicense(); + if (hadValidLicense && !this.hasValidLicense()) { + this.invalidateLicense(); } } }