diff --git a/apps/meteor/ee/client/views/admin/users/useSeatsCap.ts b/apps/meteor/ee/client/views/admin/users/useSeatsCap.ts index b4a45b49dda..fdee7f0d32a 100644 --- a/apps/meteor/ee/client/views/admin/users/useSeatsCap.ts +++ b/apps/meteor/ee/client/views/admin/users/useSeatsCap.ts @@ -19,7 +19,7 @@ export const useSeatsCap = (): SeatCapProps | undefined => { return { activeUsers: result.data.activeUsers, - maxActiveUsers: result.data.maxActiveUsers ?? Number.POSITIVE_INFINITY, + maxActiveUsers: result.data.maxActiveUsers && result.data.maxActiveUsers > 0 ? result.data.maxActiveUsers : Number.POSITIVE_INFINITY, reload: () => result.refetch(), }; }; diff --git a/apps/meteor/ee/server/api/licenses.ts b/apps/meteor/ee/server/api/licenses.ts index a2e7a75b072..3faac0a0950 100644 --- a/apps/meteor/ee/server/api/licenses.ts +++ b/apps/meteor/ee/server/api/licenses.ts @@ -71,10 +71,10 @@ API.v1.addRoute( { authRequired: true }, { async get() { - const maxActiveUsers = License.getMaxActiveUsers() || null; + const maxActiveUsers = License.getMaxActiveUsers(); const activeUsers = await Users.getActiveLocalUserCount(); - return API.v1.success({ maxActiveUsers, activeUsers }); + return API.v1.success({ maxActiveUsers: maxActiveUsers > 0 ? maxActiveUsers : null, activeUsers }); }, }, ); diff --git a/ee/packages/license/__tests__/DefaultRestrictions.spec.ts b/ee/packages/license/__tests__/DefaultRestrictions.spec.ts new file mode 100644 index 00000000000..e085aa16560 --- /dev/null +++ b/ee/packages/license/__tests__/DefaultRestrictions.spec.ts @@ -0,0 +1,49 @@ +import { LicenseImp } from '../src'; + +describe('Community Restrictions', () => { + describe('Apps from marketplace', () => { + it('should respect the default if there is no license applied', async () => { + const license = new LicenseImp(); + + license.setLicenseLimitCounter('marketplaceApps', () => 1); + + await expect(await license.shouldPreventAction('marketplaceApps')).toBe(false); + + license.setLicenseLimitCounter('marketplaceApps', () => 10); + + await expect(await license.shouldPreventAction('marketplaceApps')).toBe(true); + }); + }); + + describe('Private Apps', () => { + it('should respect the default if there is no license applied', async () => { + const license = new LicenseImp(); + + license.setLicenseLimitCounter('privateApps', () => 1); + + await expect(await license.shouldPreventAction('privateApps')).toBe(false); + + license.setLicenseLimitCounter('privateApps', () => 10); + + await expect(await license.shouldPreventAction('privateApps')).toBe(true); + }); + }); + + describe('Active Users', () => { + it('should respect the default if there is no license applied', async () => { + const license = new LicenseImp(); + + license.setLicenseLimitCounter('activeUsers', () => 1); + + await expect(await license.shouldPreventAction('activeUsers')).toBe(false); + + license.setLicenseLimitCounter('activeUsers', () => 10); + + await expect(await license.shouldPreventAction('activeUsers')).toBe(false); + + license.setLicenseLimitCounter('activeUsers', () => 100000); + + await expect(await license.shouldPreventAction('activeUsers')).toBe(false); + }); + }); +}); diff --git a/ee/packages/license/src/deprecated.ts b/ee/packages/license/src/deprecated.ts index 0a4a6b0f1bb..e9ba3c446f4 100644 --- a/ee/packages/license/src/deprecated.ts +++ b/ee/packages/license/src/deprecated.ts @@ -1,15 +1,13 @@ import type { ILicenseV3, LicenseLimitKind } from './definition/ILicenseV3'; import type { LicenseManager } from './license'; import { getModules } from './modules'; +import { defaultLimits } from './validation/validateDefaultLimits'; -const getLicenseLimit = (license: ILicenseV3 | undefined, kind: LicenseLimitKind) => { - if (!license) { - return; - } +export const getLicenseLimit = (license: ILicenseV3 | undefined, kind: LicenseLimitKind) => { + const limitList = license?.limits[kind] ?? defaultLimits[kind as keyof typeof defaultLimits]; - const limitList = license.limits[kind]; if (!limitList?.length) { - return; + return -1; } return Math.min(...limitList.map(({ max }) => max)); @@ -23,8 +21,8 @@ export function getMaxActiveUsers(this: LicenseManager) { export function getAppsConfig(this: LicenseManager) { return { - maxPrivateApps: getLicenseLimit(this.getLicense(), 'privateApps') ?? 3, - maxMarketplaceApps: getLicenseLimit(this.getLicense(), 'marketplaceApps') ?? 5, + maxPrivateApps: getLicenseLimit(this.getLicense(), 'privateApps'), + maxMarketplaceApps: getLicenseLimit(this.getLicense(), 'marketplaceApps'), }; } diff --git a/ee/packages/license/src/license.ts b/ee/packages/license/src/license.ts index fb290d541cf..a59f4544e5d 100644 --- a/ee/packages/license/src/license.ts +++ b/ee/packages/license/src/license.ts @@ -9,6 +9,7 @@ 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 { getLicenseLimit } from './deprecated'; import { DuplicatedLicenseError } from './errors/DuplicatedLicenseError'; import { InvalidLicenseError } from './errors/InvalidLicenseError'; import { NotReadyForValidation } from './errors/NotReadyForValidation'; @@ -26,6 +27,7 @@ import { getModulesToDisable } from './validation/getModulesToDisable'; import { isBehaviorsInResult } from './validation/isBehaviorsInResult'; import { isReadyForValidation } from './validation/isReadyForValidation'; import { runValidation } from './validation/runValidation'; +import { validateDefaultLimits } from './validation/validateDefaultLimits'; import { validateFormat } from './validation/validateFormat'; import { validateLicenseLimits } from './validation/validateLicenseLimits'; @@ -349,11 +351,6 @@ export class LicenseManager extends Emitter { context: Partial> = {}, { suppressLog }: Pick = {}, ): Promise { - const license = this.getLicense(); - if (!license) { - return false; - } - const options: LicenseValidationOptions = { ...(extraCount && { behaviors: ['prevent_action'] }), isNewLicense: false, @@ -367,6 +364,11 @@ export class LicenseManager extends Emitter { }, }; + const license = this.getLicense(); + if (!license) { + return isBehaviorsInResult(await validateDefaultLimits.call(this, options), ['prevent_action']); + } + const validationResult = await runValidation.call(this, license, options); const shouldPreventAction = isBehaviorsInResult(validationResult, ['prevent_action']); @@ -415,27 +417,24 @@ export class LicenseManager extends Emitter { const license = this.getLicense(); // Get all limits present in the license and their current value - const limits = ( - (license && - includeLimits && + const limits = Object.fromEntries( + (includeLimits && (await Promise.all( globalLimitKinds - .map((limitKey) => ({ - limitKey, - max: Math.max(-1, Math.min(...Array.from(license.limits[limitKey as LicenseLimitKind] || [])?.map(({ max }) => max))), - })) - .filter(({ max }) => max >= 0 && max < Infinity) - .map(async ({ max, limitKey }) => { - return { - [limitKey as LicenseLimitKind]: { - ...(loadCurrentValues ? { value: await getCurrentValueForLicenseLimit.call(this, limitKey as LicenseLimitKind) } : {}), + .map((limitKey) => [limitKey, getLicenseLimit(license, limitKey)] as const) + .filter(([, max]) => max >= 0 && max < Infinity) + .map(async ([limitKey, max]) => { + return [ + limitKey, + { + ...(loadCurrentValues && { value: await getCurrentValueForLicenseLimit.call(this, limitKey) }), max, }, - }; + ]; }), ))) || - [] - ).reduce((prev, curr) => ({ ...prev, ...curr }), {}); + [], + ); return { license: (includeLicense && license) || undefined, diff --git a/ee/packages/license/src/validation/validateDefaultLimits.ts b/ee/packages/license/src/validation/validateDefaultLimits.ts new file mode 100644 index 00000000000..d8b03f216c2 --- /dev/null +++ b/ee/packages/license/src/validation/validateDefaultLimits.ts @@ -0,0 +1,28 @@ +import type { BehaviorWithContext } from '../definition/LicenseBehavior'; +import type { LicenseLimit } from '../definition/LicenseLimit'; +import type { LicenseValidationOptions } from '../definition/LicenseValidationOptions'; +import type { LicenseManager } from '../license'; +import { validateLimits } from './validateLimits'; + +export const defaultLimits: { + privateApps: LicenseLimit[]; + marketplaceApps: LicenseLimit[]; + // monthlyActiveContacts?: LicenseLimit[]; +} = { + privateApps: [ + { + behavior: 'prevent_action', + max: 3, + }, + ], + marketplaceApps: [ + { + behavior: 'prevent_action', + max: 5, + }, + ], +}; + +export async function validateDefaultLimits(this: LicenseManager, options: LicenseValidationOptions): Promise { + return validateLimits.call(this, defaultLimits, options); +} diff --git a/ee/packages/license/src/validation/validateLicenseLimits.ts b/ee/packages/license/src/validation/validateLicenseLimits.ts index f321252ba57..bc6212e11d9 100644 --- a/ee/packages/license/src/validation/validateLicenseLimits.ts +++ b/ee/packages/license/src/validation/validateLicenseLimits.ts @@ -1,11 +1,8 @@ -import type { ILicenseV3, LicenseLimitKind } from '../definition/ILicenseV3'; +import type { ILicenseV3 } from '../definition/ILicenseV3'; 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'; -import { getResultingBehavior } from './getResultingBehavior'; +import { validateLimits } from './validateLimits'; export async function validateLicenseLimits( this: LicenseManager, @@ -14,47 +11,5 @@ export async function validateLicenseLimits( ): Promise { const { limits } = license; - 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 && isBehaviorAllowed(behavior, options)); - if (!limitList?.length) { - return []; - } - - const extraCount = options.context?.[limitKey]?.extraCount ?? 0; - const currentValue = (await getCurrentValueForLicenseLimit.call(this, limitKey, options.context?.[limitKey])) + extraCount; - - return limitList - .filter(({ max, behavior }) => { - switch (behavior) { - case 'invalidate_license': - case 'prevent_installation': - case 'disable_modules': - case 'start_fair_policy': - default: - return currentValue > max; - case 'prevent_action': - /** - * if we are validating the current count the limit should be equal or over the max, if we are validating the future count the limit should be over the max - */ - - return extraCount ? currentValue > max : currentValue >= max; - } - }) - .map((limit) => { - if (!options.suppressLog) { - logger.error({ - msg: 'Limit validation failed', - kind: limitKey, - limit, - }); - } - return getResultingBehavior(limit, { reason: 'limit', limit: limitKey }); - }); - }), - ) - ).flat(); + return validateLimits.call(this, limits, options); } diff --git a/ee/packages/license/src/validation/validateLimit.ts b/ee/packages/license/src/validation/validateLimit.ts new file mode 100644 index 00000000000..5da4334a773 --- /dev/null +++ b/ee/packages/license/src/validation/validateLimit.ts @@ -0,0 +1,28 @@ +import type { LicenseBehavior } from '../definition/LicenseBehavior'; + +/** + * Validates if the current value is over the limit + * @param max The maximum value allowed + * @param currentValue The current value + * @param behavior The behavior to be applied if the limit is reached + * @param extraCount The extra count to be added to the current value + * @returns + * - true if the limit is reached + * - false if the limit is not reached + */ +export function validateLimit(max: number, currentValue: number, behavior: LicenseBehavior, extraCount = 0) { + switch (behavior) { + case 'invalidate_license': + case 'prevent_installation': + case 'disable_modules': + case 'start_fair_policy': + default: + return currentValue > max; + case 'prevent_action': + /** + * if we are validating the current count the limit should be equal or over the max, if we are validating the future count the limit should be over the max + */ + + return extraCount ? currentValue > max : currentValue >= max; + } +} diff --git a/ee/packages/license/src/validation/validateLimits.ts b/ee/packages/license/src/validation/validateLimits.ts new file mode 100644 index 00000000000..d90f5e5f014 --- /dev/null +++ b/ee/packages/license/src/validation/validateLimits.ts @@ -0,0 +1,44 @@ +import type { ILicenseV3, LicenseLimitKind } from '../definition/ILicenseV3'; +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'; +import { getResultingBehavior } from './getResultingBehavior'; +import { validateLimit } from './validateLimit'; + +export async function validateLimits( + this: LicenseManager, + limits: ILicenseV3['limits'], + options: LicenseValidationOptions, +): Promise { + 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 && isBehaviorAllowed(behavior, options)); + if (!limitList?.length) { + return []; + } + + const extraCount = options.context?.[limitKey]?.extraCount ?? 0; + const currentValue = (await getCurrentValueForLicenseLimit.call(this, limitKey, options.context?.[limitKey])) + extraCount; + + return limitList + .filter(({ max, behavior }) => validateLimit(max, currentValue, behavior, extraCount)) + .map((limit) => { + if (!options.suppressLog) { + logger.error({ + msg: 'Limit validation failed', + kind: limitKey, + limit, + }); + } + return getResultingBehavior(limit, { reason: 'limit', limit: limitKey }); + }); + }), + ) + ).flat(); +}