From 22f7e7b9700c4e935c7c566d022833bac23c84a9 Mon Sep 17 00:00:00 2001 From: Luis Mauro Date: Mon, 4 Sep 2023 18:38:36 -0600 Subject: [PATCH 01/38] jwt package, license v3 type --- apps/meteor/package.json | 1 + .../src/ee/ILicense/ILicenseV3.ts | 93 +++++++++++++++++++ packages/jwt/.eslintrc.json | 4 + packages/jwt/__tests__/jwt.spec.ts | 90 ++++++++++++++++++ packages/jwt/jest.config.js | 5 + packages/jwt/package.json | 27 ++++++ packages/jwt/src/index.ts | 18 ++++ packages/jwt/tsconfig.json | 8 ++ yarn.lock | 74 +++++++++++++++ 9 files changed, 320 insertions(+) create mode 100644 packages/core-typings/src/ee/ILicense/ILicenseV3.ts create mode 100644 packages/jwt/.eslintrc.json create mode 100644 packages/jwt/__tests__/jwt.spec.ts create mode 100644 packages/jwt/jest.config.js create mode 100644 packages/jwt/package.json create mode 100644 packages/jwt/src/index.ts create mode 100644 packages/jwt/tsconfig.json diff --git a/apps/meteor/package.json b/apps/meteor/package.json index 5dea47ff3a10..0605852486c4 100644 --- a/apps/meteor/package.json +++ b/apps/meteor/package.json @@ -246,6 +246,7 @@ "@rocket.chat/i18n": "workspace:^", "@rocket.chat/icons": "^0.32.0", "@rocket.chat/instance-status": "workspace:^", + "@rocket.chat/jwt": "workspace:^", "@rocket.chat/layout": "next", "@rocket.chat/log-format": "workspace:^", "@rocket.chat/logger": "workspace:^", diff --git a/packages/core-typings/src/ee/ILicense/ILicenseV3.ts b/packages/core-typings/src/ee/ILicense/ILicenseV3.ts new file mode 100644 index 000000000000..0cdfac5c75e5 --- /dev/null +++ b/packages/core-typings/src/ee/ILicense/ILicenseV3.ts @@ -0,0 +1,93 @@ +export type LicenseBehavior = 'invalidate_license' | 'start_fair_policy' | 'prevent_action' | 'prevent_installation'; + +export type LicenseLimit = { + max: number; + behavior: LicenseBehavior; +}; + +export type Timestamp = string; + +export type LicensePeriod = { + validFrom?: Timestamp; + validUntil?: Timestamp; + invalidBehavior: LicenseBehavior; +} & ({ validFrom: Timestamp } | { validUntil: Timestamp }); + +export type Module = + | 'auditing' + | 'canned-responses' + | 'ldap-enterprise' + | 'livechat-enterprise' + | 'voip-enterprise' + | 'omnichannel-mobile-enterprise' + | 'engagement-dashboard' + | 'push-privacy' + | 'scalability' + | 'teams-mention' + | 'saml-enterprise' + | 'oauth-enterprise' + | 'device-management' + | 'federation' + | 'videoconference-enterprise' + | 'message-read-receipt' + | 'outlook-calendar'; + +export default interface ILicense { + version: '3.0'; + information: { + id?: string; + autoRenew: boolean; + visualExpiration: Timestamp; + notifyAdminsAt?: Timestamp; + notifyUsersAt?: Timestamp; + trial: boolean; + offline: boolean; + createdAt: Timestamp; + grantedBy: { + method: 'manual' | 'self-service' | 'sales' | 'support' | 'reseller'; + seller?: string; + }; + grantedTo?: { + name?: string; + company?: string; + email?: string; + }; + legalText?: string; + notes?: string; + tags?: { + name: string; + color: string; + }[]; + }; + validation: { + serverUrls: { + value: string; + type: 'url' | 'regex' | 'hash'; + }[]; + serverVersions?: { + value: string; + }[]; + serverUniqueId?: string; + cloudWorkspaceId?: string; + validPeriods: LicensePeriod[]; + legalTextAgreement?: { + type: 'required' | 'not-required' | 'accepted'; + acceptedVia?: 'cloud'; + }; + statisticsReport: { + required: boolean; + allowedStaleInDays?: number; + }; + }; + grantedModules: { + module: Module; + }[]; + limits: { + activeUsers?: LicenseLimit[]; + guestUsers?: LicenseLimit[]; + roomsPerGuest?: LicenseLimit[]; + privateApps?: LicenseLimit[]; + marketplaceApps?: LicenseLimit[]; + }; + cloudMeta?: Record; +} diff --git a/packages/jwt/.eslintrc.json b/packages/jwt/.eslintrc.json new file mode 100644 index 000000000000..a83aeda48e66 --- /dev/null +++ b/packages/jwt/.eslintrc.json @@ -0,0 +1,4 @@ +{ + "extends": ["@rocket.chat/eslint-config"], + "ignorePatterns": ["**/dist"] +} diff --git a/packages/jwt/__tests__/jwt.spec.ts b/packages/jwt/__tests__/jwt.spec.ts new file mode 100644 index 000000000000..302cfdf22c96 --- /dev/null +++ b/packages/jwt/__tests__/jwt.spec.ts @@ -0,0 +1,90 @@ +import { generateKeyPair, exportPKCS8, exportSPKI } from 'jose'; + +import { sign, verify } from '../src/index'; + +it('should sign and verify a jwt with RS256', async () => { + const { publicKey, privateKey } = await generateKeyPair('RS256'); + const spki = await exportSPKI(publicKey); + const pkcs8 = await exportPKCS8(privateKey); + + const licenseV3 = { + information: { + id: '64d28d096400df50b6ace670', + autoRenew: true, + createdAt: '2023-08-08T18:44:25.719+0000', + visualExpiration: '2024-09-08T18:44:25.719+0000', + notifyAdminsAt: '2024-09-01T18:44:25.719+0000', + notifyUsersAt: '2024-09-05T18:44:25.719+0000', + trial: false, + offline: false, + grantedBy: { method: 'manual', seller: 'john.rocketseed@rocket.chat' }, + grantedTo: { name: 'Alice Clientseed', company: 'Client', email: 'alice.clientseed@client.com' }, + legalText: "This license can't be used for reselling", + notes: 'Plan Premium', + tags: [{ name: 'Enterprise', color: '#CCCCCC' }], + }, + validation: { + serverUrls: [{ value: 'https://localhost:3000', type: 'url' }], + serverVersions: [{ value: '6.4' }], + cloudWorkspaceId: 'alks-a9sj0diba09shdiasodjha9s0diha9s9duabsiuhdai0sdh0a9hs09da09s8d09a80s9d8', + serverUniqueId: '64d28d096400df50b6ace670', + validUntil: '2024-09-18T18:44:25.719+0000', + validFrom: '2024-07-08T18:44:25.719+0000', + installationAllowedUntil: '2024-07-09T18:44:25.719+0000', + legalTextAgreement: { type: 'accepted', acceptedVia: 'cloud' }, + statisticsReport: { required: true, allowedStaleInDays: 5 }, + }, + grantedModules: [ + { module: 'auditing' }, + { module: 'canned-responses' }, + { module: 'ldap-enterprise' }, + { module: 'livechat-enterprise' }, + { module: 'voip-enterprise' }, + { module: 'omnichannel-mobile-enterprise' }, + { module: 'engagement-dashboard' }, + { module: 'push-privacy' }, + { module: 'scalability' }, + { module: 'teams-mention' }, + { module: 'saml-enterprise' }, + { module: 'oauth-enterprise' }, + { module: 'device-management' }, + { module: 'federation' }, + { module: 'videoconference-enterprise' }, + { module: 'message-read-receipt' }, + { module: 'outlook-calendar' }, + ], + limits: { + activeUsers: [ + { max: 500, behavior: 'start_fair_policy' }, + { max: 1000, behavior: 'prevent_action' }, + { max: 1100, behavior: 'invalidate_license' }, + ], + guestUsers: [ + { max: 200, behavior: 'start_fair_policy' }, + { max: 400, behavior: 'prevent_action' }, + { max: 500, behavior: 'invalidate_license' }, + ], + roomsPerGuest: [ + { max: 5, behavior: 'start_fair_policy' }, + { max: 10, behavior: 'prevent_action' }, + ], + privateApps: [ + { max: 5, behavior: 'start_fair_policy' }, + { max: 10, behavior: 'prevent_action' }, + { max: 11, behavior: 'invalidate_license' }, + ], + marketplaceApps: [ + { max: 5, behavior: 'start_fair_policy' }, + { max: 10, behavior: 'prevent_action' }, + { max: 11, behavior: 'invalidate_license' }, + ], + }, + cloudMeta: { lastStatisticId: '64d28d096400df50b6ace671' }, + }; + + const token = await sign(licenseV3, pkcs8); + const [payload, protectedHeader] = await verify(token, spki); + + expect(protectedHeader).toEqual({ alg: 'RS256', typ: 'JWT' }); + expect(payload).toEqual(licenseV3); +}); diff --git a/packages/jwt/jest.config.js b/packages/jwt/jest.config.js new file mode 100644 index 000000000000..6231bde11685 --- /dev/null +++ b/packages/jwt/jest.config.js @@ -0,0 +1,5 @@ +/** @type {import('ts-jest').JestConfigWithTsJest} */ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', +}; diff --git a/packages/jwt/package.json b/packages/jwt/package.json new file mode 100644 index 000000000000..b0e73e706e64 --- /dev/null +++ b/packages/jwt/package.json @@ -0,0 +1,27 @@ +{ + "name": "@rocket.chat/jwt", + "version": "0.0.1", + "private": true, + "devDependencies": { + "@types/jest": "~29.5.3", + "eslint": "~8.45.0", + "jest": "~29.6.1", + "ts-jest": "^29.1.1", + "typescript": "~5.1.6" + }, + "scripts": { + "lint": "eslint --ext .js,.jsx,.ts,.tsx .", + "lint:fix": "eslint --ext .js,.jsx,.ts,.tsx . --fix", + "test": "jest", + "build": "rm -rf dist && tsc -p tsconfig.json", + "dev": "tsc -p tsconfig.json --watch --preserveWatchOutput" + }, + "main": "./dist/index.js", + "typings": "./dist/index.d.ts", + "files": [ + "/dist" + ], + "dependencies": { + "jose": "^4.14.4" + } +} diff --git a/packages/jwt/src/index.ts b/packages/jwt/src/index.ts new file mode 100644 index 000000000000..eb43f8b2e75c --- /dev/null +++ b/packages/jwt/src/index.ts @@ -0,0 +1,18 @@ +import { SignJWT, importPKCS8, jwtVerify, importSPKI } from 'jose'; +import type { JWTPayload } from 'jose'; + +export async function sign(keyObject: object, pkcs8: string, alg = 'RS256') { + const privateKey = await importPKCS8(pkcs8, alg); + + const token = await new SignJWT(keyObject as JWTPayload).setProtectedHeader({ alg, typ: 'JWT' }).sign(privateKey); + + return token; +} + +export async function verify(jwt: string, spki: string, alg = 'RS256') { + const publicKey = await importSPKI(spki, alg); + + const { payload, protectedHeader } = await jwtVerify(jwt, publicKey, {}); + + return [payload, protectedHeader]; +} diff --git a/packages/jwt/tsconfig.json b/packages/jwt/tsconfig.json new file mode 100644 index 000000000000..a132d2e280b6 --- /dev/null +++ b/packages/jwt/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.base.server.json", + "compilerOptions": { + "rootDir": "./src", + "outDir": "./dist" + }, + "include": ["./src/**/*"] +} diff --git a/yarn.lock b/yarn.lock index 145079c6eb7a..d1c8a39c4f66 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8365,6 +8365,19 @@ __metadata: languageName: unknown linkType: soft +"@rocket.chat/jwt@workspace:^, @rocket.chat/jwt@workspace:packages/jwt": + version: 0.0.0-use.local + resolution: "@rocket.chat/jwt@workspace:packages/jwt" + dependencies: + "@types/jest": ~29.5.3 + eslint: ~8.45.0 + jest: ~29.6.1 + jose: ^4.14.4 + ts-jest: ^29.1.1 + typescript: ~5.1.6 + languageName: unknown + linkType: soft + "@rocket.chat/layout@npm:next": version: 0.32.0-dev.312 resolution: "@rocket.chat/layout@npm:0.32.0-dev.312" @@ -8587,6 +8600,7 @@ __metadata: "@rocket.chat/i18n": "workspace:^" "@rocket.chat/icons": ^0.32.0 "@rocket.chat/instance-status": "workspace:^" + "@rocket.chat/jwt": "workspace:^" "@rocket.chat/layout": next "@rocket.chat/livechat": "workspace:^" "@rocket.chat/log-format": "workspace:^" @@ -26028,6 +26042,13 @@ __metadata: languageName: node linkType: hard +"jose@npm:^4.14.4": + version: 4.14.6 + resolution: "jose@npm:4.14.6" + checksum: eae81a234e7bf1446b1bd80722b3462b014e3835b155c3a7799c1c5043163a53a0dc28d347004151b031e6b7b863403aabf8814d9cc217ce21f8c2f3ebd4b335 + languageName: node + linkType: hard + "joycon@npm:^3.0.1, joycon@npm:^3.1.1": version: 3.1.1 resolution: "joycon@npm:3.1.1" @@ -37241,6 +37262,39 @@ __metadata: languageName: node linkType: hard +"ts-jest@npm:^29.1.1": + version: 29.1.1 + resolution: "ts-jest@npm:29.1.1" + dependencies: + bs-logger: 0.x + fast-json-stable-stringify: 2.x + jest-util: ^29.0.0 + json5: ^2.2.3 + lodash.memoize: 4.x + make-error: 1.x + semver: ^7.5.3 + yargs-parser: ^21.0.1 + peerDependencies: + "@babel/core": ">=7.0.0-beta.0 <8" + "@jest/types": ^29.0.0 + babel-jest: ^29.0.0 + jest: ^29.0.0 + typescript: ">=4.3 <6" + peerDependenciesMeta: + "@babel/core": + optional: true + "@jest/types": + optional: true + babel-jest: + optional: true + esbuild: + optional: true + bin: + ts-jest: cli.js + checksum: a8c9e284ed4f819526749f6e4dc6421ec666f20ab44d31b0f02b4ed979975f7580b18aea4813172d43e39b29464a71899f8893dd29b06b4a351a3af8ba47b402 + languageName: node + linkType: hard + "ts-jest@npm:~29.0.5": version: 29.0.5 resolution: "ts-jest@npm:29.0.5" @@ -37717,6 +37771,16 @@ __metadata: languageName: node linkType: hard +"typescript@npm:~5.1.6": + version: 5.1.6 + resolution: "typescript@npm:5.1.6" + bin: + tsc: bin/tsc + tsserver: bin/tsserver + checksum: b2f2c35096035fe1f5facd1e38922ccb8558996331405eb00a5111cc948b2e733163cc22fab5db46992aba7dd520fff637f2c1df4996ff0e134e77d3249a7350 + languageName: node + linkType: hard + "typescript@npm:~5.2.2": version: 5.2.2 resolution: "typescript@npm:5.2.2" @@ -37727,6 +37791,16 @@ __metadata: languageName: node linkType: hard +"typescript@patch:typescript@~5.1.6#~builtin": + version: 5.1.6 + resolution: "typescript@patch:typescript@npm%3A5.1.6#~builtin::version=5.1.6&hash=f456af" + bin: + tsc: bin/tsc + tsserver: bin/tsserver + checksum: 21e88b0a0c0226f9cb9fd25b9626fb05b4c0f3fddac521844a13e1f30beb8f14e90bd409a9ac43c812c5946d714d6e0dee12d5d02dfc1c562c5aacfa1f49b606 + languageName: node + linkType: hard + "typescript@patch:typescript@~5.2.2#~builtin": version: 5.2.2 resolution: "typescript@patch:typescript@npm%3A5.2.2#~builtin::version=5.2.2&hash=f456af" From c6ebf8d79bdcf61a085820d8d7b643b99ef5baf4 Mon Sep 17 00:00:00 2001 From: Luis Mauro Date: Tue, 5 Sep 2023 17:29:56 -0600 Subject: [PATCH 02/38] chore: add V2 suffix to current License --- .../definition/{ILicense.ts => ILicenseV2.ts} | 6 ++--- .../app/license/definition/ILicenseV2Tag.ts | 2 +- .../license/server/lib/isUnderAppLimits.ts | 4 +-- .../license/server/license.internalService.ts | 4 +-- apps/meteor/ee/app/license/server/license.ts | 26 +++++++++---------- apps/meteor/ee/app/license/server/methods.ts | 4 +-- apps/meteor/ee/server/api/licenses.ts | 4 +-- ...getInstallationSourceFromAppStorageItem.ts | 2 +- packages/core-services/src/index.ts | 6 ++--- .../src/types/{ILicense.ts => ILicenseV2.ts} | 2 +- .../ILicense/{ILicense.ts => ILicenseV2.ts} | 6 ++--- .../src/ee/ILicense/ILicenseV2Tag.ts | 2 +- .../src/ee/ILicense/ILicenseV3.ts | 2 +- packages/core-typings/src/index.ts | 3 ++- packages/rest-typings/src/v1/licenses.ts | 4 +-- 15 files changed, 39 insertions(+), 38 deletions(-) rename apps/meteor/ee/app/license/definition/{ILicense.ts => ILicenseV2.ts} (75%) rename packages/core-typings/src/ee/ILicense/ILicenseTag.ts => apps/meteor/ee/app/license/definition/ILicenseV2Tag.ts (50%) rename packages/core-services/src/types/{ILicense.ts => ILicenseV2.ts} (77%) rename packages/core-typings/src/ee/ILicense/{ILicense.ts => ILicenseV2.ts} (72%) rename apps/meteor/ee/app/license/definition/ILicenseTag.ts => packages/core-typings/src/ee/ILicense/ILicenseV2Tag.ts (50%) diff --git a/apps/meteor/ee/app/license/definition/ILicense.ts b/apps/meteor/ee/app/license/definition/ILicenseV2.ts similarity index 75% rename from apps/meteor/ee/app/license/definition/ILicense.ts rename to apps/meteor/ee/app/license/definition/ILicenseV2.ts index 7ac4bafdc7b5..79aea6543039 100644 --- a/apps/meteor/ee/app/license/definition/ILicense.ts +++ b/apps/meteor/ee/app/license/definition/ILicenseV2.ts @@ -1,13 +1,13 @@ -import type { ILicenseTag } from './ILicenseTag'; +import type { ILicenseV2Tag } from './ILicenseV2Tag'; -export interface ILicense { +export interface ILicenseV2 { url: string; expiry: string; maxActiveUsers: number; modules: string[]; maxGuestUsers: number; maxRoomsPerGuest: number; - tag?: ILicenseTag; + tag?: ILicenseV2Tag; meta?: { trial: boolean; trialEnd: string; diff --git a/packages/core-typings/src/ee/ILicense/ILicenseTag.ts b/apps/meteor/ee/app/license/definition/ILicenseV2Tag.ts similarity index 50% rename from packages/core-typings/src/ee/ILicense/ILicenseTag.ts rename to apps/meteor/ee/app/license/definition/ILicenseV2Tag.ts index 2f11fdebd5db..c58a273b2aa4 100644 --- a/packages/core-typings/src/ee/ILicense/ILicenseTag.ts +++ b/apps/meteor/ee/app/license/definition/ILicenseV2Tag.ts @@ -1,4 +1,4 @@ -export interface ILicenseTag { +export interface ILicenseV2Tag { name: string; color: string; } diff --git a/apps/meteor/ee/app/license/server/lib/isUnderAppLimits.ts b/apps/meteor/ee/app/license/server/lib/isUnderAppLimits.ts index b53b6512e2a1..726d29100bd1 100644 --- a/apps/meteor/ee/app/license/server/lib/isUnderAppLimits.ts +++ b/apps/meteor/ee/app/license/server/lib/isUnderAppLimits.ts @@ -1,9 +1,9 @@ import { Apps } from '@rocket.chat/core-services'; import { getInstallationSourceFromAppStorageItem } from '../../../../../lib/apps/getInstallationSourceFromAppStorageItem'; -import type { ILicense, LicenseAppSources } from '../../definition/ILicense'; +import type { ILicenseV2, LicenseAppSources } from '../../definition/ILicenseV2'; -export async function isUnderAppLimits(licenseAppsConfig: NonNullable, source: LicenseAppSources): Promise { +export async function isUnderAppLimits(licenseAppsConfig: NonNullable, source: LicenseAppSources): Promise { const apps = await Apps.getApps({ enabled: true }); if (!apps || !Array.isArray(apps)) { diff --git a/apps/meteor/ee/app/license/server/license.internalService.ts b/apps/meteor/ee/app/license/server/license.internalService.ts index 047a67d323ff..a06d540e867a 100644 --- a/apps/meteor/ee/app/license/server/license.internalService.ts +++ b/apps/meteor/ee/app/license/server/license.internalService.ts @@ -1,11 +1,11 @@ -import type { ILicense } from '@rocket.chat/core-services'; +import type { ILicenseV2 } from '@rocket.chat/core-services'; import { api, ServiceClassInternal } from '@rocket.chat/core-services'; import { guestPermissions } from '../../authorization/lib/guestPermissions'; import { resetEnterprisePermissions } from '../../authorization/server/resetEnterprisePermissions'; import { getModules, hasLicense, isEnterprise, onModule, onValidateLicenses } from './license'; -export class LicenseService extends ServiceClassInternal implements ILicense { +export class LicenseService extends ServiceClassInternal implements ILicenseV2 { protected name = 'license'; constructor() { diff --git a/apps/meteor/ee/app/license/server/license.ts b/apps/meteor/ee/app/license/server/license.ts index fe0b22a0ee45..c951053b9659 100644 --- a/apps/meteor/ee/app/license/server/license.ts +++ b/apps/meteor/ee/app/license/server/license.ts @@ -5,8 +5,8 @@ import { Apps } from '@rocket.chat/core-services'; import { Users } from '@rocket.chat/models'; import { getInstallationSourceFromAppStorageItem } from '../../../../lib/apps/getInstallationSourceFromAppStorageItem'; -import type { ILicense } from '../definition/ILicense'; -import type { ILicenseTag } from '../definition/ILicenseTag'; +import type { ILicenseV2 } from '../definition/ILicenseV2'; +import type { ILicenseV2Tag } from '../definition/ILicenseV2Tag'; import type { BundleFeature } from './bundles'; import { getBundleModules, isBundle, getBundleFromModule } from './bundles'; import decrypt from './decrypt'; @@ -17,7 +17,7 @@ const EnterpriseLicenses = new EventEmitter(); interface IValidLicense { valid?: boolean; - license: ILicense; + license: ILicenseV2; } let maxGuestUsers = 0; @@ -31,11 +31,11 @@ class LicenseClass { private encryptedLicenses = new Set(); - private tags = new Set(); + private tags = new Set(); private modules = new Set(); - private appsConfig: NonNullable = { + private appsConfig: NonNullable = { maxPrivateApps: 3, maxMarketplaceApps: 5, }; @@ -53,7 +53,7 @@ class LicenseClass { return !!regex.exec(url); } - private _setAppsConfig(license: ILicense): void { + private _setAppsConfig(license: ILicenseV2): void { // If the license is valid, no limit is going to be applied to apps installation for now // This guarantees that upgraded workspaces won't be affected by the new limit right away // and gives us time to propagate the new limit schema to all licenses @@ -91,7 +91,7 @@ class LicenseClass { }); } - private _addTags(license: ILicense): void { + private _addTags(license: ILicenseV2): void { // if no tag present, it means it is an old license, so try check for bundles and use them as tags if (typeof license.tag === 'undefined') { license.modules @@ -104,7 +104,7 @@ class LicenseClass { this._addTag(license.tag); } - private _addTag(tag: ILicenseTag): void { + private _addTag(tag: ILicenseV2Tag): void { // make sure to not add duplicated tag names for (const addedTag of this.tags) { if (addedTag.name.toLowerCase() === tag.name.toLowerCase()) { @@ -115,7 +115,7 @@ class LicenseClass { this.tags.add(tag); } - addLicense(license: ILicense): void { + addLicense(license: ILicenseV2): void { this.licenses.push({ valid: undefined, license, @@ -152,11 +152,11 @@ class LicenseClass { return [...this.modules]; } - getTags(): ILicenseTag[] { + getTags(): ILicenseV2Tag[] { return [...this.tags]; } - getAppsConfig(): NonNullable { + getAppsConfig(): NonNullable { return this.appsConfig; } @@ -344,11 +344,11 @@ export function getModules(): string[] { return License.getModules(); } -export function getTags(): ILicenseTag[] { +export function getTags(): ILicenseV2Tag[] { return License.getTags(); } -export function getAppsConfig(): NonNullable { +export function getAppsConfig(): NonNullable { return License.getAppsConfig(); } diff --git a/apps/meteor/ee/app/license/server/methods.ts b/apps/meteor/ee/app/license/server/methods.ts index 96978fad6d9a..2b252bb1ae05 100644 --- a/apps/meteor/ee/app/license/server/methods.ts +++ b/apps/meteor/ee/app/license/server/methods.ts @@ -2,7 +2,7 @@ import type { ServerMethods } from '@rocket.chat/ui-contexts'; import { check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; -import type { ILicenseTag } from '../definition/ILicenseTag'; +import type { ILicenseV2Tag } from '../definition/ILicenseV2Tag'; import { getModules, getTags, hasLicense, isEnterprise } from './license'; declare module '@rocket.chat/ui-contexts' { @@ -10,7 +10,7 @@ declare module '@rocket.chat/ui-contexts' { interface ServerMethods { 'license:hasLicense'(feature: string): boolean; 'license:getModules'(): string[]; - 'license:getTags'(): ILicenseTag[]; + 'license:getTags'(): ILicenseV2Tag[]; 'license:isEnterprise'(): boolean; } } diff --git a/apps/meteor/ee/server/api/licenses.ts b/apps/meteor/ee/server/api/licenses.ts index ab8d72164a97..b0322394320b 100644 --- a/apps/meteor/ee/server/api/licenses.ts +++ b/apps/meteor/ee/server/api/licenses.ts @@ -3,10 +3,10 @@ import { check } from 'meteor/check'; import { API } from '../../../app/api/server/api'; import { hasPermissionAsync } from '../../../app/authorization/server/functions/hasPermission'; -import type { ILicense } from '../../app/license/definition/ILicense'; +import type { ILicenseV2 } from '../../app/license/definition/ILicenseV2'; import { getLicenses, validateFormat, flatModules, getMaxActiveUsers, isEnterprise } from '../../app/license/server/license'; -function licenseTransform(license: ILicense): ILicense { +function licenseTransform(license: ILicenseV2): ILicenseV2 { return { ...license, modules: flatModules(license.modules), diff --git a/apps/meteor/lib/apps/getInstallationSourceFromAppStorageItem.ts b/apps/meteor/lib/apps/getInstallationSourceFromAppStorageItem.ts index 0af2cee0c377..d8df7d465463 100644 --- a/apps/meteor/lib/apps/getInstallationSourceFromAppStorageItem.ts +++ b/apps/meteor/lib/apps/getInstallationSourceFromAppStorageItem.ts @@ -1,6 +1,6 @@ import type { IAppStorageItem } from '@rocket.chat/apps-engine/server/storage'; -import type { LicenseAppSources } from '../../ee/app/license/definition/ILicense'; +import type { LicenseAppSources } from '../../ee/app/license/definition/ILicenseV2'; /** * There have been reports of apps not being correctly migrated from versions prior to 6.0 diff --git a/packages/core-services/src/index.ts b/packages/core-services/src/index.ts index def7622c9881..e0ba436bebac 100644 --- a/packages/core-services/src/index.ts +++ b/packages/core-services/src/index.ts @@ -12,7 +12,7 @@ import type { IEnterpriseSettings } from './types/IEnterpriseSettings'; import type { IFederationService, IFederationServiceEE } from './types/IFederationService'; import type { IImportService } from './types/IImportService'; import type { ILDAPService } from './types/ILDAPService'; -import type { ILicense } from './types/ILicense'; +import type { ILicenseV2 } from './types/ILicenseV2'; import type { IMediaService, ResizeResult } from './types/IMediaService'; import type { IMessageReadsService } from './types/IMessageReadsService'; import type { IMessageService } from './types/IMessageService'; @@ -72,7 +72,7 @@ export { IDeviceManagementService, IEnterpriseSettings, ILDAPService, - ILicense, + ILicenseV2, IListRoomsFilter, ILoginResult, IMediaService, @@ -127,7 +127,7 @@ export const Authorization = proxifyWithWait('authorization'); export const Apps = proxifyWithWait('apps-engine'); export const Presence = proxifyWithWait('presence'); export const Account = proxifyWithWait('accounts'); -export const License = proxifyWithWait('license'); +export const License = proxifyWithWait('license'); export const MeteorService = proxifyWithWait('meteor'); export const Banner = proxifyWithWait('banner'); export const UiKitCoreApp = proxifyWithWait('uikit-core-app'); diff --git a/packages/core-services/src/types/ILicense.ts b/packages/core-services/src/types/ILicenseV2.ts similarity index 77% rename from packages/core-services/src/types/ILicense.ts rename to packages/core-services/src/types/ILicenseV2.ts index 7b89a006bfc0..6386b059c4e0 100644 --- a/packages/core-services/src/types/ILicense.ts +++ b/packages/core-services/src/types/ILicenseV2.ts @@ -1,6 +1,6 @@ import type { IServiceClass } from './ServiceClass'; -export interface ILicense extends IServiceClass { +export interface ILicenseV2 extends IServiceClass { hasLicense(feature: string): boolean; isEnterprise(): boolean; diff --git a/packages/core-typings/src/ee/ILicense/ILicense.ts b/packages/core-typings/src/ee/ILicense/ILicenseV2.ts similarity index 72% rename from packages/core-typings/src/ee/ILicense/ILicense.ts rename to packages/core-typings/src/ee/ILicense/ILicenseV2.ts index 8490ab1d7cbe..09725c4061f6 100644 --- a/packages/core-typings/src/ee/ILicense/ILicense.ts +++ b/packages/core-typings/src/ee/ILicense/ILicenseV2.ts @@ -1,13 +1,13 @@ -import type { ILicenseTag } from './ILicenseTag'; +import type { ILicenseV2Tag } from './ILicenseV2Tag'; -export interface ILicense { +export interface ILicenseV2 { url: string; expiry: string; maxActiveUsers: number; modules: string[]; maxGuestUsers: number; maxRoomsPerGuest: number; - tag?: ILicenseTag; + tag?: ILicenseV2Tag; meta?: { trial: boolean; trialEnd: string; diff --git a/apps/meteor/ee/app/license/definition/ILicenseTag.ts b/packages/core-typings/src/ee/ILicense/ILicenseV2Tag.ts similarity index 50% rename from apps/meteor/ee/app/license/definition/ILicenseTag.ts rename to packages/core-typings/src/ee/ILicense/ILicenseV2Tag.ts index 2f11fdebd5db..c58a273b2aa4 100644 --- a/apps/meteor/ee/app/license/definition/ILicenseTag.ts +++ b/packages/core-typings/src/ee/ILicense/ILicenseV2Tag.ts @@ -1,4 +1,4 @@ -export interface ILicenseTag { +export interface ILicenseV2Tag { name: string; color: string; } diff --git a/packages/core-typings/src/ee/ILicense/ILicenseV3.ts b/packages/core-typings/src/ee/ILicense/ILicenseV3.ts index 0cdfac5c75e5..9b62d39ab591 100644 --- a/packages/core-typings/src/ee/ILicense/ILicenseV3.ts +++ b/packages/core-typings/src/ee/ILicense/ILicenseV3.ts @@ -32,7 +32,7 @@ export type Module = | 'message-read-receipt' | 'outlook-calendar'; -export default interface ILicense { +export default interface ILicenseV3 { version: '3.0'; information: { id?: string; diff --git a/packages/core-typings/src/index.ts b/packages/core-typings/src/index.ts index 8cd004dd09f1..cb77e05ba32f 100644 --- a/packages/core-typings/src/index.ts +++ b/packages/core-typings/src/index.ts @@ -39,7 +39,8 @@ export * from './IUserSession'; export * from './IUserStatus'; export * from './IUser'; -export * from './ee/ILicense/ILicense'; +export * from './ee/ILicense/ILicenseV2'; +export * from './ee/ILicense/ILicenseV3'; export * from './ee/IAuditLog'; export * from './import'; diff --git a/packages/rest-typings/src/v1/licenses.ts b/packages/rest-typings/src/v1/licenses.ts index c6d102a967e4..6801b76e8a71 100644 --- a/packages/rest-typings/src/v1/licenses.ts +++ b/packages/rest-typings/src/v1/licenses.ts @@ -1,4 +1,4 @@ -import type { ILicense } from '@rocket.chat/core-typings'; +import type { ILicenseV2 } from '@rocket.chat/core-typings'; import Ajv from 'ajv'; const ajv = new Ajv({ @@ -24,7 +24,7 @@ export const isLicensesAddProps = ajv.compile(licensesAddProps export type LicensesEndpoints = { '/v1/licenses.get': { - GET: () => { licenses: Array }; + GET: () => { licenses: Array }; }; '/v1/licenses.add': { POST: (params: licensesAddProps) => void; From 08a25fc00449670bd422f21ebd33067591898e38 Mon Sep 17 00:00:00 2001 From: Pierre Lehnen Date: Wed, 6 Sep 2023 15:48:33 -0300 Subject: [PATCH 03/38] export license v3 --- packages/core-typings/src/ee/ILicense/ILicenseV3.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core-typings/src/ee/ILicense/ILicenseV3.ts b/packages/core-typings/src/ee/ILicense/ILicenseV3.ts index 9b62d39ab591..c445e5fc96ac 100644 --- a/packages/core-typings/src/ee/ILicense/ILicenseV3.ts +++ b/packages/core-typings/src/ee/ILicense/ILicenseV3.ts @@ -32,7 +32,7 @@ export type Module = | 'message-read-receipt' | 'outlook-calendar'; -export default interface ILicenseV3 { +export interface ILicenseV3 { version: '3.0'; information: { id?: string; From a63a72ca338f8682b6ca16ee0fe2af24d708143e Mon Sep 17 00:00:00 2001 From: Pierre Lehnen Date: Wed, 6 Sep 2023 15:59:22 -0300 Subject: [PATCH 04/38] removed duplicated types --- .../ee/app/license/definition/ILicenseV2.ts | 22 ------------------- .../app/license/definition/ILicenseV2Tag.ts | 4 ---- .../license/server/lib/isUnderAppLimits.ts | 2 +- apps/meteor/ee/app/license/server/license.ts | 3 +-- apps/meteor/ee/app/license/server/methods.ts | 2 +- apps/meteor/ee/server/api/licenses.ts | 2 +- ...getInstallationSourceFromAppStorageItem.ts | 2 +- .../src/ee/ILicense/ILicenseV2.ts | 2 ++ packages/core-typings/src/index.ts | 1 + 9 files changed, 8 insertions(+), 32 deletions(-) delete mode 100644 apps/meteor/ee/app/license/definition/ILicenseV2.ts delete mode 100644 apps/meteor/ee/app/license/definition/ILicenseV2Tag.ts diff --git a/apps/meteor/ee/app/license/definition/ILicenseV2.ts b/apps/meteor/ee/app/license/definition/ILicenseV2.ts deleted file mode 100644 index 79aea6543039..000000000000 --- a/apps/meteor/ee/app/license/definition/ILicenseV2.ts +++ /dev/null @@ -1,22 +0,0 @@ -import type { ILicenseV2Tag } from './ILicenseV2Tag'; - -export interface ILicenseV2 { - url: string; - expiry: string; - maxActiveUsers: number; - modules: string[]; - maxGuestUsers: number; - maxRoomsPerGuest: number; - tag?: ILicenseV2Tag; - meta?: { - trial: boolean; - trialEnd: string; - workspaceId: string; - }; - apps?: { - maxPrivateApps: number; - maxMarketplaceApps: number; - }; -} - -export type LicenseAppSources = 'private' | 'marketplace'; diff --git a/apps/meteor/ee/app/license/definition/ILicenseV2Tag.ts b/apps/meteor/ee/app/license/definition/ILicenseV2Tag.ts deleted file mode 100644 index c58a273b2aa4..000000000000 --- a/apps/meteor/ee/app/license/definition/ILicenseV2Tag.ts +++ /dev/null @@ -1,4 +0,0 @@ -export interface ILicenseV2Tag { - name: string; - color: string; -} diff --git a/apps/meteor/ee/app/license/server/lib/isUnderAppLimits.ts b/apps/meteor/ee/app/license/server/lib/isUnderAppLimits.ts index 726d29100bd1..b812f1081a4f 100644 --- a/apps/meteor/ee/app/license/server/lib/isUnderAppLimits.ts +++ b/apps/meteor/ee/app/license/server/lib/isUnderAppLimits.ts @@ -1,7 +1,7 @@ import { Apps } from '@rocket.chat/core-services'; +import type { ILicenseV2, LicenseAppSources } from '@rocket.chat/core-typings'; import { getInstallationSourceFromAppStorageItem } from '../../../../../lib/apps/getInstallationSourceFromAppStorageItem'; -import type { ILicenseV2, LicenseAppSources } from '../../definition/ILicenseV2'; export async function isUnderAppLimits(licenseAppsConfig: NonNullable, source: LicenseAppSources): Promise { const apps = await Apps.getApps({ enabled: true }); diff --git a/apps/meteor/ee/app/license/server/license.ts b/apps/meteor/ee/app/license/server/license.ts index c951053b9659..3ccf19ac6625 100644 --- a/apps/meteor/ee/app/license/server/license.ts +++ b/apps/meteor/ee/app/license/server/license.ts @@ -2,11 +2,10 @@ import { EventEmitter } from 'events'; import type { IAppStorageItem } from '@rocket.chat/apps-engine/server/storage'; import { Apps } from '@rocket.chat/core-services'; +import type { ILicenseV2, ILicenseV2Tag } from '@rocket.chat/core-typings'; import { Users } from '@rocket.chat/models'; import { getInstallationSourceFromAppStorageItem } from '../../../../lib/apps/getInstallationSourceFromAppStorageItem'; -import type { ILicenseV2 } from '../definition/ILicenseV2'; -import type { ILicenseV2Tag } from '../definition/ILicenseV2Tag'; import type { BundleFeature } from './bundles'; import { getBundleModules, isBundle, getBundleFromModule } from './bundles'; import decrypt from './decrypt'; diff --git a/apps/meteor/ee/app/license/server/methods.ts b/apps/meteor/ee/app/license/server/methods.ts index 2b252bb1ae05..4c85868570a4 100644 --- a/apps/meteor/ee/app/license/server/methods.ts +++ b/apps/meteor/ee/app/license/server/methods.ts @@ -1,8 +1,8 @@ +import type { ILicenseV2Tag } from '@rocket.chat/core-typings'; import type { ServerMethods } from '@rocket.chat/ui-contexts'; import { check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; -import type { ILicenseV2Tag } from '../definition/ILicenseV2Tag'; import { getModules, getTags, hasLicense, isEnterprise } from './license'; declare module '@rocket.chat/ui-contexts' { diff --git a/apps/meteor/ee/server/api/licenses.ts b/apps/meteor/ee/server/api/licenses.ts index b0322394320b..844d70e74c8c 100644 --- a/apps/meteor/ee/server/api/licenses.ts +++ b/apps/meteor/ee/server/api/licenses.ts @@ -1,9 +1,9 @@ +import type { ILicenseV2 } from '@rocket.chat/core-typings'; import { Settings, Users } from '@rocket.chat/models'; import { check } from 'meteor/check'; import { API } from '../../../app/api/server/api'; import { hasPermissionAsync } from '../../../app/authorization/server/functions/hasPermission'; -import type { ILicenseV2 } from '../../app/license/definition/ILicenseV2'; import { getLicenses, validateFormat, flatModules, getMaxActiveUsers, isEnterprise } from '../../app/license/server/license'; function licenseTransform(license: ILicenseV2): ILicenseV2 { diff --git a/apps/meteor/lib/apps/getInstallationSourceFromAppStorageItem.ts b/apps/meteor/lib/apps/getInstallationSourceFromAppStorageItem.ts index d8df7d465463..f9fe3d9abce7 100644 --- a/apps/meteor/lib/apps/getInstallationSourceFromAppStorageItem.ts +++ b/apps/meteor/lib/apps/getInstallationSourceFromAppStorageItem.ts @@ -1,6 +1,6 @@ import type { IAppStorageItem } from '@rocket.chat/apps-engine/server/storage'; +import type { LicenseAppSources } from '@rocket.chat/core-typings'; -import type { LicenseAppSources } from '../../ee/app/license/definition/ILicenseV2'; /** * There have been reports of apps not being correctly migrated from versions prior to 6.0 diff --git a/packages/core-typings/src/ee/ILicense/ILicenseV2.ts b/packages/core-typings/src/ee/ILicense/ILicenseV2.ts index 09725c4061f6..79aea6543039 100644 --- a/packages/core-typings/src/ee/ILicense/ILicenseV2.ts +++ b/packages/core-typings/src/ee/ILicense/ILicenseV2.ts @@ -18,3 +18,5 @@ export interface ILicenseV2 { maxMarketplaceApps: number; }; } + +export type LicenseAppSources = 'private' | 'marketplace'; diff --git a/packages/core-typings/src/index.ts b/packages/core-typings/src/index.ts index cb77e05ba32f..fb7ad5004a5d 100644 --- a/packages/core-typings/src/index.ts +++ b/packages/core-typings/src/index.ts @@ -40,6 +40,7 @@ export * from './IUserStatus'; export * from './IUser'; export * from './ee/ILicense/ILicenseV2'; +export * from './ee/ILicense/ILicenseV2Tag'; export * from './ee/ILicense/ILicenseV3'; export * from './ee/IAuditLog'; From f895b94983ed85289c43c3e2f3ac9cbd9b8b7acd Mon Sep 17 00:00:00 2001 From: Pierre Lehnen Date: Wed, 6 Sep 2023 16:00:11 -0300 Subject: [PATCH 05/38] removed extra line --- apps/meteor/lib/apps/getInstallationSourceFromAppStorageItem.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/meteor/lib/apps/getInstallationSourceFromAppStorageItem.ts b/apps/meteor/lib/apps/getInstallationSourceFromAppStorageItem.ts index f9fe3d9abce7..d8fd5a48f79f 100644 --- a/apps/meteor/lib/apps/getInstallationSourceFromAppStorageItem.ts +++ b/apps/meteor/lib/apps/getInstallationSourceFromAppStorageItem.ts @@ -1,7 +1,6 @@ import type { IAppStorageItem } from '@rocket.chat/apps-engine/server/storage'; import type { LicenseAppSources } from '@rocket.chat/core-typings'; - /** * There have been reports of apps not being correctly migrated from versions prior to 6.0 * From afa5da807c84941a094f33221df0cb5203e912c1 Mon Sep 17 00:00:00 2001 From: Pierre Lehnen Date: Wed, 6 Sep 2023 16:07:43 -0300 Subject: [PATCH 06/38] renamed ILicenseV2Tag back to ILicenseTag and used it on the v3 license as well --- apps/meteor/ee/app/license/server/license.ts | 10 +++++----- apps/meteor/ee/app/license/server/methods.ts | 4 ++-- .../ee/ILicense/{ILicenseV2Tag.ts => ILicenseTag.ts} | 2 +- packages/core-typings/src/ee/ILicense/ILicenseV2.ts | 4 ++-- packages/core-typings/src/ee/ILicense/ILicenseV3.ts | 7 +++---- packages/core-typings/src/index.ts | 2 +- 6 files changed, 14 insertions(+), 15 deletions(-) rename packages/core-typings/src/ee/ILicense/{ILicenseV2Tag.ts => ILicenseTag.ts} (50%) diff --git a/apps/meteor/ee/app/license/server/license.ts b/apps/meteor/ee/app/license/server/license.ts index 3ccf19ac6625..5f35351dc765 100644 --- a/apps/meteor/ee/app/license/server/license.ts +++ b/apps/meteor/ee/app/license/server/license.ts @@ -2,7 +2,7 @@ import { EventEmitter } from 'events'; import type { IAppStorageItem } from '@rocket.chat/apps-engine/server/storage'; import { Apps } from '@rocket.chat/core-services'; -import type { ILicenseV2, ILicenseV2Tag } from '@rocket.chat/core-typings'; +import type { ILicenseV2, ILicenseTag } from '@rocket.chat/core-typings'; import { Users } from '@rocket.chat/models'; import { getInstallationSourceFromAppStorageItem } from '../../../../lib/apps/getInstallationSourceFromAppStorageItem'; @@ -30,7 +30,7 @@ class LicenseClass { private encryptedLicenses = new Set(); - private tags = new Set(); + private tags = new Set(); private modules = new Set(); @@ -103,7 +103,7 @@ class LicenseClass { this._addTag(license.tag); } - private _addTag(tag: ILicenseV2Tag): void { + private _addTag(tag: ILicenseTag): void { // make sure to not add duplicated tag names for (const addedTag of this.tags) { if (addedTag.name.toLowerCase() === tag.name.toLowerCase()) { @@ -151,7 +151,7 @@ class LicenseClass { return [...this.modules]; } - getTags(): ILicenseV2Tag[] { + getTags(): ILicenseTag[] { return [...this.tags]; } @@ -343,7 +343,7 @@ export function getModules(): string[] { return License.getModules(); } -export function getTags(): ILicenseV2Tag[] { +export function getTags(): ILicenseTag[] { return License.getTags(); } diff --git a/apps/meteor/ee/app/license/server/methods.ts b/apps/meteor/ee/app/license/server/methods.ts index 4c85868570a4..6103e2750790 100644 --- a/apps/meteor/ee/app/license/server/methods.ts +++ b/apps/meteor/ee/app/license/server/methods.ts @@ -1,4 +1,4 @@ -import type { ILicenseV2Tag } from '@rocket.chat/core-typings'; +import type { ILicenseTag } from '@rocket.chat/core-typings'; import type { ServerMethods } from '@rocket.chat/ui-contexts'; import { check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; @@ -10,7 +10,7 @@ declare module '@rocket.chat/ui-contexts' { interface ServerMethods { 'license:hasLicense'(feature: string): boolean; 'license:getModules'(): string[]; - 'license:getTags'(): ILicenseV2Tag[]; + 'license:getTags'(): ILicenseTag[]; 'license:isEnterprise'(): boolean; } } diff --git a/packages/core-typings/src/ee/ILicense/ILicenseV2Tag.ts b/packages/core-typings/src/ee/ILicense/ILicenseTag.ts similarity index 50% rename from packages/core-typings/src/ee/ILicense/ILicenseV2Tag.ts rename to packages/core-typings/src/ee/ILicense/ILicenseTag.ts index c58a273b2aa4..2f11fdebd5db 100644 --- a/packages/core-typings/src/ee/ILicense/ILicenseV2Tag.ts +++ b/packages/core-typings/src/ee/ILicense/ILicenseTag.ts @@ -1,4 +1,4 @@ -export interface ILicenseV2Tag { +export interface ILicenseTag { name: string; color: string; } diff --git a/packages/core-typings/src/ee/ILicense/ILicenseV2.ts b/packages/core-typings/src/ee/ILicense/ILicenseV2.ts index 79aea6543039..57d921a24907 100644 --- a/packages/core-typings/src/ee/ILicense/ILicenseV2.ts +++ b/packages/core-typings/src/ee/ILicense/ILicenseV2.ts @@ -1,4 +1,4 @@ -import type { ILicenseV2Tag } from './ILicenseV2Tag'; +import type { ILicenseTag } from './ILicenseTag'; export interface ILicenseV2 { url: string; @@ -7,7 +7,7 @@ export interface ILicenseV2 { modules: string[]; maxGuestUsers: number; maxRoomsPerGuest: number; - tag?: ILicenseV2Tag; + tag?: ILicenseTag; meta?: { trial: boolean; trialEnd: string; diff --git a/packages/core-typings/src/ee/ILicense/ILicenseV3.ts b/packages/core-typings/src/ee/ILicense/ILicenseV3.ts index c445e5fc96ac..cab2cde3467b 100644 --- a/packages/core-typings/src/ee/ILicense/ILicenseV3.ts +++ b/packages/core-typings/src/ee/ILicense/ILicenseV3.ts @@ -1,3 +1,5 @@ +import type { ILicenseTag } from './ILicenseTag'; + export type LicenseBehavior = 'invalidate_license' | 'start_fair_policy' | 'prevent_action' | 'prevent_installation'; export type LicenseLimit = { @@ -54,10 +56,7 @@ export interface ILicenseV3 { }; legalText?: string; notes?: string; - tags?: { - name: string; - color: string; - }[]; + tags?: ILicenseTag[]; }; validation: { serverUrls: { diff --git a/packages/core-typings/src/index.ts b/packages/core-typings/src/index.ts index fb7ad5004a5d..52a4ac8f1f7e 100644 --- a/packages/core-typings/src/index.ts +++ b/packages/core-typings/src/index.ts @@ -40,7 +40,7 @@ export * from './IUserStatus'; export * from './IUser'; export * from './ee/ILicense/ILicenseV2'; -export * from './ee/ILicense/ILicenseV2Tag'; +export * from './ee/ILicense/ILicenseTag'; export * from './ee/ILicense/ILicenseV3'; export * from './ee/IAuditLog'; From 6ba51670325fbcdfc0e0332e98fe815eea758ddc Mon Sep 17 00:00:00 2001 From: Luis Mauro Date: Thu, 7 Sep 2023 15:56:05 -0600 Subject: [PATCH 07/38] add function to transform V2 into V3 --- .../ee/app/license/server/fromV2toV3.ts | 78 +++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 apps/meteor/ee/app/license/server/fromV2toV3.ts diff --git a/apps/meteor/ee/app/license/server/fromV2toV3.ts b/apps/meteor/ee/app/license/server/fromV2toV3.ts new file mode 100644 index 000000000000..c9b2828ca14d --- /dev/null +++ b/apps/meteor/ee/app/license/server/fromV2toV3.ts @@ -0,0 +1,78 @@ +/** + * FromV2ToV3 + * Transform a License V2 into a V3 representation. + */ + +import type { ILicenseV2, ILicenseV3, Module, LicenseLimit, LicensePeriod } from '@rocket.chat/core-typings'; + +export const fromV2toV3 = (v2: ILicenseV2): ILicenseV3 => { + return { + version: '3.0', + information: { + autoRenew: false, + visualExpiration: Date.parse(v2.expiry).toString(), + trial: v2.meta?.trial || false, + offline: false, + createdAt: Date.now().toString(), + grantedBy: { + method: 'manual', + seller: 'V2', + }, + tags: v2.tag ? [v2.tag] : undefined, + }, + validation: { + serverUrls: [ + { + value: v2.url, + type: 'url', + }, + ], + validPeriods: [ + { + validUntil: Date.parse(v2.expiry).toString(), + invalidBehavior: 'invalidate_license', + } as LicensePeriod, + ], + statisticsReport: { + required: false, + }, + }, + grantedModules: v2.modules.map((module) => { + return { + module: module as Module, + }; + }), + limits: { + activeUsers: [ + { + max: v2.maxActiveUsers, + behavior: 'invalidate_license', + } as LicenseLimit, + ], + guestUsers: [ + { + max: v2.maxGuestUsers, + behavior: 'invalidate_license', + } as LicenseLimit, + ], + roomsPerGuest: [ + { + max: v2.maxRoomsPerGuest, + behavior: 'invalidate_license', + } as LicenseLimit, + ], + privateApps: [ + { + max: v2.apps?.maxPrivateApps, + behavior: 'prevent_installation', + } as LicenseLimit, + ], + marketplaceApps: [ + { + max: v2.apps?.maxMarketplaceApps, + behavior: 'prevent_installation', + } as LicenseLimit, + ], + }, + }; +}; From abc66b6fd7eaafd4629e63c8b3c69fe2a0da8d85 Mon Sep 17 00:00:00 2001 From: Pierre Lehnen Date: Tue, 12 Sep 2023 01:21:00 -0300 Subject: [PATCH 08/38] validate license in v3 format --- .../ee/app/license/server/fromV2toV3.ts | 112 +++-- .../license/server/license.internalService.ts | 4 +- apps/meteor/ee/app/license/server/license.ts | 451 +++++++++++------- apps/meteor/ee/app/license/server/settings.ts | 6 +- apps/meteor/ee/app/license/server/startup.ts | 8 +- .../client/views/admin/users/useSeatsCap.ts | 1 + apps/meteor/ee/server/api/licenses.ts | 15 +- packages/core-services/src/index.ts | 6 +- .../src/types/{ILicenseV2.ts => ILicense.ts} | 2 +- .../src/ee/ILicense/ILicenseV3.ts | 8 +- packages/rest-typings/src/v1/licenses.ts | 4 +- 11 files changed, 391 insertions(+), 226 deletions(-) rename packages/core-services/src/types/{ILicenseV2.ts => ILicense.ts} (77%) diff --git a/apps/meteor/ee/app/license/server/fromV2toV3.ts b/apps/meteor/ee/app/license/server/fromV2toV3.ts index c9b2828ca14d..458deea589f9 100644 --- a/apps/meteor/ee/app/license/server/fromV2toV3.ts +++ b/apps/meteor/ee/app/license/server/fromV2toV3.ts @@ -5,20 +5,31 @@ import type { ILicenseV2, ILicenseV3, Module, LicenseLimit, LicensePeriod } from '@rocket.chat/core-typings'; +import { isBundle, getBundleFromModule, getBundleModules } from './bundles'; +import { getTagColor } from './getTagColor'; + export const fromV2toV3 = (v2: ILicenseV2): ILicenseV3 => { return { version: '3.0', information: { autoRenew: false, - visualExpiration: Date.parse(v2.expiry).toString(), + visualExpiration: new Date(Date.parse(v2.expiry)).toISOString(), trial: v2.meta?.trial || false, offline: false, - createdAt: Date.now().toString(), + createdAt: new Date().toISOString(), grantedBy: { method: 'manual', seller: 'V2', }, - tags: v2.tag ? [v2.tag] : undefined, + // if no tag present, it means it is an old license, so try check for bundles and use them as tags + tags: v2.tag + ? [v2.tag] + : [ + ...(v2.modules.filter(isBundle).map(getBundleFromModule).filter(Boolean) as string[]).map((tag) => ({ + name: tag, + color: getTagColor(tag), + })), + ], }, validation: { serverUrls: [ @@ -29,7 +40,7 @@ export const fromV2toV3 = (v2: ILicenseV2): ILicenseV3 => { ], validPeriods: [ { - validUntil: Date.parse(v2.expiry).toString(), + validUntil: new Date(Date.parse(v2.expiry)).toISOString(), invalidBehavior: 'invalidate_license', } as LicensePeriod, ], @@ -37,42 +48,65 @@ export const fromV2toV3 = (v2: ILicenseV2): ILicenseV3 => { required: false, }, }, - grantedModules: v2.modules.map((module) => { - return { - module: module as Module, - }; - }), + grantedModules: [ + ...new Set( + v2.modules + .map((licenseModule) => (isBundle(licenseModule) ? getBundleModules(licenseModule) : [licenseModule])) + .reduce((prev, curr) => [...prev, ...curr], []) + .map((licenseModule) => ({ module: licenseModule as Module })), + ), + ], limits: { - activeUsers: [ - { - max: v2.maxActiveUsers, - behavior: 'invalidate_license', - } as LicenseLimit, - ], - guestUsers: [ - { - max: v2.maxGuestUsers, - behavior: 'invalidate_license', - } as LicenseLimit, - ], - roomsPerGuest: [ - { - max: v2.maxRoomsPerGuest, - behavior: 'invalidate_license', - } as LicenseLimit, - ], - privateApps: [ - { - max: v2.apps?.maxPrivateApps, - behavior: 'prevent_installation', - } as LicenseLimit, - ], - marketplaceApps: [ - { - max: v2.apps?.maxMarketplaceApps, - behavior: 'prevent_installation', - } as LicenseLimit, - ], + ...(v2.maxActiveUsers + ? { + activeUsers: [ + { + max: v2.maxActiveUsers, + behavior: 'prevent_action', + }, + ], + } + : {}), + ...(v2.maxGuestUsers + ? { + guestUsers: [ + { + max: v2.maxGuestUsers, + behavior: 'prevent_action', + }, + ], + } + : {}), + ...(v2.maxRoomsPerGuest + ? { + roomsPerGuest: [ + { + max: v2.maxRoomsPerGuest, + behavior: 'prevent_action', + }, + ], + } + : {}), + ...(v2.apps?.maxPrivateApps + ? { + privateApps: [ + { + max: v2.apps.maxPrivateApps, + behavior: 'prevent_action', + }, + ], + } + : {}), + ...(v2.apps?.maxMarketplaceApps + ? { + marketplaceApps: [ + { + max: v2.apps.maxMarketplaceApps, + behavior: 'prevent_action', + }, + ], + } + : {}), }, }; }; diff --git a/apps/meteor/ee/app/license/server/license.internalService.ts b/apps/meteor/ee/app/license/server/license.internalService.ts index a06d540e867a..047a67d323ff 100644 --- a/apps/meteor/ee/app/license/server/license.internalService.ts +++ b/apps/meteor/ee/app/license/server/license.internalService.ts @@ -1,11 +1,11 @@ -import type { ILicenseV2 } from '@rocket.chat/core-services'; +import type { ILicense } from '@rocket.chat/core-services'; import { api, ServiceClassInternal } from '@rocket.chat/core-services'; import { guestPermissions } from '../../authorization/lib/guestPermissions'; import { resetEnterprisePermissions } from '../../authorization/server/resetEnterprisePermissions'; import { getModules, hasLicense, isEnterprise, onModule, onValidateLicenses } from './license'; -export class LicenseService extends ServiceClassInternal implements ILicenseV2 { +export class LicenseService extends ServiceClassInternal implements ILicense { protected name = 'license'; constructor() { diff --git a/apps/meteor/ee/app/license/server/license.ts b/apps/meteor/ee/app/license/server/license.ts index 5f35351dc765..4da768b42e77 100644 --- a/apps/meteor/ee/app/license/server/license.ts +++ b/apps/meteor/ee/app/license/server/license.ts @@ -2,45 +2,50 @@ import { EventEmitter } from 'events'; import type { IAppStorageItem } from '@rocket.chat/apps-engine/server/storage'; import { Apps } from '@rocket.chat/core-services'; -import type { ILicenseV2, ILicenseTag } from '@rocket.chat/core-typings'; +import type { ILicenseV2, ILicenseTag, ILicenseV3, Timestamp, LicenseBehavior } from '@rocket.chat/core-typings'; +import { Logger } from '@rocket.chat/logger'; import { Users } from '@rocket.chat/models'; import { getInstallationSourceFromAppStorageItem } from '../../../../lib/apps/getInstallationSourceFromAppStorageItem'; import type { BundleFeature } from './bundles'; -import { getBundleModules, isBundle, getBundleFromModule } from './bundles'; +import { getBundleModules, isBundle } from './bundles'; import decrypt from './decrypt'; -import { getTagColor } from './getTagColor'; +import { fromV2toV3 } from './fromV2toV3'; import { isUnderAppLimits } from './lib/isUnderAppLimits'; const EnterpriseLicenses = new EventEmitter(); -interface IValidLicense { - valid?: boolean; - license: ILicenseV2; -} - -let maxGuestUsers = 0; -let maxRoomsPerGuest = 0; -let maxActiveUsers = 0; +const logger = new Logger('License'); class LicenseClass { private url: string | null = null; - private licenses: IValidLicense[] = []; - - private encryptedLicenses = new Set(); + private encryptedLicense: string | undefined; private tags = new Set(); private modules = new Set(); - private appsConfig: NonNullable = { - maxPrivateApps: 3, - maxMarketplaceApps: 5, - }; + private unmodifiedLicense: ILicenseV2 | ILicenseV3 | undefined; + + private license: ILicenseV3 | undefined; + + private valid: boolean | undefined; + + private inFairPolicy: boolean | undefined; - private _validateExpiration(expiration: string): boolean { - return new Date() > new Date(expiration); + private _isPeriodInvalid(from?: Timestamp, until?: Timestamp): boolean { + const now = new Date(); + + if (from && now < new Date(from)) { + return true; + } + + if (until && now > new Date(until)) { + return true; + } + + return false; } private _validateURL(licenseURL: string, url: string): boolean { @@ -52,55 +57,20 @@ class LicenseClass { return !!regex.exec(url); } - private _setAppsConfig(license: ILicenseV2): void { - // If the license is valid, no limit is going to be applied to apps installation for now - // This guarantees that upgraded workspaces won't be affected by the new limit right away - // and gives us time to propagate the new limit schema to all licenses - const { maxPrivateApps = -1, maxMarketplaceApps = -1 } = license.apps || {}; - - if (maxPrivateApps === -1 || maxPrivateApps > this.appsConfig.maxPrivateApps) { - this.appsConfig.maxPrivateApps = maxPrivateApps; - } - - if (maxMarketplaceApps === -1 || maxMarketplaceApps > this.appsConfig.maxMarketplaceApps) { - this.appsConfig.maxMarketplaceApps = maxMarketplaceApps; - } - } - private _validModules(licenseModules: string[]): void { - licenseModules.forEach((licenseModule) => { - const modules = isBundle(licenseModule) ? getBundleModules(licenseModule) : [licenseModule]; - - modules.forEach((module) => { - this.modules.add(module); - EnterpriseLicenses.emit('module', { module, valid: true }); - EnterpriseLicenses.emit(`valid:${module}`); - }); + licenseModules.forEach((module) => { + this.modules.add(module); + EnterpriseLicenses.emit('module', { module, valid: true }); + EnterpriseLicenses.emit(`valid:${module}`); }); } private _invalidModules(licenseModules: string[]): void { - licenseModules.forEach((licenseModule) => { - const modules = isBundle(licenseModule) ? getBundleModules(licenseModule) : [licenseModule]; - - modules.forEach((module) => { - EnterpriseLicenses.emit('module', { module, valid: false }); - EnterpriseLicenses.emit(`invalid:${module}`); - }); + licenseModules.forEach((module) => { + EnterpriseLicenses.emit('module', { module, valid: false }); + EnterpriseLicenses.emit(`invalid:${module}`); }); - } - - private _addTags(license: ILicenseV2): void { - // if no tag present, it means it is an old license, so try check for bundles and use them as tags - if (typeof license.tag === 'undefined') { - license.modules - .filter(isBundle) - .map(getBundleFromModule) - .forEach((tag) => tag && this._addTag({ name: tag, color: getTagColor(tag) })); - return; - } - - this._addTag(license.tag); + this.modules.clear(); } private _addTag(tag: ILicenseTag): void { @@ -114,123 +84,264 @@ class LicenseClass { this.tags.add(tag); } - addLicense(license: ILicenseV2): void { - this.licenses.push({ - valid: undefined, - license, - }); + private removeCurrentLicense(): void { + const { license, valid } = this; + + this.license = undefined; + this.unmodifiedLicense = undefined; + this.valid = undefined; + this.inFairPolicy = undefined; + + if (!license || !valid) { + return; + } + + this.valid = false; + EnterpriseLicenses.emit('invalidate'); + this._invalidModules(license.grantedModules.map(({ module }) => module)); + } + + public async setLicenseV3(license: ILicenseV3): Promise { + this.removeCurrentLicense(); + + this.unmodifiedLicense = license; + this.license = license; - this.validate(); + return this.validate(); } - lockLicense(encryptedLicense: string): void { - this.encryptedLicenses.add(encryptedLicense); + public async setLicenseV2(license: ILicenseV2): Promise { + this.removeCurrentLicense(); + + const licenseV3 = fromV2toV3(license); + + this.unmodifiedLicense = license; + this.license = licenseV3; + + return this.validate(); } - isLicenseDuplicate(encryptedLicense: string): boolean { - if (this.encryptedLicenses.has(encryptedLicense)) { - return true; - } + public lockLicense(encryptedLicense: string): void { + this.encryptedLicense = encryptedLicense; + } - return false; + public isLicenseDuplicate(encryptedLicense: string): boolean { + return Boolean(this.encryptedLicense && this.encryptedLicense === encryptedLicense); } - hasModule(module: string): boolean { + public hasModule(module: string): boolean { return this.modules.has(module); } - hasAnyValidLicense(): boolean { - return this.licenses.some((item) => item.valid); + public hasValidLicense(): boolean { + return Boolean(this.license && this.valid); } - getLicenses(): IValidLicense[] { - return this.licenses; + public getUnmodifiedLicense(): ILicenseV2 | ILicenseV3 | undefined { + if (this.valid) { + return this.unmodifiedLicense; + } } - getModules(): string[] { + public getModules(): string[] { return [...this.modules]; } - getTags(): ILicenseTag[] { + public getTags(): ILicenseTag[] { return [...this.tags]; } - getAppsConfig(): NonNullable { - return this.appsConfig; - } - - setURL(url: string): void { + public async setURL(url: string): Promise { this.url = url.replace(/\/$/, '').replace(/^https?:\/\/(.*)$/, '$1'); - this.validate(); + await this.validate(); } - validate(): void { - this.licenses = this.licenses.map((item) => { - const { license } = item; + private validateLicenseUrl(license: ILicenseV3, behaviorFilter: (behavior: LicenseBehavior) => boolean): LicenseBehavior[] { + if (!behaviorFilter('invalidate_license')) { + return []; + } + + const { + validation: { serverUrls }, + } = license; - if (license.url) { - if (!this.url) { - return item; - } - if (!this._validateURL(license.url, this.url)) { - this.invalidate(item); - console.error(`#### License error: invalid url, licensed to ${license.url}, used on ${this.url}`); - this._invalidModules(license.modules); - return item; + const { url: workspaceUrl } = this; + + if (!workspaceUrl) { + logger.error('Unable to validate license URL without knowing the workspace URL.'); + return ['invalidate_license']; + } + + return serverUrls + .filter((url) => { + switch (url.type) { + case 'regex': + // #TODO + break; + case 'hash': + // #TODO + break; + case 'url': + return !this._validateURL(url.value, workspaceUrl); } - } - if (license.expiry && this._validateExpiration(license.expiry)) { - this.invalidate(item); - console.error(`#### License error: expired, valid until ${license.expiry}`); - this._invalidModules(license.modules); - return item; - } + return false; + }) + .map((url) => { + logger.error({ + msg: 'Url validation failed', + url, + workspaceUrl, + }); + return 'invalidate_license'; + }); + } - if (license.maxGuestUsers > maxGuestUsers) { - maxGuestUsers = license.maxGuestUsers; - } + private validateLicensePeriods(license: ILicenseV3, behaviorFilter: (behavior: LicenseBehavior) => boolean): LicenseBehavior[] { + const { + validation: { validPeriods }, + } = license; + + return validPeriods + .filter( + ({ validFrom, validUntil, invalidBehavior }) => behaviorFilter(invalidBehavior) && this._isPeriodInvalid(validFrom, validUntil), + ) + .map((period) => { + logger.error({ + msg: 'Period validation failed', + period, + }); + return period.invalidBehavior; + }); + } - if (license.maxRoomsPerGuest > maxRoomsPerGuest) { - maxRoomsPerGuest = license.maxRoomsPerGuest; - } + private async validateLicenseLimits( + license: ILicenseV3, + behaviorFilter: (behavior: LicenseBehavior) => boolean, + ): Promise { + const { limits } = license; + + const limitKeys = Object.keys(limits) as (keyof ILicenseV3['limits'])[]; + 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)); + if (!limitList?.length) { + return []; + } + + const currentValue = await this.getCurrentValueForLicenseLimit(limitKey); + return limitList + .filter(({ max }) => max < currentValue) + .map((limit) => { + logger.error({ + msg: 'Limit validation failed', + kind: limitKey, + limit, + }); + return limit.behavior; + }); + }), + ) + ).reduce((prev, curr) => [...new Set([...prev, ...curr])], []); + } - if (license.maxActiveUsers > maxActiveUsers) { - maxActiveUsers = license.maxActiveUsers; - } + private async shouldPreventAction(action: keyof ILicenseV3['limits'], newCount = 1): Promise { + if (!this.valid) { + return false; + } - this._setAppsConfig(license); + const currentValue = (await this.getCurrentValueForLicenseLimit(action)) + newCount; + return Boolean( + this.license?.limits[action] + ?.filter(({ behavior, max }) => behavior === 'prevent_action' && max >= 0) + .some(({ max }) => max < currentValue), + ); + } - this._validModules(license.modules); + private async runValidation(license: ILicenseV3, behaviorsToValidate: LicenseBehavior[] = []): Promise { + const shouldValidateBehavior = (behavior: LicenseBehavior) => !behaviorsToValidate?.length || behaviorsToValidate.includes(behavior); - this._addTags(license); + return [ + ...new Set([ + ...this.validateLicenseUrl(license, shouldValidateBehavior), + ...this.validateLicensePeriods(license, shouldValidateBehavior), + ...(await this.validateLicenseLimits(license, shouldValidateBehavior)), + ]), + ]; + } - console.log('#### License validated:', license.modules.join(', ')); + private async validate(): Promise { + if (this.license) { + // #TODO: Only include 'prevent_installation' here if this is actually the initial installation of the license + const behaviorsTriggered = await this.runValidation(this.license, [ + 'invalidate_license', + 'prevent_installation', + 'start_fair_policy', + ]); - item.valid = true; - return item; - }); + if (behaviorsTriggered.includes('invalidate_license') || behaviorsTriggered.includes('prevent_installation')) { + return; + } + + this.valid = true; + this.inFairPolicy = behaviorsTriggered.includes('start_fair_policy'); + + if (this.license.information.tags) { + for (const tag of this.license.information.tags) { + this._addTag(tag); + } + } + + this._validModules(this.license.grantedModules.map(({ module }) => module)); + console.log('#### License validated:', this.license.grantedModules.map(({ module }) => module).join(', ')); + } EnterpriseLicenses.emit('validate'); - this.showLicenses(); + this.showLicense(); + } + + private async getCurrentValueForLicenseLimit(limitKey: keyof ILicenseV3['limits']): Promise { + switch (limitKey) { + case 'activeUsers': + return this.getCurrentActiveUsers(); + case 'guestUsers': + return this.getCurrentGuestUsers(); + case 'privateApps': + return this.getCurrentPrivateAppsCount(); + case 'marketplaceApps': + return this.getCurrentMarketplaceAppsCount(); + default: + return 0; + } } - invalidate(item: IValidLicense): void { - item.valid = false; + private async getCurrentActiveUsers(): Promise { + return Users.getActiveLocalUserCount(); + } - EnterpriseLicenses.emit('invalidate'); + private async getCurrentGuestUsers(): Promise { + // #TODO: Load current count + return 0; } - async canAddNewUser(userCount = 1): Promise { - if (!maxActiveUsers) { - return true; - } + private async getCurrentPrivateAppsCount(): Promise { + // #TODO: Load current count + return 0; + } + + private async getCurrentMarketplaceAppsCount(): Promise { + // #TODO: Load current count + return 0; + } - return maxActiveUsers > (await Users.getActiveLocalUserCount()) + userCount; + public async canAddNewUser(userCount = 1): Promise { + return !(await this.shouldPreventAction('activeUsers', userCount)); } - async canEnableApp(app: IAppStorageItem): Promise { + public async canEnableApp(app: IAppStorageItem): Promise { if (!(await Apps.isInitialized())) { return false; } @@ -241,34 +352,44 @@ class LicenseClass { return true; } - return isUnderAppLimits(this.appsConfig, getInstallationSourceFromAppStorageItem(app)); + return isUnderAppLimits(getAppsConfig(), getInstallationSourceFromAppStorageItem(app)); } - showLicenses(): void { + private showLicense(): void { if (!process.env.LICENSE_DEBUG || process.env.LICENSE_DEBUG === 'false') { return; } - this.licenses - .filter((item) => item.valid) - .forEach((item) => { - const { license } = item; - - console.log('---- License enabled ----'); - console.log(' url ->', license.url); - console.log(' expiry ->', license.expiry); - console.log(' maxActiveUsers ->', license.maxActiveUsers); - console.log(' maxGuestUsers ->', license.maxGuestUsers); - console.log(' maxRoomsPerGuest ->', license.maxRoomsPerGuest); - console.log(' modules ->', license.modules.join(', ')); - console.log('-------------------------'); - }); + if (!this.license || !this.valid) { + return; + } + + const { + validation: { serverUrls, validPeriods }, + limits, + grantedModules, + } = this.license; + + console.log('---- License enabled ----'); + console.log(' url ->', JSON.stringify(serverUrls)); + console.log(' periods ->', JSON.stringify(validPeriods)); + console.log(' limits ->', JSON.stringify(limits)); + console.log(' modules ->', grantedModules.map(({ module }) => module).join(', ')); + console.log('-------------------------'); + } + + public getMaxActiveUsers(): number { + return (this.valid && this.license?.limits.activeUsers?.find(({ behavior }) => behavior === 'prevent_action')?.max) || 0; + } + + public startedFairPolicy(): boolean { + return Boolean(this.valid && this.inFairPolicy); } } const License = new LicenseClass(); -export function addLicense(encryptedLicense: string): boolean { +export async function setLicense(encryptedLicense: string): Promise { if (!encryptedLicense || String(encryptedLicense).trim() === '' || License.isLicenseDuplicate(encryptedLicense)) { return false; } @@ -285,7 +406,8 @@ export function addLicense(encryptedLicense: string): boolean { console.log('##### Raw license ->', decrypted); } - License.addLicense(JSON.parse(decrypted)); + // #TODO: Check license version and call setLicenseV2 or setLicenseV3 + await License.setLicenseV2(JSON.parse(decrypted)); License.lockLicense(encryptedLicense); return true; @@ -311,8 +433,8 @@ export function validateFormat(encryptedLicense: string): boolean { return true; } -export function setURL(url: string): void { - License.setURL(url); +export async function setURL(url: string): Promise { + await License.setURL(url); } export function hasLicense(feature: string): boolean { @@ -320,23 +442,26 @@ export function hasLicense(feature: string): boolean { } export function isEnterprise(): boolean { - return License.hasAnyValidLicense(); + return License.hasValidLicense(); } export function getMaxGuestUsers(): number { - return maxGuestUsers; + // #TODO: Adjust any place currently using this function to stop doing so. + return 0; } export function getMaxRoomsPerGuest(): number { - return maxRoomsPerGuest; + // #TODO: Adjust any place currently using this function to stop doing so. + return 0; } export function getMaxActiveUsers(): number { - return maxActiveUsers; + // #TODO: Adjust any place currently using this function to stop doing so. + return License.getMaxActiveUsers(); } -export function getLicenses(): IValidLicense[] { - return License.getLicenses(); +export function getUnmodifiedLicense(): ILicenseV3 | ILicenseV2 | undefined { + return License.getUnmodifiedLicense(); } export function getModules(): string[] { @@ -348,7 +473,11 @@ export function getTags(): ILicenseTag[] { } export function getAppsConfig(): NonNullable { - return License.getAppsConfig(); + // #TODO: Adjust any place currently using this function to stop doing so. + return { + maxPrivateApps: -1, + maxMarketplaceApps: -1, + }; } export async function canAddNewUser(userCount = 1): Promise { diff --git a/apps/meteor/ee/app/license/server/settings.ts b/apps/meteor/ee/app/license/server/settings.ts index a5a07ba0200f..663f52045bf0 100644 --- a/apps/meteor/ee/app/license/server/settings.ts +++ b/apps/meteor/ee/app/license/server/settings.ts @@ -2,7 +2,7 @@ import { Settings } from '@rocket.chat/models'; import { Meteor } from 'meteor/meteor'; import { settings, settingsRegistry } from '../../../../app/settings/server'; -import { addLicense } from './license'; +import { setLicense } from './license'; Meteor.startup(async () => { await settingsRegistry.addGroup('Enterprise', async function () { @@ -29,7 +29,7 @@ settings.watch('Enterprise_License', async (license) => { return; } - if (!addLicense(license)) { + if (!(await setLicense(license))) { await Settings.updateValueById('Enterprise_License_Status', 'Invalid'); return; } @@ -38,7 +38,7 @@ settings.watch('Enterprise_License', async (license) => { }); if (process.env.ROCKETCHAT_LICENSE) { - addLicense(process.env.ROCKETCHAT_LICENSE); + await setLicense(process.env.ROCKETCHAT_LICENSE); Meteor.startup(async () => { if (settings.get('Enterprise_License')) { diff --git a/apps/meteor/ee/app/license/server/startup.ts b/apps/meteor/ee/app/license/server/startup.ts index 4a7a0776fc0d..b9da1261e1a0 100644 --- a/apps/meteor/ee/app/license/server/startup.ts +++ b/apps/meteor/ee/app/license/server/startup.ts @@ -1,13 +1,13 @@ import { settings } from '../../../../app/settings/server'; import { callbacks } from '../../../../lib/callbacks'; -import { addLicense, setURL } from './license'; +import { setLicense, setURL } from './license'; settings.watch('Site_Url', (value) => { if (value) { - setURL(value); + void setURL(value); } }); -callbacks.add('workspaceLicenseChanged', (updatedLicense) => { - addLicense(updatedLicense); +callbacks.add('workspaceLicenseChanged', async (updatedLicense) => { + await setLicense(updatedLicense); }); diff --git a/apps/meteor/ee/client/views/admin/users/useSeatsCap.ts b/apps/meteor/ee/client/views/admin/users/useSeatsCap.ts index eb029d91f537..b4a45b49dda5 100644 --- a/apps/meteor/ee/client/views/admin/users/useSeatsCap.ts +++ b/apps/meteor/ee/client/views/admin/users/useSeatsCap.ts @@ -8,6 +8,7 @@ export type SeatCapProps = { }; export const useSeatsCap = (): SeatCapProps | undefined => { + // #TODO: Stop using this endpoint const fetch = useEndpoint('GET', '/v1/licenses.maxActiveUsers'); const result = useQuery(['/v1/licenses.maxActiveUsers'], () => fetch()); diff --git a/apps/meteor/ee/server/api/licenses.ts b/apps/meteor/ee/server/api/licenses.ts index 844d70e74c8c..a33678c74a6b 100644 --- a/apps/meteor/ee/server/api/licenses.ts +++ b/apps/meteor/ee/server/api/licenses.ts @@ -1,15 +1,17 @@ -import type { ILicenseV2 } from '@rocket.chat/core-typings'; +import type { ILicenseV2, ILicenseV3 } from '@rocket.chat/core-typings'; import { Settings, Users } from '@rocket.chat/models'; import { check } from 'meteor/check'; import { API } from '../../../app/api/server/api'; import { hasPermissionAsync } from '../../../app/authorization/server/functions/hasPermission'; -import { getLicenses, validateFormat, flatModules, getMaxActiveUsers, isEnterprise } from '../../app/license/server/license'; +import { getUnmodifiedLicense, validateFormat, flatModules, getMaxActiveUsers, isEnterprise } from '../../app/license/server/license'; -function licenseTransform(license: ILicenseV2): ILicenseV2 { +const isLicenseV2 = (license: ILicenseV2 | ILicenseV3): license is ILicenseV2 => 'modules' in license; + +function licenseTransform(license: ILicenseV2 | ILicenseV3): ILicenseV2 | (ILicenseV3 & { modules: string[] }) { return { ...license, - modules: flatModules(license.modules), + modules: isLicenseV2(license) ? flatModules(license.modules) : license.grantedModules.map(({ module }) => module), }; } @@ -22,9 +24,8 @@ API.v1.addRoute( return API.v1.unauthorized(); } - const licenses = getLicenses() - .filter(({ valid }) => valid) - .map(({ license }) => licenseTransform(license)); + const license = getUnmodifiedLicense(); + const licenses = license ? [licenseTransform(license)] : []; return API.v1.success({ licenses }); }, diff --git a/packages/core-services/src/index.ts b/packages/core-services/src/index.ts index e0ba436bebac..def7622c9881 100644 --- a/packages/core-services/src/index.ts +++ b/packages/core-services/src/index.ts @@ -12,7 +12,7 @@ import type { IEnterpriseSettings } from './types/IEnterpriseSettings'; import type { IFederationService, IFederationServiceEE } from './types/IFederationService'; import type { IImportService } from './types/IImportService'; import type { ILDAPService } from './types/ILDAPService'; -import type { ILicenseV2 } from './types/ILicenseV2'; +import type { ILicense } from './types/ILicense'; import type { IMediaService, ResizeResult } from './types/IMediaService'; import type { IMessageReadsService } from './types/IMessageReadsService'; import type { IMessageService } from './types/IMessageService'; @@ -72,7 +72,7 @@ export { IDeviceManagementService, IEnterpriseSettings, ILDAPService, - ILicenseV2, + ILicense, IListRoomsFilter, ILoginResult, IMediaService, @@ -127,7 +127,7 @@ export const Authorization = proxifyWithWait('authorization'); export const Apps = proxifyWithWait('apps-engine'); export const Presence = proxifyWithWait('presence'); export const Account = proxifyWithWait('accounts'); -export const License = proxifyWithWait('license'); +export const License = proxifyWithWait('license'); export const MeteorService = proxifyWithWait('meteor'); export const Banner = proxifyWithWait('banner'); export const UiKitCoreApp = proxifyWithWait('uikit-core-app'); diff --git a/packages/core-services/src/types/ILicenseV2.ts b/packages/core-services/src/types/ILicense.ts similarity index 77% rename from packages/core-services/src/types/ILicenseV2.ts rename to packages/core-services/src/types/ILicense.ts index 6386b059c4e0..7b89a006bfc0 100644 --- a/packages/core-services/src/types/ILicenseV2.ts +++ b/packages/core-services/src/types/ILicense.ts @@ -1,6 +1,6 @@ import type { IServiceClass } from './ServiceClass'; -export interface ILicenseV2 extends IServiceClass { +export interface ILicense extends IServiceClass { hasLicense(feature: string): boolean; isEnterprise(): boolean; diff --git a/packages/core-typings/src/ee/ILicense/ILicenseV3.ts b/packages/core-typings/src/ee/ILicense/ILicenseV3.ts index cab2cde3467b..935e6fa62228 100644 --- a/packages/core-typings/src/ee/ILicense/ILicenseV3.ts +++ b/packages/core-typings/src/ee/ILicense/ILicenseV3.ts @@ -2,9 +2,9 @@ import type { ILicenseTag } from './ILicenseTag'; export type LicenseBehavior = 'invalidate_license' | 'start_fair_policy' | 'prevent_action' | 'prevent_installation'; -export type LicenseLimit = { +export type LicenseLimit = { max: number; - behavior: LicenseBehavior; + behavior: T; }; export type Timestamp = string; @@ -12,7 +12,7 @@ export type Timestamp = string; export type LicensePeriod = { validFrom?: Timestamp; validUntil?: Timestamp; - invalidBehavior: LicenseBehavior; + invalidBehavior: Exclude; } & ({ validFrom: Timestamp } | { validUntil: Timestamp }); export type Module = @@ -84,7 +84,7 @@ export interface ILicenseV3 { limits: { activeUsers?: LicenseLimit[]; guestUsers?: LicenseLimit[]; - roomsPerGuest?: LicenseLimit[]; + roomsPerGuest?: LicenseLimit<'prevent_action'>[]; privateApps?: LicenseLimit[]; marketplaceApps?: LicenseLimit[]; }; diff --git a/packages/rest-typings/src/v1/licenses.ts b/packages/rest-typings/src/v1/licenses.ts index 6801b76e8a71..48a3167da3df 100644 --- a/packages/rest-typings/src/v1/licenses.ts +++ b/packages/rest-typings/src/v1/licenses.ts @@ -1,4 +1,4 @@ -import type { ILicenseV2 } from '@rocket.chat/core-typings'; +import type { ILicenseV2, ILicenseV3 } from '@rocket.chat/core-typings'; import Ajv from 'ajv'; const ajv = new Ajv({ @@ -24,7 +24,7 @@ export const isLicensesAddProps = ajv.compile(licensesAddProps export type LicensesEndpoints = { '/v1/licenses.get': { - GET: () => { licenses: Array }; + GET: () => { licenses: Array }; }; '/v1/licenses.add': { POST: (params: licensesAddProps) => void; From 4c66bc0673c81279e78d1d2b9a44dd4cfb5dd5ee Mon Sep 17 00:00:00 2001 From: Luis Mauro Date: Wed, 13 Sep 2023 11:59:57 -0600 Subject: [PATCH 09/38] remove unused type imports --- apps/meteor/ee/app/license/server/fromV2toV3.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/meteor/ee/app/license/server/fromV2toV3.ts b/apps/meteor/ee/app/license/server/fromV2toV3.ts index 458deea589f9..4ed07985a3fd 100644 --- a/apps/meteor/ee/app/license/server/fromV2toV3.ts +++ b/apps/meteor/ee/app/license/server/fromV2toV3.ts @@ -3,7 +3,7 @@ * Transform a License V2 into a V3 representation. */ -import type { ILicenseV2, ILicenseV3, Module, LicenseLimit, LicensePeriod } from '@rocket.chat/core-typings'; +import type { ILicenseV2, ILicenseV3, Module } from '@rocket.chat/core-typings'; import { isBundle, getBundleFromModule, getBundleModules } from './bundles'; import { getTagColor } from './getTagColor'; @@ -42,7 +42,7 @@ export const fromV2toV3 = (v2: ILicenseV2): ILicenseV3 => { { validUntil: new Date(Date.parse(v2.expiry)).toISOString(), invalidBehavior: 'invalidate_license', - } as LicensePeriod, + }, ], statisticsReport: { required: false, From 96674a02ff840a895b29087dbaff31a2feea326a Mon Sep 17 00:00:00 2001 From: Pierre Lehnen Date: Fri, 15 Sep 2023 12:43:57 -0300 Subject: [PATCH 10/38] Removing references to old license structure --- .../authorization/server/validateUserRoles.js | 39 ++++--- apps/meteor/ee/app/license/server/index.ts | 2 +- .../ee/app/license/server/lib/getAppCount.ts | 21 ++++ apps/meteor/ee/app/license/server/license.ts | 110 +++++++++++++----- .../ee/server/startup/maxRoomsPerGuest.ts | 7 +- apps/meteor/ee/server/startup/seatsCap.ts | 26 +---- .../src/ee/ILicense/ILicenseV3.ts | 2 + .../model-typings/src/models/IUsersModel.ts | 2 +- 8 files changed, 133 insertions(+), 76 deletions(-) create mode 100644 apps/meteor/ee/app/license/server/lib/getAppCount.ts diff --git a/apps/meteor/ee/app/authorization/server/validateUserRoles.js b/apps/meteor/ee/app/authorization/server/validateUserRoles.js index fe8e3410bc01..6263fc51d694 100644 --- a/apps/meteor/ee/app/authorization/server/validateUserRoles.js +++ b/apps/meteor/ee/app/authorization/server/validateUserRoles.js @@ -1,29 +1,42 @@ import { Users } from '@rocket.chat/models'; import { Meteor } from 'meteor/meteor'; -import { isEnterprise, getMaxGuestUsers } from '../../license/server'; +import { i18n } from '../../../../server/lib/i18n'; +import { isEnterprise, canAddNewGuestUser, canAddNewUser } from '../../license/server/license'; export const validateUserRoles = async function (userId, userData) { if (!isEnterprise()) { return; } - if (!userData.roles.includes('guest')) { + const isGuest = Boolean(userData.roles?.includes('guest') && userData.roles.length === 1); + const currentUserData = userData._id ? await Users.findOneById(userData._id) : null; + const wasGuest = Boolean(currentUserData.roles?.includes('guest') && currentUserData.roles.length === 1); + + if (currentUserData?.type === 'app') { return; } - if (userData.roles.length >= 2) { - throw new Meteor.Error('error-guests-cant-have-other-roles', "Guest users can't receive any other role", { - method: 'insertOrUpdateUser', - field: 'Assign_role', - }); + if (isGuest) { + if (wasGuest) { + return; + } + + if (!(await canAddNewGuestUser())) { + throw new Meteor.Error('error-max-guests-number-reached', 'Maximum number of guests reached.', { + method: 'insertOrUpdateUser', + field: 'Assign_role', + }); + } + + return; + } + + if (!wasGuest && userData._id) { + return; } - const guestCount = await Users.getActiveLocalGuestCount(userData._id); - if (guestCount >= getMaxGuestUsers()) { - throw new Meteor.Error('error-max-guests-number-reached', 'Maximum number of guests reached.', { - method: 'insertOrUpdateUser', - field: 'Assign_role', - }); + if (!(await canAddNewUser())) { + throw new Meteor.Error('error-license-user-limit-reached', i18n.t('error-license-user-limit-reached')); } }; diff --git a/apps/meteor/ee/app/license/server/index.ts b/apps/meteor/ee/app/license/server/index.ts index f7d83ed388b8..f13e315d2866 100644 --- a/apps/meteor/ee/app/license/server/index.ts +++ b/apps/meteor/ee/app/license/server/index.ts @@ -2,6 +2,6 @@ import './settings'; import './methods'; import './startup'; -export { onLicense, overwriteClassOnLicense, isEnterprise, getMaxGuestUsers } from './license'; +export { onLicense, overwriteClassOnLicense, isEnterprise } from './license'; export { getStatistics } from './getStatistics'; diff --git a/apps/meteor/ee/app/license/server/lib/getAppCount.ts b/apps/meteor/ee/app/license/server/lib/getAppCount.ts new file mode 100644 index 000000000000..f408143218de --- /dev/null +++ b/apps/meteor/ee/app/license/server/lib/getAppCount.ts @@ -0,0 +1,21 @@ +import { Apps } from '@rocket.chat/core-services'; +import type { LicenseAppSources } from '@rocket.chat/core-typings'; + +import { getInstallationSourceFromAppStorageItem } from '../../../../../lib/apps/getInstallationSourceFromAppStorageItem'; + +export async function getAppCount(source: LicenseAppSources): Promise { + if (!(await Apps.isInitialized())) { + return 0; + } + + const apps = await Apps.getApps({ enabled: true }); + + if (!apps || !Array.isArray(apps)) { + return 0; + } + + const storageItems = await Promise.all(apps.map((app) => Apps.getAppStorageItemById(app.id))); + const activeAppsFromSameSource = storageItems.filter((item) => item && getInstallationSourceFromAppStorageItem(item) === source); + + return activeAppsFromSameSource.length; +} diff --git a/apps/meteor/ee/app/license/server/license.ts b/apps/meteor/ee/app/license/server/license.ts index 4da768b42e77..838afc143536 100644 --- a/apps/meteor/ee/app/license/server/license.ts +++ b/apps/meteor/ee/app/license/server/license.ts @@ -2,21 +2,23 @@ import { EventEmitter } from 'events'; import type { IAppStorageItem } from '@rocket.chat/apps-engine/server/storage'; import { Apps } from '@rocket.chat/core-services'; -import type { ILicenseV2, ILicenseTag, ILicenseV3, Timestamp, LicenseBehavior } from '@rocket.chat/core-typings'; +import type { ILicenseV2, ILicenseTag, ILicenseV3, Timestamp, LicenseBehavior, IUser, LicenseLimitKind } from '@rocket.chat/core-typings'; import { Logger } from '@rocket.chat/logger'; -import { Users } from '@rocket.chat/models'; +import { Users, Subscriptions } from '@rocket.chat/models'; import { getInstallationSourceFromAppStorageItem } from '../../../../lib/apps/getInstallationSourceFromAppStorageItem'; import type { BundleFeature } from './bundles'; import { getBundleModules, isBundle } from './bundles'; import decrypt from './decrypt'; import { fromV2toV3 } from './fromV2toV3'; -import { isUnderAppLimits } from './lib/isUnderAppLimits'; +import { getAppCount } from './lib/getAppCount'; const EnterpriseLicenses = new EventEmitter(); const logger = new Logger('License'); +type LimitContext = T extends 'roomsPerGuest' ? { userId: IUser['_id'] } : Record; + class LicenseClass { private url: string | null = null; @@ -248,12 +250,16 @@ class LicenseClass { ).reduce((prev, curr) => [...new Set([...prev, ...curr])], []); } - private async shouldPreventAction(action: keyof ILicenseV3['limits'], newCount = 1): Promise { + private async shouldPreventAction( + action: T, + context?: Partial>, + newCount = 1, + ): Promise { if (!this.valid) { return false; } - const currentValue = (await this.getCurrentValueForLicenseLimit(action)) + newCount; + const currentValue = (await this.getCurrentValueForLicenseLimit(action, context)) + newCount; return Boolean( this.license?.limits[action] ?.filter(({ behavior, max }) => behavior === 'prevent_action' && max >= 0) @@ -303,7 +309,10 @@ class LicenseClass { this.showLicense(); } - private async getCurrentValueForLicenseLimit(limitKey: keyof ILicenseV3['limits']): Promise { + private async getCurrentValueForLicenseLimit( + limitKey: T, + context?: Partial>, + ): Promise { switch (limitKey) { case 'activeUsers': return this.getCurrentActiveUsers(); @@ -313,6 +322,11 @@ class LicenseClass { return this.getCurrentPrivateAppsCount(); case 'marketplaceApps': return this.getCurrentMarketplaceAppsCount(); + case 'roomsPerGuest': + if (context?.userId) { + return Subscriptions.countByUserId(context.userId); + } + return 0; default: return 0; } @@ -323,22 +337,35 @@ class LicenseClass { } private async getCurrentGuestUsers(): Promise { - // #TODO: Load current count - return 0; + return Users.getActiveLocalGuestCount(); } private async getCurrentPrivateAppsCount(): Promise { - // #TODO: Load current count - return 0; + return getAppCount('private'); } private async getCurrentMarketplaceAppsCount(): Promise { - // #TODO: Load current count - return 0; + return getAppCount('marketplace'); } public async canAddNewUser(userCount = 1): Promise { - return !(await this.shouldPreventAction('activeUsers', userCount)); + return !(await this.shouldPreventAction('activeUsers', {}, userCount)); + } + + public async canAddNewGuestUser(guestCount = 1): Promise { + return !(await this.shouldPreventAction('guestUsers', {}, guestCount)); + } + + public async canAddNewPrivateApp(appCount = 1): Promise { + return !(await this.shouldPreventAction('privateApps', {}, appCount)); + } + + public async canAddNewMarketplaceApp(appCount = 1): Promise { + return !(await this.shouldPreventAction('marketplaceApps', {}, appCount)); + } + + public async canAddNewGuestSubscription(guest: IUser['_id'], roomCount = 1): Promise { + return !(await this.shouldPreventAction('roomsPerGuest', { userId: guest }, roomCount)); } public async canEnableApp(app: IAppStorageItem): Promise { @@ -352,7 +379,13 @@ class LicenseClass { return true; } - return isUnderAppLimits(getAppsConfig(), getInstallationSourceFromAppStorageItem(app)); + const source = getInstallationSourceFromAppStorageItem(app); + switch (source) { + case 'private': + return this.canAddNewPrivateApp(); + default: + return this.canAddNewMarketplaceApp(); + } } private showLicense(): void { @@ -378,13 +411,22 @@ class LicenseClass { console.log('-------------------------'); } - public getMaxActiveUsers(): number { - return (this.valid && this.license?.limits.activeUsers?.find(({ behavior }) => behavior === 'prevent_action')?.max) || 0; - } - public startedFairPolicy(): boolean { return Boolean(this.valid && this.inFairPolicy); } + + public getLicenseLimit(kind: LicenseLimitKind): number | undefined { + if (!this.valid || !this.license) { + return; + } + + const limitList = this.license.limits[kind]; + if (!limitList?.length) { + return; + } + + return Math.min(...limitList.map(({ max }) => max)); + } } const License = new LicenseClass(); @@ -445,19 +487,9 @@ export function isEnterprise(): boolean { return License.hasValidLicense(); } -export function getMaxGuestUsers(): number { - // #TODO: Adjust any place currently using this function to stop doing so. - return 0; -} - -export function getMaxRoomsPerGuest(): number { - // #TODO: Adjust any place currently using this function to stop doing so. - return 0; -} - export function getMaxActiveUsers(): number { // #TODO: Adjust any place currently using this function to stop doing so. - return License.getMaxActiveUsers(); + return License.getLicenseLimit('activeUsers') ?? 0; } export function getUnmodifiedLicense(): ILicenseV3 | ILicenseV2 | undefined { @@ -475,8 +507,8 @@ export function getTags(): ILicenseTag[] { export function getAppsConfig(): NonNullable { // #TODO: Adjust any place currently using this function to stop doing so. return { - maxPrivateApps: -1, - maxMarketplaceApps: -1, + maxPrivateApps: License.getLicenseLimit('privateApps') ?? -1, + maxMarketplaceApps: License.getLicenseLimit('marketplaceApps') ?? -1, }; } @@ -484,6 +516,22 @@ export async function canAddNewUser(userCount = 1): Promise { return License.canAddNewUser(userCount); } +export async function canAddNewGuestUser(guestCount = 1): Promise { + return License.canAddNewGuestUser(guestCount); +} + +export async function canAddNewGuestSubscription(guest: IUser['_id'], roomCount = 1): Promise { + return License.canAddNewGuestSubscription(guest, roomCount); +} + +export async function canAddNewPrivateApp(appCount = 1): Promise { + return License.canAddNewPrivateApp(appCount); +} + +export async function canAddNewMarketplaceApp(appCount = 1): Promise { + return License.canAddNewMarketplaceApp(appCount); +} + export async function canEnableApp(app: IAppStorageItem): Promise { return License.canEnableApp(app); } diff --git a/apps/meteor/ee/server/startup/maxRoomsPerGuest.ts b/apps/meteor/ee/server/startup/maxRoomsPerGuest.ts index f4e2452ec806..9a8bac6d1d2a 100644 --- a/apps/meteor/ee/server/startup/maxRoomsPerGuest.ts +++ b/apps/meteor/ee/server/startup/maxRoomsPerGuest.ts @@ -1,17 +1,14 @@ -import { Subscriptions } from '@rocket.chat/models'; import { Meteor } from 'meteor/meteor'; import { callbacks } from '../../../lib/callbacks'; import { i18n } from '../../../server/lib/i18n'; -import { getMaxRoomsPerGuest } from '../../app/license/server/license'; +import { canAddNewGuestSubscription } from '../../app/license/server/license'; callbacks.add( 'beforeAddedToRoom', async ({ user }) => { if (user.roles?.includes('guest')) { - const totalSubscriptions = await Subscriptions.countByUserId(user._id); - - if (totalSubscriptions >= getMaxRoomsPerGuest()) { + if (!(await canAddNewGuestSubscription(user._id))) { throw new Meteor.Error('error-max-rooms-per-guest-reached', i18n.t('error-max-rooms-per-guest-reached')); } } diff --git a/apps/meteor/ee/server/startup/seatsCap.ts b/apps/meteor/ee/server/startup/seatsCap.ts index b390539ad6b1..36ec066ab86f 100644 --- a/apps/meteor/ee/server/startup/seatsCap.ts +++ b/apps/meteor/ee/server/startup/seatsCap.ts @@ -62,31 +62,7 @@ callbacks.add( callbacks.add( 'validateUserRoles', - async (userData: Partial) => { - const isGuest = userData.roles?.includes('guest'); - if (isGuest) { - await validateUserRoles(Meteor.userId(), userData); - return; - } - - if (!userData._id) { - return; - } - - const currentUserData = await Users.findOneById(userData._id); - if (currentUserData?.type === 'app') { - return; - } - - const wasGuest = currentUserData?.roles?.length === 1 && currentUserData.roles.includes('guest'); - if (!wasGuest) { - return; - } - - if (!(await canAddNewUser())) { - throw new Meteor.Error('error-license-user-limit-reached', i18n.t('error-license-user-limit-reached')); - } - }, + async (userData: Partial) => validateUserRoles(Meteor.userId(), userData), callbacks.priority.MEDIUM, 'check-max-user-seats', ); diff --git a/packages/core-typings/src/ee/ILicense/ILicenseV3.ts b/packages/core-typings/src/ee/ILicense/ILicenseV3.ts index 935e6fa62228..06a60dcb8c0f 100644 --- a/packages/core-typings/src/ee/ILicense/ILicenseV3.ts +++ b/packages/core-typings/src/ee/ILicense/ILicenseV3.ts @@ -90,3 +90,5 @@ export interface ILicenseV3 { }; cloudMeta?: Record; } + +export type LicenseLimitKind = keyof ILicenseV3['limits']; diff --git a/packages/model-typings/src/models/IUsersModel.ts b/packages/model-typings/src/models/IUsersModel.ts index c0ce51f79f45..1ee2a432c3df 100644 --- a/packages/model-typings/src/models/IUsersModel.ts +++ b/packages/model-typings/src/models/IUsersModel.ts @@ -372,7 +372,7 @@ export interface IUsersModel extends IBaseModel { getUsersToSendOfflineEmail(userIds: string[]): FindCursor>; countActiveUsersByService(service: string, options?: FindOptions): Promise; getActiveLocalUserCount(): Promise; - getActiveLocalGuestCount(): Promise; + getActiveLocalGuestCount(exceptions?: IUser['_id'] | IUser['_id'][]): Promise; removeOlderResumeTokensByUserId(userId: string, fromDate: Date): Promise; findAllUsersWithPendingAvatar(): FindCursor; updateCustomFieldsById(userId: string, customFields: Record): Promise; From 1056427d9f76a8a46c90c7091f38c25dad28ddfc Mon Sep 17 00:00:00 2001 From: Pierre Lehnen Date: Fri, 15 Sep 2023 13:01:05 -0300 Subject: [PATCH 11/38] removed unused file --- .../license/server/lib/isUnderAppLimits.ts | 26 ------------------- 1 file changed, 26 deletions(-) delete mode 100644 apps/meteor/ee/app/license/server/lib/isUnderAppLimits.ts diff --git a/apps/meteor/ee/app/license/server/lib/isUnderAppLimits.ts b/apps/meteor/ee/app/license/server/lib/isUnderAppLimits.ts deleted file mode 100644 index b812f1081a4f..000000000000 --- a/apps/meteor/ee/app/license/server/lib/isUnderAppLimits.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { Apps } from '@rocket.chat/core-services'; -import type { ILicenseV2, LicenseAppSources } from '@rocket.chat/core-typings'; - -import { getInstallationSourceFromAppStorageItem } from '../../../../../lib/apps/getInstallationSourceFromAppStorageItem'; - -export async function isUnderAppLimits(licenseAppsConfig: NonNullable, source: LicenseAppSources): Promise { - const apps = await Apps.getApps({ enabled: true }); - - if (!apps || !Array.isArray(apps)) { - return true; - } - - const storageItems = await Promise.all(apps.map((app) => Apps.getAppStorageItemById(app.id))); - const activeAppsFromSameSource = storageItems.filter((item) => item && getInstallationSourceFromAppStorageItem(item) === source); - - const configKey = `max${source.charAt(0).toUpperCase()}${source.slice(1)}Apps` as keyof typeof licenseAppsConfig; - const configLimit = licenseAppsConfig[configKey]; - - // If the workspace can install unlimited apps - // the config will be -1 - if (configLimit === -1) { - return true; - } - - return activeAppsFromSameSource.length < configLimit; -} From c8c527315e465de7cec3da1fca107df15f65fcb7 Mon Sep 17 00:00:00 2001 From: Pierre Lehnen Date: Fri, 15 Sep 2023 16:17:43 -0300 Subject: [PATCH 12/38] Implemented disable_modules --- .../client/views/hooks/useUpgradeTabParams.ts | 9 +- .../ee/app/license/server/fromV2toV3.ts | 5 +- apps/meteor/ee/app/license/server/license.ts | 149 ++++++++++++------ apps/meteor/ee/server/api/licenses.ts | 16 +- apps/meteor/ee/server/startup/upsell.ts | 14 +- .../src/ee/ILicense/ILicenseV3.ts | 16 +- 6 files changed, 129 insertions(+), 80 deletions(-) diff --git a/apps/meteor/client/views/hooks/useUpgradeTabParams.ts b/apps/meteor/client/views/hooks/useUpgradeTabParams.ts index e051b69db8fa..d7e5966745e6 100644 --- a/apps/meteor/client/views/hooks/useUpgradeTabParams.ts +++ b/apps/meteor/client/views/hooks/useUpgradeTabParams.ts @@ -16,9 +16,12 @@ export const useUpgradeTabParams = (): { tabType: UpgradeTabVariant | false; tri const hasValidLicense = licensesData?.licenses.some((license) => license.modules.length > 0) ?? false; const hadExpiredTrials = cloudWorkspaceHadTrial ?? false; - const trialLicense = licensesData?.licenses?.find(({ meta }) => meta?.trial); - const isTrial = licensesData?.licenses?.every(({ meta }) => meta?.trial) ?? false; - const trialEndDate = trialLicense?.meta ? format(new Date(trialLicense.meta.trialEnd), 'yyyy-MM-dd') : undefined; + // #TODO: Update to use license v3 format, load meta info from license.information + const licenseMeta = licensesData?.licenses?.map((license: any) => (license.meta ?? license.cloudMeta) as Record); + + const trialLicense = licenseMeta?.find((meta) => meta?.trial); + const isTrial = licenseMeta?.every((meta) => meta?.trial) ?? false; + const trialEndDate = trialLicense ? format(new Date(trialLicense.trialEnd), 'yyyy-MM-dd') : undefined; const upgradeTabType = getUpgradeTabType({ registered, diff --git a/apps/meteor/ee/app/license/server/fromV2toV3.ts b/apps/meteor/ee/app/license/server/fromV2toV3.ts index 4ed07985a3fd..fc86d1208bc3 100644 --- a/apps/meteor/ee/app/license/server/fromV2toV3.ts +++ b/apps/meteor/ee/app/license/server/fromV2toV3.ts @@ -3,7 +3,7 @@ * Transform a License V2 into a V3 representation. */ -import type { ILicenseV2, ILicenseV3, Module } from '@rocket.chat/core-typings'; +import type { ILicenseV2, ILicenseV3, LicenseModule } from '@rocket.chat/core-typings'; import { isBundle, getBundleFromModule, getBundleModules } from './bundles'; import { getTagColor } from './getTagColor'; @@ -53,7 +53,7 @@ export const fromV2toV3 = (v2: ILicenseV2): ILicenseV3 => { v2.modules .map((licenseModule) => (isBundle(licenseModule) ? getBundleModules(licenseModule) : [licenseModule])) .reduce((prev, curr) => [...prev, ...curr], []) - .map((licenseModule) => ({ module: licenseModule as Module })), + .map((licenseModule) => ({ module: licenseModule as LicenseModule })), ), ], limits: { @@ -108,5 +108,6 @@ export const fromV2toV3 = (v2: ILicenseV2): ILicenseV3 => { } : {}), }, + cloudMeta: v2.meta, }; }; diff --git a/apps/meteor/ee/app/license/server/license.ts b/apps/meteor/ee/app/license/server/license.ts index 838afc143536..d2e59c99278a 100644 --- a/apps/meteor/ee/app/license/server/license.ts +++ b/apps/meteor/ee/app/license/server/license.ts @@ -2,13 +2,22 @@ import { EventEmitter } from 'events'; import type { IAppStorageItem } from '@rocket.chat/apps-engine/server/storage'; import { Apps } from '@rocket.chat/core-services'; -import type { ILicenseV2, ILicenseTag, ILicenseV3, Timestamp, LicenseBehavior, IUser, LicenseLimitKind } from '@rocket.chat/core-typings'; +import type { + ILicenseV2, + ILicenseTag, + ILicenseV3, + Timestamp, + LicenseBehavior, + IUser, + LicenseLimit, + LicensePeriod, + LicenseLimitKind, + LicenseModule, +} from '@rocket.chat/core-typings'; import { Logger } from '@rocket.chat/logger'; import { Users, Subscriptions } from '@rocket.chat/models'; import { getInstallationSourceFromAppStorageItem } from '../../../../lib/apps/getInstallationSourceFromAppStorageItem'; -import type { BundleFeature } from './bundles'; -import { getBundleModules, isBundle } from './bundles'; import decrypt from './decrypt'; import { fromV2toV3 } from './fromV2toV3'; import { getAppCount } from './lib/getAppCount'; @@ -19,6 +28,11 @@ const logger = new Logger('License'); type LimitContext = T extends 'roomsPerGuest' ? { userId: IUser['_id'] } : Record; +type BehaviorWithContext = { + behavior: LicenseBehavior; + modules?: LicenseModule[]; +}; + class LicenseClass { private url: string | null = null; @@ -26,7 +40,7 @@ class LicenseClass { private tags = new Set(); - private modules = new Set(); + private modules = new Set(); private unmodifiedLicense: ILicenseV2 | ILicenseV3 | undefined; @@ -59,7 +73,7 @@ class LicenseClass { return !!regex.exec(url); } - private _validModules(licenseModules: string[]): void { + private _validModules(licenseModules: LicenseModule[]): void { licenseModules.forEach((module) => { this.modules.add(module); EnterpriseLicenses.emit('module', { module, valid: true }); @@ -67,12 +81,12 @@ class LicenseClass { }); } - private _invalidModules(licenseModules: string[]): void { + private _invalidModules(licenseModules: LicenseModule[]): void { licenseModules.forEach((module) => { EnterpriseLicenses.emit('module', { module, valid: false }); EnterpriseLicenses.emit(`invalid:${module}`); + this.modules.delete(module); }); - this.modules.clear(); } private _addTag(tag: ILicenseTag): void { @@ -100,7 +114,8 @@ class LicenseClass { this.valid = false; EnterpriseLicenses.emit('invalidate'); - this._invalidModules(license.grantedModules.map(({ module }) => module)); + this._invalidModules([...this.modules]); + this.modules.clear(); } public async setLicenseV3(license: ILicenseV3): Promise { @@ -131,7 +146,7 @@ class LicenseClass { return Boolean(this.encryptedLicense && this.encryptedLicense === encryptedLicense); } - public hasModule(module: string): boolean { + public hasModule(module: LicenseModule): boolean { return this.modules.has(module); } @@ -139,13 +154,22 @@ class LicenseClass { return Boolean(this.license && this.valid); } - public getUnmodifiedLicense(): ILicenseV2 | ILicenseV3 | undefined { - if (this.valid) { - return this.unmodifiedLicense; + public getUnmodifiedLicenseAndModules(): { license: ILicenseV2 | ILicenseV3; modules: LicenseModule[] } | undefined { + if (this.valid && this.unmodifiedLicense) { + return { + license: this.unmodifiedLicense, + modules: [...this.modules], + }; + } + } + + public getLicense(): ILicenseV3 | undefined { + if (this.valid && this.license) { + return this.license; } } - public getModules(): string[] { + public getModules(): LicenseModule[] { return [...this.modules]; } @@ -159,7 +183,42 @@ class LicenseClass { await this.validate(); } - private validateLicenseUrl(license: ILicenseV3, behaviorFilter: (behavior: LicenseBehavior) => boolean): LicenseBehavior[] { + private getResultingBehavior(data: LicenseLimit | LicensePeriod | Partial): BehaviorWithContext { + const behavior = 'invalidBehavior' in data ? data.invalidBehavior : data.behavior; + + switch (behavior) { + case 'disable_modules': + return { + behavior, + modules: ('modules' in data && data.modules) || [], + }; + + default: + return { + behavior, + } as BehaviorWithContext; + } + } + + private filterValidationResult(result: BehaviorWithContext[], expectedBehavior: LicenseBehavior): BehaviorWithContext[] { + return result.filter(({ behavior }) => behavior === expectedBehavior) as BehaviorWithContext[]; + } + + private isBehaviorsInResult(result: BehaviorWithContext[], expectedBehaviors: LicenseBehavior[]): boolean { + return result.some(({ behavior }) => expectedBehaviors.includes(behavior)); + } + + private getModulesToDisable(validationResult: BehaviorWithContext[]): LicenseModule[] { + return [ + ...new Set([ + ...this.filterValidationResult(validationResult, 'disable_modules') + .map(({ modules }) => modules || []) + .reduce((prev, curr) => [...prev, ...curr], []), + ]), + ]; + } + + private validateLicenseUrl(license: ILicenseV3, behaviorFilter: (behavior: LicenseBehavior) => boolean): BehaviorWithContext[] { if (!behaviorFilter('invalidate_license')) { return []; } @@ -172,7 +231,7 @@ class LicenseClass { if (!workspaceUrl) { logger.error('Unable to validate license URL without knowing the workspace URL.'); - return ['invalidate_license']; + return [this.getResultingBehavior({ behavior: 'invalidate_license' })]; } return serverUrls @@ -196,11 +255,11 @@ class LicenseClass { url, workspaceUrl, }); - return 'invalidate_license'; + return this.getResultingBehavior({ behavior: 'invalidate_license' }); }); } - private validateLicensePeriods(license: ILicenseV3, behaviorFilter: (behavior: LicenseBehavior) => boolean): LicenseBehavior[] { + private validateLicensePeriods(license: ILicenseV3, behaviorFilter: (behavior: LicenseBehavior) => boolean): BehaviorWithContext[] { const { validation: { validPeriods }, } = license; @@ -214,14 +273,14 @@ class LicenseClass { msg: 'Period validation failed', period, }); - return period.invalidBehavior; + return this.getResultingBehavior(period); }); } private async validateLicenseLimits( license: ILicenseV3, behaviorFilter: (behavior: LicenseBehavior) => boolean, - ): Promise { + ): Promise { const { limits } = license; const limitKeys = Object.keys(limits) as (keyof ILicenseV3['limits'])[]; @@ -243,11 +302,11 @@ class LicenseClass { kind: limitKey, limit, }); - return limit.behavior; + return this.getResultingBehavior(limit); }); }), ) - ).reduce((prev, curr) => [...new Set([...prev, ...curr])], []); + ).reduce((prev, curr) => [...prev, ...curr], []); } private async shouldPreventAction( @@ -267,7 +326,7 @@ class LicenseClass { ); } - private async runValidation(license: ILicenseV3, behaviorsToValidate: LicenseBehavior[] = []): Promise { + private async runValidation(license: ILicenseV3, behaviorsToValidate: LicenseBehavior[] = []): Promise { const shouldValidateBehavior = (behavior: LicenseBehavior) => !behaviorsToValidate?.length || behaviorsToValidate.includes(behavior); return [ @@ -286,14 +345,15 @@ class LicenseClass { 'invalidate_license', 'prevent_installation', 'start_fair_policy', + 'disable_modules', ]); - if (behaviorsTriggered.includes('invalidate_license') || behaviorsTriggered.includes('prevent_installation')) { + if (this.isBehaviorsInResult(behaviorsTriggered, ['invalidate_license', 'prevent_installation'])) { return; } this.valid = true; - this.inFairPolicy = behaviorsTriggered.includes('start_fair_policy'); + this.inFairPolicy = this.isBehaviorsInResult(behaviorsTriggered, ['start_fair_policy']); if (this.license.information.tags) { for (const tag of this.license.information.tags) { @@ -301,8 +361,11 @@ class LicenseClass { } } - this._validModules(this.license.grantedModules.map(({ module }) => module)); - console.log('#### License validated:', this.license.grantedModules.map(({ module }) => module).join(', ')); + const disabledModules = this.getModulesToDisable(behaviorsTriggered); + const modulesToEnable = this.license.grantedModules.filter(({ module }) => !disabledModules.includes(module)); + + this._validModules(modulesToEnable.map(({ module }) => module)); + console.log('#### License validated:', modulesToEnable.join(', ')); } EnterpriseLicenses.emit('validate'); @@ -400,14 +463,13 @@ class LicenseClass { const { validation: { serverUrls, validPeriods }, limits, - grantedModules, } = this.license; console.log('---- License enabled ----'); console.log(' url ->', JSON.stringify(serverUrls)); console.log(' periods ->', JSON.stringify(validPeriods)); console.log(' limits ->', JSON.stringify(limits)); - console.log(' modules ->', grantedModules.map(({ module }) => module).join(', ')); + console.log(' modules ->', [...this.modules].join(', ')); console.log('-------------------------'); } @@ -480,7 +542,7 @@ export async function setURL(url: string): Promise { } export function hasLicense(feature: string): boolean { - return License.hasModule(feature); + return License.hasModule(feature as LicenseModule); } export function isEnterprise(): boolean { @@ -492,11 +554,15 @@ export function getMaxActiveUsers(): number { return License.getLicenseLimit('activeUsers') ?? 0; } -export function getUnmodifiedLicense(): ILicenseV3 | ILicenseV2 | undefined { - return License.getUnmodifiedLicense(); +export function getUnmodifiedLicenseAndModules(): { license: ILicenseV2 | ILicenseV3; modules: LicenseModule[] } | undefined { + return License.getUnmodifiedLicenseAndModules(); +} + +export function getLicense(): ILicenseV3 | undefined { + return License.getLicense(); } -export function getModules(): string[] { +export function getModules(): LicenseModule[] { return License.getModules(); } @@ -536,7 +602,7 @@ export async function canEnableApp(app: IAppStorageItem): Promise { return License.canEnableApp(app); } -export function onLicense(feature: BundleFeature, cb: (...args: any[]) => void): void | Promise { +export function onLicense(feature: LicenseModule, cb: (...args: any[]) => void): void | Promise { if (hasLicense(feature)) { return cb(); } @@ -544,7 +610,7 @@ export function onLicense(feature: BundleFeature, cb: (...args: any[]) => void): EnterpriseLicenses.once(`valid:${feature}`, cb); } -function onValidFeature(feature: BundleFeature, cb: () => void): () => void { +function onValidFeature(feature: LicenseModule, cb: () => void): () => void { EnterpriseLicenses.on(`valid:${feature}`, cb); if (hasLicense(feature)) { @@ -556,7 +622,7 @@ function onValidFeature(feature: BundleFeature, cb: () => void): () => void { }; } -function onInvalidFeature(feature: BundleFeature, cb: () => void): () => void { +function onInvalidFeature(feature: LicenseModule, cb: () => void): () => void { EnterpriseLicenses.on(`invalid:${feature}`, cb); if (!hasLicense(feature)) { @@ -569,7 +635,7 @@ function onInvalidFeature(feature: BundleFeature, cb: () => void): () => void { } export function onToggledFeature( - feature: BundleFeature, + feature: LicenseModule, { up, down, @@ -616,22 +682,13 @@ export function onInvalidateLicense(cb: (...args: any[]) => void): void { EnterpriseLicenses.on('invalidate', cb); } -export function flatModules(modulesAndBundles: string[]): string[] { - const bundles = modulesAndBundles.filter(isBundle); - const modules = modulesAndBundles.filter((x) => !isBundle(x)); - - const modulesFromBundles = bundles.map(getBundleModules).flat(); - - return modules.concat(modulesFromBundles); -} - interface IOverrideClassProperties { [key: string]: (...args: any[]) => any; } type Class = { new (...args: any[]): any }; -export async function overwriteClassOnLicense(license: BundleFeature, original: Class, overwrite: IOverrideClassProperties): Promise { +export async function overwriteClassOnLicense(license: LicenseModule, original: Class, overwrite: IOverrideClassProperties): Promise { await onLicense(license, () => { Object.entries(overwrite).forEach(([key, value]) => { const originalFn = original.prototype[key]; diff --git a/apps/meteor/ee/server/api/licenses.ts b/apps/meteor/ee/server/api/licenses.ts index a33678c74a6b..f670d61e3019 100644 --- a/apps/meteor/ee/server/api/licenses.ts +++ b/apps/meteor/ee/server/api/licenses.ts @@ -1,19 +1,9 @@ -import type { ILicenseV2, ILicenseV3 } from '@rocket.chat/core-typings'; import { Settings, Users } from '@rocket.chat/models'; import { check } from 'meteor/check'; import { API } from '../../../app/api/server/api'; import { hasPermissionAsync } from '../../../app/authorization/server/functions/hasPermission'; -import { getUnmodifiedLicense, validateFormat, flatModules, getMaxActiveUsers, isEnterprise } from '../../app/license/server/license'; - -const isLicenseV2 = (license: ILicenseV2 | ILicenseV3): license is ILicenseV2 => 'modules' in license; - -function licenseTransform(license: ILicenseV2 | ILicenseV3): ILicenseV2 | (ILicenseV3 & { modules: string[] }) { - return { - ...license, - modules: isLicenseV2(license) ? flatModules(license.modules) : license.grantedModules.map(({ module }) => module), - }; -} +import { getUnmodifiedLicenseAndModules, validateFormat, getMaxActiveUsers, isEnterprise } from '../../app/license/server/license'; API.v1.addRoute( 'licenses.get', @@ -24,8 +14,8 @@ API.v1.addRoute( return API.v1.unauthorized(); } - const license = getUnmodifiedLicense(); - const licenses = license ? [licenseTransform(license)] : []; + const license = getUnmodifiedLicenseAndModules(); + const licenses = license ? [license] : []; return API.v1.success({ licenses }); }, diff --git a/apps/meteor/ee/server/startup/upsell.ts b/apps/meteor/ee/server/startup/upsell.ts index c9e4c513276c..fdea300ff9a2 100644 --- a/apps/meteor/ee/server/startup/upsell.ts +++ b/apps/meteor/ee/server/startup/upsell.ts @@ -1,18 +1,12 @@ import { Settings } from '@rocket.chat/models'; import { Meteor } from 'meteor/meteor'; -import { onValidateLicenses, getLicenses } from '../../app/license/server/license'; +import { onValidateLicenses, getLicense } from '../../app/license/server/license'; const handleHadTrial = (): void => { - getLicenses().forEach(({ valid, license }): void => { - if (!valid) { - return; - } - - if (license.meta?.trial) { - void Settings.updateValueById('Cloud_Workspace_Had_Trial', true); - } - }); + if (getLicense()?.information.trial) { + void Settings.updateValueById('Cloud_Workspace_Had_Trial', true); + } }; Meteor.startup(() => { diff --git a/packages/core-typings/src/ee/ILicense/ILicenseV3.ts b/packages/core-typings/src/ee/ILicense/ILicenseV3.ts index 06a60dcb8c0f..c4954fd604f5 100644 --- a/packages/core-typings/src/ee/ILicense/ILicenseV3.ts +++ b/packages/core-typings/src/ee/ILicense/ILicenseV3.ts @@ -1,21 +1,24 @@ import type { ILicenseTag } from './ILicenseTag'; -export type LicenseBehavior = 'invalidate_license' | 'start_fair_policy' | 'prevent_action' | 'prevent_installation'; +export type LicenseBehavior = 'invalidate_license' | 'start_fair_policy' | 'prevent_action' | 'prevent_installation' | 'disable_modules'; export type LicenseLimit = { max: number; behavior: T; -}; +} & (T extends 'disable_modules' ? { behavior: T; modules: LicenseModule[] } : { behavior: T }); export type Timestamp = string; +export type LicensePeriodBehavior = Exclude; + export type LicensePeriod = { validFrom?: Timestamp; validUntil?: Timestamp; - invalidBehavior: Exclude; -} & ({ validFrom: Timestamp } | { validUntil: Timestamp }); + invalidBehavior: LicenseBehavior; +} & ({ validFrom: Timestamp } | { validUntil: Timestamp }) & + ({ invalidBehavior: 'disable_modules'; modules: LicenseModule[] } | { invalidBehavior: Exclude }); -export type Module = +export type LicenseModule = | 'auditing' | 'canned-responses' | 'ldap-enterprise' @@ -79,7 +82,7 @@ export interface ILicenseV3 { }; }; grantedModules: { - module: Module; + module: LicenseModule; }[]; limits: { activeUsers?: LicenseLimit[]; @@ -87,6 +90,7 @@ export interface ILicenseV3 { roomsPerGuest?: LicenseLimit<'prevent_action'>[]; privateApps?: LicenseLimit[]; marketplaceApps?: LicenseLimit[]; + monthlyActiveContacts?: LicenseLimit[]; }; cloudMeta?: Record; } From 118e22fe5ed774ae415ad6ae7ac964a9c3ba5502 Mon Sep 17 00:00:00 2001 From: Pierre Lehnen Date: Mon, 18 Sep 2023 12:59:54 -0300 Subject: [PATCH 13/38] Fixed trial information --- .../client/views/hooks/useUpgradeTabParams.ts | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/apps/meteor/client/views/hooks/useUpgradeTabParams.ts b/apps/meteor/client/views/hooks/useUpgradeTabParams.ts index d7e5966745e6..761c0c365802 100644 --- a/apps/meteor/client/views/hooks/useUpgradeTabParams.ts +++ b/apps/meteor/client/views/hooks/useUpgradeTabParams.ts @@ -1,3 +1,4 @@ +import type { ILicenseV2, ILicenseV3 } from '@rocket.chat/core-typings'; import { useSetting } from '@rocket.chat/ui-contexts'; import { format } from 'date-fns'; @@ -16,12 +17,14 @@ export const useUpgradeTabParams = (): { tabType: UpgradeTabVariant | false; tri const hasValidLicense = licensesData?.licenses.some((license) => license.modules.length > 0) ?? false; const hadExpiredTrials = cloudWorkspaceHadTrial ?? false; - // #TODO: Update to use license v3 format, load meta info from license.information - const licenseMeta = licensesData?.licenses?.map((license: any) => (license.meta ?? license.cloudMeta) as Record); + const licenses = (licensesData?.licenses || []) as (Partial & { modules: string[] })[]; - const trialLicense = licenseMeta?.find((meta) => meta?.trial); - const isTrial = licenseMeta?.every((meta) => meta?.trial) ?? false; - const trialEndDate = trialLicense ? format(new Date(trialLicense.trialEnd), 'yyyy-MM-dd') : undefined; + const trialLicense = licenses.find(({ meta, information }) => information?.trial ?? meta?.trial); + const isTrial = Boolean(trialLicense); + const trialEndDate = + trialLicense?.meta?.trialEnd || trialLicense?.cloudMeta?.trialEnd + ? format(new Date(trialLicense.meta?.trialEnd ?? trialLicense.cloudMeta?.trialEnd), 'yyyy-MM-dd') + : undefined; const upgradeTabType = getUpgradeTabType({ registered, From de75365a5180aa006bc21b45cfaf61d92f9cb45e Mon Sep 17 00:00:00 2001 From: Pierre Lehnen Date: Mon, 18 Sep 2023 13:56:21 -0300 Subject: [PATCH 14/38] missing null-check --- apps/meteor/ee/app/authorization/server/validateUserRoles.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/meteor/ee/app/authorization/server/validateUserRoles.js b/apps/meteor/ee/app/authorization/server/validateUserRoles.js index 6263fc51d694..b0203f664395 100644 --- a/apps/meteor/ee/app/authorization/server/validateUserRoles.js +++ b/apps/meteor/ee/app/authorization/server/validateUserRoles.js @@ -11,7 +11,7 @@ export const validateUserRoles = async function (userId, userData) { const isGuest = Boolean(userData.roles?.includes('guest') && userData.roles.length === 1); const currentUserData = userData._id ? await Users.findOneById(userData._id) : null; - const wasGuest = Boolean(currentUserData.roles?.includes('guest') && currentUserData.roles.length === 1); + const wasGuest = Boolean(currentUserData?.roles?.includes('guest') && currentUserData.roles.length === 1); if (currentUserData?.type === 'app') { return; From eec34a6e769fa8fad8ba07cb11eb89deaeaec4c9 Mon Sep 17 00:00:00 2001 From: Pierre Lehnen Date: Mon, 18 Sep 2023 19:21:48 -0300 Subject: [PATCH 15/38] moved the license code to a separate package --- apps/meteor/app/api/server/v1/federation.ts | 2 +- .../app/statistics/server/lib/statistics.ts | 2 +- .../client/views/hooks/useUpgradeTabParams.ts | 2 +- .../ee/app/api-enterprise/server/index.ts | 2 +- .../authorization/server/validateUserRoles.js | 6 +- .../ee/app/canned-responses/server/index.ts | 2 +- .../ee/app/license/server/canEnableApp.ts | 25 + .../ee/app/license/server/getStatistics.ts | 5 +- apps/meteor/ee/app/license/server/index.ts | 2 - .../ee/app/license/server/lib/getAppCount.ts | 2 +- .../license/server/license.internalService.ts | 8 +- apps/meteor/ee/app/license/server/license.ts | 700 ------------------ apps/meteor/ee/app/license/server/methods.ts | 6 +- apps/meteor/ee/app/license/server/settings.ts | 2 +- apps/meteor/ee/app/license/server/startup.ts | 9 +- .../server/business-hour/Helper.ts | 2 +- .../app/livechat-enterprise/server/index.ts | 2 +- .../server/lib/LivechatEnterprise.ts | 6 +- .../app/message-read-receipt/server/index.ts | 2 +- .../meteor/ee/app/settings/server/settings.ts | 6 +- .../server/services/voipService.ts | 2 +- .../ee/client/hooks/useHasLicenseModule.ts | 5 +- apps/meteor/ee/client/lib/onToggledFeature.ts | 4 +- apps/meteor/ee/server/api/api.ts | 3 +- apps/meteor/ee/server/api/chat.ts | 4 +- apps/meteor/ee/server/api/licenses.ts | 2 +- apps/meteor/ee/server/api/roles.ts | 2 +- apps/meteor/ee/server/api/sessions.ts | 14 +- .../endpoints/appsCountHandler.ts | 2 +- .../ee/server/apps/communication/rest.ts | 3 +- apps/meteor/ee/server/apps/orchestrator.js | 2 +- apps/meteor/ee/server/configuration/ldap.ts | 2 +- apps/meteor/ee/server/configuration/oauth.ts | 2 +- .../server/configuration/outlookCalendar.ts | 2 +- apps/meteor/ee/server/configuration/saml.ts | 2 +- .../server/configuration/videoConference.ts | 2 +- apps/meteor/ee/server/lib/syncUserRoles.ts | 4 +- .../ee/server/methods/getReadReceipts.ts | 4 +- apps/meteor/ee/server/models/startup.ts | 2 +- .../ee/server/startup/apps/trialExpiration.ts | 2 +- apps/meteor/ee/server/startup/audit.ts | 3 +- .../ee/server/startup/deviceManagement.ts | 3 +- .../ee/server/startup/engagementDashboard.ts | 3 +- .../ee/server/startup/maxRoomsPerGuest.ts | 4 +- apps/meteor/ee/server/startup/seatsCap.ts | 10 +- apps/meteor/ee/server/startup/services.ts | 2 +- apps/meteor/ee/server/startup/upsell.ts | 5 +- ...getInstallationSourceFromAppStorageItem.ts | 2 +- apps/meteor/package.json | 1 + apps/meteor/server/startup/migrations/v278.ts | 2 +- ee/packages/license/.eslintrc.json | 4 + ee/packages/license/package.json | 29 + ee/packages/license/src/actionBlockers.ts | 10 + .../packages/license/src}/decrypt.ts | 0 .../license/src/definition}/ILicenseTag.ts | 0 .../license/src/definition}/ILicenseV2.ts | 0 .../license/src/definition}/ILicenseV3.ts | 40 +- .../license/src/definition/LicenseBehavior.ts | 8 + .../license/src/definition/LicenseLimit.ts | 7 + .../license/src/definition/LicenseModule.ts | 18 + .../license/src/definition/LicensePeriod.ts | 13 + .../license/src/definition/LimitContext.ts | 5 + ee/packages/license/src/deprecated.ts | 25 + ee/packages/license/src/encryptedLicense.ts | 7 + ee/packages/license/src/events/deprecated.ts | 12 + ee/packages/license/src/events/emitter.ts | 19 + ee/packages/license/src/events/listeners.ts | 69 ++ .../src/events/overwriteClassOnLicense.ts | 19 + ee/packages/license/src/index.ts | 36 + ee/packages/license/src/license.ts | 134 ++++ ee/packages/license/src/logger.ts | 3 + ee/packages/license/src/modules.ts | 27 + ee/packages/license/src/showLicense.ts | 26 + ee/packages/license/src/tags.ts | 22 + .../packages/license/src/v2}/bundles.ts | 0 .../packages/license/src/v2/convertToV3.ts | 7 +- .../packages/license/src/v2}/getTagColor.ts | 0 .../getCurrentValueForLicenseLimit.ts | 27 + .../src/validation/getModulesToDisable.ts | 15 + .../src/validation/getResultingBehavior.ts | 20 + .../src/validation/isBehaviorsInResult.ts | 4 + .../license/src/validation/runValidation.ts | 17 + .../src/validation/shouldPreventAction.ts | 17 + .../license/src/validation/validateFormat.ts | 14 + .../src/validation/validateLicenseLimits.ts | 37 + .../src/validation/validateLicensePeriods.ts | 38 + .../src/validation/validateLicenseUrl.ts | 55 ++ ee/packages/license/src/workspaceUrl.ts | 11 + ee/packages/license/tsconfig.json | 9 + packages/core-typings/src/index.ts | 3 - packages/rest-typings/package.json | 1 + packages/rest-typings/src/v1/licenses.ts | 2 +- yarn.lock | 17 + 93 files changed, 893 insertions(+), 827 deletions(-) create mode 100644 apps/meteor/ee/app/license/server/canEnableApp.ts delete mode 100644 apps/meteor/ee/app/license/server/license.ts create mode 100644 ee/packages/license/.eslintrc.json create mode 100644 ee/packages/license/package.json create mode 100644 ee/packages/license/src/actionBlockers.ts rename {apps/meteor/ee/app/license/server => ee/packages/license/src}/decrypt.ts (100%) rename {packages/core-typings/src/ee/ILicense => ee/packages/license/src/definition}/ILicenseTag.ts (100%) rename {packages/core-typings/src/ee/ILicense => ee/packages/license/src/definition}/ILicenseV2.ts (100%) rename {packages/core-typings/src/ee/ILicense => ee/packages/license/src/definition}/ILicenseV3.ts (53%) create mode 100644 ee/packages/license/src/definition/LicenseBehavior.ts create mode 100644 ee/packages/license/src/definition/LicenseLimit.ts create mode 100644 ee/packages/license/src/definition/LicenseModule.ts create mode 100644 ee/packages/license/src/definition/LicensePeriod.ts create mode 100644 ee/packages/license/src/definition/LimitContext.ts create mode 100644 ee/packages/license/src/deprecated.ts create mode 100644 ee/packages/license/src/encryptedLicense.ts create mode 100644 ee/packages/license/src/events/deprecated.ts create mode 100644 ee/packages/license/src/events/emitter.ts create mode 100644 ee/packages/license/src/events/listeners.ts create mode 100644 ee/packages/license/src/events/overwriteClassOnLicense.ts create mode 100644 ee/packages/license/src/index.ts create mode 100644 ee/packages/license/src/license.ts create mode 100644 ee/packages/license/src/logger.ts create mode 100644 ee/packages/license/src/modules.ts create mode 100644 ee/packages/license/src/showLicense.ts create mode 100644 ee/packages/license/src/tags.ts rename {apps/meteor/ee/app/license/server => ee/packages/license/src/v2}/bundles.ts (100%) rename apps/meteor/ee/app/license/server/fromV2toV3.ts => ee/packages/license/src/v2/convertToV3.ts (90%) rename {apps/meteor/ee/app/license/server => ee/packages/license/src/v2}/getTagColor.ts (100%) create mode 100644 ee/packages/license/src/validation/getCurrentValueForLicenseLimit.ts create mode 100644 ee/packages/license/src/validation/getModulesToDisable.ts create mode 100644 ee/packages/license/src/validation/getResultingBehavior.ts create mode 100644 ee/packages/license/src/validation/isBehaviorsInResult.ts create mode 100644 ee/packages/license/src/validation/runValidation.ts create mode 100644 ee/packages/license/src/validation/shouldPreventAction.ts create mode 100644 ee/packages/license/src/validation/validateFormat.ts create mode 100644 ee/packages/license/src/validation/validateLicenseLimits.ts create mode 100644 ee/packages/license/src/validation/validateLicensePeriods.ts create mode 100644 ee/packages/license/src/validation/validateLicenseUrl.ts create mode 100644 ee/packages/license/src/workspaceUrl.ts create mode 100644 ee/packages/license/tsconfig.json diff --git a/apps/meteor/app/api/server/v1/federation.ts b/apps/meteor/app/api/server/v1/federation.ts index 02fc30763eeb..480e826c351b 100644 --- a/apps/meteor/app/api/server/v1/federation.ts +++ b/apps/meteor/app/api/server/v1/federation.ts @@ -1,7 +1,7 @@ import { Federation, FederationEE } from '@rocket.chat/core-services'; +import { isEnterprise } from '@rocket.chat/license'; import { isFederationVerifyMatrixIdProps } from '@rocket.chat/rest-typings'; -import { isEnterprise } from '../../../../ee/app/license/server'; import { API } from '../api'; API.v1.addRoute( diff --git a/apps/meteor/app/statistics/server/lib/statistics.ts b/apps/meteor/app/statistics/server/lib/statistics.ts index 8cfe45b42232..54470a209196 100644 --- a/apps/meteor/app/statistics/server/lib/statistics.ts +++ b/apps/meteor/app/statistics/server/lib/statistics.ts @@ -27,7 +27,7 @@ import { } from '@rocket.chat/models'; import { MongoInternals } from 'meteor/mongo'; -import { getStatistics as getEnterpriseStatistics } from '../../../../ee/app/license/server'; +import { getStatistics as getEnterpriseStatistics } from '../../../../ee/app/license/server/getStatistics'; import { readSecondaryPreferred } from '../../../../server/database/readSecondaryPreferred'; import { isRunningMs } from '../../../../server/lib/isRunningMs'; import { getControl } from '../../../../server/lib/migrations'; diff --git a/apps/meteor/client/views/hooks/useUpgradeTabParams.ts b/apps/meteor/client/views/hooks/useUpgradeTabParams.ts index 761c0c365802..50188a5d4e4f 100644 --- a/apps/meteor/client/views/hooks/useUpgradeTabParams.ts +++ b/apps/meteor/client/views/hooks/useUpgradeTabParams.ts @@ -1,4 +1,4 @@ -import type { ILicenseV2, ILicenseV3 } from '@rocket.chat/core-typings'; +import type { ILicenseV2, ILicenseV3 } from '@rocket.chat/license'; import { useSetting } from '@rocket.chat/ui-contexts'; import { format } from 'date-fns'; diff --git a/apps/meteor/ee/app/api-enterprise/server/index.ts b/apps/meteor/ee/app/api-enterprise/server/index.ts index 6af539bda36c..5c28424bbb3f 100644 --- a/apps/meteor/ee/app/api-enterprise/server/index.ts +++ b/apps/meteor/ee/app/api-enterprise/server/index.ts @@ -1,4 +1,4 @@ -import { onLicense } from '../../license/server'; +import { onLicense } from '@rocket.chat/license'; await onLicense('canned-responses', async () => { await import('./canned-responses'); diff --git a/apps/meteor/ee/app/authorization/server/validateUserRoles.js b/apps/meteor/ee/app/authorization/server/validateUserRoles.js index b0203f664395..1cea4b20824a 100644 --- a/apps/meteor/ee/app/authorization/server/validateUserRoles.js +++ b/apps/meteor/ee/app/authorization/server/validateUserRoles.js @@ -1,8 +1,8 @@ +import { isEnterprise, preventNewGuests, preventNewUsers } from '@rocket.chat/license'; import { Users } from '@rocket.chat/models'; import { Meteor } from 'meteor/meteor'; import { i18n } from '../../../../server/lib/i18n'; -import { isEnterprise, canAddNewGuestUser, canAddNewUser } from '../../license/server/license'; export const validateUserRoles = async function (userId, userData) { if (!isEnterprise()) { @@ -22,7 +22,7 @@ export const validateUserRoles = async function (userId, userData) { return; } - if (!(await canAddNewGuestUser())) { + if (await preventNewGuests()) { throw new Meteor.Error('error-max-guests-number-reached', 'Maximum number of guests reached.', { method: 'insertOrUpdateUser', field: 'Assign_role', @@ -36,7 +36,7 @@ export const validateUserRoles = async function (userId, userData) { return; } - if (!(await canAddNewUser())) { + if (await preventNewUsers()) { throw new Meteor.Error('error-license-user-limit-reached', i18n.t('error-license-user-limit-reached')); } }; diff --git a/apps/meteor/ee/app/canned-responses/server/index.ts b/apps/meteor/ee/app/canned-responses/server/index.ts index 47249c017b83..9e91153af2e7 100644 --- a/apps/meteor/ee/app/canned-responses/server/index.ts +++ b/apps/meteor/ee/app/canned-responses/server/index.ts @@ -1,4 +1,4 @@ -import { onLicense } from '../../license/server'; +import { onLicense } from '@rocket.chat/license'; await onLicense('canned-responses', async () => { const { createSettings } = await import('./settings'); diff --git a/apps/meteor/ee/app/license/server/canEnableApp.ts b/apps/meteor/ee/app/license/server/canEnableApp.ts new file mode 100644 index 000000000000..cb32600fd66d --- /dev/null +++ b/apps/meteor/ee/app/license/server/canEnableApp.ts @@ -0,0 +1,25 @@ +import type { IAppStorageItem } from '@rocket.chat/apps-engine/server/storage'; +import { Apps } from '@rocket.chat/core-services'; +import { preventNewPrivateApps, preventNewMarketplaceApps } from '@rocket.chat/license'; + +import { getInstallationSourceFromAppStorageItem } from '../../../../lib/apps/getInstallationSourceFromAppStorageItem'; + +export const canEnableApp = async (app: IAppStorageItem): Promise => { + if (!(await Apps.isInitialized())) { + return false; + } + + // Migrated apps were installed before the validation was implemented + // so they're always allowed to be enabled + if (app.migrated) { + return true; + } + + const source = getInstallationSourceFromAppStorageItem(app); + switch (source) { + case 'private': + return !(await preventNewPrivateApps()); + default: + return !(await preventNewMarketplaceApps()); + } +}; diff --git a/apps/meteor/ee/app/license/server/getStatistics.ts b/apps/meteor/ee/app/license/server/getStatistics.ts index d7f81e416bfd..f0ff6b6562d0 100644 --- a/apps/meteor/ee/app/license/server/getStatistics.ts +++ b/apps/meteor/ee/app/license/server/getStatistics.ts @@ -1,10 +1,9 @@ import { log } from 'console'; import { Analytics } from '@rocket.chat/core-services'; +import { getModules, getTags, hasModule } from '@rocket.chat/license'; import { CannedResponse, OmnichannelServiceLevelAgreements, LivechatRooms, LivechatTag, LivechatUnit, Users } from '@rocket.chat/models'; -import { getModules, getTags, hasLicense } from './license'; - type ENTERPRISE_STATISTICS = GenericStats & Partial; type GenericStats = { @@ -45,7 +44,7 @@ export async function getStatistics(): Promise { // These models are only available on EE license so don't import them inside CE license as it will break the build async function getEEStatistics(): Promise { - if (!hasLicense('livechat-enterprise')) { + if (!hasModule('livechat-enterprise')) { return; } diff --git a/apps/meteor/ee/app/license/server/index.ts b/apps/meteor/ee/app/license/server/index.ts index f13e315d2866..403922524fa8 100644 --- a/apps/meteor/ee/app/license/server/index.ts +++ b/apps/meteor/ee/app/license/server/index.ts @@ -2,6 +2,4 @@ import './settings'; import './methods'; import './startup'; -export { onLicense, overwriteClassOnLicense, isEnterprise } from './license'; - export { getStatistics } from './getStatistics'; diff --git a/apps/meteor/ee/app/license/server/lib/getAppCount.ts b/apps/meteor/ee/app/license/server/lib/getAppCount.ts index f408143218de..a05813f596bb 100644 --- a/apps/meteor/ee/app/license/server/lib/getAppCount.ts +++ b/apps/meteor/ee/app/license/server/lib/getAppCount.ts @@ -1,5 +1,5 @@ import { Apps } from '@rocket.chat/core-services'; -import type { LicenseAppSources } from '@rocket.chat/core-typings'; +import type { LicenseAppSources } from '@rocket.chat/license'; import { getInstallationSourceFromAppStorageItem } from '../../../../../lib/apps/getInstallationSourceFromAppStorageItem'; diff --git a/apps/meteor/ee/app/license/server/license.internalService.ts b/apps/meteor/ee/app/license/server/license.internalService.ts index 047a67d323ff..354c52aa865c 100644 --- a/apps/meteor/ee/app/license/server/license.internalService.ts +++ b/apps/meteor/ee/app/license/server/license.internalService.ts @@ -1,9 +1,9 @@ import type { ILicense } from '@rocket.chat/core-services'; import { api, ServiceClassInternal } from '@rocket.chat/core-services'; +import { getModules, hasModule, isEnterprise, onModule, onValidateLicense, type LicenseModule } from '@rocket.chat/license'; import { guestPermissions } from '../../authorization/lib/guestPermissions'; import { resetEnterprisePermissions } from '../../authorization/server/resetEnterprisePermissions'; -import { getModules, hasLicense, isEnterprise, onModule, onValidateLicenses } from './license'; export class LicenseService extends ServiceClassInternal implements ILicense { protected name = 'license'; @@ -11,7 +11,7 @@ export class LicenseService extends ServiceClassInternal implements ILicense { constructor() { super(); - onValidateLicenses((): void => { + onValidateLicense((): void => { if (!isEnterprise()) { return; } @@ -34,8 +34,8 @@ export class LicenseService extends ServiceClassInternal implements ILicense { await resetEnterprisePermissions(); } - hasLicense(feature: string): boolean { - return hasLicense(feature); + hasLicense(feature: LicenseModule): boolean { + return hasModule(feature); } isEnterprise(): boolean { diff --git a/apps/meteor/ee/app/license/server/license.ts b/apps/meteor/ee/app/license/server/license.ts deleted file mode 100644 index d2e59c99278a..000000000000 --- a/apps/meteor/ee/app/license/server/license.ts +++ /dev/null @@ -1,700 +0,0 @@ -import { EventEmitter } from 'events'; - -import type { IAppStorageItem } from '@rocket.chat/apps-engine/server/storage'; -import { Apps } from '@rocket.chat/core-services'; -import type { - ILicenseV2, - ILicenseTag, - ILicenseV3, - Timestamp, - LicenseBehavior, - IUser, - LicenseLimit, - LicensePeriod, - LicenseLimitKind, - LicenseModule, -} from '@rocket.chat/core-typings'; -import { Logger } from '@rocket.chat/logger'; -import { Users, Subscriptions } from '@rocket.chat/models'; - -import { getInstallationSourceFromAppStorageItem } from '../../../../lib/apps/getInstallationSourceFromAppStorageItem'; -import decrypt from './decrypt'; -import { fromV2toV3 } from './fromV2toV3'; -import { getAppCount } from './lib/getAppCount'; - -const EnterpriseLicenses = new EventEmitter(); - -const logger = new Logger('License'); - -type LimitContext = T extends 'roomsPerGuest' ? { userId: IUser['_id'] } : Record; - -type BehaviorWithContext = { - behavior: LicenseBehavior; - modules?: LicenseModule[]; -}; - -class LicenseClass { - private url: string | null = null; - - private encryptedLicense: string | undefined; - - private tags = new Set(); - - private modules = new Set(); - - private unmodifiedLicense: ILicenseV2 | ILicenseV3 | undefined; - - private license: ILicenseV3 | undefined; - - private valid: boolean | undefined; - - private inFairPolicy: boolean | undefined; - - private _isPeriodInvalid(from?: Timestamp, until?: Timestamp): boolean { - const now = new Date(); - - if (from && now < new Date(from)) { - return true; - } - - if (until && now > new Date(until)) { - return true; - } - - return false; - } - - private _validateURL(licenseURL: string, url: string): boolean { - licenseURL = licenseURL - .replace(/\./g, '\\.') // convert dots to literal - .replace(/\*/g, '.*'); // convert * to .* - const regex = new RegExp(`^${licenseURL}$`, 'i'); - - return !!regex.exec(url); - } - - private _validModules(licenseModules: LicenseModule[]): void { - licenseModules.forEach((module) => { - this.modules.add(module); - EnterpriseLicenses.emit('module', { module, valid: true }); - EnterpriseLicenses.emit(`valid:${module}`); - }); - } - - private _invalidModules(licenseModules: LicenseModule[]): void { - licenseModules.forEach((module) => { - EnterpriseLicenses.emit('module', { module, valid: false }); - EnterpriseLicenses.emit(`invalid:${module}`); - this.modules.delete(module); - }); - } - - private _addTag(tag: ILicenseTag): void { - // make sure to not add duplicated tag names - for (const addedTag of this.tags) { - if (addedTag.name.toLowerCase() === tag.name.toLowerCase()) { - return; - } - } - - this.tags.add(tag); - } - - private removeCurrentLicense(): void { - const { license, valid } = this; - - this.license = undefined; - this.unmodifiedLicense = undefined; - this.valid = undefined; - this.inFairPolicy = undefined; - - if (!license || !valid) { - return; - } - - this.valid = false; - EnterpriseLicenses.emit('invalidate'); - this._invalidModules([...this.modules]); - this.modules.clear(); - } - - public async setLicenseV3(license: ILicenseV3): Promise { - this.removeCurrentLicense(); - - this.unmodifiedLicense = license; - this.license = license; - - return this.validate(); - } - - public async setLicenseV2(license: ILicenseV2): Promise { - this.removeCurrentLicense(); - - const licenseV3 = fromV2toV3(license); - - this.unmodifiedLicense = license; - this.license = licenseV3; - - return this.validate(); - } - - public lockLicense(encryptedLicense: string): void { - this.encryptedLicense = encryptedLicense; - } - - public isLicenseDuplicate(encryptedLicense: string): boolean { - return Boolean(this.encryptedLicense && this.encryptedLicense === encryptedLicense); - } - - public hasModule(module: LicenseModule): boolean { - return this.modules.has(module); - } - - public hasValidLicense(): boolean { - return Boolean(this.license && this.valid); - } - - public getUnmodifiedLicenseAndModules(): { license: ILicenseV2 | ILicenseV3; modules: LicenseModule[] } | undefined { - if (this.valid && this.unmodifiedLicense) { - return { - license: this.unmodifiedLicense, - modules: [...this.modules], - }; - } - } - - public getLicense(): ILicenseV3 | undefined { - if (this.valid && this.license) { - return this.license; - } - } - - public getModules(): LicenseModule[] { - return [...this.modules]; - } - - public getTags(): ILicenseTag[] { - return [...this.tags]; - } - - public async setURL(url: string): Promise { - this.url = url.replace(/\/$/, '').replace(/^https?:\/\/(.*)$/, '$1'); - - await this.validate(); - } - - private getResultingBehavior(data: LicenseLimit | LicensePeriod | Partial): BehaviorWithContext { - const behavior = 'invalidBehavior' in data ? data.invalidBehavior : data.behavior; - - switch (behavior) { - case 'disable_modules': - return { - behavior, - modules: ('modules' in data && data.modules) || [], - }; - - default: - return { - behavior, - } as BehaviorWithContext; - } - } - - private filterValidationResult(result: BehaviorWithContext[], expectedBehavior: LicenseBehavior): BehaviorWithContext[] { - return result.filter(({ behavior }) => behavior === expectedBehavior) as BehaviorWithContext[]; - } - - private isBehaviorsInResult(result: BehaviorWithContext[], expectedBehaviors: LicenseBehavior[]): boolean { - return result.some(({ behavior }) => expectedBehaviors.includes(behavior)); - } - - private getModulesToDisable(validationResult: BehaviorWithContext[]): LicenseModule[] { - return [ - ...new Set([ - ...this.filterValidationResult(validationResult, 'disable_modules') - .map(({ modules }) => modules || []) - .reduce((prev, curr) => [...prev, ...curr], []), - ]), - ]; - } - - private validateLicenseUrl(license: ILicenseV3, behaviorFilter: (behavior: LicenseBehavior) => boolean): BehaviorWithContext[] { - if (!behaviorFilter('invalidate_license')) { - return []; - } - - const { - validation: { serverUrls }, - } = license; - - const { url: workspaceUrl } = this; - - if (!workspaceUrl) { - logger.error('Unable to validate license URL without knowing the workspace URL.'); - return [this.getResultingBehavior({ behavior: 'invalidate_license' })]; - } - - return serverUrls - .filter((url) => { - switch (url.type) { - case 'regex': - // #TODO - break; - case 'hash': - // #TODO - break; - case 'url': - return !this._validateURL(url.value, workspaceUrl); - } - - return false; - }) - .map((url) => { - logger.error({ - msg: 'Url validation failed', - url, - workspaceUrl, - }); - return this.getResultingBehavior({ behavior: 'invalidate_license' }); - }); - } - - private validateLicensePeriods(license: ILicenseV3, behaviorFilter: (behavior: LicenseBehavior) => boolean): BehaviorWithContext[] { - const { - validation: { validPeriods }, - } = license; - - return validPeriods - .filter( - ({ validFrom, validUntil, invalidBehavior }) => behaviorFilter(invalidBehavior) && this._isPeriodInvalid(validFrom, validUntil), - ) - .map((period) => { - logger.error({ - msg: 'Period validation failed', - period, - }); - return this.getResultingBehavior(period); - }); - } - - private async validateLicenseLimits( - license: ILicenseV3, - behaviorFilter: (behavior: LicenseBehavior) => boolean, - ): Promise { - const { limits } = license; - - const limitKeys = Object.keys(limits) as (keyof ILicenseV3['limits'])[]; - 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)); - if (!limitList?.length) { - return []; - } - - const currentValue = await this.getCurrentValueForLicenseLimit(limitKey); - return limitList - .filter(({ max }) => max < currentValue) - .map((limit) => { - logger.error({ - msg: 'Limit validation failed', - kind: limitKey, - limit, - }); - return this.getResultingBehavior(limit); - }); - }), - ) - ).reduce((prev, curr) => [...prev, ...curr], []); - } - - private async shouldPreventAction( - action: T, - context?: Partial>, - newCount = 1, - ): Promise { - if (!this.valid) { - return false; - } - - const currentValue = (await this.getCurrentValueForLicenseLimit(action, context)) + newCount; - return Boolean( - this.license?.limits[action] - ?.filter(({ behavior, max }) => behavior === 'prevent_action' && max >= 0) - .some(({ max }) => max < currentValue), - ); - } - - private async runValidation(license: ILicenseV3, behaviorsToValidate: LicenseBehavior[] = []): Promise { - const shouldValidateBehavior = (behavior: LicenseBehavior) => !behaviorsToValidate?.length || behaviorsToValidate.includes(behavior); - - return [ - ...new Set([ - ...this.validateLicenseUrl(license, shouldValidateBehavior), - ...this.validateLicensePeriods(license, shouldValidateBehavior), - ...(await this.validateLicenseLimits(license, shouldValidateBehavior)), - ]), - ]; - } - - private async validate(): Promise { - if (this.license) { - // #TODO: Only include 'prevent_installation' here if this is actually the initial installation of the license - const behaviorsTriggered = await this.runValidation(this.license, [ - 'invalidate_license', - 'prevent_installation', - 'start_fair_policy', - 'disable_modules', - ]); - - if (this.isBehaviorsInResult(behaviorsTriggered, ['invalidate_license', 'prevent_installation'])) { - return; - } - - this.valid = true; - this.inFairPolicy = this.isBehaviorsInResult(behaviorsTriggered, ['start_fair_policy']); - - if (this.license.information.tags) { - for (const tag of this.license.information.tags) { - this._addTag(tag); - } - } - - const disabledModules = this.getModulesToDisable(behaviorsTriggered); - const modulesToEnable = this.license.grantedModules.filter(({ module }) => !disabledModules.includes(module)); - - this._validModules(modulesToEnable.map(({ module }) => module)); - console.log('#### License validated:', modulesToEnable.join(', ')); - } - - EnterpriseLicenses.emit('validate'); - this.showLicense(); - } - - private async getCurrentValueForLicenseLimit( - limitKey: T, - context?: Partial>, - ): Promise { - switch (limitKey) { - case 'activeUsers': - return this.getCurrentActiveUsers(); - case 'guestUsers': - return this.getCurrentGuestUsers(); - case 'privateApps': - return this.getCurrentPrivateAppsCount(); - case 'marketplaceApps': - return this.getCurrentMarketplaceAppsCount(); - case 'roomsPerGuest': - if (context?.userId) { - return Subscriptions.countByUserId(context.userId); - } - return 0; - default: - return 0; - } - } - - private async getCurrentActiveUsers(): Promise { - return Users.getActiveLocalUserCount(); - } - - private async getCurrentGuestUsers(): Promise { - return Users.getActiveLocalGuestCount(); - } - - private async getCurrentPrivateAppsCount(): Promise { - return getAppCount('private'); - } - - private async getCurrentMarketplaceAppsCount(): Promise { - return getAppCount('marketplace'); - } - - public async canAddNewUser(userCount = 1): Promise { - return !(await this.shouldPreventAction('activeUsers', {}, userCount)); - } - - public async canAddNewGuestUser(guestCount = 1): Promise { - return !(await this.shouldPreventAction('guestUsers', {}, guestCount)); - } - - public async canAddNewPrivateApp(appCount = 1): Promise { - return !(await this.shouldPreventAction('privateApps', {}, appCount)); - } - - public async canAddNewMarketplaceApp(appCount = 1): Promise { - return !(await this.shouldPreventAction('marketplaceApps', {}, appCount)); - } - - public async canAddNewGuestSubscription(guest: IUser['_id'], roomCount = 1): Promise { - return !(await this.shouldPreventAction('roomsPerGuest', { userId: guest }, roomCount)); - } - - public async canEnableApp(app: IAppStorageItem): Promise { - if (!(await Apps.isInitialized())) { - return false; - } - - // Migrated apps were installed before the validation was implemented - // so they're always allowed to be enabled - if (app.migrated) { - return true; - } - - const source = getInstallationSourceFromAppStorageItem(app); - switch (source) { - case 'private': - return this.canAddNewPrivateApp(); - default: - return this.canAddNewMarketplaceApp(); - } - } - - private showLicense(): void { - if (!process.env.LICENSE_DEBUG || process.env.LICENSE_DEBUG === 'false') { - return; - } - - if (!this.license || !this.valid) { - return; - } - - const { - validation: { serverUrls, validPeriods }, - limits, - } = this.license; - - console.log('---- License enabled ----'); - console.log(' url ->', JSON.stringify(serverUrls)); - console.log(' periods ->', JSON.stringify(validPeriods)); - console.log(' limits ->', JSON.stringify(limits)); - console.log(' modules ->', [...this.modules].join(', ')); - console.log('-------------------------'); - } - - public startedFairPolicy(): boolean { - return Boolean(this.valid && this.inFairPolicy); - } - - public getLicenseLimit(kind: LicenseLimitKind): number | undefined { - if (!this.valid || !this.license) { - return; - } - - const limitList = this.license.limits[kind]; - if (!limitList?.length) { - return; - } - - return Math.min(...limitList.map(({ max }) => max)); - } -} - -const License = new LicenseClass(); - -export async function setLicense(encryptedLicense: string): Promise { - if (!encryptedLicense || String(encryptedLicense).trim() === '' || License.isLicenseDuplicate(encryptedLicense)) { - return false; - } - - console.log('### New Enterprise License'); - - try { - const decrypted = decrypt(encryptedLicense); - if (!decrypted) { - return false; - } - - if (process.env.LICENSE_DEBUG && process.env.LICENSE_DEBUG !== 'false') { - console.log('##### Raw license ->', decrypted); - } - - // #TODO: Check license version and call setLicenseV2 or setLicenseV3 - await License.setLicenseV2(JSON.parse(decrypted)); - License.lockLicense(encryptedLicense); - - return true; - } catch (e) { - console.error('##### Invalid license'); - if (process.env.LICENSE_DEBUG && process.env.LICENSE_DEBUG !== 'false') { - console.error('##### Invalid raw license ->', encryptedLicense, e); - } - return false; - } -} - -export function validateFormat(encryptedLicense: string): boolean { - if (!encryptedLicense || String(encryptedLicense).trim() === '') { - return false; - } - - const decrypted = decrypt(encryptedLicense); - if (!decrypted) { - return false; - } - - return true; -} - -export async function setURL(url: string): Promise { - await License.setURL(url); -} - -export function hasLicense(feature: string): boolean { - return License.hasModule(feature as LicenseModule); -} - -export function isEnterprise(): boolean { - return License.hasValidLicense(); -} - -export function getMaxActiveUsers(): number { - // #TODO: Adjust any place currently using this function to stop doing so. - return License.getLicenseLimit('activeUsers') ?? 0; -} - -export function getUnmodifiedLicenseAndModules(): { license: ILicenseV2 | ILicenseV3; modules: LicenseModule[] } | undefined { - return License.getUnmodifiedLicenseAndModules(); -} - -export function getLicense(): ILicenseV3 | undefined { - return License.getLicense(); -} - -export function getModules(): LicenseModule[] { - return License.getModules(); -} - -export function getTags(): ILicenseTag[] { - return License.getTags(); -} - -export function getAppsConfig(): NonNullable { - // #TODO: Adjust any place currently using this function to stop doing so. - return { - maxPrivateApps: License.getLicenseLimit('privateApps') ?? -1, - maxMarketplaceApps: License.getLicenseLimit('marketplaceApps') ?? -1, - }; -} - -export async function canAddNewUser(userCount = 1): Promise { - return License.canAddNewUser(userCount); -} - -export async function canAddNewGuestUser(guestCount = 1): Promise { - return License.canAddNewGuestUser(guestCount); -} - -export async function canAddNewGuestSubscription(guest: IUser['_id'], roomCount = 1): Promise { - return License.canAddNewGuestSubscription(guest, roomCount); -} - -export async function canAddNewPrivateApp(appCount = 1): Promise { - return License.canAddNewPrivateApp(appCount); -} - -export async function canAddNewMarketplaceApp(appCount = 1): Promise { - return License.canAddNewMarketplaceApp(appCount); -} - -export async function canEnableApp(app: IAppStorageItem): Promise { - return License.canEnableApp(app); -} - -export function onLicense(feature: LicenseModule, cb: (...args: any[]) => void): void | Promise { - if (hasLicense(feature)) { - return cb(); - } - - EnterpriseLicenses.once(`valid:${feature}`, cb); -} - -function onValidFeature(feature: LicenseModule, cb: () => void): () => void { - EnterpriseLicenses.on(`valid:${feature}`, cb); - - if (hasLicense(feature)) { - cb(); - } - - return (): void => { - EnterpriseLicenses.off(`valid:${feature}`, cb); - }; -} - -function onInvalidFeature(feature: LicenseModule, cb: () => void): () => void { - EnterpriseLicenses.on(`invalid:${feature}`, cb); - - if (!hasLicense(feature)) { - cb(); - } - - return (): void => { - EnterpriseLicenses.off(`invalid:${feature}`, cb); - }; -} - -export function onToggledFeature( - feature: LicenseModule, - { - up, - down, - }: { - up?: () => Promise | void; - down?: () => Promise | void; - }, -): () => void { - let enabled = hasLicense(feature); - - const offValidFeature = onValidFeature(feature, () => { - if (!enabled) { - void up?.(); - enabled = true; - } - }); - - const offInvalidFeature = onInvalidFeature(feature, () => { - if (enabled) { - void down?.(); - enabled = false; - } - }); - - if (enabled) { - void up?.(); - } - - return (): void => { - offValidFeature(); - offInvalidFeature(); - }; -} - -export function onModule(cb: (...args: any[]) => void): void { - EnterpriseLicenses.on('module', cb); -} - -export function onValidateLicenses(cb: (...args: any[]) => void): void { - EnterpriseLicenses.on('validate', cb); -} - -export function onInvalidateLicense(cb: (...args: any[]) => void): void { - EnterpriseLicenses.on('invalidate', cb); -} - -interface IOverrideClassProperties { - [key: string]: (...args: any[]) => any; -} - -type Class = { new (...args: any[]): any }; - -export async function overwriteClassOnLicense(license: LicenseModule, original: Class, overwrite: IOverrideClassProperties): Promise { - await onLicense(license, () => { - Object.entries(overwrite).forEach(([key, value]) => { - const originalFn = original.prototype[key]; - original.prototype[key] = function (...args: any[]): any { - return value.call(this, originalFn, ...args); - }; - }); - }); -} diff --git a/apps/meteor/ee/app/license/server/methods.ts b/apps/meteor/ee/app/license/server/methods.ts index 6103e2750790..194218df0213 100644 --- a/apps/meteor/ee/app/license/server/methods.ts +++ b/apps/meteor/ee/app/license/server/methods.ts @@ -1,10 +1,8 @@ -import type { ILicenseTag } from '@rocket.chat/core-typings'; +import { getModules, getTags, hasModule, isEnterprise, type ILicenseTag, type LicenseModule } from '@rocket.chat/license'; import type { ServerMethods } from '@rocket.chat/ui-contexts'; import { check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; -import { getModules, getTags, hasLicense, isEnterprise } from './license'; - declare module '@rocket.chat/ui-contexts' { // eslint-disable-next-line @typescript-eslint/naming-convention interface ServerMethods { @@ -19,7 +17,7 @@ Meteor.methods({ 'license:hasLicense'(feature: string) { check(feature, String); - return hasLicense(feature); + return hasModule(feature as LicenseModule); }, 'license:getModules'() { return getModules(); diff --git a/apps/meteor/ee/app/license/server/settings.ts b/apps/meteor/ee/app/license/server/settings.ts index 663f52045bf0..751dca92a716 100644 --- a/apps/meteor/ee/app/license/server/settings.ts +++ b/apps/meteor/ee/app/license/server/settings.ts @@ -1,8 +1,8 @@ +import { setLicense } from '@rocket.chat/license'; import { Settings } from '@rocket.chat/models'; import { Meteor } from 'meteor/meteor'; import { settings, settingsRegistry } from '../../../../app/settings/server'; -import { setLicense } from './license'; Meteor.startup(async () => { await settingsRegistry.addGroup('Enterprise', async function () { diff --git a/apps/meteor/ee/app/license/server/startup.ts b/apps/meteor/ee/app/license/server/startup.ts index b9da1261e1a0..64444e5e88f3 100644 --- a/apps/meteor/ee/app/license/server/startup.ts +++ b/apps/meteor/ee/app/license/server/startup.ts @@ -1,13 +1,18 @@ +import { setLicense, setWorkspaceUrl, setLicenseLimitCounter } from '@rocket.chat/license'; + import { settings } from '../../../../app/settings/server'; import { callbacks } from '../../../../lib/callbacks'; -import { setLicense, setURL } from './license'; +import { getAppCount } from './lib/getAppCount'; settings.watch('Site_Url', (value) => { if (value) { - void setURL(value); + void setWorkspaceUrl(value); } }); callbacks.add('workspaceLicenseChanged', async (updatedLicense) => { await setLicense(updatedLicense); }); + +setLicenseLimitCounter('privateApps', () => getAppCount('private')); +setLicenseLimitCounter('marketplaceApps', () => getAppCount('marketplace')); diff --git a/apps/meteor/ee/app/livechat-enterprise/server/business-hour/Helper.ts b/apps/meteor/ee/app/livechat-enterprise/server/business-hour/Helper.ts index 5839b717349d..23617fd9ba8f 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/business-hour/Helper.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/business-hour/Helper.ts @@ -1,10 +1,10 @@ import type { ILivechatBusinessHour } from '@rocket.chat/core-typings'; import { LivechatBusinessHourTypes } from '@rocket.chat/core-typings'; +import { isEnterprise } from '@rocket.chat/license'; import { LivechatBusinessHours, LivechatDepartment, LivechatDepartmentAgents, Users } from '@rocket.chat/models'; import moment from 'moment-timezone'; import { businessHourLogger } from '../../../../../app/livechat/server/lib/logger'; -import { isEnterprise } from '../../../license/server/license'; const getAllAgentIdsWithoutDepartment = async (): Promise => { // Fetch departments with agents excluding archived ones (disabled ones still can be tied to business hours) diff --git a/apps/meteor/ee/app/livechat-enterprise/server/index.ts b/apps/meteor/ee/app/livechat-enterprise/server/index.ts index 13ebdd6a3521..553f673bc11f 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/index.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/index.ts @@ -1,3 +1,4 @@ +import { onLicense } from '@rocket.chat/license'; import { Meteor } from 'meteor/meteor'; import './methods/addMonitor'; @@ -25,7 +26,6 @@ import './hooks/onTransferFailure'; import './lib/routing/LoadBalancing'; import './lib/routing/LoadRotation'; import './lib/AutoCloseOnHoldScheduler'; -import { onLicense } from '../../license/server'; import './business-hour'; import { createDefaultPriorities } from './priorities'; diff --git a/apps/meteor/ee/app/livechat-enterprise/server/lib/LivechatEnterprise.ts b/apps/meteor/ee/app/livechat-enterprise/server/lib/LivechatEnterprise.ts index 83a2963a54d8..805377839756 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/lib/LivechatEnterprise.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/lib/LivechatEnterprise.ts @@ -1,4 +1,5 @@ import type { IOmnichannelBusinessUnit, IOmnichannelServiceLevelAgreements, LivechatDepartmentDTO } from '@rocket.chat/core-typings'; +import { hasModule } from '@rocket.chat/license'; import { Users, LivechatDepartment as LivechatDepartmentRaw, @@ -14,7 +15,6 @@ import { updateDepartmentAgents } from '../../../../../app/livechat/server/lib/H import { callbacks } from '../../../../../lib/callbacks'; import { addUserRolesAsync } from '../../../../../server/lib/roles/addUserRoles'; import { removeUserFromRolesAsync } from '../../../../../server/lib/roles/removeUserFromRoles'; -import { hasLicense } from '../../../license/server/license'; import { updateSLAInquiries } from './Helper'; import { removeSLAFromRooms } from './SlaHelper'; @@ -195,7 +195,7 @@ export const LivechatEnterprise = { const department = _id ? await LivechatDepartmentRaw.findOneById(_id, { projection: { _id: 1, archived: 1, enabled: 1 } }) : null; - if (!hasLicense('livechat-enterprise')) { + if (!hasModule('livechat-enterprise')) { const totalDepartments = await LivechatDepartmentRaw.countTotal(); if (!department && totalDepartments >= 1) { throw new Meteor.Error('error-max-departments-number-reached', 'Maximum number of departments reached', { @@ -279,6 +279,6 @@ export const LivechatEnterprise = { }, async isDepartmentCreationAvailable() { - return hasLicense('livechat-enterprise') || (await LivechatDepartmentRaw.countTotal()) === 0; + return hasModule('livechat-enterprise') || (await LivechatDepartmentRaw.countTotal()) === 0; }, }; diff --git a/apps/meteor/ee/app/message-read-receipt/server/index.ts b/apps/meteor/ee/app/message-read-receipt/server/index.ts index a7682e4165f0..cf1e51b1eb44 100644 --- a/apps/meteor/ee/app/message-read-receipt/server/index.ts +++ b/apps/meteor/ee/app/message-read-receipt/server/index.ts @@ -1,4 +1,4 @@ -import { onLicense } from '../../license/server'; +import { onLicense } from '@rocket.chat/license'; await onLicense('message-read-receipt', async () => { await import('./hooks'); diff --git a/apps/meteor/ee/app/settings/server/settings.ts b/apps/meteor/ee/app/settings/server/settings.ts index afb5a4378ec8..978e1161b756 100644 --- a/apps/meteor/ee/app/settings/server/settings.ts +++ b/apps/meteor/ee/app/settings/server/settings.ts @@ -1,10 +1,10 @@ import type { ISetting, SettingValue } from '@rocket.chat/core-typings'; +import { isEnterprise, hasModule, onValidateLicense, type LicenseModule } from '@rocket.chat/license'; import { Settings } from '@rocket.chat/models'; import { Meteor } from 'meteor/meteor'; import { settings, SettingsEvents } from '../../../../app/settings/server'; import { use } from '../../../../app/settings/server/Middleware'; -import { isEnterprise, hasLicense, onValidateLicenses } from '../../license/server/license'; export function changeSettingValue(record: ISetting): SettingValue { if (!record.enterprise) { @@ -20,7 +20,7 @@ export function changeSettingValue(record: ISetting): SettingValue { } for (const moduleName of record.modules) { - if (!hasLicense(moduleName)) { + if (!hasModule(moduleName as LicenseModule)) { return record.invalidValue; } } @@ -58,5 +58,5 @@ async function updateSettings(): Promise { Meteor.startup(async () => { await updateSettings(); - onValidateLicenses(updateSettings); + onValidateLicense(updateSettings); }); diff --git a/apps/meteor/ee/app/voip-enterprise/server/services/voipService.ts b/apps/meteor/ee/app/voip-enterprise/server/services/voipService.ts index a28e459e57fb..38f3cf3541ce 100644 --- a/apps/meteor/ee/app/voip-enterprise/server/services/voipService.ts +++ b/apps/meteor/ee/app/voip-enterprise/server/services/voipService.ts @@ -1,8 +1,8 @@ import type { ILivechatAgent, ILivechatVisitor, IVoipRoomClosingInfo, IUser, IVoipRoom } from '@rocket.chat/core-typings'; +import { overwriteClassOnLicense } from '@rocket.chat/license'; import type { IOmniRoomClosingMessage } from '../../../../../server/services/omnichannel-voip/internalTypes'; import { OmnichannelVoipService } from '../../../../../server/services/omnichannel-voip/service'; -import { overwriteClassOnLicense } from '../../../license/server'; import { calculateOnHoldTimeForRoom } from '../lib/calculateOnHoldTimeForRoom'; await overwriteClassOnLicense('voip-enterprise', OmnichannelVoipService, { diff --git a/apps/meteor/ee/client/hooks/useHasLicenseModule.ts b/apps/meteor/ee/client/hooks/useHasLicenseModule.ts index a1492d39a013..c7d76b093c3b 100644 --- a/apps/meteor/ee/client/hooks/useHasLicenseModule.ts +++ b/apps/meteor/ee/client/hooks/useHasLicenseModule.ts @@ -1,9 +1,8 @@ +import type { LicenseModule } from '@rocket.chat/license'; import { useMethod, useUserId } from '@rocket.chat/ui-contexts'; import { useQuery } from '@tanstack/react-query'; -import type { BundleFeature } from '../../app/license/server/bundles'; - -export const useHasLicenseModule = (licenseName: BundleFeature): 'loading' | boolean => { +export const useHasLicenseModule = (licenseName: LicenseModule): 'loading' | boolean => { const method = useMethod('license:getModules'); const uid = useUserId(); diff --git a/apps/meteor/ee/client/lib/onToggledFeature.ts b/apps/meteor/ee/client/lib/onToggledFeature.ts index 86ab08723745..ae2e4ad9f4a8 100644 --- a/apps/meteor/ee/client/lib/onToggledFeature.ts +++ b/apps/meteor/ee/client/lib/onToggledFeature.ts @@ -1,11 +1,11 @@ +import type { LicenseModule } from '@rocket.chat/license'; import { QueryObserver } from '@tanstack/react-query'; import { queryClient } from '../../../client/lib/queryClient'; -import type { BundleFeature } from '../../app/license/server/bundles'; import { fetchFeatures } from './fetchFeatures'; export const onToggledFeature = ( - feature: BundleFeature, + feature: LicenseModule, { up, down, diff --git a/apps/meteor/ee/server/api/api.ts b/apps/meteor/ee/server/api/api.ts index f152a94ebcbf..bb3a73bfc8f4 100644 --- a/apps/meteor/ee/server/api/api.ts +++ b/apps/meteor/ee/server/api/api.ts @@ -1,7 +1,8 @@ +import { isEnterprise } from '@rocket.chat/license'; + import { API } from '../../../app/api/server/api'; import type { NonEnterpriseTwoFactorOptions, Options } from '../../../app/api/server/definition'; import { use } from '../../../app/settings/server/Middleware'; -import { isEnterprise } from '../../app/license/server/license'; // Overwrites two factor method to enforce 2FA check for enterprise APIs when // no license was provided to prevent abuse on enterprise APIs. diff --git a/apps/meteor/ee/server/api/chat.ts b/apps/meteor/ee/server/api/chat.ts index 5d21b20f2038..9d20c19cfbe9 100644 --- a/apps/meteor/ee/server/api/chat.ts +++ b/apps/meteor/ee/server/api/chat.ts @@ -1,8 +1,8 @@ import type { IMessage, ReadReceipt } from '@rocket.chat/core-typings'; +import { hasModule } from '@rocket.chat/license'; import { Meteor } from 'meteor/meteor'; import { API } from '../../../app/api/server/api'; -import { hasLicense } from '../../app/license/server/license'; type GetMessageReadReceiptsProps = { messageId: IMessage['_id']; @@ -24,7 +24,7 @@ API.v1.addRoute( { authRequired: true }, { async get() { - if (!hasLicense('message-read-receipt')) { + if (!hasModule('message-read-receipt')) { throw new Meteor.Error('error-action-not-allowed', 'This is an enterprise feature'); } diff --git a/apps/meteor/ee/server/api/licenses.ts b/apps/meteor/ee/server/api/licenses.ts index f670d61e3019..0e41bdcafc68 100644 --- a/apps/meteor/ee/server/api/licenses.ts +++ b/apps/meteor/ee/server/api/licenses.ts @@ -1,9 +1,9 @@ +import { getUnmodifiedLicenseAndModules, validateFormat, getMaxActiveUsers, isEnterprise } from '@rocket.chat/license'; import { Settings, Users } from '@rocket.chat/models'; import { check } from 'meteor/check'; import { API } from '../../../app/api/server/api'; import { hasPermissionAsync } from '../../../app/authorization/server/functions/hasPermission'; -import { getUnmodifiedLicenseAndModules, validateFormat, getMaxActiveUsers, isEnterprise } from '../../app/license/server/license'; API.v1.addRoute( 'licenses.get', diff --git a/apps/meteor/ee/server/api/roles.ts b/apps/meteor/ee/server/api/roles.ts index 712e7583b709..1e0b11be1d8c 100644 --- a/apps/meteor/ee/server/api/roles.ts +++ b/apps/meteor/ee/server/api/roles.ts @@ -1,11 +1,11 @@ import type { IRole } from '@rocket.chat/core-typings'; +import { isEnterprise } from '@rocket.chat/license'; import { Roles } from '@rocket.chat/models'; import Ajv from 'ajv'; import { API } from '../../../app/api/server/api'; import { hasPermissionAsync } from '../../../app/authorization/server/functions/hasPermission'; import { settings } from '../../../app/settings/server/index'; -import { isEnterprise } from '../../app/license/server'; import { insertRoleAsync } from '../lib/roles/insertRole'; import { updateRole } from '../lib/roles/updateRole'; diff --git a/apps/meteor/ee/server/api/sessions.ts b/apps/meteor/ee/server/api/sessions.ts index 41c30aba401b..c46296715f39 100644 --- a/apps/meteor/ee/server/api/sessions.ts +++ b/apps/meteor/ee/server/api/sessions.ts @@ -1,4 +1,5 @@ import type { IUser, ISession, DeviceManagementSession, DeviceManagementPopulatedSession } from '@rocket.chat/core-typings'; +import { hasModule } from '@rocket.chat/license'; import { Users, Sessions } from '@rocket.chat/models'; import type { PaginatedResult, PaginatedRequest } from '@rocket.chat/rest-typings'; import { escapeRegExp } from '@rocket.chat/string-helpers'; @@ -7,7 +8,6 @@ import Ajv from 'ajv'; import { API } from '../../../app/api/server/api'; import { getPaginationItems } from '../../../app/api/server/helpers/getPaginationItems'; import { Notifications } from '../../../app/notifications/server'; -import { hasLicense } from '../../app/license/server/license'; const ajv = new Ajv({ coerceTypes: true }); @@ -85,7 +85,7 @@ API.v1.addRoute( { authRequired: true, validateParams: isSessionsPaginateProps }, { async get() { - if (!hasLicense('device-management')) { + if (!hasModule('device-management')) { return API.v1.unauthorized(); } @@ -108,7 +108,7 @@ API.v1.addRoute( { authRequired: true, validateParams: isSessionsProps }, { async get() { - if (!hasLicense('device-management')) { + if (!hasModule('device-management')) { return API.v1.unauthorized(); } @@ -127,7 +127,7 @@ API.v1.addRoute( { authRequired: true, validateParams: isSessionsProps }, { async post() { - if (!hasLicense('device-management')) { + if (!hasModule('device-management')) { return API.v1.unauthorized(); } @@ -153,7 +153,7 @@ API.v1.addRoute( { authRequired: true, twoFactorRequired: true, validateParams: isSessionsPaginateProps, permissionsRequired: ['view-device-management'] }, { async get() { - if (!hasLicense('device-management')) { + if (!hasModule('device-management')) { return API.v1.unauthorized(); } @@ -193,7 +193,7 @@ API.v1.addRoute( { authRequired: true, twoFactorRequired: true, validateParams: isSessionsProps, permissionsRequired: ['view-device-management'] }, { async get() { - if (!hasLicense('device-management')) { + if (!hasModule('device-management')) { return API.v1.unauthorized(); } @@ -212,7 +212,7 @@ API.v1.addRoute( { authRequired: true, twoFactorRequired: true, validateParams: isSessionsProps, permissionsRequired: ['logout-device-management'] }, { async post() { - if (!hasLicense('device-management')) { + if (!hasModule('device-management')) { return API.v1.unauthorized(); } diff --git a/apps/meteor/ee/server/apps/communication/endpoints/appsCountHandler.ts b/apps/meteor/ee/server/apps/communication/endpoints/appsCountHandler.ts index 96247e704545..191c5b45f0eb 100644 --- a/apps/meteor/ee/server/apps/communication/endpoints/appsCountHandler.ts +++ b/apps/meteor/ee/server/apps/communication/endpoints/appsCountHandler.ts @@ -1,9 +1,9 @@ import type { AppManager } from '@rocket.chat/apps-engine/server/AppManager'; +import { getAppsConfig } from '@rocket.chat/license'; import { API } from '../../../../../app/api/server'; import type { SuccessResult } from '../../../../../app/api/server/definition'; import { getInstallationSourceFromAppStorageItem } from '../../../../../lib/apps/getInstallationSourceFromAppStorageItem'; -import { getAppsConfig } from '../../../../app/license/server/license'; import type { AppsRestApi } from '../rest'; type AppsCountResult = { diff --git a/apps/meteor/ee/server/apps/communication/rest.ts b/apps/meteor/ee/server/apps/communication/rest.ts index 1203d0d8c911..e458527c40d7 100644 --- a/apps/meteor/ee/server/apps/communication/rest.ts +++ b/apps/meteor/ee/server/apps/communication/rest.ts @@ -3,6 +3,7 @@ import type { IAppInfo } from '@rocket.chat/apps-engine/definition/metadata'; import type { AppManager } from '@rocket.chat/apps-engine/server/AppManager'; import { AppInstallationSource } from '@rocket.chat/apps-engine/server/storage'; import type { IUser, IMessage } from '@rocket.chat/core-typings'; +import { isEnterprise } from '@rocket.chat/license'; import { Settings, Users } from '@rocket.chat/models'; import { serverFetch as fetch } from '@rocket.chat/server-fetch'; import { Meteor } from 'meteor/meteor'; @@ -17,7 +18,7 @@ import { settings } from '../../../../app/settings/server'; import { Info } from '../../../../app/utils/rocketchat.info'; import { i18n } from '../../../../server/lib/i18n'; import { sendMessagesToAdmins } from '../../../../server/lib/sendMessagesToAdmins'; -import { canEnableApp, isEnterprise } from '../../../app/license/server/license'; +import { canEnableApp } from '../../../app/license/server/canEnableApp'; import { formatAppInstanceForRest } from '../../../lib/misc/formatAppInstanceForRest'; import { appEnableCheck } from '../marketplace/appEnableCheck'; import { notifyAppInstall } from '../marketplace/appInstall'; diff --git a/apps/meteor/ee/server/apps/orchestrator.js b/apps/meteor/ee/server/apps/orchestrator.js index c21508cbc626..9e4d6f00e7f0 100644 --- a/apps/meteor/ee/server/apps/orchestrator.js +++ b/apps/meteor/ee/server/apps/orchestrator.js @@ -19,7 +19,7 @@ import { } from '../../../app/apps/server/converters'; import { AppThreadsConverter } from '../../../app/apps/server/converters/threads'; import { settings, settingsRegistry } from '../../../app/settings/server'; -import { canEnableApp } from '../../app/license/server/license'; +import { canEnableApp } from '../../app/license/server/canEnableApp'; import { AppServerNotifier, AppsRestApi, AppUIKitInteractionApi } from './communication'; import { AppRealLogsStorage, AppRealStorage, ConfigurableAppSourceStorage } from './storage'; diff --git a/apps/meteor/ee/server/configuration/ldap.ts b/apps/meteor/ee/server/configuration/ldap.ts index 40815e213b0c..9eea73c78f50 100644 --- a/apps/meteor/ee/server/configuration/ldap.ts +++ b/apps/meteor/ee/server/configuration/ldap.ts @@ -1,12 +1,12 @@ import type { IImportUser, ILDAPEntry, IUser } from '@rocket.chat/core-typings'; import { cronJobs } from '@rocket.chat/cron'; +import { onLicense } from '@rocket.chat/license'; import { Meteor } from 'meteor/meteor'; import { settings } from '../../../app/settings/server'; import { callbacks } from '../../../lib/callbacks'; import type { LDAPConnection } from '../../../server/lib/ldap/Connection'; import { logger } from '../../../server/lib/ldap/Logger'; -import { onLicense } from '../../app/license/server'; import { LDAPEEManager } from '../lib/ldap/Manager'; import { LDAPEE } from '../sdk'; import { addSettings, ldapIntervalValuesToCronMap } from '../settings/ldap'; diff --git a/apps/meteor/ee/server/configuration/oauth.ts b/apps/meteor/ee/server/configuration/oauth.ts index 984670af6003..a273e3cb78d9 100644 --- a/apps/meteor/ee/server/configuration/oauth.ts +++ b/apps/meteor/ee/server/configuration/oauth.ts @@ -1,11 +1,11 @@ import type { IUser } from '@rocket.chat/core-typings'; +import { onLicense } from '@rocket.chat/license'; import { Logger } from '@rocket.chat/logger'; import { Roles } from '@rocket.chat/models'; import { capitalize } from '@rocket.chat/string-helpers'; import { settings } from '../../../app/settings/server'; import { callbacks } from '../../../lib/callbacks'; -import { onLicense } from '../../app/license/server'; import { OAuthEEManager } from '../lib/oauth/Manager'; interface IOAuthUserService { diff --git a/apps/meteor/ee/server/configuration/outlookCalendar.ts b/apps/meteor/ee/server/configuration/outlookCalendar.ts index cf36ddeb0cab..280295a5736f 100644 --- a/apps/meteor/ee/server/configuration/outlookCalendar.ts +++ b/apps/meteor/ee/server/configuration/outlookCalendar.ts @@ -1,7 +1,7 @@ import { Calendar } from '@rocket.chat/core-services'; +import { onLicense } from '@rocket.chat/license'; import { Meteor } from 'meteor/meteor'; -import { onLicense } from '../../app/license/server'; import { addSettings } from '../settings/outlookCalendar'; Meteor.startup(() => diff --git a/apps/meteor/ee/server/configuration/saml.ts b/apps/meteor/ee/server/configuration/saml.ts index 1e50fc7160b5..a7e82b019978 100644 --- a/apps/meteor/ee/server/configuration/saml.ts +++ b/apps/meteor/ee/server/configuration/saml.ts @@ -1,10 +1,10 @@ +import { onLicense } from '@rocket.chat/license'; import { Roles, Users } from '@rocket.chat/models'; import type { ISAMLUser } from '../../../app/meteor-accounts-saml/server/definition/ISAMLUser'; import { SAMLUtils } from '../../../app/meteor-accounts-saml/server/lib/Utils'; import { settings } from '../../../app/settings/server'; import { ensureArray } from '../../../lib/utils/arrayUtils'; -import { onLicense } from '../../app/license/server'; import { addSettings } from '../settings/saml'; await onLicense('saml-enterprise', () => { diff --git a/apps/meteor/ee/server/configuration/videoConference.ts b/apps/meteor/ee/server/configuration/videoConference.ts index a9debed01b19..9de6266c8386 100644 --- a/apps/meteor/ee/server/configuration/videoConference.ts +++ b/apps/meteor/ee/server/configuration/videoConference.ts @@ -1,12 +1,12 @@ import { VideoConf } from '@rocket.chat/core-services'; import type { IRoom, IUser, VideoConference } from '@rocket.chat/core-typings'; import { VideoConferenceStatus } from '@rocket.chat/core-typings'; +import { onLicense } from '@rocket.chat/license'; import { Rooms, Subscriptions } from '@rocket.chat/models'; import { Meteor } from 'meteor/meteor'; import { callbacks } from '../../../lib/callbacks'; import { videoConfTypes } from '../../../server/lib/videoConfTypes'; -import { onLicense } from '../../app/license/server'; import { addSettings } from '../settings/video-conference'; Meteor.startup(async () => { diff --git a/apps/meteor/ee/server/lib/syncUserRoles.ts b/apps/meteor/ee/server/lib/syncUserRoles.ts index e38f9de3c310..9b7cf9bb577a 100644 --- a/apps/meteor/ee/server/lib/syncUserRoles.ts +++ b/apps/meteor/ee/server/lib/syncUserRoles.ts @@ -1,11 +1,11 @@ import { api } from '@rocket.chat/core-services'; import type { IUser, IRole, AtLeast } from '@rocket.chat/core-typings'; +import { preventNewUsers } from '@rocket.chat/license'; import { Users } from '@rocket.chat/models'; import { settings } from '../../../app/settings/server'; import { addUserRolesAsync } from '../../../server/lib/roles/addUserRoles'; import { removeUserFromRolesAsync } from '../../../server/lib/roles/removeUserFromRoles'; -import { canAddNewUser } from '../../app/license/server/license'; type setUserRolesOptions = { // If specified, the function will not add nor remove any role that is not on this list. @@ -72,7 +72,7 @@ export async function syncUserRoles( } const wasGuest = existingRoles.length === 1 && existingRoles[0] === 'guest'; - if (wasGuest && !(await canAddNewUser())) { + if (wasGuest && (await preventNewUsers())) { throw new Error('error-license-user-limit-reached'); } diff --git a/apps/meteor/ee/server/methods/getReadReceipts.ts b/apps/meteor/ee/server/methods/getReadReceipts.ts index a30eec300c41..a08972ea9198 100644 --- a/apps/meteor/ee/server/methods/getReadReceipts.ts +++ b/apps/meteor/ee/server/methods/getReadReceipts.ts @@ -1,11 +1,11 @@ import type { ReadReceipt as ReadReceiptType, IMessage } from '@rocket.chat/core-typings'; +import { hasModule } from '@rocket.chat/license'; import { Messages } from '@rocket.chat/models'; import type { ServerMethods } from '@rocket.chat/ui-contexts'; import { check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; import { canAccessRoomIdAsync } from '../../../app/authorization/server/functions/canAccessRoom'; -import { hasLicense } from '../../app/license/server/license'; import { ReadReceipt } from '../lib/message-read-receipt/ReadReceipt'; declare module '@rocket.chat/ui-contexts' { @@ -17,7 +17,7 @@ declare module '@rocket.chat/ui-contexts' { Meteor.methods({ async getReadReceipts({ messageId }) { - if (!hasLicense('message-read-receipt')) { + if (!hasModule('message-read-receipt')) { throw new Meteor.Error('error-action-not-allowed', 'This is an enterprise feature', { method: 'getReadReceipts' }); } diff --git a/apps/meteor/ee/server/models/startup.ts b/apps/meteor/ee/server/models/startup.ts index 580f4c025e07..ab989ffd13ab 100644 --- a/apps/meteor/ee/server/models/startup.ts +++ b/apps/meteor/ee/server/models/startup.ts @@ -1,4 +1,4 @@ -import { onLicense } from '../../app/license/server/license'; +import { onLicense } from '@rocket.chat/license'; // To facilitate our lives with the stream // Collection will be registered on CE too diff --git a/apps/meteor/ee/server/startup/apps/trialExpiration.ts b/apps/meteor/ee/server/startup/apps/trialExpiration.ts index 1c214ba0a406..89bd65436f75 100644 --- a/apps/meteor/ee/server/startup/apps/trialExpiration.ts +++ b/apps/meteor/ee/server/startup/apps/trialExpiration.ts @@ -1,6 +1,6 @@ +import { onInvalidateLicense } from '@rocket.chat/license'; import { Meteor } from 'meteor/meteor'; -import { onInvalidateLicense } from '../../../app/license/server/license'; import { Apps } from '../../apps'; Meteor.startup(() => { diff --git a/apps/meteor/ee/server/startup/audit.ts b/apps/meteor/ee/server/startup/audit.ts index 441429e51b22..14e3b0ef4fb5 100644 --- a/apps/meteor/ee/server/startup/audit.ts +++ b/apps/meteor/ee/server/startup/audit.ts @@ -1,4 +1,5 @@ -import { onLicense } from '../../app/license/server'; +import { onLicense } from '@rocket.chat/license'; + import { createPermissions } from '../lib/audit/startup'; await onLicense('auditing', async () => { diff --git a/apps/meteor/ee/server/startup/deviceManagement.ts b/apps/meteor/ee/server/startup/deviceManagement.ts index a9a1c805f72d..1e47a36d445a 100644 --- a/apps/meteor/ee/server/startup/deviceManagement.ts +++ b/apps/meteor/ee/server/startup/deviceManagement.ts @@ -1,4 +1,5 @@ -import { onToggledFeature } from '../../app/license/server/license'; +import { onToggledFeature } from '@rocket.chat/license'; + import { addSettings } from '../settings/deviceManagement'; let stopListening: (() => void) | undefined; diff --git a/apps/meteor/ee/server/startup/engagementDashboard.ts b/apps/meteor/ee/server/startup/engagementDashboard.ts index 2fc393379bf3..a4aa88098afe 100644 --- a/apps/meteor/ee/server/startup/engagementDashboard.ts +++ b/apps/meteor/ee/server/startup/engagementDashboard.ts @@ -1,7 +1,6 @@ +import { onToggledFeature } from '@rocket.chat/license'; import { Meteor } from 'meteor/meteor'; -import { onToggledFeature } from '../../app/license/server/license'; - onToggledFeature('engagement-dashboard', { up: () => Meteor.startup(async () => { diff --git a/apps/meteor/ee/server/startup/maxRoomsPerGuest.ts b/apps/meteor/ee/server/startup/maxRoomsPerGuest.ts index 9a8bac6d1d2a..45c1ffc556db 100644 --- a/apps/meteor/ee/server/startup/maxRoomsPerGuest.ts +++ b/apps/meteor/ee/server/startup/maxRoomsPerGuest.ts @@ -1,14 +1,14 @@ +import { preventNewGuestSubscriptions } from '@rocket.chat/license'; import { Meteor } from 'meteor/meteor'; import { callbacks } from '../../../lib/callbacks'; import { i18n } from '../../../server/lib/i18n'; -import { canAddNewGuestSubscription } from '../../app/license/server/license'; callbacks.add( 'beforeAddedToRoom', async ({ user }) => { if (user.roles?.includes('guest')) { - if (!(await canAddNewGuestSubscription(user._id))) { + if (await preventNewGuestSubscriptions(user._id)) { throw new Meteor.Error('error-max-rooms-per-guest-reached', i18n.t('error-max-rooms-per-guest-reached')); } } diff --git a/apps/meteor/ee/server/startup/seatsCap.ts b/apps/meteor/ee/server/startup/seatsCap.ts index 36ec066ab86f..2c59ffedf9fc 100644 --- a/apps/meteor/ee/server/startup/seatsCap.ts +++ b/apps/meteor/ee/server/startup/seatsCap.ts @@ -1,4 +1,5 @@ import type { IUser } from '@rocket.chat/core-typings'; +import { preventNewUsers, getMaxActiveUsers, onValidateLicense } from '@rocket.chat/license'; import { Users } from '@rocket.chat/models'; import { Meteor } from 'meteor/meteor'; import { throttle } from 'underscore'; @@ -6,7 +7,6 @@ import { throttle } from 'underscore'; import { callbacks } from '../../../lib/callbacks'; import { i18n } from '../../../server/lib/i18n'; import { validateUserRoles } from '../../app/authorization/server/validateUserRoles'; -import { canAddNewUser, getMaxActiveUsers, onValidateLicenses } from '../../app/license/server/license'; import { createSeatsLimitBanners, disableDangerBannerDiscardingDismissal, @@ -22,7 +22,7 @@ callbacks.add( return; } - if (!(await canAddNewUser())) { + if (await preventNewUsers()) { throw new Meteor.Error('error-license-user-limit-reached', i18n.t('error-license-user-limit-reached')); } }, @@ -33,7 +33,7 @@ callbacks.add( callbacks.add( 'beforeUserImport', async ({ userCount }) => { - if (!(await canAddNewUser(userCount))) { + if (await preventNewUsers(userCount)) { throw new Meteor.Error('error-license-user-limit-reached', i18n.t('error-license-user-limit-reached')); } }, @@ -52,7 +52,7 @@ callbacks.add( return; } - if (!(await canAddNewUser())) { + if (await preventNewUsers()) { throw new Meteor.Error('error-license-user-limit-reached', i18n.t('error-license-user-limit-reached')); } }, @@ -113,5 +113,5 @@ Meteor.startup(async () => { await handleMaxSeatsBanners(); - onValidateLicenses(handleMaxSeatsBanners); + onValidateLicense(handleMaxSeatsBanners); }); diff --git a/apps/meteor/ee/server/startup/services.ts b/apps/meteor/ee/server/startup/services.ts index 5288b9a8e10e..0b3004b48a84 100644 --- a/apps/meteor/ee/server/startup/services.ts +++ b/apps/meteor/ee/server/startup/services.ts @@ -1,8 +1,8 @@ import { api } from '@rocket.chat/core-services'; +import { isEnterprise, onLicense } from '@rocket.chat/license'; import { isRunningMs } from '../../../server/lib/isRunningMs'; import { FederationService } from '../../../server/services/federation/service'; -import { isEnterprise, onLicense } from '../../app/license/server'; import { LicenseService } from '../../app/license/server/license.internalService'; import { OmnichannelEE } from '../../app/livechat-enterprise/server/services/omnichannel.internalService'; import { EnterpriseSettings } from '../../app/settings/server/settings.internalService'; diff --git a/apps/meteor/ee/server/startup/upsell.ts b/apps/meteor/ee/server/startup/upsell.ts index fdea300ff9a2..66825be92059 100644 --- a/apps/meteor/ee/server/startup/upsell.ts +++ b/apps/meteor/ee/server/startup/upsell.ts @@ -1,8 +1,7 @@ +import { onValidateLicense, getLicense } from '@rocket.chat/license'; import { Settings } from '@rocket.chat/models'; import { Meteor } from 'meteor/meteor'; -import { onValidateLicenses, getLicense } from '../../app/license/server/license'; - const handleHadTrial = (): void => { if (getLicense()?.information.trial) { void Settings.updateValueById('Cloud_Workspace_Had_Trial', true); @@ -10,5 +9,5 @@ const handleHadTrial = (): void => { }; Meteor.startup(() => { - onValidateLicenses(handleHadTrial); + onValidateLicense(handleHadTrial); }); diff --git a/apps/meteor/lib/apps/getInstallationSourceFromAppStorageItem.ts b/apps/meteor/lib/apps/getInstallationSourceFromAppStorageItem.ts index d8fd5a48f79f..8ac29d191576 100644 --- a/apps/meteor/lib/apps/getInstallationSourceFromAppStorageItem.ts +++ b/apps/meteor/lib/apps/getInstallationSourceFromAppStorageItem.ts @@ -1,5 +1,5 @@ import type { IAppStorageItem } from '@rocket.chat/apps-engine/server/storage'; -import type { LicenseAppSources } from '@rocket.chat/core-typings'; +import type { LicenseAppSources } from '@rocket.chat/license'; /** * There have been reports of apps not being correctly migrated from versions prior to 6.0 diff --git a/apps/meteor/package.json b/apps/meteor/package.json index 0605852486c4..69bd345bc8fb 100644 --- a/apps/meteor/package.json +++ b/apps/meteor/package.json @@ -248,6 +248,7 @@ "@rocket.chat/instance-status": "workspace:^", "@rocket.chat/jwt": "workspace:^", "@rocket.chat/layout": "next", + "@rocket.chat/license": "workspace:^", "@rocket.chat/log-format": "workspace:^", "@rocket.chat/logger": "workspace:^", "@rocket.chat/logo": "^0.31.27", diff --git a/apps/meteor/server/startup/migrations/v278.ts b/apps/meteor/server/startup/migrations/v278.ts index 57986fd1064f..694464230f7b 100644 --- a/apps/meteor/server/startup/migrations/v278.ts +++ b/apps/meteor/server/startup/migrations/v278.ts @@ -1,7 +1,7 @@ +import { isEnterprise } from '@rocket.chat/license'; import { Banners, Settings } from '@rocket.chat/models'; import { settings } from '../../../app/settings/server'; -import { isEnterprise } from '../../../ee/app/license/server'; import { addMigration } from '../../lib/migrations'; addMigration({ diff --git a/ee/packages/license/.eslintrc.json b/ee/packages/license/.eslintrc.json new file mode 100644 index 000000000000..a83aeda48e66 --- /dev/null +++ b/ee/packages/license/.eslintrc.json @@ -0,0 +1,4 @@ +{ + "extends": ["@rocket.chat/eslint-config"], + "ignorePatterns": ["**/dist"] +} diff --git a/ee/packages/license/package.json b/ee/packages/license/package.json new file mode 100644 index 000000000000..9b236ce0df8e --- /dev/null +++ b/ee/packages/license/package.json @@ -0,0 +1,29 @@ +{ + "name": "@rocket.chat/license", + "version": "0.0.1", + "private": true, + "devDependencies": { + "@types/jest": "~29.5.3", + "eslint": "~8.45.0", + "jest": "~29.6.1", + "ts-jest": "~29.0.5", + "typescript": "~5.1.6" + }, + "scripts": { + "lint": "eslint --ext .js,.jsx,.ts,.tsx .", + "lint:fix": "eslint --ext .js,.jsx,.ts,.tsx . --fix", + "test": "jest", + "build": "rm -rf dist && tsc -p tsconfig.json", + "dev": "tsc -p tsconfig.json --watch --preserveWatchOutput" + }, + "main": "./dist/index.js", + "typings": "./dist/index.d.ts", + "files": [ + "/dist" + ], + "dependencies": { + "@rocket.chat/core-typings": "workspace:^", + "@rocket.chat/logger": "workspace:^", + "@rocket.chat/models": "workspace:^" + } +} diff --git a/ee/packages/license/src/actionBlockers.ts b/ee/packages/license/src/actionBlockers.ts new file mode 100644 index 000000000000..6876512a147e --- /dev/null +++ b/ee/packages/license/src/actionBlockers.ts @@ -0,0 +1,10 @@ +import type { IUser } from '@rocket.chat/core-typings'; + +import { shouldPreventAction } from './validation/shouldPreventAction'; + +export const preventNewUsers = async (userCount = 1) => shouldPreventAction('activeUsers', {}, userCount); +export const preventNewGuests = async (guestCount = 1) => shouldPreventAction('guestUsers', {}, guestCount); +export const preventNewPrivateApps = async (appCount = 1) => shouldPreventAction('privateApps', {}, appCount); +export const preventNewMarketplaceApps = async (appCount = 1) => shouldPreventAction('marketplaceApps', {}, appCount); +export const preventNewGuestSubscriptions = async (guest: IUser['_id'], roomCount = 1) => + shouldPreventAction('roomsPerGuest', { userId: guest }, roomCount); diff --git a/apps/meteor/ee/app/license/server/decrypt.ts b/ee/packages/license/src/decrypt.ts similarity index 100% rename from apps/meteor/ee/app/license/server/decrypt.ts rename to ee/packages/license/src/decrypt.ts diff --git a/packages/core-typings/src/ee/ILicense/ILicenseTag.ts b/ee/packages/license/src/definition/ILicenseTag.ts similarity index 100% rename from packages/core-typings/src/ee/ILicense/ILicenseTag.ts rename to ee/packages/license/src/definition/ILicenseTag.ts diff --git a/packages/core-typings/src/ee/ILicense/ILicenseV2.ts b/ee/packages/license/src/definition/ILicenseV2.ts similarity index 100% rename from packages/core-typings/src/ee/ILicense/ILicenseV2.ts rename to ee/packages/license/src/definition/ILicenseV2.ts diff --git a/packages/core-typings/src/ee/ILicense/ILicenseV3.ts b/ee/packages/license/src/definition/ILicenseV3.ts similarity index 53% rename from packages/core-typings/src/ee/ILicense/ILicenseV3.ts rename to ee/packages/license/src/definition/ILicenseV3.ts index c4954fd604f5..d3a2d7f572a3 100644 --- a/packages/core-typings/src/ee/ILicense/ILicenseV3.ts +++ b/ee/packages/license/src/definition/ILicenseV3.ts @@ -1,41 +1,7 @@ import type { ILicenseTag } from './ILicenseTag'; - -export type LicenseBehavior = 'invalidate_license' | 'start_fair_policy' | 'prevent_action' | 'prevent_installation' | 'disable_modules'; - -export type LicenseLimit = { - max: number; - behavior: T; -} & (T extends 'disable_modules' ? { behavior: T; modules: LicenseModule[] } : { behavior: T }); - -export type Timestamp = string; - -export type LicensePeriodBehavior = Exclude; - -export type LicensePeriod = { - validFrom?: Timestamp; - validUntil?: Timestamp; - invalidBehavior: LicenseBehavior; -} & ({ validFrom: Timestamp } | { validUntil: Timestamp }) & - ({ invalidBehavior: 'disable_modules'; modules: LicenseModule[] } | { invalidBehavior: Exclude }); - -export type LicenseModule = - | 'auditing' - | 'canned-responses' - | 'ldap-enterprise' - | 'livechat-enterprise' - | 'voip-enterprise' - | 'omnichannel-mobile-enterprise' - | 'engagement-dashboard' - | 'push-privacy' - | 'scalability' - | 'teams-mention' - | 'saml-enterprise' - | 'oauth-enterprise' - | 'device-management' - | 'federation' - | 'videoconference-enterprise' - | 'message-read-receipt' - | 'outlook-calendar'; +import type { LicenseLimit } from './LicenseLimit'; +import type { LicenseModule } from './LicenseModule'; +import type { LicensePeriod, Timestamp } from './LicensePeriod'; export interface ILicenseV3 { version: '3.0'; diff --git a/ee/packages/license/src/definition/LicenseBehavior.ts b/ee/packages/license/src/definition/LicenseBehavior.ts new file mode 100644 index 000000000000..b6d52bbfa8c5 --- /dev/null +++ b/ee/packages/license/src/definition/LicenseBehavior.ts @@ -0,0 +1,8 @@ +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[]; +}; diff --git a/ee/packages/license/src/definition/LicenseLimit.ts b/ee/packages/license/src/definition/LicenseLimit.ts new file mode 100644 index 000000000000..40e5a62f597a --- /dev/null +++ b/ee/packages/license/src/definition/LicenseLimit.ts @@ -0,0 +1,7 @@ +import type { LicenseBehavior } from './LicenseBehavior'; +import type { LicenseModule } from './LicenseModule'; + +export type LicenseLimit = { + max: number; + behavior: T; +} & (T extends 'disable_modules' ? { behavior: T; modules: LicenseModule[] } : { behavior: T }); diff --git a/ee/packages/license/src/definition/LicenseModule.ts b/ee/packages/license/src/definition/LicenseModule.ts new file mode 100644 index 000000000000..8ecebba1983b --- /dev/null +++ b/ee/packages/license/src/definition/LicenseModule.ts @@ -0,0 +1,18 @@ +export type LicenseModule = + | 'auditing' + | 'canned-responses' + | 'ldap-enterprise' + | 'livechat-enterprise' + | 'voip-enterprise' + | 'omnichannel-mobile-enterprise' + | 'engagement-dashboard' + | 'push-privacy' + | 'scalability' + | 'teams-mention' + | 'saml-enterprise' + | 'oauth-enterprise' + | 'device-management' + | 'federation' + | 'videoconference-enterprise' + | 'message-read-receipt' + | 'outlook-calendar'; diff --git a/ee/packages/license/src/definition/LicensePeriod.ts b/ee/packages/license/src/definition/LicensePeriod.ts new file mode 100644 index 000000000000..d9bae6198fde --- /dev/null +++ b/ee/packages/license/src/definition/LicensePeriod.ts @@ -0,0 +1,13 @@ +import type { LicenseBehavior } from './LicenseBehavior'; +import type { LicenseModule } from './LicenseModule'; + +export type Timestamp = string; + +export type LicensePeriod = { + validFrom?: Timestamp; + validUntil?: Timestamp; + invalidBehavior: LicenseBehavior; +} & ({ validFrom: Timestamp } | { validUntil: Timestamp }) & + ({ invalidBehavior: 'disable_modules'; modules: LicenseModule[] } | { invalidBehavior: Exclude }); + +export type LicensePeriodBehavior = Exclude; diff --git a/ee/packages/license/src/definition/LimitContext.ts b/ee/packages/license/src/definition/LimitContext.ts new file mode 100644 index 000000000000..a2c44744bd75 --- /dev/null +++ b/ee/packages/license/src/definition/LimitContext.ts @@ -0,0 +1,5 @@ +import type { IUser } from '@rocket.chat/core-typings'; + +import type { LicenseLimitKind } from './ILicenseV3'; + +export type LimitContext = T extends 'roomsPerGuest' ? { userId: IUser['_id'] } : Record; diff --git a/ee/packages/license/src/deprecated.ts b/ee/packages/license/src/deprecated.ts new file mode 100644 index 000000000000..3071a364f8db --- /dev/null +++ b/ee/packages/license/src/deprecated.ts @@ -0,0 +1,25 @@ +import type { LicenseLimitKind } from './definition/ILicenseV3'; +import { getLicense } from './license'; + +const getLicenseLimit = (kind: LicenseLimitKind) => { + const license = getLicense(); + if (!license) { + return; + } + + const limitList = license.limits[kind]; + if (!limitList?.length) { + return; + } + + return Math.min(...limitList.map(({ max }) => max)); +}; + +// #TODO: Remove references to those functions + +export const getMaxActiveUsers = () => getLicenseLimit('activeUsers') ?? 0; + +export const getAppsConfig = () => ({ + maxPrivateApps: getLicenseLimit('privateApps') ?? -1, + maxMarketplaceApps: getLicenseLimit('marketplaceApps') ?? -1, +}); diff --git a/ee/packages/license/src/encryptedLicense.ts b/ee/packages/license/src/encryptedLicense.ts new file mode 100644 index 000000000000..29cf1bcb9390 --- /dev/null +++ b/ee/packages/license/src/encryptedLicense.ts @@ -0,0 +1,7 @@ +let lockedLicense: string | undefined; + +export const lockLicense = (encryptedLicense: string) => { + lockedLicense = encryptedLicense; +}; + +export const isLicenseDuplicate = (encryptedLicense: string) => Boolean(lockedLicense && lockedLicense === encryptedLicense); diff --git a/ee/packages/license/src/events/deprecated.ts b/ee/packages/license/src/events/deprecated.ts new file mode 100644 index 000000000000..0eebeb2173d9 --- /dev/null +++ b/ee/packages/license/src/events/deprecated.ts @@ -0,0 +1,12 @@ +import type { LicenseModule } from '../definition/LicenseModule'; +import { hasModule } from '../modules'; +import { EnterpriseLicenses } from './emitter'; + +// #TODO: Remove this onLicense handler +export const onLicense = (feature: LicenseModule, cb: (...args: any[]) => void): void | Promise => { + if (hasModule(feature)) { + return cb(); + } + + EnterpriseLicenses.once(`valid:${feature}`, cb); +}; diff --git a/ee/packages/license/src/events/emitter.ts b/ee/packages/license/src/events/emitter.ts new file mode 100644 index 000000000000..d9ce189df008 --- /dev/null +++ b/ee/packages/license/src/events/emitter.ts @@ -0,0 +1,19 @@ +import { EventEmitter } from 'events'; + +import type { LicenseModule } from '../definition/LicenseModule'; + +export const EnterpriseLicenses = new EventEmitter(); + +export const licenseValidated = () => EnterpriseLicenses.emit('validate'); + +export const licenseRemoved = () => EnterpriseLicenses.emit('invalidate'); + +export const moduleValidated = (module: LicenseModule) => { + EnterpriseLicenses.emit('module', { module, valid: true }); + EnterpriseLicenses.emit(`valid:${module}`); +}; + +export const moduleRemoved = (module: LicenseModule) => { + EnterpriseLicenses.emit('module', { module, valid: false }); + EnterpriseLicenses.emit(`invalid:${module}`); +}; diff --git a/ee/packages/license/src/events/listeners.ts b/ee/packages/license/src/events/listeners.ts new file mode 100644 index 000000000000..49dde70af512 --- /dev/null +++ b/ee/packages/license/src/events/listeners.ts @@ -0,0 +1,69 @@ +import type { LicenseModule } from '../definition/LicenseModule'; +import { hasModule } from '../modules'; +import { EnterpriseLicenses } from './emitter'; + +export const onValidFeature = (feature: LicenseModule, cb: () => void) => { + EnterpriseLicenses.on(`valid:${feature}`, cb); + + if (hasModule(feature)) { + cb(); + } + + return (): void => { + EnterpriseLicenses.off(`valid:${feature}`, cb); + }; +}; + +export const onInvalidFeature = (feature: LicenseModule, cb: () => void) => { + EnterpriseLicenses.on(`invalid:${feature}`, cb); + + if (!hasModule(feature)) { + cb(); + } + + return (): void => { + EnterpriseLicenses.off(`invalid:${feature}`, cb); + }; +}; + +export const onToggledFeature = ( + feature: LicenseModule, + { up, down }: { up?: () => Promise | void; down?: () => Promise | void }, +): (() => void) => { + let enabled = hasModule(feature); + + const offValidFeature = onValidFeature(feature, () => { + if (!enabled) { + void up?.(); + enabled = true; + } + }); + + const offInvalidFeature = onInvalidFeature(feature, () => { + if (enabled) { + void down?.(); + enabled = false; + } + }); + + if (enabled) { + void up?.(); + } + + return (): void => { + offValidFeature(); + offInvalidFeature(); + }; +}; + +export const onModule = (cb: (...args: any[]) => void) => { + EnterpriseLicenses.on('module', cb); +}; + +export const onValidateLicense = (cb: (...args: any[]) => void) => { + EnterpriseLicenses.on('validate', cb); +}; + +export const onInvalidateLicense = (cb: (...args: any[]) => void) => { + EnterpriseLicenses.on('invalidate', cb); +}; diff --git a/ee/packages/license/src/events/overwriteClassOnLicense.ts b/ee/packages/license/src/events/overwriteClassOnLicense.ts new file mode 100644 index 000000000000..27d9133a7a33 --- /dev/null +++ b/ee/packages/license/src/events/overwriteClassOnLicense.ts @@ -0,0 +1,19 @@ +import type { LicenseModule } from '../definition/LicenseModule'; +import { onLicense } from './deprecated'; + +interface IOverrideClassProperties { + [key: string]: (...args: any[]) => any; +} + +type Class = { new (...args: any[]): any }; + +export async function overwriteClassOnLicense(license: LicenseModule, original: Class, overwrite: IOverrideClassProperties): Promise { + await onLicense(license, () => { + Object.entries(overwrite).forEach(([key, value]) => { + const originalFn = original.prototype[key]; + original.prototype[key] = function (...args: any[]): any { + return value.call(this, originalFn, ...args); + }; + }); + }); +} diff --git a/ee/packages/license/src/index.ts b/ee/packages/license/src/index.ts new file mode 100644 index 000000000000..fc2d6e9275e4 --- /dev/null +++ b/ee/packages/license/src/index.ts @@ -0,0 +1,36 @@ +import { overwriteClassOnLicense } from './events/overwriteClassOnLicense'; +import { getLicense, getUnmodifiedLicenseAndModules, isEnterprise, setLicense } from './license'; +import { hasModule, getModules } from './modules'; +import { getTags } from './tags'; +import { setLicenseLimitCounter, getCurrentValueForLicenseLimit } from './validation/getCurrentValueForLicenseLimit'; +import { validateFormat } from './validation/validateFormat'; +import { setWorkspaceUrl } from './workspaceUrl'; + +export * from './definition/ILicenseTag'; +export * from './definition/ILicenseV2'; +export * from './definition/ILicenseV3'; +export * from './definition/LicenseBehavior'; +export * from './definition/LicenseLimit'; +export * from './definition/LicenseModule'; +export * from './definition/LicensePeriod'; +export * from './definition/LimitContext'; + +export * from './events/deprecated'; +export * from './events/listeners'; +export * from './deprecated'; +export * from './actionBlockers'; + +export { + setLicense, + validateFormat, + setWorkspaceUrl, + hasModule, + isEnterprise, + getUnmodifiedLicenseAndModules, + getLicense, + getModules, + getTags, + overwriteClassOnLicense, + setLicenseLimitCounter, + getCurrentValueForLicenseLimit, +}; diff --git a/ee/packages/license/src/license.ts b/ee/packages/license/src/license.ts new file mode 100644 index 000000000000..8141ec42f482 --- /dev/null +++ b/ee/packages/license/src/license.ts @@ -0,0 +1,134 @@ +import decrypt from './decrypt'; +import type { ILicenseV2 } from './definition/ILicenseV2'; +import type { ILicenseV3 } from './definition/ILicenseV3'; +import type { BehaviorWithContext } from './definition/LicenseBehavior'; +import { isLicenseDuplicate, lockLicense } from './encryptedLicense'; +import { licenseRemoved, licenseValidated } from './events/emitter'; +import { logger } from './logger'; +import { getModules, invalidateAll, notifyValidatedModules } from './modules'; +import { showLicense } from './showLicense'; +import { addTags } from './tags'; +import { convertToV3 } from './v2/convertToV3'; +import { getModulesToDisable } from './validation/getModulesToDisable'; +import { isBehaviorsInResult } from './validation/isBehaviorsInResult'; +import { runValidation } from './validation/runValidation'; + +let unmodifiedLicense: ILicenseV2 | ILicenseV3 | undefined; +let license: ILicenseV3 | undefined; +let valid: boolean | undefined; +let inFairPolicy: boolean | undefined; + +const removeCurrentLicense = () => { + const oldLicense = license; + const wasValid = valid; + + license = undefined; + unmodifiedLicense = undefined; + valid = undefined; + inFairPolicy = undefined; + + if (!oldLicense || !wasValid) { + return; + } + + valid = false; + + licenseRemoved(); + invalidateAll(); +}; + +const processValidationResult = (result: BehaviorWithContext[]) => { + if (!license || isBehaviorsInResult(result, ['invalidate_license', 'prevent_installation'])) { + return; + } + + valid = true; + inFairPolicy = isBehaviorsInResult(result, ['start_fair_policy']); + + if (license.information.tags) { + addTags(license.information.tags); + } + + const disabledModules = getModulesToDisable(result); + const modulesToEnable = license.grantedModules.filter(({ module }) => !disabledModules.includes(module)); + + notifyValidatedModules(modulesToEnable.map(({ module }) => module)); + logger.log({ msg: 'License validated', modules: modulesToEnable }); + + licenseValidated(); + showLicense(license, valid); +}; + +export const validateLicense = async () => { + if (!license) { + return; + } + + // #TODO: Only include 'prevent_installation' here if this is actually the initial installation of the license + const validationResult = await runValidation(license, [ + 'invalidate_license', + 'prevent_installation', + 'start_fair_policy', + 'disable_modules', + ]); + processValidationResult(validationResult); +}; + +const setLicenseV3 = async (newLicense: ILicenseV3, originalLicense?: ILicenseV2 | ILicenseV3) => { + removeCurrentLicense(); + unmodifiedLicense = originalLicense || newLicense; + license = newLicense; + + await validateLicense(); +}; + +const setLicenseV2 = async (newLicense: ILicenseV2) => setLicenseV3(convertToV3(newLicense), newLicense); + +export const setLicense = async (encryptedLicense: string): Promise => { + if (!encryptedLicense || String(encryptedLicense).trim() === '' || isLicenseDuplicate(encryptedLicense)) { + return false; + } + + logger.info('New Enterprise License'); + try { + const decrypted = decrypt(encryptedLicense); + if (!decrypted) { + return false; + } + + if (process.env.LICENSE_DEBUG && process.env.LICENSE_DEBUG !== 'false') { + logger.debug({ msg: 'license', decrypted }); + } + + // #TODO: Check license version and call setLicenseV2 or setLicenseV3 + await setLicenseV2(JSON.parse(decrypted)); + lockLicense(encryptedLicense); + + return true; + } catch (e) { + logger.error('Invalid license'); + if (process.env.LICENSE_DEBUG && process.env.LICENSE_DEBUG !== 'false') { + logger.error({ msg: 'Invalid raw license', encryptedLicense, e }); + } + return false; + } +}; + +export const isEnterprise = () => Boolean(license && valid); + +export const getUnmodifiedLicenseAndModules = () => { + if (valid && unmodifiedLicense) { + return { + license: unmodifiedLicense, + modules: getModules(), + }; + } +}; + +export const getLicense = () => { + if (valid && license) { + return license; + } +}; + +export const startedFairPolicy = () => Boolean(inFairPolicy); diff --git a/ee/packages/license/src/logger.ts b/ee/packages/license/src/logger.ts new file mode 100644 index 000000000000..120b08691c6c --- /dev/null +++ b/ee/packages/license/src/logger.ts @@ -0,0 +1,3 @@ +import { Logger } from '@rocket.chat/logger'; + +export const logger = new Logger('License'); diff --git a/ee/packages/license/src/modules.ts b/ee/packages/license/src/modules.ts new file mode 100644 index 000000000000..d61552dbec4b --- /dev/null +++ b/ee/packages/license/src/modules.ts @@ -0,0 +1,27 @@ +import type { LicenseModule } from './definition/LicenseModule'; +import { moduleRemoved, moduleValidated } from './events/emitter'; + +const modules = new Set(); + +export const notifyValidatedModules = (licenseModules: LicenseModule[]) => { + licenseModules.forEach((module) => { + modules.add(module); + moduleValidated(module); + }); +}; + +export const notifyInvalidatedModules = (licenseModules: LicenseModule[]) => { + licenseModules.forEach((module) => { + moduleRemoved(module); + modules.delete(module); + }); +}; + +export const invalidateAll = () => { + notifyInvalidatedModules([...modules]); + modules.clear(); +}; + +export const getModules = () => [...modules]; + +export const hasModule = (module: LicenseModule) => modules.has(module); diff --git a/ee/packages/license/src/showLicense.ts b/ee/packages/license/src/showLicense.ts new file mode 100644 index 000000000000..b7d131e8e438 --- /dev/null +++ b/ee/packages/license/src/showLicense.ts @@ -0,0 +1,26 @@ +import type { ILicenseV3 } from './definition/ILicenseV3'; +import { getModules } from './modules'; + +export const showLicense = (license: ILicenseV3 | undefined, valid: boolean | undefined) => { + if (!process.env.LICENSE_DEBUG || process.env.LICENSE_DEBUG === 'false') { + return; + } + + if (!license || !valid) { + return; + } + + const { + validation: { serverUrls, validPeriods }, + limits, + } = license; + + const modules = getModules(); + + console.log('---- License enabled ----'); + console.log(' url ->', JSON.stringify(serverUrls)); + console.log(' periods ->', JSON.stringify(validPeriods)); + console.log(' limits ->', JSON.stringify(limits)); + console.log(' modules ->', modules.join(', ')); + console.log('-------------------------'); +}; diff --git a/ee/packages/license/src/tags.ts b/ee/packages/license/src/tags.ts new file mode 100644 index 000000000000..207b716d2274 --- /dev/null +++ b/ee/packages/license/src/tags.ts @@ -0,0 +1,22 @@ +import type { ILicenseTag } from './definition/ILicenseTag'; + +const tags = new Set(); + +export const addTag = (tag: ILicenseTag) => { + // make sure to not add duplicated tag names + for (const addedTag of tags) { + if (addedTag.name.toLowerCase() === tag.name.toLowerCase()) { + return; + } + } + + tags.add(tag); +}; + +export const addTags = (tags: ILicenseTag[]) => { + for (const tag of tags) { + addTag(tag); + } +}; + +export const getTags = () => [...tags]; diff --git a/apps/meteor/ee/app/license/server/bundles.ts b/ee/packages/license/src/v2/bundles.ts similarity index 100% rename from apps/meteor/ee/app/license/server/bundles.ts rename to ee/packages/license/src/v2/bundles.ts diff --git a/apps/meteor/ee/app/license/server/fromV2toV3.ts b/ee/packages/license/src/v2/convertToV3.ts similarity index 90% rename from apps/meteor/ee/app/license/server/fromV2toV3.ts rename to ee/packages/license/src/v2/convertToV3.ts index fc86d1208bc3..ddd21d327032 100644 --- a/apps/meteor/ee/app/license/server/fromV2toV3.ts +++ b/ee/packages/license/src/v2/convertToV3.ts @@ -3,12 +3,13 @@ * Transform a License V2 into a V3 representation. */ -import type { ILicenseV2, ILicenseV3, LicenseModule } from '@rocket.chat/core-typings'; - +import type { ILicenseV2 } from '../definition/ILicenseV2'; +import type { ILicenseV3 } from '../definition/ILicenseV3'; +import type { LicenseModule } from '../definition/LicenseModule'; import { isBundle, getBundleFromModule, getBundleModules } from './bundles'; import { getTagColor } from './getTagColor'; -export const fromV2toV3 = (v2: ILicenseV2): ILicenseV3 => { +export const convertToV3 = (v2: ILicenseV2): ILicenseV3 => { return { version: '3.0', information: { diff --git a/apps/meteor/ee/app/license/server/getTagColor.ts b/ee/packages/license/src/v2/getTagColor.ts similarity index 100% rename from apps/meteor/ee/app/license/server/getTagColor.ts rename to ee/packages/license/src/v2/getTagColor.ts diff --git a/ee/packages/license/src/validation/getCurrentValueForLicenseLimit.ts b/ee/packages/license/src/validation/getCurrentValueForLicenseLimit.ts new file mode 100644 index 000000000000..bbe89516f449 --- /dev/null +++ b/ee/packages/license/src/validation/getCurrentValueForLicenseLimit.ts @@ -0,0 +1,27 @@ +import type { IUser } from '@rocket.chat/core-typings'; +import { Subscriptions, Users } from '@rocket.chat/models'; + +import type { LicenseLimitKind } from '../definition/ILicenseV3'; + +type LimitContext = T extends 'roomsPerGuest' ? { userId: IUser['_id'] } : Record; + +const dataCounters = new Map) => Promise>(); + +export const setLicenseLimitCounter = (limitKey: T, fn: (context?: LimitContext) => Promise) => { + dataCounters.set(limitKey, fn as (context?: LimitContext) => Promise); +}; + +setLicenseLimitCounter('activeUsers', () => Users.getActiveLocalUserCount()); +setLicenseLimitCounter('guestUsers', () => Users.getActiveLocalGuestCount()); +setLicenseLimitCounter('roomsPerGuest', async (context) => (context?.userId ? Subscriptions.countByUserId(context.userId) : 0)); + +export const getCurrentValueForLicenseLimit = async ( + limitKey: T, + context?: Partial>, +): Promise => { + if (dataCounters.has(limitKey)) { + return dataCounters.get(limitKey)?.(context as LimitContext | undefined) ?? 0; + } + + return 0; +}; diff --git a/ee/packages/license/src/validation/getModulesToDisable.ts b/ee/packages/license/src/validation/getModulesToDisable.ts new file mode 100644 index 000000000000..62b625374976 --- /dev/null +++ b/ee/packages/license/src/validation/getModulesToDisable.ts @@ -0,0 +1,15 @@ +import type { BehaviorWithContext, LicenseBehavior } from '../definition/LicenseBehavior'; +import type { LicenseModule } from '../definition/LicenseModule'; + +const filterValidationResult = (result: BehaviorWithContext[], expectedBehavior: LicenseBehavior) => + result.filter(({ behavior }) => behavior === expectedBehavior) as BehaviorWithContext[]; + +export const getModulesToDisable = (validationResult: BehaviorWithContext[]): LicenseModule[] => { + return [ + ...new Set([ + ...filterValidationResult(validationResult, 'disable_modules') + .map(({ modules }) => modules || []) + .reduce((prev, curr) => [...prev, ...curr], []), + ]), + ]; +}; diff --git a/ee/packages/license/src/validation/getResultingBehavior.ts b/ee/packages/license/src/validation/getResultingBehavior.ts new file mode 100644 index 000000000000..47e2d91b8b89 --- /dev/null +++ b/ee/packages/license/src/validation/getResultingBehavior.ts @@ -0,0 +1,20 @@ +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 => { + const behavior = 'invalidBehavior' in data ? data.invalidBehavior : data.behavior; + + switch (behavior) { + case 'disable_modules': + return { + behavior, + modules: ('modules' in data && data.modules) || [], + }; + + default: + return { + behavior, + } as BehaviorWithContext; + } +}; diff --git a/ee/packages/license/src/validation/isBehaviorsInResult.ts b/ee/packages/license/src/validation/isBehaviorsInResult.ts new file mode 100644 index 000000000000..7e6ed89db8ec --- /dev/null +++ b/ee/packages/license/src/validation/isBehaviorsInResult.ts @@ -0,0 +1,4 @@ +import type { BehaviorWithContext, LicenseBehavior } from '../definition/LicenseBehavior'; + +export const isBehaviorsInResult = (result: BehaviorWithContext[], expectedBehaviors: LicenseBehavior[]) => + result.some(({ behavior }) => expectedBehaviors.includes(behavior)); diff --git a/ee/packages/license/src/validation/runValidation.ts b/ee/packages/license/src/validation/runValidation.ts new file mode 100644 index 000000000000..6a75917c311d --- /dev/null +++ b/ee/packages/license/src/validation/runValidation.ts @@ -0,0 +1,17 @@ +import type { ILicenseV3 } from '../definition/ILicenseV3'; +import type { LicenseBehavior, BehaviorWithContext } from '../definition/LicenseBehavior'; +import { validateLicenseLimits } from './validateLicenseLimits'; +import { validateLicensePeriods } from './validateLicensePeriods'; +import { validateLicenseUrl } from './validateLicenseUrl'; + +export const runValidation = async (license: ILicenseV3, behaviorsToValidate: LicenseBehavior[] = []): Promise => { + const shouldValidateBehavior = (behavior: LicenseBehavior) => !behaviorsToValidate?.length || behaviorsToValidate.includes(behavior); + + return [ + ...new Set([ + ...validateLicenseUrl(license, shouldValidateBehavior), + ...validateLicensePeriods(license, shouldValidateBehavior), + ...(await validateLicenseLimits(license, shouldValidateBehavior)), + ]), + ]; +}; diff --git a/ee/packages/license/src/validation/shouldPreventAction.ts b/ee/packages/license/src/validation/shouldPreventAction.ts new file mode 100644 index 000000000000..8b8cc83d67ab --- /dev/null +++ b/ee/packages/license/src/validation/shouldPreventAction.ts @@ -0,0 +1,17 @@ +import type { LicenseLimitKind } from '../definition/ILicenseV3'; +import type { LimitContext } from '../definition/LimitContext'; +import { getLicense } from '../license'; +import { getCurrentValueForLicenseLimit } from './getCurrentValueForLicenseLimit'; + +export const shouldPreventAction = async ( + action: T, + context?: Partial>, + newCount = 1, +): Promise => { + const license = getLicense(); + + const currentValue = (await getCurrentValueForLicenseLimit(action, context)) + newCount; + return Boolean( + license?.limits[action]?.filter(({ behavior, max }) => behavior === 'prevent_action' && max >= 0).some(({ max }) => max < currentValue), + ); +}; diff --git a/ee/packages/license/src/validation/validateFormat.ts b/ee/packages/license/src/validation/validateFormat.ts new file mode 100644 index 000000000000..8d57a5509c5f --- /dev/null +++ b/ee/packages/license/src/validation/validateFormat.ts @@ -0,0 +1,14 @@ +import decrypt from '../decrypt'; + +export const validateFormat = (encryptedLicense: string): boolean => { + if (!encryptedLicense || String(encryptedLicense).trim() === '') { + return false; + } + + const decrypted = decrypt(encryptedLicense); + if (!decrypted) { + return false; + } + + return true; +}; diff --git a/ee/packages/license/src/validation/validateLicenseLimits.ts b/ee/packages/license/src/validation/validateLicenseLimits.ts new file mode 100644 index 000000000000..2bfa4a2d1b75 --- /dev/null +++ b/ee/packages/license/src/validation/validateLicenseLimits.ts @@ -0,0 +1,37 @@ +import type { ILicenseV3 } from '../definition/ILicenseV3'; +import type { BehaviorWithContext, LicenseBehavior } from '../definition/LicenseBehavior'; +import { logger } from '../logger'; +import { getCurrentValueForLicenseLimit } from './getCurrentValueForLicenseLimit'; +import { getResultingBehavior } from './getResultingBehavior'; + +export const validateLicenseLimits = async ( + license: ILicenseV3, + behaviorFilter: (behavior: LicenseBehavior) => boolean, +): Promise => { + const { limits } = license; + + const limitKeys = Object.keys(limits) as (keyof ILicenseV3['limits'])[]; + 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)); + if (!limitList?.length) { + return []; + } + + const currentValue = await getCurrentValueForLicenseLimit(limitKey); + return limitList + .filter(({ max }) => max < currentValue) + .map((limit) => { + logger.error({ + msg: 'Limit validation failed', + kind: limitKey, + limit, + }); + return getResultingBehavior(limit); + }); + }), + ) + ).reduce((prev, curr) => [...prev, ...curr], []); +}; diff --git a/ee/packages/license/src/validation/validateLicensePeriods.ts b/ee/packages/license/src/validation/validateLicensePeriods.ts new file mode 100644 index 000000000000..b0967ecba49b --- /dev/null +++ b/ee/packages/license/src/validation/validateLicensePeriods.ts @@ -0,0 +1,38 @@ +import type { ILicenseV3 } from '../definition/ILicenseV3'; +import type { BehaviorWithContext, LicenseBehavior } from '../definition/LicenseBehavior'; +import type { Timestamp } from '../definition/LicensePeriod'; +import { logger } from '../logger'; +import { getResultingBehavior } from './getResultingBehavior'; + +export const isPeriodInvalid = (from?: Timestamp, until?: Timestamp) => { + const now = new Date(); + + if (from && now < new Date(from)) { + return true; + } + + if (until && now > new Date(until)) { + return true; + } + + return false; +}; + +export const validateLicensePeriods = ( + license: ILicenseV3, + behaviorFilter: (behavior: LicenseBehavior) => boolean, +): BehaviorWithContext[] => { + const { + validation: { validPeriods }, + } = license; + + return validPeriods + .filter(({ validFrom, validUntil, invalidBehavior }) => behaviorFilter(invalidBehavior) && isPeriodInvalid(validFrom, validUntil)) + .map((period) => { + logger.error({ + msg: 'Period validation failed', + period, + }); + return getResultingBehavior(period); + }); +}; diff --git a/ee/packages/license/src/validation/validateLicenseUrl.ts b/ee/packages/license/src/validation/validateLicenseUrl.ts new file mode 100644 index 000000000000..db7e91b4f870 --- /dev/null +++ b/ee/packages/license/src/validation/validateLicenseUrl.ts @@ -0,0 +1,55 @@ +import type { ILicenseV3 } from '../definition/ILicenseV3'; +import type { BehaviorWithContext, LicenseBehavior } from '../definition/LicenseBehavior'; +import { logger } from '../logger'; +import { getWorkspaceUrl } from '../workspaceUrl'; +import { getResultingBehavior } from './getResultingBehavior'; + +export const validateUrl = (licenseURL: string, url: string) => { + licenseURL = licenseURL + .replace(/\./g, '\\.') // convert dots to literal + .replace(/\*/g, '.*'); // convert * to .* + const regex = new RegExp(`^${licenseURL}$`, 'i'); + + return !!regex.exec(url); +}; + +export const validateLicenseUrl = (license: ILicenseV3, behaviorFilter: (behavior: LicenseBehavior) => boolean): BehaviorWithContext[] => { + if (!behaviorFilter('invalidate_license')) { + return []; + } + + const { + validation: { serverUrls }, + } = license; + + const workspaceUrl = getWorkspaceUrl(); + + if (!workspaceUrl) { + logger.error('Unable to validate license URL without knowing the workspace URL.'); + return [getResultingBehavior({ behavior: 'invalidate_license' })]; + } + + return serverUrls + .filter((url) => { + switch (url.type) { + case 'regex': + // #TODO + break; + case 'hash': + // #TODO + break; + case 'url': + return !validateUrl(url.value, workspaceUrl); + } + + return false; + }) + .map((url) => { + logger.error({ + msg: 'Url validation failed', + url, + workspaceUrl, + }); + return getResultingBehavior({ behavior: 'invalidate_license' }); + }); +}; diff --git a/ee/packages/license/src/workspaceUrl.ts b/ee/packages/license/src/workspaceUrl.ts new file mode 100644 index 000000000000..f8edb62bf5ee --- /dev/null +++ b/ee/packages/license/src/workspaceUrl.ts @@ -0,0 +1,11 @@ +import { validateLicense } from './license'; + +let workspaceUrl: string | undefined; + +export const setWorkspaceUrl = async (url: string) => { + workspaceUrl = url.replace(/\/$/, '').replace(/^https?:\/\/(.*)$/, '$1'); + + await validateLicense(); +}; + +export const getWorkspaceUrl = () => workspaceUrl; diff --git a/ee/packages/license/tsconfig.json b/ee/packages/license/tsconfig.json new file mode 100644 index 000000000000..539d1c0af1b8 --- /dev/null +++ b/ee/packages/license/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../../tsconfig.base.server.json", + "compilerOptions": { + "declaration": true, + "rootDir": "./src", + "outDir": "./dist" + }, + "include": ["./src/**/*"] +} diff --git a/packages/core-typings/src/index.ts b/packages/core-typings/src/index.ts index 52a4ac8f1f7e..459e5680900b 100644 --- a/packages/core-typings/src/index.ts +++ b/packages/core-typings/src/index.ts @@ -39,9 +39,6 @@ export * from './IUserSession'; export * from './IUserStatus'; export * from './IUser'; -export * from './ee/ILicense/ILicenseV2'; -export * from './ee/ILicense/ILicenseTag'; -export * from './ee/ILicense/ILicenseV3'; export * from './ee/IAuditLog'; export * from './import'; diff --git a/packages/rest-typings/package.json b/packages/rest-typings/package.json index 8b6f60f294b3..9da7694d28b9 100644 --- a/packages/rest-typings/package.json +++ b/packages/rest-typings/package.json @@ -26,6 +26,7 @@ "dependencies": { "@rocket.chat/apps-engine": "1.41.0-alpha.290", "@rocket.chat/core-typings": "workspace:^", + "@rocket.chat/license": "workspace:^", "@rocket.chat/message-parser": "next", "@rocket.chat/ui-kit": "^0.32.1", "ajv": "^8.11.0", diff --git a/packages/rest-typings/src/v1/licenses.ts b/packages/rest-typings/src/v1/licenses.ts index 48a3167da3df..96c67e2654bb 100644 --- a/packages/rest-typings/src/v1/licenses.ts +++ b/packages/rest-typings/src/v1/licenses.ts @@ -1,4 +1,4 @@ -import type { ILicenseV2, ILicenseV3 } from '@rocket.chat/core-typings'; +import type { ILicenseV2, ILicenseV3 } from '@rocket.chat/license'; import Ajv from 'ajv'; const ajv = new Ajv({ diff --git a/yarn.lock b/yarn.lock index d1c8a39c4f66..e41890272ce7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8390,6 +8390,21 @@ __metadata: languageName: node linkType: hard +"@rocket.chat/license@workspace:^, @rocket.chat/license@workspace:ee/packages/license": + version: 0.0.0-use.local + resolution: "@rocket.chat/license@workspace:ee/packages/license" + dependencies: + "@rocket.chat/core-typings": "workspace:^" + "@rocket.chat/logger": "workspace:^" + "@rocket.chat/models": "workspace:^" + "@types/jest": ~29.5.3 + eslint: ~8.45.0 + jest: ~29.6.1 + ts-jest: ~29.0.5 + typescript: ~5.1.6 + languageName: unknown + linkType: soft + "@rocket.chat/livechat@workspace:^, @rocket.chat/livechat@workspace:packages/livechat": version: 0.0.0-use.local resolution: "@rocket.chat/livechat@workspace:packages/livechat" @@ -8602,6 +8617,7 @@ __metadata: "@rocket.chat/instance-status": "workspace:^" "@rocket.chat/jwt": "workspace:^" "@rocket.chat/layout": next + "@rocket.chat/license": "workspace:^" "@rocket.chat/livechat": "workspace:^" "@rocket.chat/log-format": "workspace:^" "@rocket.chat/logger": "workspace:^" @@ -9276,6 +9292,7 @@ __metadata: "@rocket.chat/apps-engine": 1.41.0-alpha.290 "@rocket.chat/core-typings": "workspace:^" "@rocket.chat/eslint-config": "workspace:^" + "@rocket.chat/license": "workspace:^" "@rocket.chat/message-parser": next "@rocket.chat/ui-kit": ^0.32.1 "@types/jest": ~29.5.3 From f6156351116e2b4e8171e4d868a806b10c0d8da8 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Mon, 18 Sep 2023 21:47:38 -0300 Subject: [PATCH 16/38] add license package to dockerfile --- ee/apps/account-service/Dockerfile | 6 ++++++ ee/apps/authorization-service/Dockerfile | 6 ++++++ ee/apps/ddp-streamer/Dockerfile | 6 ++++++ ee/apps/omnichannel-transcript/Dockerfile | 6 ++++++ ee/apps/presence-service/Dockerfile | 6 ++++++ ee/apps/queue-worker/Dockerfile | 6 ++++++ ee/apps/stream-hub-service/Dockerfile | 6 ++++++ 7 files changed, 42 insertions(+) diff --git a/ee/apps/account-service/Dockerfile b/ee/apps/account-service/Dockerfile index d7ccb734071b..f2aacafcdf48 100644 --- a/ee/apps/account-service/Dockerfile +++ b/ee/apps/account-service/Dockerfile @@ -22,6 +22,12 @@ COPY ./packages/model-typings/dist packages/model-typings/dist COPY ./packages/models/package.json packages/models/package.json COPY ./packages/models/dist packages/models/dist +COPY ./packages/logger/package.json packages/logger/package.json +COPY ./packages/logger/dist packages/logger/dist + +COPY ./ee/packages/license/package.json packages/license/package.json +COPY ./ee/packages/license/dist packages/license/dist + COPY ./ee/apps/${SERVICE}/dist . COPY ./package.json . diff --git a/ee/apps/authorization-service/Dockerfile b/ee/apps/authorization-service/Dockerfile index d7ccb734071b..f2aacafcdf48 100644 --- a/ee/apps/authorization-service/Dockerfile +++ b/ee/apps/authorization-service/Dockerfile @@ -22,6 +22,12 @@ COPY ./packages/model-typings/dist packages/model-typings/dist COPY ./packages/models/package.json packages/models/package.json COPY ./packages/models/dist packages/models/dist +COPY ./packages/logger/package.json packages/logger/package.json +COPY ./packages/logger/dist packages/logger/dist + +COPY ./ee/packages/license/package.json packages/license/package.json +COPY ./ee/packages/license/dist packages/license/dist + COPY ./ee/apps/${SERVICE}/dist . COPY ./package.json . diff --git a/ee/apps/ddp-streamer/Dockerfile b/ee/apps/ddp-streamer/Dockerfile index 5250e48bf106..51a4b0c6d4f6 100644 --- a/ee/apps/ddp-streamer/Dockerfile +++ b/ee/apps/ddp-streamer/Dockerfile @@ -28,6 +28,12 @@ COPY ./packages/model-typings/dist packages/model-typings/dist COPY ./packages/models/package.json packages/models/package.json COPY ./packages/models/dist packages/models/dist +COPY ./packages/logger/package.json packages/logger/package.json +COPY ./packages/logger/dist packages/logger/dist + +COPY ./ee/packages/license/package.json packages/license/package.json +COPY ./ee/packages/license/dist packages/license/dist + COPY ./packages/instance-status/package.json packages/instance-status/package.json COPY ./packages/instance-status/dist packages/instance-status/dist diff --git a/ee/apps/omnichannel-transcript/Dockerfile b/ee/apps/omnichannel-transcript/Dockerfile index 95fb836e9f27..1eb92cde30b1 100644 --- a/ee/apps/omnichannel-transcript/Dockerfile +++ b/ee/apps/omnichannel-transcript/Dockerfile @@ -22,6 +22,12 @@ COPY ./packages/model-typings/dist packages/model-typings/dist COPY ./packages/models/package.json packages/models/package.json COPY ./packages/models/dist packages/models/dist +COPY ./packages/logger/package.json packages/logger/package.json +COPY ./packages/logger/dist packages/logger/dist + +COPY ./ee/packages/license/package.json packages/license/package.json +COPY ./ee/packages/license/dist packages/license/dist + COPY ./ee/packages/omnichannel-services/package.json ee/packages/omnichannel-services/package.json COPY ./ee/packages/omnichannel-services/dist ee/packages/omnichannel-services/dist diff --git a/ee/apps/presence-service/Dockerfile b/ee/apps/presence-service/Dockerfile index f85c45246f29..7d9b0f953019 100644 --- a/ee/apps/presence-service/Dockerfile +++ b/ee/apps/presence-service/Dockerfile @@ -25,6 +25,12 @@ COPY ./packages/model-typings/dist packages/model-typings/dist COPY ./packages/models/package.json packages/models/package.json COPY ./packages/models/dist packages/models/dist +COPY ./packages/logger/package.json packages/logger/package.json +COPY ./packages/logger/dist packages/logger/dist + +COPY ./ee/packages/license/package.json packages/license/package.json +COPY ./ee/packages/license/dist packages/license/dist + COPY ./packages/ui-contexts/package.json packages/ui-contexts/package.json COPY ./packages/ui-contexts/dist packages/ui-contexts/dist diff --git a/ee/apps/queue-worker/Dockerfile b/ee/apps/queue-worker/Dockerfile index 95fb836e9f27..1eb92cde30b1 100644 --- a/ee/apps/queue-worker/Dockerfile +++ b/ee/apps/queue-worker/Dockerfile @@ -22,6 +22,12 @@ COPY ./packages/model-typings/dist packages/model-typings/dist COPY ./packages/models/package.json packages/models/package.json COPY ./packages/models/dist packages/models/dist +COPY ./packages/logger/package.json packages/logger/package.json +COPY ./packages/logger/dist packages/logger/dist + +COPY ./ee/packages/license/package.json packages/license/package.json +COPY ./ee/packages/license/dist packages/license/dist + COPY ./ee/packages/omnichannel-services/package.json ee/packages/omnichannel-services/package.json COPY ./ee/packages/omnichannel-services/dist ee/packages/omnichannel-services/dist diff --git a/ee/apps/stream-hub-service/Dockerfile b/ee/apps/stream-hub-service/Dockerfile index c06115c887f5..14ed6b9aa4b2 100644 --- a/ee/apps/stream-hub-service/Dockerfile +++ b/ee/apps/stream-hub-service/Dockerfile @@ -25,6 +25,12 @@ COPY ./packages/models/dist packages/models/dist COPY ./packages/logger/package.json packages/logger/package.json COPY ./packages/logger/dist packages/logger/dist +COPY ./ee/packages/license/package.json packages/license/package.json +COPY ./ee/packages/license/dist packages/license/dist + +COPY ./packages/logger/package.json packages/logger/package.json +COPY ./packages/logger/dist packages/logger/dist + COPY ./ee/apps/${SERVICE}/dist . COPY ./package.json . From 608ed68d140cddbc0f3e698104067e7de2823423 Mon Sep 17 00:00:00 2001 From: Pierre Lehnen Date: Tue, 19 Sep 2023 16:32:00 -0300 Subject: [PATCH 17/38] avoid triggering "invalidate" events when an active license is replaced with another valid license. --- ee/packages/license/src/actionBlockers.ts | 8 ++ ee/packages/license/src/events/emitter.ts | 33 ++++++-- ee/packages/license/src/license.ts | 81 ++++++++++++------- ee/packages/license/src/modules.ts | 20 +++++ ee/packages/license/src/pendingLicense.ts | 30 +++++++ ee/packages/license/src/tags.ts | 5 +- .../getCurrentValueForLicenseLimit.ts | 3 + .../src/validation/getModulesToDisable.ts | 2 +- .../src/validation/validateLicenseLimits.ts | 2 +- ee/packages/license/src/workspaceUrl.ts | 6 +- 10 files changed, 151 insertions(+), 39 deletions(-) create mode 100644 ee/packages/license/src/pendingLicense.ts diff --git a/ee/packages/license/src/actionBlockers.ts b/ee/packages/license/src/actionBlockers.ts index 6876512a147e..ad170ad29250 100644 --- a/ee/packages/license/src/actionBlockers.ts +++ b/ee/packages/license/src/actionBlockers.ts @@ -8,3 +8,11 @@ export const preventNewPrivateApps = async (appCount = 1) => shouldPreventAction export const preventNewMarketplaceApps = async (appCount = 1) => shouldPreventAction('marketplaceApps', {}, appCount); export const preventNewGuestSubscriptions = async (guest: IUser['_id'], roomCount = 1) => shouldPreventAction('roomsPerGuest', { userId: guest }, roomCount); +export const preventNewActiveContacts = async (contactCount = 1) => shouldPreventAction('monthlyActiveContacts', {}, contactCount); + +export const userLimitReached = async () => preventNewUsers(0); +export const guestLimitReached = async () => preventNewGuests(0); +export const privateAppLimitReached = async () => preventNewPrivateApps(0); +export const marketplaceAppLimitReached = async () => preventNewMarketplaceApps(0); +export const guestSubscriptionLimitReached = async (guest: IUser['_id']) => preventNewGuestSubscriptions(guest, 0); +export const macLimitReached = async () => preventNewActiveContacts(0); diff --git a/ee/packages/license/src/events/emitter.ts b/ee/packages/license/src/events/emitter.ts index d9ce189df008..8b16e5527121 100644 --- a/ee/packages/license/src/events/emitter.ts +++ b/ee/packages/license/src/events/emitter.ts @@ -1,19 +1,40 @@ import { EventEmitter } from 'events'; import type { LicenseModule } from '../definition/LicenseModule'; +import { logger } from '../logger'; export const EnterpriseLicenses = new EventEmitter(); -export const licenseValidated = () => EnterpriseLicenses.emit('validate'); +export const licenseValidated = () => { + try { + EnterpriseLicenses.emit('validate'); + } catch (error) { + logger.error({ msg: 'Error running license validated event', error }); + } +}; -export const licenseRemoved = () => EnterpriseLicenses.emit('invalidate'); +export const licenseRemoved = () => { + try { + EnterpriseLicenses.emit('invalidate'); + } catch (error) { + logger.error({ msg: 'Error running license invalidated event', error }); + } +}; export const moduleValidated = (module: LicenseModule) => { - EnterpriseLicenses.emit('module', { module, valid: true }); - EnterpriseLicenses.emit(`valid:${module}`); + try { + EnterpriseLicenses.emit('module', { module, valid: true }); + EnterpriseLicenses.emit(`valid:${module}`); + } catch (error) { + logger.error({ msg: 'Error running module added event', error }); + } }; export const moduleRemoved = (module: LicenseModule) => { - EnterpriseLicenses.emit('module', { module, valid: false }); - EnterpriseLicenses.emit(`invalid:${module}`); + try { + EnterpriseLicenses.emit('module', { module, valid: false }); + EnterpriseLicenses.emit(`invalid:${module}`); + } catch (error) { + logger.error({ msg: 'Error running module removed event', error }); + } }; diff --git a/ee/packages/license/src/license.ts b/ee/packages/license/src/license.ts index 8141ec42f482..64f75eb501ee 100644 --- a/ee/packages/license/src/license.ts +++ b/ee/packages/license/src/license.ts @@ -5,36 +5,28 @@ import type { BehaviorWithContext } from './definition/LicenseBehavior'; import { isLicenseDuplicate, lockLicense } from './encryptedLicense'; import { licenseRemoved, licenseValidated } from './events/emitter'; import { logger } from './logger'; -import { getModules, invalidateAll, notifyValidatedModules } from './modules'; +import { getModules, invalidateAll, replaceModules } from './modules'; +import { clearPendingLicense, hasPendingLicense, isPendingLicense, setPendingLicense } from './pendingLicense'; import { showLicense } from './showLicense'; -import { addTags } from './tags'; +import { replaceTags } from './tags'; import { convertToV3 } from './v2/convertToV3'; import { getModulesToDisable } from './validation/getModulesToDisable'; import { isBehaviorsInResult } from './validation/isBehaviorsInResult'; import { runValidation } from './validation/runValidation'; +import { validateFormat } from './validation/validateFormat'; +import { getWorkspaceUrl } from './workspaceUrl'; let unmodifiedLicense: ILicenseV2 | ILicenseV3 | undefined; let license: ILicenseV3 | undefined; let valid: boolean | undefined; let inFairPolicy: boolean | undefined; -const removeCurrentLicense = () => { - const oldLicense = license; - const wasValid = valid; - +const clearLicenseData = () => { license = undefined; unmodifiedLicense = undefined; valid = undefined; inFairPolicy = undefined; - - if (!oldLicense || !wasValid) { - return; - } - valid = false; - - licenseRemoved(); - invalidateAll(); }; const processValidationResult = (result: BehaviorWithContext[]) => { @@ -46,13 +38,13 @@ const processValidationResult = (result: BehaviorWithContext[]) => { inFairPolicy = isBehaviorsInResult(result, ['start_fair_policy']); if (license.information.tags) { - addTags(license.information.tags); + replaceTags(license.information.tags); } const disabledModules = getModulesToDisable(result); const modulesToEnable = license.grantedModules.filter(({ module }) => !disabledModules.includes(module)); - notifyValidatedModules(modulesToEnable.map(({ module }) => module)); + replaceModules(modulesToEnable.map(({ module }) => module)); logger.log({ msg: 'License validated', modules: modulesToEnable }); licenseValidated(); @@ -60,7 +52,7 @@ const processValidationResult = (result: BehaviorWithContext[]) => { }; export const validateLicense = async () => { - if (!license) { + if (!license || !getWorkspaceUrl()) { return; } @@ -74,18 +66,54 @@ export const validateLicense = async () => { processValidationResult(validationResult); }; -const setLicenseV3 = async (newLicense: ILicenseV3, originalLicense?: ILicenseV2 | ILicenseV3) => { - removeCurrentLicense(); - unmodifiedLicense = originalLicense || newLicense; - license = newLicense; +const setLicenseV3 = async (newLicense: ILicenseV3, encryptedLicense: string, originalLicense?: ILicenseV2 | ILicenseV3) => { + const hadValidLicense = isEnterprise(); + clearLicenseData(); - await validateLicense(); + try { + unmodifiedLicense = originalLicense || newLicense; + license = newLicense; + clearPendingLicense(); + + await validateLicense(); + lockLicense(encryptedLicense); + } finally { + if (hadValidLicense && !isEnterprise()) { + licenseRemoved(); + invalidateAll(); + } + } }; -const setLicenseV2 = async (newLicense: ILicenseV2) => setLicenseV3(convertToV3(newLicense), newLicense); +const setLicenseV2 = async (newLicense: ILicenseV2, encryptedLicense: string) => + setLicenseV3(convertToV3(newLicense), encryptedLicense, newLicense); + +// Can only validate licenses once the workspace URL is set +export const isReadyForValidation = () => Boolean(getWorkspaceUrl()); + +export const setLicense = async (encryptedLicense: string, forceSet = false): Promise => { + if (!encryptedLicense || String(encryptedLicense).trim() === '') { + return false; + } + + if (isLicenseDuplicate(encryptedLicense)) { + // If there is a pending license but the user is trying to revert to the license that is currently active + if (hasPendingLicense() && !isPendingLicense(encryptedLicense)) { + // simply remove the pending license + clearPendingLicense(); + return true; + } + + return false; + } + + if (!isReadyForValidation() && !forceSet) { + // If we can't validate the license data yet, but is a valid license string, store it to validate when we can + if (validateFormat(encryptedLicense)) { + setPendingLicense(encryptedLicense); + return true; + } -export const setLicense = async (encryptedLicense: string): Promise => { - if (!encryptedLicense || String(encryptedLicense).trim() === '' || isLicenseDuplicate(encryptedLicense)) { return false; } @@ -101,8 +129,7 @@ export const setLicense = async (encryptedLicense: string): Promise => } // #TODO: Check license version and call setLicenseV2 or setLicenseV3 - await setLicenseV2(JSON.parse(decrypted)); - lockLicense(encryptedLicense); + await setLicenseV2(JSON.parse(decrypted), encryptedLicense); return true; } catch (e) { diff --git a/ee/packages/license/src/modules.ts b/ee/packages/license/src/modules.ts index d61552dbec4b..bfdb2bbc996b 100644 --- a/ee/packages/license/src/modules.ts +++ b/ee/packages/license/src/modules.ts @@ -25,3 +25,23 @@ export const invalidateAll = () => { export const getModules = () => [...modules]; export const hasModule = (module: LicenseModule) => modules.has(module); + +export const replaceModules = (newModules: LicenseModule[]) => { + for (const moduleName of newModules) { + if (modules.has(moduleName)) { + continue; + } + + modules.add(moduleName); + moduleValidated(moduleName); + } + + for (const moduleName of modules) { + if (newModules.includes(moduleName)) { + continue; + } + + moduleRemoved(moduleName); + modules.delete(moduleName); + } +}; diff --git a/ee/packages/license/src/pendingLicense.ts b/ee/packages/license/src/pendingLicense.ts new file mode 100644 index 000000000000..582a52f93736 --- /dev/null +++ b/ee/packages/license/src/pendingLicense.ts @@ -0,0 +1,30 @@ +import { setLicense } from './license'; +import { logger } from './logger'; + +let pendingLicense: string; + +export const setPendingLicense = (encryptedLicense: string) => { + pendingLicense = encryptedLicense; + if (pendingLicense) { + logger.info('Storing license as pending validation.'); + } +}; + +export const applyPendingLicense = async () => { + if (pendingLicense) { + logger.info('Applying pending license.'); + await setLicense(pendingLicense, true); + } +}; + +export const hasPendingLicense = () => Boolean(pendingLicense); + +export const isPendingLicense = (encryptedLicense: string) => !!pendingLicense && pendingLicense === encryptedLicense; + +export const clearPendingLicense = () => { + if (pendingLicense) { + logger.info('Removing pending license.'); + } + + pendingLicense = ''; +}; diff --git a/ee/packages/license/src/tags.ts b/ee/packages/license/src/tags.ts index 207b716d2274..b115b164cd64 100644 --- a/ee/packages/license/src/tags.ts +++ b/ee/packages/license/src/tags.ts @@ -13,8 +13,9 @@ export const addTag = (tag: ILicenseTag) => { tags.add(tag); }; -export const addTags = (tags: ILicenseTag[]) => { - for (const tag of tags) { +export const replaceTags = (newTags: ILicenseTag[]) => { + tags.clear(); + for (const tag of newTags) { addTag(tag); } }; diff --git a/ee/packages/license/src/validation/getCurrentValueForLicenseLimit.ts b/ee/packages/license/src/validation/getCurrentValueForLicenseLimit.ts index bbe89516f449..cb86c5ff3dfe 100644 --- a/ee/packages/license/src/validation/getCurrentValueForLicenseLimit.ts +++ b/ee/packages/license/src/validation/getCurrentValueForLicenseLimit.ts @@ -2,6 +2,7 @@ import type { IUser } from '@rocket.chat/core-typings'; import { Subscriptions, Users } from '@rocket.chat/models'; import type { LicenseLimitKind } from '../definition/ILicenseV3'; +import { logger } from '../logger'; type LimitContext = T extends 'roomsPerGuest' ? { userId: IUser['_id'] } : Record; @@ -23,5 +24,7 @@ export const getCurrentValueForLicenseLimit = async return dataCounters.get(limitKey)?.(context as LimitContext | undefined) ?? 0; } + logger.error({ msg: 'Unable to validate license limit due to missing data counter.', limitKey }); + return 0; }; diff --git a/ee/packages/license/src/validation/getModulesToDisable.ts b/ee/packages/license/src/validation/getModulesToDisable.ts index 62b625374976..d42426e8af26 100644 --- a/ee/packages/license/src/validation/getModulesToDisable.ts +++ b/ee/packages/license/src/validation/getModulesToDisable.ts @@ -9,7 +9,7 @@ export const getModulesToDisable = (validationResult: BehaviorWithContext[]): Li ...new Set([ ...filterValidationResult(validationResult, 'disable_modules') .map(({ modules }) => modules || []) - .reduce((prev, curr) => [...prev, ...curr], []), + .flat(), ]), ]; }; diff --git a/ee/packages/license/src/validation/validateLicenseLimits.ts b/ee/packages/license/src/validation/validateLicenseLimits.ts index 2bfa4a2d1b75..32b0a1229d9a 100644 --- a/ee/packages/license/src/validation/validateLicenseLimits.ts +++ b/ee/packages/license/src/validation/validateLicenseLimits.ts @@ -33,5 +33,5 @@ export const validateLicenseLimits = async ( }); }), ) - ).reduce((prev, curr) => [...prev, ...curr], []); + ).flat(); }; diff --git a/ee/packages/license/src/workspaceUrl.ts b/ee/packages/license/src/workspaceUrl.ts index f8edb62bf5ee..8d384b2d453d 100644 --- a/ee/packages/license/src/workspaceUrl.ts +++ b/ee/packages/license/src/workspaceUrl.ts @@ -1,11 +1,13 @@ -import { validateLicense } from './license'; +import { applyPendingLicense, hasPendingLicense } from './pendingLicense'; let workspaceUrl: string | undefined; export const setWorkspaceUrl = async (url: string) => { workspaceUrl = url.replace(/\/$/, '').replace(/^https?:\/\/(.*)$/, '$1'); - await validateLicense(); + if (hasPendingLicense()) { + await applyPendingLicense(); + } }; export const getWorkspaceUrl = () => workspaceUrl; From 564810bffe3736ecfb9e807d7d573b2b6a9dd922 Mon Sep 17 00:00:00 2001 From: Luis Mauro Date: Wed, 20 Sep 2023 19:30:29 -0600 Subject: [PATCH 18/38] handle V3 and V2 --- ee/packages/license/package.json | 1 + ee/packages/license/src/decrypt.ts | 15 +++++++++++++++ ee/packages/license/src/license.ts | 5 +++-- packages/jwt/tsconfig.json | 1 + yarn.lock | 1 + 5 files changed, 21 insertions(+), 2 deletions(-) diff --git a/ee/packages/license/package.json b/ee/packages/license/package.json index 9b236ce0df8e..60122ac88139 100644 --- a/ee/packages/license/package.json +++ b/ee/packages/license/package.json @@ -23,6 +23,7 @@ ], "dependencies": { "@rocket.chat/core-typings": "workspace:^", + "@rocket.chat/jwt": "workspace:^", "@rocket.chat/logger": "workspace:^", "@rocket.chat/models": "workspace:^" } diff --git a/ee/packages/license/src/decrypt.ts b/ee/packages/license/src/decrypt.ts index 62e34817aec6..87fd55b507d5 100644 --- a/ee/packages/license/src/decrypt.ts +++ b/ee/packages/license/src/decrypt.ts @@ -1,9 +1,24 @@ import crypto from 'crypto'; +import { verify } from '@rocket.chat/jwt'; + const publicKey = 'LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUlJQ0lqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FnOEFNSUlDQ2dLQ0FnRUFxV1Nza2Q5LzZ6Ung4a3lQY2ljcwpiMzJ3Mnd4VnV3N3lCVDk2clEvOEQreU1lQ01POXdTU3BIYS85bkZ5d293RXRpZ3B0L3dyb1BOK1ZHU3didHdQCkZYQmVxRWxCbmRHRkFsODZlNStFbGlIOEt6L2hHbkNtSk5tWHB4RUsyUkUwM1g0SXhzWVg3RERCN010eC9pcXMKY2pCL091dlNCa2ppU2xlUzdibE5JVC9kQTdLNC9DSjNvaXUwMmJMNEV4Y2xDSGVwenFOTWVQM3dVWmdweE9uZgpOT3VkOElYWUs3M3pTY3VFOEUxNTdZd3B6Q0twVmFIWDdaSmY4UXVOc09PNVcvYUlqS2wzTDYyNjkrZUlPRXJHCndPTm1hSG56Zmc5RkxwSmh6Z3BPMzhhVm43NnZENUtLakJhaldza1krNGEyZ1NRbUtOZUZxYXFPb3p5RUZNMGUKY0ZXWlZWWjNMZWg0dkVNb1lWUHlJeng5Nng4ZjIveW1QbmhJdXZRdjV3TjRmeWVwYTdFWTVVQ2NwNzF6OGtmUAo0RmNVelBBMElEV3lNaWhYUi9HNlhnUVFaNEdiL3FCQmh2cnZpSkNGemZZRGNKZ0w3RmVnRllIUDNQR0wwN1FnCnZMZXZNSytpUVpQcnhyYnh5U3FkUE9rZ3VyS2pWclhUVXI0QTlUZ2lMeUlYNVVsSnEzRS9SVjdtZk9xWm5MVGEKU0NWWEhCaHVQbG5DR1pSMDFUb1RDZktoTUcxdTBDRm5MMisxNWhDOWZxT21XdjlRa2U0M3FsSjBQZ0YzVkovWAp1eC9tVHBuazlnbmJHOUpIK21mSDM5Um9GdlROaW5Zd1NNdll6dXRWT242OXNPemR3aERsYTkwbDNBQ2g0eENWCks3Sk9YK3VIa29OdTNnMmlWeGlaVU0wQ0F3RUFBUT09Ci0tLS0tRU5EIFBVQkxJQyBLRVktLS0tLQo='; +// #TODO: use async/await export default function decrypt(encrypted: string): string { + // handle V3 + if (encrypted.startsWith('RCV3_')) { + let decrypted = ''; + const jwt = encrypted.substring(5); + + verify(jwt, publicKey).then(([payload, _header]) => { + decrypted = JSON.stringify(payload); + }); + + return decrypted; + } + const decrypted = crypto.publicDecrypt(Buffer.from(publicKey, 'base64').toString('utf-8'), Buffer.from(encrypted, 'base64')); return decrypted.toString('utf-8'); diff --git a/ee/packages/license/src/license.ts b/ee/packages/license/src/license.ts index 64f75eb501ee..12767271a6bc 100644 --- a/ee/packages/license/src/license.ts +++ b/ee/packages/license/src/license.ts @@ -128,8 +128,9 @@ export const setLicense = async (encryptedLicense: string, forceSet = false): Pr logger.debug({ msg: 'license', decrypted }); } - // #TODO: Check license version and call setLicenseV2 or setLicenseV3 - await setLicenseV2(JSON.parse(decrypted), encryptedLicense); + encryptedLicense.startsWith('RCV3_') + ? await setLicenseV3(JSON.parse(decrypted), encryptedLicense) + : await setLicenseV2(JSON.parse(decrypted), encryptedLicense); return true; } catch (e) { diff --git a/packages/jwt/tsconfig.json b/packages/jwt/tsconfig.json index a132d2e280b6..52e9dd8c4976 100644 --- a/packages/jwt/tsconfig.json +++ b/packages/jwt/tsconfig.json @@ -1,6 +1,7 @@ { "extends": "../../tsconfig.base.server.json", "compilerOptions": { + "declaration": true, "rootDir": "./src", "outDir": "./dist" }, diff --git a/yarn.lock b/yarn.lock index e41890272ce7..c3b2c6157d97 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8395,6 +8395,7 @@ __metadata: resolution: "@rocket.chat/license@workspace:ee/packages/license" dependencies: "@rocket.chat/core-typings": "workspace:^" + "@rocket.chat/jwt": "workspace:^" "@rocket.chat/logger": "workspace:^" "@rocket.chat/models": "workspace:^" "@types/jest": ~29.5.3 From 284f12e2c45059fd0fbb5ad7eb43a9e1e57b2aa4 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Thu, 21 Sep 2023 22:27:12 -0300 Subject: [PATCH 19/38] jwt package --- ee/apps/account-service/Dockerfile | 3 +++ ee/apps/authorization-service/Dockerfile | 3 +++ ee/apps/ddp-streamer/Dockerfile | 6 +++--- ee/apps/omnichannel-transcript/Dockerfile | 6 +++--- ee/apps/presence-service/Dockerfile | 3 +++ ee/apps/queue-worker/Dockerfile | 6 +++--- ee/apps/stream-hub-service/Dockerfile | 6 +++--- 7 files changed, 21 insertions(+), 12 deletions(-) diff --git a/ee/apps/account-service/Dockerfile b/ee/apps/account-service/Dockerfile index f2aacafcdf48..dbd8717e8716 100644 --- a/ee/apps/account-service/Dockerfile +++ b/ee/apps/account-service/Dockerfile @@ -19,6 +19,9 @@ COPY ./packages/rest-typings/dist packages/rest-typings/dist COPY ./packages/model-typings/package.json packages/model-typings/package.json COPY ./packages/model-typings/dist packages/model-typings/dist +COPY ./packages/jwt/package.json packages/jwt/package.json +COPY ./packages/jwt/dist packages/jwt/dist + COPY ./packages/models/package.json packages/models/package.json COPY ./packages/models/dist packages/models/dist diff --git a/ee/apps/authorization-service/Dockerfile b/ee/apps/authorization-service/Dockerfile index f2aacafcdf48..dbd8717e8716 100644 --- a/ee/apps/authorization-service/Dockerfile +++ b/ee/apps/authorization-service/Dockerfile @@ -19,6 +19,9 @@ COPY ./packages/rest-typings/dist packages/rest-typings/dist COPY ./packages/model-typings/package.json packages/model-typings/package.json COPY ./packages/model-typings/dist packages/model-typings/dist +COPY ./packages/jwt/package.json packages/jwt/package.json +COPY ./packages/jwt/dist packages/jwt/dist + COPY ./packages/models/package.json packages/models/package.json COPY ./packages/models/dist packages/models/dist diff --git a/ee/apps/ddp-streamer/Dockerfile b/ee/apps/ddp-streamer/Dockerfile index 51a4b0c6d4f6..9386aac4f21e 100644 --- a/ee/apps/ddp-streamer/Dockerfile +++ b/ee/apps/ddp-streamer/Dockerfile @@ -25,6 +25,9 @@ COPY ./packages/ui-contexts/dist packages/ui-contexts/dist COPY ./packages/model-typings/package.json packages/model-typings/package.json COPY ./packages/model-typings/dist packages/model-typings/dist +COPY ./packages/jwt/package.json packages/jwt/package.json +COPY ./packages/jwt/dist packages/jwt/dist + COPY ./packages/models/package.json packages/models/package.json COPY ./packages/models/dist packages/models/dist @@ -37,9 +40,6 @@ COPY ./ee/packages/license/dist packages/license/dist COPY ./packages/instance-status/package.json packages/instance-status/package.json COPY ./packages/instance-status/dist packages/instance-status/dist -COPY ./packages/logger/package.json packages/logger/package.json -COPY ./packages/logger/dist packages/logger/dist - COPY ./ee/apps/${SERVICE}/dist . COPY ./package.json . diff --git a/ee/apps/omnichannel-transcript/Dockerfile b/ee/apps/omnichannel-transcript/Dockerfile index 1eb92cde30b1..e6a1aa00fc88 100644 --- a/ee/apps/omnichannel-transcript/Dockerfile +++ b/ee/apps/omnichannel-transcript/Dockerfile @@ -19,6 +19,9 @@ COPY ./packages/rest-typings/dist packages/rest-typings/dist COPY ./packages/model-typings/package.json packages/model-typings/package.json COPY ./packages/model-typings/dist packages/model-typings/dist +COPY ./packages/jwt/package.json packages/jwt/package.json +COPY ./packages/jwt/dist packages/jwt/dist + COPY ./packages/models/package.json packages/models/package.json COPY ./packages/models/dist packages/models/dist @@ -37,9 +40,6 @@ COPY ./ee/packages/pdf-worker/dist ee/packages/pdf-worker/dist COPY ./packages/tools/package.json packages/tools/package.json COPY ./packages/tools/dist packages/tools/dist -COPY ./packages/logger/package.json packages/logger/package.json -COPY ./packages/logger/dist packages/logger/dist - COPY ./ee/apps/${SERVICE}/dist . COPY ./package.json . diff --git a/ee/apps/presence-service/Dockerfile b/ee/apps/presence-service/Dockerfile index 7d9b0f953019..aabf78295b8f 100644 --- a/ee/apps/presence-service/Dockerfile +++ b/ee/apps/presence-service/Dockerfile @@ -22,6 +22,9 @@ COPY ./packages/rest-typings/dist packages/rest-typings/dist COPY ./packages/model-typings/package.json packages/model-typings/package.json COPY ./packages/model-typings/dist packages/model-typings/dist +COPY ./packages/jwt/package.json packages/jwt/package.json +COPY ./packages/jwt/dist packages/jwt/dist + COPY ./packages/models/package.json packages/models/package.json COPY ./packages/models/dist packages/models/dist diff --git a/ee/apps/queue-worker/Dockerfile b/ee/apps/queue-worker/Dockerfile index 1eb92cde30b1..e6a1aa00fc88 100644 --- a/ee/apps/queue-worker/Dockerfile +++ b/ee/apps/queue-worker/Dockerfile @@ -19,6 +19,9 @@ COPY ./packages/rest-typings/dist packages/rest-typings/dist COPY ./packages/model-typings/package.json packages/model-typings/package.json COPY ./packages/model-typings/dist packages/model-typings/dist +COPY ./packages/jwt/package.json packages/jwt/package.json +COPY ./packages/jwt/dist packages/jwt/dist + COPY ./packages/models/package.json packages/models/package.json COPY ./packages/models/dist packages/models/dist @@ -37,9 +40,6 @@ COPY ./ee/packages/pdf-worker/dist ee/packages/pdf-worker/dist COPY ./packages/tools/package.json packages/tools/package.json COPY ./packages/tools/dist packages/tools/dist -COPY ./packages/logger/package.json packages/logger/package.json -COPY ./packages/logger/dist packages/logger/dist - COPY ./ee/apps/${SERVICE}/dist . COPY ./package.json . diff --git a/ee/apps/stream-hub-service/Dockerfile b/ee/apps/stream-hub-service/Dockerfile index 14ed6b9aa4b2..dbd8717e8716 100644 --- a/ee/apps/stream-hub-service/Dockerfile +++ b/ee/apps/stream-hub-service/Dockerfile @@ -19,6 +19,9 @@ COPY ./packages/rest-typings/dist packages/rest-typings/dist COPY ./packages/model-typings/package.json packages/model-typings/package.json COPY ./packages/model-typings/dist packages/model-typings/dist +COPY ./packages/jwt/package.json packages/jwt/package.json +COPY ./packages/jwt/dist packages/jwt/dist + COPY ./packages/models/package.json packages/models/package.json COPY ./packages/models/dist packages/models/dist @@ -28,9 +31,6 @@ COPY ./packages/logger/dist packages/logger/dist COPY ./ee/packages/license/package.json packages/license/package.json COPY ./ee/packages/license/dist packages/license/dist -COPY ./packages/logger/package.json packages/logger/package.json -COPY ./packages/logger/dist packages/logger/dist - COPY ./ee/apps/${SERVICE}/dist . COPY ./package.json . From ed4543269b8d44c908a7c14301ff4aa09792b54d Mon Sep 17 00:00:00 2001 From: Pierre Lehnen Date: Fri, 22 Sep 2023 11:22:29 -0300 Subject: [PATCH 20/38] renamed isEnterprise to hasValidLicense and fixed some invalid imports that came with the develop rebase --- apps/meteor/app/api/server/v1/federation.ts | 4 ++-- .../client/views/hooks/useUpgradeTabParams.ts | 4 ++-- .../ee/app/api-enterprise/server/index.ts | 4 ++-- .../authorization/server/validateUserRoles.js | 8 ++++---- .../ee/app/canned-responses/server/index.ts | 4 ++-- .../ee/app/license/server/canEnableApp.ts | 6 +++--- .../ee/app/license/server/getStatistics.ts | 8 ++++---- .../ee/app/license/server/lib/getAppCount.ts | 4 ++-- .../license/server/license.internalService.ts | 20 +++++++++---------- apps/meteor/ee/app/license/server/methods.ts | 12 +++++------ apps/meteor/ee/app/license/server/settings.ts | 6 +++--- apps/meteor/ee/app/license/server/startup.ts | 10 +++++----- .../server/business-hour/Helper.ts | 4 ++-- .../app/livechat-enterprise/server/index.ts | 4 ++-- .../server/lib/LivechatEnterprise.ts | 6 +++--- .../app/message-read-receipt/server/index.ts | 4 ++-- .../meteor/ee/app/settings/server/settings.ts | 8 ++++---- .../server/services/voipService.ts | 4 ++-- .../ee/client/hooks/useHasLicenseModule.ts | 4 ++-- apps/meteor/ee/client/lib/onToggledFeature.ts | 4 ++-- apps/meteor/ee/server/api/api.ts | 4 ++-- apps/meteor/ee/server/api/chat.ts | 4 ++-- apps/meteor/ee/server/api/licenses.ts | 10 +++++----- apps/meteor/ee/server/api/roles.ts | 6 +++--- apps/meteor/ee/server/api/sessions.ts | 14 ++++++------- .../endpoints/appsCountHandler.ts | 4 ++-- .../ee/server/apps/communication/rest.ts | 4 ++-- apps/meteor/ee/server/configuration/ldap.ts | 4 ++-- apps/meteor/ee/server/configuration/oauth.ts | 4 ++-- .../server/configuration/outlookCalendar.ts | 4 ++-- apps/meteor/ee/server/configuration/saml.ts | 6 +++--- .../server/configuration/videoConference.ts | 4 ++-- apps/meteor/ee/server/lib/syncUserRoles.ts | 4 ++-- .../server/local-services/instance/service.ts | 2 +- .../ee/server/methods/getReadReceipts.ts | 4 ++-- apps/meteor/ee/server/models/startup.ts | 4 ++-- .../ee/server/startup/apps/trialExpiration.ts | 4 ++-- apps/meteor/ee/server/startup/audit.ts | 4 ++-- .../ee/server/startup/deviceManagement.ts | 4 ++-- .../ee/server/startup/engagementDashboard.ts | 4 ++-- .../ee/server/startup/maxRoomsPerGuest.ts | 4 ++-- apps/meteor/ee/server/startup/seatsCap.ts | 12 +++++------ apps/meteor/ee/server/startup/services.ts | 6 +++--- apps/meteor/ee/server/startup/upsell.ts | 6 +++--- ...getInstallationSourceFromAppStorageItem.ts | 4 ++-- .../server/services/authorization/service.ts | 2 +- apps/meteor/server/startup/migrations/v278.ts | 4 ++-- ee/packages/license/src/index.ts | 4 ++-- ee/packages/license/src/license.ts | 6 +++--- .../src/OmnichannelTranscript.ts | 2 +- .../omnichannel-services/src/QueueWorker.ts | 2 +- ee/packages/presence/src/Presence.ts | 2 +- packages/core-services/src/types/ILicense.ts | 4 ++-- 53 files changed, 142 insertions(+), 142 deletions(-) diff --git a/apps/meteor/app/api/server/v1/federation.ts b/apps/meteor/app/api/server/v1/federation.ts index 480e826c351b..c28cb5ad0f61 100644 --- a/apps/meteor/app/api/server/v1/federation.ts +++ b/apps/meteor/app/api/server/v1/federation.ts @@ -1,5 +1,5 @@ import { Federation, FederationEE } from '@rocket.chat/core-services'; -import { isEnterprise } from '@rocket.chat/license'; +import * as License from '@rocket.chat/license'; import { isFederationVerifyMatrixIdProps } from '@rocket.chat/rest-typings'; import { API } from '../api'; @@ -14,7 +14,7 @@ API.v1.addRoute( async get() { const { matrixIds } = this.queryParams; - const federationService = isEnterprise() ? FederationEE : Federation; + const federationService = License.hasValidLicense() ? FederationEE : Federation; const results = await federationService.verifyMatrixIds(matrixIds); diff --git a/apps/meteor/client/views/hooks/useUpgradeTabParams.ts b/apps/meteor/client/views/hooks/useUpgradeTabParams.ts index 50188a5d4e4f..072d9712e038 100644 --- a/apps/meteor/client/views/hooks/useUpgradeTabParams.ts +++ b/apps/meteor/client/views/hooks/useUpgradeTabParams.ts @@ -1,4 +1,4 @@ -import type { ILicenseV2, ILicenseV3 } from '@rocket.chat/license'; +import type * as License from '@rocket.chat/license'; import { useSetting } from '@rocket.chat/ui-contexts'; import { format } from 'date-fns'; @@ -17,7 +17,7 @@ export const useUpgradeTabParams = (): { tabType: UpgradeTabVariant | false; tri const hasValidLicense = licensesData?.licenses.some((license) => license.modules.length > 0) ?? false; const hadExpiredTrials = cloudWorkspaceHadTrial ?? false; - const licenses = (licensesData?.licenses || []) as (Partial & { modules: string[] })[]; + const licenses = (licensesData?.licenses || []) as (Partial & { modules: string[] })[]; const trialLicense = licenses.find(({ meta, information }) => information?.trial ?? meta?.trial); const isTrial = Boolean(trialLicense); diff --git a/apps/meteor/ee/app/api-enterprise/server/index.ts b/apps/meteor/ee/app/api-enterprise/server/index.ts index 5c28424bbb3f..2efa48e7e2c8 100644 --- a/apps/meteor/ee/app/api-enterprise/server/index.ts +++ b/apps/meteor/ee/app/api-enterprise/server/index.ts @@ -1,5 +1,5 @@ -import { onLicense } from '@rocket.chat/license'; +import * as License from '@rocket.chat/license'; -await onLicense('canned-responses', async () => { +await License.onLicense('canned-responses', async () => { await import('./canned-responses'); }); diff --git a/apps/meteor/ee/app/authorization/server/validateUserRoles.js b/apps/meteor/ee/app/authorization/server/validateUserRoles.js index 1cea4b20824a..f13ff55ce726 100644 --- a/apps/meteor/ee/app/authorization/server/validateUserRoles.js +++ b/apps/meteor/ee/app/authorization/server/validateUserRoles.js @@ -1,11 +1,11 @@ -import { isEnterprise, preventNewGuests, preventNewUsers } from '@rocket.chat/license'; +import * as License from '@rocket.chat/license'; import { Users } from '@rocket.chat/models'; import { Meteor } from 'meteor/meteor'; import { i18n } from '../../../../server/lib/i18n'; export const validateUserRoles = async function (userId, userData) { - if (!isEnterprise()) { + if (!License.hasValidLicense()) { return; } @@ -22,7 +22,7 @@ export const validateUserRoles = async function (userId, userData) { return; } - if (await preventNewGuests()) { + if (await License.preventNewGuests()) { throw new Meteor.Error('error-max-guests-number-reached', 'Maximum number of guests reached.', { method: 'insertOrUpdateUser', field: 'Assign_role', @@ -36,7 +36,7 @@ export const validateUserRoles = async function (userId, userData) { return; } - if (await preventNewUsers()) { + if (await License.preventNewUsers()) { throw new Meteor.Error('error-license-user-limit-reached', i18n.t('error-license-user-limit-reached')); } }; diff --git a/apps/meteor/ee/app/canned-responses/server/index.ts b/apps/meteor/ee/app/canned-responses/server/index.ts index 9e91153af2e7..5976689f0b20 100644 --- a/apps/meteor/ee/app/canned-responses/server/index.ts +++ b/apps/meteor/ee/app/canned-responses/server/index.ts @@ -1,6 +1,6 @@ -import { onLicense } from '@rocket.chat/license'; +import * as License from '@rocket.chat/license'; -await onLicense('canned-responses', async () => { +await License.onLicense('canned-responses', async () => { const { createSettings } = await import('./settings'); await import('./permissions'); await import('./hooks/onRemoveAgentDepartment'); diff --git a/apps/meteor/ee/app/license/server/canEnableApp.ts b/apps/meteor/ee/app/license/server/canEnableApp.ts index cb32600fd66d..b49f69d4bc08 100644 --- a/apps/meteor/ee/app/license/server/canEnableApp.ts +++ b/apps/meteor/ee/app/license/server/canEnableApp.ts @@ -1,6 +1,6 @@ import type { IAppStorageItem } from '@rocket.chat/apps-engine/server/storage'; import { Apps } from '@rocket.chat/core-services'; -import { preventNewPrivateApps, preventNewMarketplaceApps } from '@rocket.chat/license'; +import * as License from '@rocket.chat/license'; import { getInstallationSourceFromAppStorageItem } from '../../../../lib/apps/getInstallationSourceFromAppStorageItem'; @@ -18,8 +18,8 @@ export const canEnableApp = async (app: IAppStorageItem): Promise => { const source = getInstallationSourceFromAppStorageItem(app); switch (source) { case 'private': - return !(await preventNewPrivateApps()); + return !(await License.preventNewPrivateApps()); default: - return !(await preventNewMarketplaceApps()); + return !(await License.preventNewMarketplaceApps()); } }; diff --git a/apps/meteor/ee/app/license/server/getStatistics.ts b/apps/meteor/ee/app/license/server/getStatistics.ts index f0ff6b6562d0..66b05167a8fb 100644 --- a/apps/meteor/ee/app/license/server/getStatistics.ts +++ b/apps/meteor/ee/app/license/server/getStatistics.ts @@ -1,7 +1,7 @@ import { log } from 'console'; import { Analytics } from '@rocket.chat/core-services'; -import { getModules, getTags, hasModule } from '@rocket.chat/license'; +import * as License from '@rocket.chat/license'; import { CannedResponse, OmnichannelServiceLevelAgreements, LivechatRooms, LivechatTag, LivechatUnit, Users } from '@rocket.chat/models'; type ENTERPRISE_STATISTICS = GenericStats & Partial; @@ -27,8 +27,8 @@ type EEOnlyStats = { export async function getStatistics(): Promise { const genericStats: GenericStats = { - modules: getModules(), - tags: getTags().map(({ name }) => name), + modules: License.getModules(), + tags: License.getTags().map(({ name }) => name), seatRequests: await Analytics.getSeatRequestCount(), }; @@ -44,7 +44,7 @@ export async function getStatistics(): Promise { // These models are only available on EE license so don't import them inside CE license as it will break the build async function getEEStatistics(): Promise { - if (!hasModule('livechat-enterprise')) { + if (!License.hasModule('livechat-enterprise')) { return; } diff --git a/apps/meteor/ee/app/license/server/lib/getAppCount.ts b/apps/meteor/ee/app/license/server/lib/getAppCount.ts index a05813f596bb..9a87cb78a481 100644 --- a/apps/meteor/ee/app/license/server/lib/getAppCount.ts +++ b/apps/meteor/ee/app/license/server/lib/getAppCount.ts @@ -1,9 +1,9 @@ import { Apps } from '@rocket.chat/core-services'; -import type { LicenseAppSources } from '@rocket.chat/license'; +import type * as License from '@rocket.chat/license'; import { getInstallationSourceFromAppStorageItem } from '../../../../../lib/apps/getInstallationSourceFromAppStorageItem'; -export async function getAppCount(source: LicenseAppSources): Promise { +export async function getAppCount(source: License.LicenseAppSources): Promise { if (!(await Apps.isInitialized())) { return 0; } diff --git a/apps/meteor/ee/app/license/server/license.internalService.ts b/apps/meteor/ee/app/license/server/license.internalService.ts index 354c52aa865c..b3e14fa7502c 100644 --- a/apps/meteor/ee/app/license/server/license.internalService.ts +++ b/apps/meteor/ee/app/license/server/license.internalService.ts @@ -1,6 +1,6 @@ import type { ILicense } from '@rocket.chat/core-services'; import { api, ServiceClassInternal } from '@rocket.chat/core-services'; -import { getModules, hasModule, isEnterprise, onModule, onValidateLicense, type LicenseModule } from '@rocket.chat/license'; +import * as License from '@rocket.chat/license'; import { guestPermissions } from '../../authorization/lib/guestPermissions'; import { resetEnterprisePermissions } from '../../authorization/server/resetEnterprisePermissions'; @@ -11,8 +11,8 @@ export class LicenseService extends ServiceClassInternal implements ILicense { constructor() { super(); - onValidateLicense((): void => { - if (!isEnterprise()) { + License.onValidateLicense((): void => { + if (!License.hasValidLicense()) { return; } @@ -20,13 +20,13 @@ export class LicenseService extends ServiceClassInternal implements ILicense { void resetEnterprisePermissions(); }); - onModule((licenseModule) => { + License.onModule((licenseModule) => { void api.broadcast('license.module', licenseModule); }); } async started(): Promise { - if (!isEnterprise()) { + if (!License.hasValidLicense()) { return; } @@ -34,16 +34,16 @@ export class LicenseService extends ServiceClassInternal implements ILicense { await resetEnterprisePermissions(); } - hasLicense(feature: LicenseModule): boolean { - return hasModule(feature); + hasModule(feature: License.LicenseModule): boolean { + return License.hasModule(feature); } - isEnterprise(): boolean { - return isEnterprise(); + hasValidLicense(): boolean { + return License.hasValidLicense(); } getModules(): string[] { - return getModules(); + return License.getModules(); } getGuestPermissions(): string[] { diff --git a/apps/meteor/ee/app/license/server/methods.ts b/apps/meteor/ee/app/license/server/methods.ts index 194218df0213..d5f82536bf37 100644 --- a/apps/meteor/ee/app/license/server/methods.ts +++ b/apps/meteor/ee/app/license/server/methods.ts @@ -1,4 +1,4 @@ -import { getModules, getTags, hasModule, isEnterprise, type ILicenseTag, type LicenseModule } from '@rocket.chat/license'; +import * as License from '@rocket.chat/license'; import type { ServerMethods } from '@rocket.chat/ui-contexts'; import { check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; @@ -8,7 +8,7 @@ declare module '@rocket.chat/ui-contexts' { interface ServerMethods { 'license:hasLicense'(feature: string): boolean; 'license:getModules'(): string[]; - 'license:getTags'(): ILicenseTag[]; + 'license:getTags'(): License.ILicenseTag[]; 'license:isEnterprise'(): boolean; } } @@ -17,15 +17,15 @@ Meteor.methods({ 'license:hasLicense'(feature: string) { check(feature, String); - return hasModule(feature as LicenseModule); + return License.hasModule(feature as License.LicenseModule); }, 'license:getModules'() { - return getModules(); + return License.getModules(); }, 'license:getTags'() { - return getTags(); + return License.getTags(); }, 'license:isEnterprise'() { - return isEnterprise(); + return License.hasValidLicense(); }, }); diff --git a/apps/meteor/ee/app/license/server/settings.ts b/apps/meteor/ee/app/license/server/settings.ts index 751dca92a716..9b328ecb2031 100644 --- a/apps/meteor/ee/app/license/server/settings.ts +++ b/apps/meteor/ee/app/license/server/settings.ts @@ -1,4 +1,4 @@ -import { setLicense } from '@rocket.chat/license'; +import * as License from '@rocket.chat/license'; import { Settings } from '@rocket.chat/models'; import { Meteor } from 'meteor/meteor'; @@ -29,7 +29,7 @@ settings.watch('Enterprise_License', async (license) => { return; } - if (!(await setLicense(license))) { + if (!(await License.setLicense(license))) { await Settings.updateValueById('Enterprise_License_Status', 'Invalid'); return; } @@ -38,7 +38,7 @@ settings.watch('Enterprise_License', async (license) => { }); if (process.env.ROCKETCHAT_LICENSE) { - await setLicense(process.env.ROCKETCHAT_LICENSE); + await License.setLicense(process.env.ROCKETCHAT_LICENSE); Meteor.startup(async () => { if (settings.get('Enterprise_License')) { diff --git a/apps/meteor/ee/app/license/server/startup.ts b/apps/meteor/ee/app/license/server/startup.ts index 64444e5e88f3..5c72051e8130 100644 --- a/apps/meteor/ee/app/license/server/startup.ts +++ b/apps/meteor/ee/app/license/server/startup.ts @@ -1,4 +1,4 @@ -import { setLicense, setWorkspaceUrl, setLicenseLimitCounter } from '@rocket.chat/license'; +import * as License from '@rocket.chat/license'; import { settings } from '../../../../app/settings/server'; import { callbacks } from '../../../../lib/callbacks'; @@ -6,13 +6,13 @@ import { getAppCount } from './lib/getAppCount'; settings.watch('Site_Url', (value) => { if (value) { - void setWorkspaceUrl(value); + void License.setWorkspaceUrl(value); } }); callbacks.add('workspaceLicenseChanged', async (updatedLicense) => { - await setLicense(updatedLicense); + await License.setLicense(updatedLicense); }); -setLicenseLimitCounter('privateApps', () => getAppCount('private')); -setLicenseLimitCounter('marketplaceApps', () => getAppCount('marketplace')); +License.setLicenseLimitCounter('privateApps', () => getAppCount('private')); +License.setLicenseLimitCounter('marketplaceApps', () => getAppCount('marketplace')); diff --git a/apps/meteor/ee/app/livechat-enterprise/server/business-hour/Helper.ts b/apps/meteor/ee/app/livechat-enterprise/server/business-hour/Helper.ts index 23617fd9ba8f..6ef0364e4eed 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/business-hour/Helper.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/business-hour/Helper.ts @@ -1,6 +1,6 @@ import type { ILivechatBusinessHour } from '@rocket.chat/core-typings'; import { LivechatBusinessHourTypes } from '@rocket.chat/core-typings'; -import { isEnterprise } from '@rocket.chat/license'; +import * as License from '@rocket.chat/license'; import { LivechatBusinessHours, LivechatDepartment, LivechatDepartmentAgents, Users } from '@rocket.chat/models'; import moment from 'moment-timezone'; @@ -105,7 +105,7 @@ export const removeBusinessHourByAgentIds = async (agentIds: string[], businessH }; export const resetDefaultBusinessHourIfNeeded = async (): Promise => { - if (isEnterprise()) { + if (License.hasValidLicense()) { return; } diff --git a/apps/meteor/ee/app/livechat-enterprise/server/index.ts b/apps/meteor/ee/app/livechat-enterprise/server/index.ts index 553f673bc11f..d80a112a9079 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/index.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/index.ts @@ -1,4 +1,4 @@ -import { onLicense } from '@rocket.chat/license'; +import * as License from '@rocket.chat/license'; import { Meteor } from 'meteor/meteor'; import './methods/addMonitor'; @@ -29,7 +29,7 @@ import './lib/AutoCloseOnHoldScheduler'; import './business-hour'; import { createDefaultPriorities } from './priorities'; -await onLicense('livechat-enterprise', async () => { +await License.onLicense('livechat-enterprise', async () => { require('./api'); require('./hooks'); await import('./startup'); diff --git a/apps/meteor/ee/app/livechat-enterprise/server/lib/LivechatEnterprise.ts b/apps/meteor/ee/app/livechat-enterprise/server/lib/LivechatEnterprise.ts index 805377839756..b4de90489fc7 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/lib/LivechatEnterprise.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/lib/LivechatEnterprise.ts @@ -1,5 +1,5 @@ import type { IOmnichannelBusinessUnit, IOmnichannelServiceLevelAgreements, LivechatDepartmentDTO } from '@rocket.chat/core-typings'; -import { hasModule } from '@rocket.chat/license'; +import * as License from '@rocket.chat/license'; import { Users, LivechatDepartment as LivechatDepartmentRaw, @@ -195,7 +195,7 @@ export const LivechatEnterprise = { const department = _id ? await LivechatDepartmentRaw.findOneById(_id, { projection: { _id: 1, archived: 1, enabled: 1 } }) : null; - if (!hasModule('livechat-enterprise')) { + if (!License.hasModule('livechat-enterprise')) { const totalDepartments = await LivechatDepartmentRaw.countTotal(); if (!department && totalDepartments >= 1) { throw new Meteor.Error('error-max-departments-number-reached', 'Maximum number of departments reached', { @@ -279,6 +279,6 @@ export const LivechatEnterprise = { }, async isDepartmentCreationAvailable() { - return hasModule('livechat-enterprise') || (await LivechatDepartmentRaw.countTotal()) === 0; + return License.hasModule('livechat-enterprise') || (await LivechatDepartmentRaw.countTotal()) === 0; }, }; diff --git a/apps/meteor/ee/app/message-read-receipt/server/index.ts b/apps/meteor/ee/app/message-read-receipt/server/index.ts index cf1e51b1eb44..24e4057f1eb7 100644 --- a/apps/meteor/ee/app/message-read-receipt/server/index.ts +++ b/apps/meteor/ee/app/message-read-receipt/server/index.ts @@ -1,5 +1,5 @@ -import { onLicense } from '@rocket.chat/license'; +import * as License from '@rocket.chat/license'; -await onLicense('message-read-receipt', async () => { +await License.onLicense('message-read-receipt', async () => { await import('./hooks'); }); diff --git a/apps/meteor/ee/app/settings/server/settings.ts b/apps/meteor/ee/app/settings/server/settings.ts index 978e1161b756..4c90420bd50d 100644 --- a/apps/meteor/ee/app/settings/server/settings.ts +++ b/apps/meteor/ee/app/settings/server/settings.ts @@ -1,5 +1,5 @@ import type { ISetting, SettingValue } from '@rocket.chat/core-typings'; -import { isEnterprise, hasModule, onValidateLicense, type LicenseModule } from '@rocket.chat/license'; +import * as License from '@rocket.chat/license'; import { Settings } from '@rocket.chat/models'; import { Meteor } from 'meteor/meteor'; @@ -11,7 +11,7 @@ export function changeSettingValue(record: ISetting): SettingValue { return record.value; } - if (!isEnterprise()) { + if (!License.hasValidLicense()) { return record.invalidValue; } @@ -20,7 +20,7 @@ export function changeSettingValue(record: ISetting): SettingValue { } for (const moduleName of record.modules) { - if (!hasModule(moduleName as LicenseModule)) { + if (!License.hasModule(moduleName as License.LicenseModule)) { return record.invalidValue; } } @@ -58,5 +58,5 @@ async function updateSettings(): Promise { Meteor.startup(async () => { await updateSettings(); - onValidateLicense(updateSettings); + License.onValidateLicense(updateSettings); }); diff --git a/apps/meteor/ee/app/voip-enterprise/server/services/voipService.ts b/apps/meteor/ee/app/voip-enterprise/server/services/voipService.ts index 38f3cf3541ce..d083fe882ef6 100644 --- a/apps/meteor/ee/app/voip-enterprise/server/services/voipService.ts +++ b/apps/meteor/ee/app/voip-enterprise/server/services/voipService.ts @@ -1,11 +1,11 @@ import type { ILivechatAgent, ILivechatVisitor, IVoipRoomClosingInfo, IUser, IVoipRoom } from '@rocket.chat/core-typings'; -import { overwriteClassOnLicense } from '@rocket.chat/license'; +import * as License from '@rocket.chat/license'; import type { IOmniRoomClosingMessage } from '../../../../../server/services/omnichannel-voip/internalTypes'; import { OmnichannelVoipService } from '../../../../../server/services/omnichannel-voip/service'; import { calculateOnHoldTimeForRoom } from '../lib/calculateOnHoldTimeForRoom'; -await overwriteClassOnLicense('voip-enterprise', OmnichannelVoipService, { +await License.overwriteClassOnLicense('voip-enterprise', OmnichannelVoipService, { async getRoomClosingData( _originalFn: ( closer: ILivechatVisitor | ILivechatAgent, diff --git a/apps/meteor/ee/client/hooks/useHasLicenseModule.ts b/apps/meteor/ee/client/hooks/useHasLicenseModule.ts index c7d76b093c3b..012324ba9704 100644 --- a/apps/meteor/ee/client/hooks/useHasLicenseModule.ts +++ b/apps/meteor/ee/client/hooks/useHasLicenseModule.ts @@ -1,8 +1,8 @@ -import type { LicenseModule } from '@rocket.chat/license'; +import type * as License from '@rocket.chat/license'; import { useMethod, useUserId } from '@rocket.chat/ui-contexts'; import { useQuery } from '@tanstack/react-query'; -export const useHasLicenseModule = (licenseName: LicenseModule): 'loading' | boolean => { +export const useHasLicenseModule = (licenseName: License.LicenseModule): 'loading' | boolean => { const method = useMethod('license:getModules'); const uid = useUserId(); diff --git a/apps/meteor/ee/client/lib/onToggledFeature.ts b/apps/meteor/ee/client/lib/onToggledFeature.ts index ae2e4ad9f4a8..4319f2b268f8 100644 --- a/apps/meteor/ee/client/lib/onToggledFeature.ts +++ b/apps/meteor/ee/client/lib/onToggledFeature.ts @@ -1,11 +1,11 @@ -import type { LicenseModule } from '@rocket.chat/license'; +import type * as License from '@rocket.chat/license'; import { QueryObserver } from '@tanstack/react-query'; import { queryClient } from '../../../client/lib/queryClient'; import { fetchFeatures } from './fetchFeatures'; export const onToggledFeature = ( - feature: LicenseModule, + feature: License.LicenseModule, { up, down, diff --git a/apps/meteor/ee/server/api/api.ts b/apps/meteor/ee/server/api/api.ts index bb3a73bfc8f4..61e62440e1c7 100644 --- a/apps/meteor/ee/server/api/api.ts +++ b/apps/meteor/ee/server/api/api.ts @@ -1,4 +1,4 @@ -import { isEnterprise } from '@rocket.chat/license'; +import * as License from '@rocket.chat/license'; import { API } from '../../../app/api/server/api'; import type { NonEnterpriseTwoFactorOptions, Options } from '../../../app/api/server/definition'; @@ -11,7 +11,7 @@ const isNonEnterpriseTwoFactorOptions = (options?: Options): options is NonEnter !!options && 'forceTwoFactorAuthenticationForNonEnterprise' in options && Boolean(options.forceTwoFactorAuthenticationForNonEnterprise); API.v1.processTwoFactor = use(API.v1.processTwoFactor, ([params, ...context], next) => { - if (isNonEnterpriseTwoFactorOptions(params.options) && !isEnterprise()) { + if (isNonEnterpriseTwoFactorOptions(params.options) && !License.hasValidLicense()) { const options: NonEnterpriseTwoFactorOptions = { ...params.options, twoFactorOptions: { diff --git a/apps/meteor/ee/server/api/chat.ts b/apps/meteor/ee/server/api/chat.ts index 9d20c19cfbe9..eb63ee4e1ce1 100644 --- a/apps/meteor/ee/server/api/chat.ts +++ b/apps/meteor/ee/server/api/chat.ts @@ -1,5 +1,5 @@ import type { IMessage, ReadReceipt } from '@rocket.chat/core-typings'; -import { hasModule } from '@rocket.chat/license'; +import * as License from '@rocket.chat/license'; import { Meteor } from 'meteor/meteor'; import { API } from '../../../app/api/server/api'; @@ -24,7 +24,7 @@ API.v1.addRoute( { authRequired: true }, { async get() { - if (!hasModule('message-read-receipt')) { + if (!License.hasModule('message-read-receipt')) { throw new Meteor.Error('error-action-not-allowed', 'This is an enterprise feature'); } diff --git a/apps/meteor/ee/server/api/licenses.ts b/apps/meteor/ee/server/api/licenses.ts index 0e41bdcafc68..acdbabcbddd8 100644 --- a/apps/meteor/ee/server/api/licenses.ts +++ b/apps/meteor/ee/server/api/licenses.ts @@ -1,4 +1,4 @@ -import { getUnmodifiedLicenseAndModules, validateFormat, getMaxActiveUsers, isEnterprise } from '@rocket.chat/license'; +import * as License from '@rocket.chat/license'; import { Settings, Users } from '@rocket.chat/models'; import { check } from 'meteor/check'; @@ -14,7 +14,7 @@ API.v1.addRoute( return API.v1.unauthorized(); } - const license = getUnmodifiedLicenseAndModules(); + const license = License.getUnmodifiedLicenseAndModules(); const licenses = license ? [license] : []; return API.v1.success({ licenses }); @@ -36,7 +36,7 @@ API.v1.addRoute( } const { license } = this.bodyParams; - if (!validateFormat(license)) { + if (!License.validateFormat(license)) { return API.v1.failure('Invalid license'); } @@ -52,7 +52,7 @@ API.v1.addRoute( { authRequired: true }, { async get() { - const maxActiveUsers = getMaxActiveUsers() || null; + const maxActiveUsers = License.getMaxActiveUsers() || null; const activeUsers = await Users.getActiveLocalUserCount(); return API.v1.success({ maxActiveUsers, activeUsers }); @@ -65,7 +65,7 @@ API.v1.addRoute( { authOrAnonRequired: true }, { get() { - const isEnterpriseEdtion = isEnterprise(); + const isEnterpriseEdtion = License.hasValidLicense(); return API.v1.success({ isEnterprise: isEnterpriseEdtion }); }, }, diff --git a/apps/meteor/ee/server/api/roles.ts b/apps/meteor/ee/server/api/roles.ts index 1e0b11be1d8c..eb8f762d58c8 100644 --- a/apps/meteor/ee/server/api/roles.ts +++ b/apps/meteor/ee/server/api/roles.ts @@ -1,5 +1,5 @@ import type { IRole } from '@rocket.chat/core-typings'; -import { isEnterprise } from '@rocket.chat/license'; +import * as License from '@rocket.chat/license'; import { Roles } from '@rocket.chat/models'; import Ajv from 'ajv'; @@ -96,7 +96,7 @@ API.v1.addRoute( { authRequired: true }, { async post() { - if (!isEnterprise()) { + if (!License.hasValidLicense()) { throw new Meteor.Error('error-action-not-allowed', 'This is an enterprise feature'); } @@ -154,7 +154,7 @@ API.v1.addRoute( const role = await Roles.findOne(roleId); - if (!isEnterprise() && !role?.protected) { + if (!License.hasValidLicense() && !role?.protected) { throw new Meteor.Error('error-action-not-allowed', 'This is an enterprise feature'); } diff --git a/apps/meteor/ee/server/api/sessions.ts b/apps/meteor/ee/server/api/sessions.ts index c46296715f39..8330a647f51d 100644 --- a/apps/meteor/ee/server/api/sessions.ts +++ b/apps/meteor/ee/server/api/sessions.ts @@ -1,5 +1,5 @@ import type { IUser, ISession, DeviceManagementSession, DeviceManagementPopulatedSession } from '@rocket.chat/core-typings'; -import { hasModule } from '@rocket.chat/license'; +import * as License from '@rocket.chat/license'; import { Users, Sessions } from '@rocket.chat/models'; import type { PaginatedResult, PaginatedRequest } from '@rocket.chat/rest-typings'; import { escapeRegExp } from '@rocket.chat/string-helpers'; @@ -85,7 +85,7 @@ API.v1.addRoute( { authRequired: true, validateParams: isSessionsPaginateProps }, { async get() { - if (!hasModule('device-management')) { + if (!License.hasModule('device-management')) { return API.v1.unauthorized(); } @@ -108,7 +108,7 @@ API.v1.addRoute( { authRequired: true, validateParams: isSessionsProps }, { async get() { - if (!hasModule('device-management')) { + if (!License.hasModule('device-management')) { return API.v1.unauthorized(); } @@ -127,7 +127,7 @@ API.v1.addRoute( { authRequired: true, validateParams: isSessionsProps }, { async post() { - if (!hasModule('device-management')) { + if (!License.hasModule('device-management')) { return API.v1.unauthorized(); } @@ -153,7 +153,7 @@ API.v1.addRoute( { authRequired: true, twoFactorRequired: true, validateParams: isSessionsPaginateProps, permissionsRequired: ['view-device-management'] }, { async get() { - if (!hasModule('device-management')) { + if (!License.hasModule('device-management')) { return API.v1.unauthorized(); } @@ -193,7 +193,7 @@ API.v1.addRoute( { authRequired: true, twoFactorRequired: true, validateParams: isSessionsProps, permissionsRequired: ['view-device-management'] }, { async get() { - if (!hasModule('device-management')) { + if (!License.hasModule('device-management')) { return API.v1.unauthorized(); } @@ -212,7 +212,7 @@ API.v1.addRoute( { authRequired: true, twoFactorRequired: true, validateParams: isSessionsProps, permissionsRequired: ['logout-device-management'] }, { async post() { - if (!hasModule('device-management')) { + if (!License.hasModule('device-management')) { return API.v1.unauthorized(); } diff --git a/apps/meteor/ee/server/apps/communication/endpoints/appsCountHandler.ts b/apps/meteor/ee/server/apps/communication/endpoints/appsCountHandler.ts index 191c5b45f0eb..634d85f73258 100644 --- a/apps/meteor/ee/server/apps/communication/endpoints/appsCountHandler.ts +++ b/apps/meteor/ee/server/apps/communication/endpoints/appsCountHandler.ts @@ -1,5 +1,5 @@ import type { AppManager } from '@rocket.chat/apps-engine/server/AppManager'; -import { getAppsConfig } from '@rocket.chat/license'; +import * as License from '@rocket.chat/license'; import { API } from '../../../../../app/api/server'; import type { SuccessResult } from '../../../../../app/api/server/definition'; @@ -23,7 +23,7 @@ export const appsCountHandler = (apiManager: AppsRestApi) => const manager = apiManager._manager as AppManager; const apps = manager.get({ enabled: true }); - const { maxMarketplaceApps, maxPrivateApps } = getAppsConfig(); + const { maxMarketplaceApps, maxPrivateApps } = License.getAppsConfig(); return API.v1.success({ totalMarketplaceEnabled: apps.filter((app) => getInstallationSourceFromAppStorageItem(app.getStorageItem()) === 'marketplace') diff --git a/apps/meteor/ee/server/apps/communication/rest.ts b/apps/meteor/ee/server/apps/communication/rest.ts index e458527c40d7..a492e1f0abc4 100644 --- a/apps/meteor/ee/server/apps/communication/rest.ts +++ b/apps/meteor/ee/server/apps/communication/rest.ts @@ -3,7 +3,7 @@ import type { IAppInfo } from '@rocket.chat/apps-engine/definition/metadata'; import type { AppManager } from '@rocket.chat/apps-engine/server/AppManager'; import { AppInstallationSource } from '@rocket.chat/apps-engine/server/storage'; import type { IUser, IMessage } from '@rocket.chat/core-typings'; -import { isEnterprise } from '@rocket.chat/license'; +import * as License from '@rocket.chat/license'; import { Settings, Users } from '@rocket.chat/models'; import { serverFetch as fetch } from '@rocket.chat/server-fetch'; import { Meteor } from 'meteor/meteor'; @@ -1150,7 +1150,7 @@ export class AppsRestApi { const storedApp = prl.getStorageItem(); const { installationSource, marketplaceInfo } = storedApp; - if (!isEnterprise() && installationSource === AppInstallationSource.MARKETPLACE) { + if (!License.hasValidLicense() && installationSource === AppInstallationSource.MARKETPLACE) { try { const baseUrl = orchestrator.getMarketplaceUrl() as string; const headers = getDefaultHeaders(); diff --git a/apps/meteor/ee/server/configuration/ldap.ts b/apps/meteor/ee/server/configuration/ldap.ts index 9eea73c78f50..ea74a8e6ffa7 100644 --- a/apps/meteor/ee/server/configuration/ldap.ts +++ b/apps/meteor/ee/server/configuration/ldap.ts @@ -1,6 +1,6 @@ import type { IImportUser, ILDAPEntry, IUser } from '@rocket.chat/core-typings'; import { cronJobs } from '@rocket.chat/cron'; -import { onLicense } from '@rocket.chat/license'; +import * as License from '@rocket.chat/license'; import { Meteor } from 'meteor/meteor'; import { settings } from '../../../app/settings/server'; @@ -12,7 +12,7 @@ import { LDAPEE } from '../sdk'; import { addSettings, ldapIntervalValuesToCronMap } from '../settings/ldap'; Meteor.startup(async () => { - await onLicense('ldap-enterprise', async () => { + await License.onLicense('ldap-enterprise', async () => { await addSettings(); // Configure background sync cronjob diff --git a/apps/meteor/ee/server/configuration/oauth.ts b/apps/meteor/ee/server/configuration/oauth.ts index a273e3cb78d9..c6571dcd7143 100644 --- a/apps/meteor/ee/server/configuration/oauth.ts +++ b/apps/meteor/ee/server/configuration/oauth.ts @@ -1,5 +1,5 @@ import type { IUser } from '@rocket.chat/core-typings'; -import { onLicense } from '@rocket.chat/license'; +import * as License from '@rocket.chat/license'; import { Logger } from '@rocket.chat/logger'; import { Roles } from '@rocket.chat/models'; import { capitalize } from '@rocket.chat/string-helpers'; @@ -54,7 +54,7 @@ function getChannelsMap(channelsMap: string): Record | undefined { } } -await onLicense('oauth-enterprise', () => { +await License.onLicense('oauth-enterprise', () => { callbacks.add('afterProcessOAuthUser', async (auth: IOAuthUserService) => { auth.serviceName = capitalize(auth.serviceName); const settings = getOAuthSettings(auth.serviceName); diff --git a/apps/meteor/ee/server/configuration/outlookCalendar.ts b/apps/meteor/ee/server/configuration/outlookCalendar.ts index 280295a5736f..451a8fadd466 100644 --- a/apps/meteor/ee/server/configuration/outlookCalendar.ts +++ b/apps/meteor/ee/server/configuration/outlookCalendar.ts @@ -1,11 +1,11 @@ import { Calendar } from '@rocket.chat/core-services'; -import { onLicense } from '@rocket.chat/license'; +import * as License from '@rocket.chat/license'; import { Meteor } from 'meteor/meteor'; import { addSettings } from '../settings/outlookCalendar'; Meteor.startup(() => - onLicense('outlook-calendar', async () => { + License.onLicense('outlook-calendar', async () => { addSettings(); await Calendar.setupNextNotification(); diff --git a/apps/meteor/ee/server/configuration/saml.ts b/apps/meteor/ee/server/configuration/saml.ts index a7e82b019978..cbb3350118ff 100644 --- a/apps/meteor/ee/server/configuration/saml.ts +++ b/apps/meteor/ee/server/configuration/saml.ts @@ -1,4 +1,4 @@ -import { onLicense } from '@rocket.chat/license'; +import * as License from '@rocket.chat/license'; import { Roles, Users } from '@rocket.chat/models'; import type { ISAMLUser } from '../../../app/meteor-accounts-saml/server/definition/ISAMLUser'; @@ -7,7 +7,7 @@ import { settings } from '../../../app/settings/server'; import { ensureArray } from '../../../lib/utils/arrayUtils'; import { addSettings } from '../settings/saml'; -await onLicense('saml-enterprise', () => { +await License.onLicense('saml-enterprise', () => { SAMLUtils.events.on('mapUser', async ({ profile, userObject }: { profile: Record; userObject: ISAMLUser }) => { const roleAttributeName = settings.get('SAML_Custom_Default_role_attribute_name') as string; const roleAttributeSync = settings.get('SAML_Custom_Default_role_attribute_sync'); @@ -67,4 +67,4 @@ await onLicense('saml-enterprise', () => { }); // For setting creation we add the listener first because the event is emmited during startup -SAMLUtils.events.on('addSettings', (name: string): void | Promise => onLicense('saml-enterprise', () => addSettings(name))); +SAMLUtils.events.on('addSettings', (name: string): void | Promise => License.onLicense('saml-enterprise', () => addSettings(name))); diff --git a/apps/meteor/ee/server/configuration/videoConference.ts b/apps/meteor/ee/server/configuration/videoConference.ts index 9de6266c8386..9a59ff850e6e 100644 --- a/apps/meteor/ee/server/configuration/videoConference.ts +++ b/apps/meteor/ee/server/configuration/videoConference.ts @@ -1,7 +1,7 @@ import { VideoConf } from '@rocket.chat/core-services'; import type { IRoom, IUser, VideoConference } from '@rocket.chat/core-typings'; import { VideoConferenceStatus } from '@rocket.chat/core-typings'; -import { onLicense } from '@rocket.chat/license'; +import * as License from '@rocket.chat/license'; import { Rooms, Subscriptions } from '@rocket.chat/models'; import { Meteor } from 'meteor/meteor'; @@ -10,7 +10,7 @@ import { videoConfTypes } from '../../../server/lib/videoConfTypes'; import { addSettings } from '../settings/video-conference'; Meteor.startup(async () => { - await onLicense('videoconference-enterprise', async () => { + await License.onLicense('videoconference-enterprise', async () => { await addSettings(); videoConfTypes.registerVideoConferenceType( diff --git a/apps/meteor/ee/server/lib/syncUserRoles.ts b/apps/meteor/ee/server/lib/syncUserRoles.ts index 9b7cf9bb577a..82d0590c30ea 100644 --- a/apps/meteor/ee/server/lib/syncUserRoles.ts +++ b/apps/meteor/ee/server/lib/syncUserRoles.ts @@ -1,6 +1,6 @@ import { api } from '@rocket.chat/core-services'; import type { IUser, IRole, AtLeast } from '@rocket.chat/core-typings'; -import { preventNewUsers } from '@rocket.chat/license'; +import * as License from '@rocket.chat/license'; import { Users } from '@rocket.chat/models'; import { settings } from '../../../app/settings/server'; @@ -72,7 +72,7 @@ export async function syncUserRoles( } const wasGuest = existingRoles.length === 1 && existingRoles[0] === 'guest'; - if (wasGuest && (await preventNewUsers())) { + if (wasGuest && (await License.preventNewUsers())) { throw new Error('error-license-user-limit-reached'); } diff --git a/apps/meteor/ee/server/local-services/instance/service.ts b/apps/meteor/ee/server/local-services/instance/service.ts index 85c021f74769..5bb755ee347f 100644 --- a/apps/meteor/ee/server/local-services/instance/service.ts +++ b/apps/meteor/ee/server/local-services/instance/service.ts @@ -143,7 +143,7 @@ export class InstanceService extends ServiceClassInternal implements IInstanceSe await InstanceStatus.registerInstance('rocket.chat', instance); - const hasLicense = await License.hasLicense('scalability'); + const hasLicense = await License.hasModule('scalability'); if (!hasLicense) { return; } diff --git a/apps/meteor/ee/server/methods/getReadReceipts.ts b/apps/meteor/ee/server/methods/getReadReceipts.ts index a08972ea9198..867333b0145d 100644 --- a/apps/meteor/ee/server/methods/getReadReceipts.ts +++ b/apps/meteor/ee/server/methods/getReadReceipts.ts @@ -1,5 +1,5 @@ import type { ReadReceipt as ReadReceiptType, IMessage } from '@rocket.chat/core-typings'; -import { hasModule } from '@rocket.chat/license'; +import * as License from '@rocket.chat/license'; import { Messages } from '@rocket.chat/models'; import type { ServerMethods } from '@rocket.chat/ui-contexts'; import { check } from 'meteor/check'; @@ -17,7 +17,7 @@ declare module '@rocket.chat/ui-contexts' { Meteor.methods({ async getReadReceipts({ messageId }) { - if (!hasModule('message-read-receipt')) { + if (!License.hasModule('message-read-receipt')) { throw new Meteor.Error('error-action-not-allowed', 'This is an enterprise feature', { method: 'getReadReceipts' }); } diff --git a/apps/meteor/ee/server/models/startup.ts b/apps/meteor/ee/server/models/startup.ts index ab989ffd13ab..5b4085db44cf 100644 --- a/apps/meteor/ee/server/models/startup.ts +++ b/apps/meteor/ee/server/models/startup.ts @@ -1,4 +1,4 @@ -import { onLicense } from '@rocket.chat/license'; +import * as License from '@rocket.chat/license'; // To facilitate our lives with the stream // Collection will be registered on CE too @@ -8,7 +8,7 @@ import('./OmnichannelServiceLevelAgreements'); import('./AuditLog'); import('./ReadReceipts'); -await onLicense('livechat-enterprise', () => { +await License.onLicense('livechat-enterprise', () => { import('./CannedResponse'); import('./LivechatTag'); import('./LivechatUnit'); diff --git a/apps/meteor/ee/server/startup/apps/trialExpiration.ts b/apps/meteor/ee/server/startup/apps/trialExpiration.ts index 89bd65436f75..de3bb13c4464 100644 --- a/apps/meteor/ee/server/startup/apps/trialExpiration.ts +++ b/apps/meteor/ee/server/startup/apps/trialExpiration.ts @@ -1,10 +1,10 @@ -import { onInvalidateLicense } from '@rocket.chat/license'; +import * as License from '@rocket.chat/license'; import { Meteor } from 'meteor/meteor'; import { Apps } from '../../apps'; Meteor.startup(() => { - onInvalidateLicense(() => { + License.onInvalidateLicense(() => { void Apps.disableApps(); }); }); diff --git a/apps/meteor/ee/server/startup/audit.ts b/apps/meteor/ee/server/startup/audit.ts index 14e3b0ef4fb5..637336e66dad 100644 --- a/apps/meteor/ee/server/startup/audit.ts +++ b/apps/meteor/ee/server/startup/audit.ts @@ -1,8 +1,8 @@ -import { onLicense } from '@rocket.chat/license'; +import * as License from '@rocket.chat/license'; import { createPermissions } from '../lib/audit/startup'; -await onLicense('auditing', async () => { +await License.onLicense('auditing', async () => { await import('../lib/audit/methods'); await createPermissions(); diff --git a/apps/meteor/ee/server/startup/deviceManagement.ts b/apps/meteor/ee/server/startup/deviceManagement.ts index 1e47a36d445a..1de5cd24a222 100644 --- a/apps/meteor/ee/server/startup/deviceManagement.ts +++ b/apps/meteor/ee/server/startup/deviceManagement.ts @@ -1,9 +1,9 @@ -import { onToggledFeature } from '@rocket.chat/license'; +import * as License from '@rocket.chat/license'; import { addSettings } from '../settings/deviceManagement'; let stopListening: (() => void) | undefined; -onToggledFeature('device-management', { +License.onToggledFeature('device-management', { up: async () => { const { createPermissions, createEmailTemplates } = await import('../lib/deviceManagement/startup'); const { listenSessionLogin } = await import('../lib/deviceManagement/session'); diff --git a/apps/meteor/ee/server/startup/engagementDashboard.ts b/apps/meteor/ee/server/startup/engagementDashboard.ts index a4aa88098afe..c54d86528009 100644 --- a/apps/meteor/ee/server/startup/engagementDashboard.ts +++ b/apps/meteor/ee/server/startup/engagementDashboard.ts @@ -1,7 +1,7 @@ -import { onToggledFeature } from '@rocket.chat/license'; +import * as License from '@rocket.chat/license'; import { Meteor } from 'meteor/meteor'; -onToggledFeature('engagement-dashboard', { +License.onToggledFeature('engagement-dashboard', { up: () => Meteor.startup(async () => { const { prepareAnalytics, attachCallbacks } = await import('../lib/engagementDashboard/startup'); diff --git a/apps/meteor/ee/server/startup/maxRoomsPerGuest.ts b/apps/meteor/ee/server/startup/maxRoomsPerGuest.ts index 45c1ffc556db..af28fa986055 100644 --- a/apps/meteor/ee/server/startup/maxRoomsPerGuest.ts +++ b/apps/meteor/ee/server/startup/maxRoomsPerGuest.ts @@ -1,4 +1,4 @@ -import { preventNewGuestSubscriptions } from '@rocket.chat/license'; +import * as License from '@rocket.chat/license'; import { Meteor } from 'meteor/meteor'; import { callbacks } from '../../../lib/callbacks'; @@ -8,7 +8,7 @@ callbacks.add( 'beforeAddedToRoom', async ({ user }) => { if (user.roles?.includes('guest')) { - if (await preventNewGuestSubscriptions(user._id)) { + if (await License.preventNewGuestSubscriptions(user._id)) { throw new Meteor.Error('error-max-rooms-per-guest-reached', i18n.t('error-max-rooms-per-guest-reached')); } } diff --git a/apps/meteor/ee/server/startup/seatsCap.ts b/apps/meteor/ee/server/startup/seatsCap.ts index 2c59ffedf9fc..590245319f0d 100644 --- a/apps/meteor/ee/server/startup/seatsCap.ts +++ b/apps/meteor/ee/server/startup/seatsCap.ts @@ -1,5 +1,5 @@ import type { IUser } from '@rocket.chat/core-typings'; -import { preventNewUsers, getMaxActiveUsers, onValidateLicense } from '@rocket.chat/license'; +import * as License from '@rocket.chat/license'; import { Users } from '@rocket.chat/models'; import { Meteor } from 'meteor/meteor'; import { throttle } from 'underscore'; @@ -22,7 +22,7 @@ callbacks.add( return; } - if (await preventNewUsers()) { + if (await License.preventNewUsers()) { throw new Meteor.Error('error-license-user-limit-reached', i18n.t('error-license-user-limit-reached')); } }, @@ -33,7 +33,7 @@ callbacks.add( callbacks.add( 'beforeUserImport', async ({ userCount }) => { - if (await preventNewUsers(userCount)) { + if (await License.preventNewUsers(userCount)) { throw new Meteor.Error('error-license-user-limit-reached', i18n.t('error-license-user-limit-reached')); } }, @@ -52,7 +52,7 @@ callbacks.add( return; } - if (await preventNewUsers()) { + if (await License.preventNewUsers()) { throw new Meteor.Error('error-license-user-limit-reached', i18n.t('error-license-user-limit-reached')); } }, @@ -68,7 +68,7 @@ callbacks.add( ); const handleMaxSeatsBanners = throttle(async function _handleMaxSeatsBanners() { - const maxActiveUsers = getMaxActiveUsers(); + const maxActiveUsers = License.getMaxActiveUsers(); if (!maxActiveUsers) { await disableWarningBannerDiscardingDismissal(); @@ -113,5 +113,5 @@ Meteor.startup(async () => { await handleMaxSeatsBanners(); - onValidateLicense(handleMaxSeatsBanners); + License.onValidateLicense(handleMaxSeatsBanners); }); diff --git a/apps/meteor/ee/server/startup/services.ts b/apps/meteor/ee/server/startup/services.ts index 0b3004b48a84..ac82fe5e754f 100644 --- a/apps/meteor/ee/server/startup/services.ts +++ b/apps/meteor/ee/server/startup/services.ts @@ -1,5 +1,5 @@ import { api } from '@rocket.chat/core-services'; -import { isEnterprise, onLicense } from '@rocket.chat/license'; +import * as License from '@rocket.chat/license'; import { isRunningMs } from '../../../server/lib/isRunningMs'; import { FederationService } from '../../../server/services/federation/service'; @@ -26,13 +26,13 @@ if (!isRunningMs()) { let federationService: FederationService; void (async () => { - if (!isEnterprise()) { + if (!License.hasValidLicense()) { federationService = await FederationService.createFederationService(); api.registerService(federationService); } })(); -await onLicense('federation', async () => { +await License.onLicense('federation', async () => { const federationServiceEE = new FederationServiceEE(); if (federationService) { api.destroyService(federationService); diff --git a/apps/meteor/ee/server/startup/upsell.ts b/apps/meteor/ee/server/startup/upsell.ts index 66825be92059..ada354c4fb87 100644 --- a/apps/meteor/ee/server/startup/upsell.ts +++ b/apps/meteor/ee/server/startup/upsell.ts @@ -1,13 +1,13 @@ -import { onValidateLicense, getLicense } from '@rocket.chat/license'; +import * as License from '@rocket.chat/license'; import { Settings } from '@rocket.chat/models'; import { Meteor } from 'meteor/meteor'; const handleHadTrial = (): void => { - if (getLicense()?.information.trial) { + if (License.getLicense()?.information.trial) { void Settings.updateValueById('Cloud_Workspace_Had_Trial', true); } }; Meteor.startup(() => { - onValidateLicense(handleHadTrial); + License.onValidateLicense(handleHadTrial); }); diff --git a/apps/meteor/lib/apps/getInstallationSourceFromAppStorageItem.ts b/apps/meteor/lib/apps/getInstallationSourceFromAppStorageItem.ts index 8ac29d191576..f70a3c1456ee 100644 --- a/apps/meteor/lib/apps/getInstallationSourceFromAppStorageItem.ts +++ b/apps/meteor/lib/apps/getInstallationSourceFromAppStorageItem.ts @@ -1,5 +1,5 @@ import type { IAppStorageItem } from '@rocket.chat/apps-engine/server/storage'; -import type { LicenseAppSources } from '@rocket.chat/license'; +import type * as License from '@rocket.chat/license'; /** * There have been reports of apps not being correctly migrated from versions prior to 6.0 @@ -7,6 +7,6 @@ import type { LicenseAppSources } from '@rocket.chat/license'; * This function is a workaround to get the installation source of an app from the app storage item * even if the installationSource property is not set. */ -export function getInstallationSourceFromAppStorageItem(item: IAppStorageItem): LicenseAppSources { +export function getInstallationSourceFromAppStorageItem(item: IAppStorageItem): License.LicenseAppSources { return item.installationSource || ('marketplaceInfo' in item ? 'marketplace' : 'private'); } diff --git a/apps/meteor/server/services/authorization/service.ts b/apps/meteor/server/services/authorization/service.ts index 99863305f7c1..6918d40af871 100644 --- a/apps/meteor/server/services/authorization/service.ts +++ b/apps/meteor/server/services/authorization/service.ts @@ -39,7 +39,7 @@ export class Authorization extends ServiceClass implements IAuthorization { } async started(): Promise { - if (!(await License.isEnterprise())) { + if (!(await License.hasValidLicense())) { return; } diff --git a/apps/meteor/server/startup/migrations/v278.ts b/apps/meteor/server/startup/migrations/v278.ts index 694464230f7b..3504e21a7cc1 100644 --- a/apps/meteor/server/startup/migrations/v278.ts +++ b/apps/meteor/server/startup/migrations/v278.ts @@ -1,4 +1,4 @@ -import { isEnterprise } from '@rocket.chat/license'; +import * as License from '@rocket.chat/license'; import { Banners, Settings } from '@rocket.chat/models'; import { settings } from '../../../app/settings/server'; @@ -16,7 +16,7 @@ addMigration({ const LDAPEnabled = settings.get('LDAP_Enable'); const SAMLEnabled = settings.get('SAML_Custom_Default'); - const isEE = isEnterprise(); + const isEE = License.hasValidLicense(); if (!isEE && (isCustomOAuthEnabled || LDAPEnabled || SAMLEnabled)) { return; diff --git a/ee/packages/license/src/index.ts b/ee/packages/license/src/index.ts index fc2d6e9275e4..877238f84a40 100644 --- a/ee/packages/license/src/index.ts +++ b/ee/packages/license/src/index.ts @@ -1,5 +1,5 @@ import { overwriteClassOnLicense } from './events/overwriteClassOnLicense'; -import { getLicense, getUnmodifiedLicenseAndModules, isEnterprise, setLicense } from './license'; +import { getLicense, getUnmodifiedLicenseAndModules, hasValidLicense, setLicense } from './license'; import { hasModule, getModules } from './modules'; import { getTags } from './tags'; import { setLicenseLimitCounter, getCurrentValueForLicenseLimit } from './validation/getCurrentValueForLicenseLimit'; @@ -25,7 +25,7 @@ export { validateFormat, setWorkspaceUrl, hasModule, - isEnterprise, + hasValidLicense, getUnmodifiedLicenseAndModules, getLicense, getModules, diff --git a/ee/packages/license/src/license.ts b/ee/packages/license/src/license.ts index 12767271a6bc..a162dd5a569d 100644 --- a/ee/packages/license/src/license.ts +++ b/ee/packages/license/src/license.ts @@ -67,7 +67,7 @@ export const validateLicense = async () => { }; const setLicenseV3 = async (newLicense: ILicenseV3, encryptedLicense: string, originalLicense?: ILicenseV2 | ILicenseV3) => { - const hadValidLicense = isEnterprise(); + const hadValidLicense = hasValidLicense(); clearLicenseData(); try { @@ -78,7 +78,7 @@ const setLicenseV3 = async (newLicense: ILicenseV3, encryptedLicense: string, or await validateLicense(); lockLicense(encryptedLicense); } finally { - if (hadValidLicense && !isEnterprise()) { + if (hadValidLicense && !hasValidLicense()) { licenseRemoved(); invalidateAll(); } @@ -142,7 +142,7 @@ export const setLicense = async (encryptedLicense: string, forceSet = false): Pr } }; -export const isEnterprise = () => Boolean(license && valid); +export const hasValidLicense = () => Boolean(license && valid); export const getUnmodifiedLicenseAndModules = () => { if (valid && unmodifiedLicense) { diff --git a/ee/packages/omnichannel-services/src/OmnichannelTranscript.ts b/ee/packages/omnichannel-services/src/OmnichannelTranscript.ts index 802a6e15d0eb..899d298fb445 100644 --- a/ee/packages/omnichannel-services/src/OmnichannelTranscript.ts +++ b/ee/packages/omnichannel-services/src/OmnichannelTranscript.ts @@ -78,7 +78,7 @@ export class OmnichannelTranscript extends ServiceClass implements IOmnichannelT async started(): Promise { try { - this.shouldWork = await licenseService.hasLicense('scalability'); + this.shouldWork = await licenseService.hasModule('scalability'); } catch (e: unknown) { // ignore } diff --git a/ee/packages/omnichannel-services/src/QueueWorker.ts b/ee/packages/omnichannel-services/src/QueueWorker.ts index 141cb937f475..bfb69362fac6 100644 --- a/ee/packages/omnichannel-services/src/QueueWorker.ts +++ b/ee/packages/omnichannel-services/src/QueueWorker.ts @@ -35,7 +35,7 @@ export class QueueWorker extends ServiceClass implements IQueueWorkerService { async started(): Promise { try { - this.shouldWork = await License.hasLicense('scalability'); + this.shouldWork = await License.hasModule('scalability'); } catch (e: unknown) { // ignore } diff --git a/ee/packages/presence/src/Presence.ts b/ee/packages/presence/src/Presence.ts index 238cd445def4..fb656fc3e158 100755 --- a/ee/packages/presence/src/Presence.ts +++ b/ee/packages/presence/src/Presence.ts @@ -65,7 +65,7 @@ export class Presence extends ServiceClass implements IPresence { try { await Settings.updateValueById('Presence_broadcast_disabled', false); - this.hasLicense = await License.hasLicense('scalability'); + this.hasLicense = await License.hasModule('scalability'); } catch (e: unknown) { // ignore } diff --git a/packages/core-services/src/types/ILicense.ts b/packages/core-services/src/types/ILicense.ts index 7b89a006bfc0..c9247f8887ce 100644 --- a/packages/core-services/src/types/ILicense.ts +++ b/packages/core-services/src/types/ILicense.ts @@ -1,9 +1,9 @@ import type { IServiceClass } from './ServiceClass'; export interface ILicense extends IServiceClass { - hasLicense(feature: string): boolean; + hasModule(feature: string): boolean; - isEnterprise(): boolean; + hasValidLicense(): boolean; getModules(): string[]; From e248a34fa2ed05ca29e1253cef968fc771caf262 Mon Sep 17 00:00:00 2001 From: Pierre Lehnen Date: Fri, 22 Sep 2023 11:26:42 -0300 Subject: [PATCH 21/38] added limitReached event --- ee/packages/license/src/events/emitter.ts | 9 +++++++++ ee/packages/license/src/events/listeners.ts | 5 +++++ 2 files changed, 14 insertions(+) diff --git a/ee/packages/license/src/events/emitter.ts b/ee/packages/license/src/events/emitter.ts index 8b16e5527121..85080c123c1d 100644 --- a/ee/packages/license/src/events/emitter.ts +++ b/ee/packages/license/src/events/emitter.ts @@ -1,5 +1,6 @@ import { EventEmitter } from 'events'; +import type { LicenseLimitKind } from '../definition/ILicenseV3'; import type { LicenseModule } from '../definition/LicenseModule'; import { logger } from '../logger'; @@ -38,3 +39,11 @@ export const moduleRemoved = (module: LicenseModule) => { logger.error({ msg: 'Error running module removed event', error }); } }; + +export const limitReached = (limitKind: LicenseLimitKind) => { + try { + EnterpriseLicenses.emit(`limitReached:${limitKind}`); + } 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 49dde70af512..08beaf30fe3a 100644 --- a/ee/packages/license/src/events/listeners.ts +++ b/ee/packages/license/src/events/listeners.ts @@ -1,3 +1,4 @@ +import type { LicenseLimitKind } from '../definition/ILicenseV3'; import type { LicenseModule } from '../definition/LicenseModule'; import { hasModule } from '../modules'; import { EnterpriseLicenses } from './emitter'; @@ -67,3 +68,7 @@ export const onValidateLicense = (cb: (...args: any[]) => void) => { export const onInvalidateLicense = (cb: (...args: any[]) => void) => { EnterpriseLicenses.on('invalidate', cb); }; + +export const onLimitReached = (limitKind: LicenseLimitKind, cb: (...args: any[]) => void) => { + EnterpriseLicenses.on(`limitReached:${limitKind}`, cb); +}; From 734debb7f436a780140b6fca8698b9bd58af205a Mon Sep 17 00:00:00 2001 From: Pierre Lehnen Date: Fri, 22 Sep 2023 11:45:23 -0300 Subject: [PATCH 22/38] fixed typescript version --- ee/packages/license/package.json | 2 +- packages/jwt/package.json | 2 +- yarn.lock | 24 ++---------------------- 3 files changed, 4 insertions(+), 24 deletions(-) diff --git a/ee/packages/license/package.json b/ee/packages/license/package.json index 60122ac88139..5a205b7feef1 100644 --- a/ee/packages/license/package.json +++ b/ee/packages/license/package.json @@ -7,7 +7,7 @@ "eslint": "~8.45.0", "jest": "~29.6.1", "ts-jest": "~29.0.5", - "typescript": "~5.1.6" + "typescript": "~5.2.2" }, "scripts": { "lint": "eslint --ext .js,.jsx,.ts,.tsx .", diff --git a/packages/jwt/package.json b/packages/jwt/package.json index b0e73e706e64..8ec0c5a2c104 100644 --- a/packages/jwt/package.json +++ b/packages/jwt/package.json @@ -7,7 +7,7 @@ "eslint": "~8.45.0", "jest": "~29.6.1", "ts-jest": "^29.1.1", - "typescript": "~5.1.6" + "typescript": "~5.2.2" }, "scripts": { "lint": "eslint --ext .js,.jsx,.ts,.tsx .", diff --git a/yarn.lock b/yarn.lock index c3b2c6157d97..9c4a47338989 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8374,7 +8374,7 @@ __metadata: jest: ~29.6.1 jose: ^4.14.4 ts-jest: ^29.1.1 - typescript: ~5.1.6 + typescript: ~5.2.2 languageName: unknown linkType: soft @@ -8402,7 +8402,7 @@ __metadata: eslint: ~8.45.0 jest: ~29.6.1 ts-jest: ~29.0.5 - typescript: ~5.1.6 + typescript: ~5.2.2 languageName: unknown linkType: soft @@ -37789,16 +37789,6 @@ __metadata: languageName: node linkType: hard -"typescript@npm:~5.1.6": - version: 5.1.6 - resolution: "typescript@npm:5.1.6" - bin: - tsc: bin/tsc - tsserver: bin/tsserver - checksum: b2f2c35096035fe1f5facd1e38922ccb8558996331405eb00a5111cc948b2e733163cc22fab5db46992aba7dd520fff637f2c1df4996ff0e134e77d3249a7350 - languageName: node - linkType: hard - "typescript@npm:~5.2.2": version: 5.2.2 resolution: "typescript@npm:5.2.2" @@ -37809,16 +37799,6 @@ __metadata: languageName: node linkType: hard -"typescript@patch:typescript@~5.1.6#~builtin": - version: 5.1.6 - resolution: "typescript@patch:typescript@npm%3A5.1.6#~builtin::version=5.1.6&hash=f456af" - bin: - tsc: bin/tsc - tsserver: bin/tsserver - checksum: 21e88b0a0c0226f9cb9fd25b9626fb05b4c0f3fddac521844a13e1f30beb8f14e90bd409a9ac43c812c5946d714d6e0dee12d5d02dfc1c562c5aacfa1f49b606 - languageName: node - linkType: hard - "typescript@patch:typescript@~5.2.2#~builtin": version: 5.2.2 resolution: "typescript@patch:typescript@npm%3A5.2.2#~builtin::version=5.2.2&hash=f456af" From c182f21d8a17c137455cd118a2132a840350f7e8 Mon Sep 17 00:00:00 2001 From: Pierre Lehnen Date: Fri, 22 Sep 2023 15:13:51 -0300 Subject: [PATCH 23/38] moved data counters to @rocket.chat/meteor --- apps/meteor/ee/app/license/server/startup.ts | 6 ++++++ ee/packages/license/package.json | 3 +-- ee/packages/license/src/license.ts | 5 +++-- ee/packages/license/src/pendingLicense.ts | 2 +- .../validation/getCurrentValueForLicenseLimit.ts | 15 ++++++++++----- yarn.lock | 1 - 6 files changed, 21 insertions(+), 11 deletions(-) diff --git a/apps/meteor/ee/app/license/server/startup.ts b/apps/meteor/ee/app/license/server/startup.ts index 5c72051e8130..53f50d8048df 100644 --- a/apps/meteor/ee/app/license/server/startup.ts +++ b/apps/meteor/ee/app/license/server/startup.ts @@ -1,4 +1,5 @@ import * as License from '@rocket.chat/license'; +import { Subscriptions, Users } from '@rocket.chat/models'; import { settings } from '../../../../app/settings/server'; import { callbacks } from '../../../../lib/callbacks'; @@ -14,5 +15,10 @@ callbacks.add('workspaceLicenseChanged', async (updatedLicense) => { await License.setLicense(updatedLicense); }); +License.setLicenseLimitCounter('activeUsers', () => Users.getActiveLocalUserCount()); +License.setLicenseLimitCounter('guestUsers', () => Users.getActiveLocalGuestCount()); +License.setLicenseLimitCounter('roomsPerGuest', async (context) => (context?.userId ? Subscriptions.countByUserId(context.userId) : 0)); License.setLicenseLimitCounter('privateApps', () => getAppCount('private')); License.setLicenseLimitCounter('marketplaceApps', () => getAppCount('marketplace')); +// #TODO: Get real value +License.setLicenseLimitCounter('monthlyActiveContacts', async () => 0); diff --git a/ee/packages/license/package.json b/ee/packages/license/package.json index 5a205b7feef1..5dc6b69078b8 100644 --- a/ee/packages/license/package.json +++ b/ee/packages/license/package.json @@ -24,7 +24,6 @@ "dependencies": { "@rocket.chat/core-typings": "workspace:^", "@rocket.chat/jwt": "workspace:^", - "@rocket.chat/logger": "workspace:^", - "@rocket.chat/models": "workspace:^" + "@rocket.chat/logger": "workspace:^" } } diff --git a/ee/packages/license/src/license.ts b/ee/packages/license/src/license.ts index a162dd5a569d..56707b03d7c7 100644 --- a/ee/packages/license/src/license.ts +++ b/ee/packages/license/src/license.ts @@ -10,6 +10,7 @@ import { clearPendingLicense, hasPendingLicense, isPendingLicense, setPendingLic import { showLicense } from './showLicense'; import { replaceTags } from './tags'; import { convertToV3 } from './v2/convertToV3'; +import { hasAllDataCounters } from './validation/getCurrentValueForLicenseLimit'; import { getModulesToDisable } from './validation/getModulesToDisable'; import { isBehaviorsInResult } from './validation/isBehaviorsInResult'; import { runValidation } from './validation/runValidation'; @@ -88,8 +89,8 @@ const setLicenseV3 = async (newLicense: ILicenseV3, encryptedLicense: string, or const setLicenseV2 = async (newLicense: ILicenseV2, encryptedLicense: string) => setLicenseV3(convertToV3(newLicense), encryptedLicense, newLicense); -// Can only validate licenses once the workspace URL is set -export const isReadyForValidation = () => Boolean(getWorkspaceUrl()); +// Can only validate licenses once the workspace URL and the data counter functions are set +export const isReadyForValidation = () => Boolean(getWorkspaceUrl() && hasAllDataCounters()); export const setLicense = async (encryptedLicense: string, forceSet = false): Promise => { if (!encryptedLicense || String(encryptedLicense).trim() === '') { diff --git a/ee/packages/license/src/pendingLicense.ts b/ee/packages/license/src/pendingLicense.ts index 582a52f93736..6c584761e5d4 100644 --- a/ee/packages/license/src/pendingLicense.ts +++ b/ee/packages/license/src/pendingLicense.ts @@ -13,7 +13,7 @@ export const setPendingLicense = (encryptedLicense: string) => { export const applyPendingLicense = async () => { if (pendingLicense) { logger.info('Applying pending license.'); - await setLicense(pendingLicense, true); + await setLicense(pendingLicense); } }; diff --git a/ee/packages/license/src/validation/getCurrentValueForLicenseLimit.ts b/ee/packages/license/src/validation/getCurrentValueForLicenseLimit.ts index cb86c5ff3dfe..2e8edfa6e2d5 100644 --- a/ee/packages/license/src/validation/getCurrentValueForLicenseLimit.ts +++ b/ee/packages/license/src/validation/getCurrentValueForLicenseLimit.ts @@ -1,8 +1,8 @@ import type { IUser } from '@rocket.chat/core-typings'; -import { Subscriptions, Users } from '@rocket.chat/models'; import type { LicenseLimitKind } from '../definition/ILicenseV3'; import { logger } from '../logger'; +import { applyPendingLicense, hasPendingLicense } from '../pendingLicense'; type LimitContext = T extends 'roomsPerGuest' ? { userId: IUser['_id'] } : Record; @@ -10,11 +10,11 @@ const dataCounters = new Map(limitKey: T, fn: (context?: LimitContext) => Promise) => { dataCounters.set(limitKey, fn as (context?: LimitContext) => Promise); -}; -setLicenseLimitCounter('activeUsers', () => Users.getActiveLocalUserCount()); -setLicenseLimitCounter('guestUsers', () => Users.getActiveLocalGuestCount()); -setLicenseLimitCounter('roomsPerGuest', async (context) => (context?.userId ? Subscriptions.countByUserId(context.userId) : 0)); + if (hasPendingLicense() && hasAllDataCounters()) { + void applyPendingLicense(); + } +}; export const getCurrentValueForLicenseLimit = async ( limitKey: T, @@ -28,3 +28,8 @@ export const getCurrentValueForLicenseLimit = async return 0; }; + +export const hasAllDataCounters = () => + (['activeUsers', 'guestUsers', 'roomsPerGuest', 'privateApps', 'marketplaceApps', 'monthlyActiveContacts'] as LicenseLimitKind[]).every( + (limitKey) => dataCounters.has(limitKey), + ); diff --git a/yarn.lock b/yarn.lock index 9c4a47338989..71f0730329cb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8397,7 +8397,6 @@ __metadata: "@rocket.chat/core-typings": "workspace:^" "@rocket.chat/jwt": "workspace:^" "@rocket.chat/logger": "workspace:^" - "@rocket.chat/models": "workspace:^" "@types/jest": ~29.5.3 eslint: ~8.45.0 jest: ~29.6.1 From e69a35ea45ff7b221870bd64fdf945e6282e6d8e Mon Sep 17 00:00:00 2001 From: Pierre Lehnen Date: Fri, 22 Sep 2023 15:18:26 -0300 Subject: [PATCH 24/38] use visualExpiration attribute for trial end date --- apps/meteor/client/views/hooks/useUpgradeTabParams.ts | 6 ++---- ee/packages/license/src/v2/convertToV3.ts | 2 +- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/apps/meteor/client/views/hooks/useUpgradeTabParams.ts b/apps/meteor/client/views/hooks/useUpgradeTabParams.ts index 072d9712e038..88e21f668e02 100644 --- a/apps/meteor/client/views/hooks/useUpgradeTabParams.ts +++ b/apps/meteor/client/views/hooks/useUpgradeTabParams.ts @@ -21,10 +21,8 @@ export const useUpgradeTabParams = (): { tabType: UpgradeTabVariant | false; tri const trialLicense = licenses.find(({ meta, information }) => information?.trial ?? meta?.trial); const isTrial = Boolean(trialLicense); - const trialEndDate = - trialLicense?.meta?.trialEnd || trialLicense?.cloudMeta?.trialEnd - ? format(new Date(trialLicense.meta?.trialEnd ?? trialLicense.cloudMeta?.trialEnd), 'yyyy-MM-dd') - : undefined; + const trialEndDateStr = trialLicense?.information?.visualExpiration || trialLicense?.meta?.trialEnd || trialLicense?.cloudMeta?.trialEnd; + const trialEndDate = trialEndDateStr ? format(new Date(trialEndDateStr), 'yyyy-MM-dd') : undefined; const upgradeTabType = getUpgradeTabType({ registered, diff --git a/ee/packages/license/src/v2/convertToV3.ts b/ee/packages/license/src/v2/convertToV3.ts index ddd21d327032..a4d5cf118573 100644 --- a/ee/packages/license/src/v2/convertToV3.ts +++ b/ee/packages/license/src/v2/convertToV3.ts @@ -14,7 +14,7 @@ export const convertToV3 = (v2: ILicenseV2): ILicenseV3 => { version: '3.0', information: { autoRenew: false, - visualExpiration: new Date(Date.parse(v2.expiry)).toISOString(), + visualExpiration: new Date(Date.parse(v2.meta?.trialEnd || v2.expiry)).toISOString(), trial: v2.meta?.trial || false, offline: false, createdAt: new Date().toISOString(), From 490a0c74150b28c45cd6b6341f3ac4e72d1a3837 Mon Sep 17 00:00:00 2001 From: Pierre Lehnen Date: Mon, 25 Sep 2023 14:25:58 -0300 Subject: [PATCH 25/38] export internal license data when running with test_mode --- ee/packages/license/src/data.ts | 26 ++++++ ee/packages/license/src/deprecated.ts | 11 +++ ee/packages/license/src/encryptedLicense.ts | 7 +- ee/packages/license/src/fairPolicy.ts | 3 + ee/packages/license/src/index.ts | 15 +++- ee/packages/license/src/license.ts | 90 +++---------------- ee/packages/license/src/modules.ts | 2 +- ee/packages/license/src/pendingLicense.ts | 19 ++-- ee/packages/license/src/tags.ts | 2 +- .../src/validation/isReadyForValidation.ts | 5 ++ .../src/validation/processValidationResult.ts | 31 +++++++ .../license/src/validation/validateLicense.ts | 19 ++++ 12 files changed, 131 insertions(+), 99 deletions(-) create mode 100644 ee/packages/license/src/data.ts create mode 100644 ee/packages/license/src/fairPolicy.ts create mode 100644 ee/packages/license/src/validation/isReadyForValidation.ts create mode 100644 ee/packages/license/src/validation/processValidationResult.ts create mode 100644 ee/packages/license/src/validation/validateLicense.ts diff --git a/ee/packages/license/src/data.ts b/ee/packages/license/src/data.ts new file mode 100644 index 000000000000..d640f1a26045 --- /dev/null +++ b/ee/packages/license/src/data.ts @@ -0,0 +1,26 @@ +import type { ILicenseV2 } from './definition/ILicenseV2'; +import type { ILicenseV3 } from './definition/ILicenseV3'; + +export const licenseData: { + license: ILicenseV3 | undefined; + unmodifiedLicense: ILicenseV2 | ILicenseV3 | undefined; + valid: boolean | undefined; + inFairPolicy: boolean | undefined; + pendingLicense: string; + lockedLicense: string | undefined; +} = { + license: undefined, + unmodifiedLicense: undefined, + valid: undefined, + inFairPolicy: undefined, + pendingLicense: '', + lockedLicense: undefined, +}; + +export const clearLicenseData = () => { + licenseData.license = undefined; + licenseData.unmodifiedLicense = undefined; + licenseData.inFairPolicy = undefined; + licenseData.valid = false; + licenseData.pendingLicense = ''; +}; diff --git a/ee/packages/license/src/deprecated.ts b/ee/packages/license/src/deprecated.ts index 3071a364f8db..9ffc128dda83 100644 --- a/ee/packages/license/src/deprecated.ts +++ b/ee/packages/license/src/deprecated.ts @@ -1,5 +1,7 @@ +import { licenseData } from './data'; import type { LicenseLimitKind } from './definition/ILicenseV3'; import { getLicense } from './license'; +import { getModules } from './modules'; const getLicenseLimit = (kind: LicenseLimitKind) => { const license = getLicense(); @@ -23,3 +25,12 @@ export const getAppsConfig = () => ({ maxPrivateApps: getLicenseLimit('privateApps') ?? -1, maxMarketplaceApps: getLicenseLimit('marketplaceApps') ?? -1, }); + +export const getUnmodifiedLicenseAndModules = () => { + if (licenseData.valid && licenseData.unmodifiedLicense) { + return { + license: licenseData.unmodifiedLicense, + modules: getModules(), + }; + } +}; diff --git a/ee/packages/license/src/encryptedLicense.ts b/ee/packages/license/src/encryptedLicense.ts index 29cf1bcb9390..b18b25ea8929 100644 --- a/ee/packages/license/src/encryptedLicense.ts +++ b/ee/packages/license/src/encryptedLicense.ts @@ -1,7 +1,8 @@ -let lockedLicense: string | undefined; +import { licenseData } from './data'; export const lockLicense = (encryptedLicense: string) => { - lockedLicense = encryptedLicense; + licenseData.lockedLicense = encryptedLicense; }; -export const isLicenseDuplicate = (encryptedLicense: string) => Boolean(lockedLicense && lockedLicense === encryptedLicense); +export const isLicenseDuplicate = (encryptedLicense: string) => + Boolean(licenseData.lockedLicense && licenseData.lockedLicense === encryptedLicense); diff --git a/ee/packages/license/src/fairPolicy.ts b/ee/packages/license/src/fairPolicy.ts new file mode 100644 index 000000000000..66ac8a19ebd5 --- /dev/null +++ b/ee/packages/license/src/fairPolicy.ts @@ -0,0 +1,3 @@ +import { licenseData } from './data'; + +export const startedFairPolicy = () => Boolean(licenseData.inFairPolicy); diff --git a/ee/packages/license/src/index.ts b/ee/packages/license/src/index.ts index 877238f84a40..075c2a496949 100644 --- a/ee/packages/license/src/index.ts +++ b/ee/packages/license/src/index.ts @@ -1,7 +1,8 @@ +import { licenseData } from './data'; import { overwriteClassOnLicense } from './events/overwriteClassOnLicense'; -import { getLicense, getUnmodifiedLicenseAndModules, hasValidLicense, setLicense } from './license'; -import { hasModule, getModules } from './modules'; -import { getTags } from './tags'; +import { getLicense, hasValidLicense, setLicense } from './license'; +import { modules as modulesData, hasModule, getModules } from './modules'; +import { tags as tagsData, getTags } from './tags'; import { setLicenseLimitCounter, getCurrentValueForLicenseLimit } from './validation/getCurrentValueForLicenseLimit'; import { validateFormat } from './validation/validateFormat'; import { setWorkspaceUrl } from './workspaceUrl'; @@ -20,17 +21,23 @@ export * from './events/listeners'; export * from './deprecated'; export * from './actionBlockers'; +const { modules, tags, data } = process.env.TEST_MODE + ? { modules: modulesData, tags: tagsData, data: licenseData } + : { modules: undefined, tags: undefined, data: undefined }; + export { setLicense, validateFormat, setWorkspaceUrl, hasModule, hasValidLicense, - getUnmodifiedLicenseAndModules, getLicense, + modules, getModules, + tags, getTags, overwriteClassOnLicense, setLicenseLimitCounter, getCurrentValueForLicenseLimit, + data, }; diff --git a/ee/packages/license/src/license.ts b/ee/packages/license/src/license.ts index 56707b03d7c7..c134d168e27b 100644 --- a/ee/packages/license/src/license.ts +++ b/ee/packages/license/src/license.ts @@ -1,80 +1,24 @@ +import { clearLicenseData, licenseData } from './data'; import decrypt from './decrypt'; import type { ILicenseV2 } from './definition/ILicenseV2'; import type { ILicenseV3 } from './definition/ILicenseV3'; -import type { BehaviorWithContext } from './definition/LicenseBehavior'; import { isLicenseDuplicate, lockLicense } from './encryptedLicense'; -import { licenseRemoved, licenseValidated } from './events/emitter'; +import { licenseRemoved } from './events/emitter'; import { logger } from './logger'; -import { getModules, invalidateAll, replaceModules } from './modules'; +import { invalidateAll } from './modules'; import { clearPendingLicense, hasPendingLicense, isPendingLicense, setPendingLicense } from './pendingLicense'; -import { showLicense } from './showLicense'; -import { replaceTags } from './tags'; import { convertToV3 } from './v2/convertToV3'; -import { hasAllDataCounters } from './validation/getCurrentValueForLicenseLimit'; -import { getModulesToDisable } from './validation/getModulesToDisable'; -import { isBehaviorsInResult } from './validation/isBehaviorsInResult'; -import { runValidation } from './validation/runValidation'; +import { isReadyForValidation } from './validation/isReadyForValidation'; import { validateFormat } from './validation/validateFormat'; -import { getWorkspaceUrl } from './workspaceUrl'; - -let unmodifiedLicense: ILicenseV2 | ILicenseV3 | undefined; -let license: ILicenseV3 | undefined; -let valid: boolean | undefined; -let inFairPolicy: boolean | undefined; - -const clearLicenseData = () => { - license = undefined; - unmodifiedLicense = undefined; - valid = undefined; - inFairPolicy = undefined; - valid = false; -}; - -const processValidationResult = (result: BehaviorWithContext[]) => { - if (!license || isBehaviorsInResult(result, ['invalidate_license', 'prevent_installation'])) { - return; - } - - valid = true; - inFairPolicy = isBehaviorsInResult(result, ['start_fair_policy']); - - if (license.information.tags) { - replaceTags(license.information.tags); - } - - const disabledModules = getModulesToDisable(result); - const modulesToEnable = license.grantedModules.filter(({ module }) => !disabledModules.includes(module)); - - replaceModules(modulesToEnable.map(({ module }) => module)); - logger.log({ msg: 'License validated', modules: modulesToEnable }); - - licenseValidated(); - showLicense(license, valid); -}; - -export const validateLicense = async () => { - if (!license || !getWorkspaceUrl()) { - return; - } - - // #TODO: Only include 'prevent_installation' here if this is actually the initial installation of the license - const validationResult = await runValidation(license, [ - 'invalidate_license', - 'prevent_installation', - 'start_fair_policy', - 'disable_modules', - ]); - processValidationResult(validationResult); -}; +import { validateLicense } from './validation/validateLicense'; const setLicenseV3 = async (newLicense: ILicenseV3, encryptedLicense: string, originalLicense?: ILicenseV2 | ILicenseV3) => { const hadValidLicense = hasValidLicense(); clearLicenseData(); try { - unmodifiedLicense = originalLicense || newLicense; - license = newLicense; - clearPendingLicense(); + licenseData.unmodifiedLicense = originalLicense || newLicense; + licenseData.license = newLicense; await validateLicense(); lockLicense(encryptedLicense); @@ -89,9 +33,6 @@ const setLicenseV3 = async (newLicense: ILicenseV3, encryptedLicense: string, or const setLicenseV2 = async (newLicense: ILicenseV2, encryptedLicense: string) => setLicenseV3(convertToV3(newLicense), encryptedLicense, newLicense); -// Can only validate licenses once the workspace URL and the data counter functions are set -export const isReadyForValidation = () => Boolean(getWorkspaceUrl() && hasAllDataCounters()); - export const setLicense = async (encryptedLicense: string, forceSet = false): Promise => { if (!encryptedLicense || String(encryptedLicense).trim() === '') { return false; @@ -143,21 +84,10 @@ export const setLicense = async (encryptedLicense: string, forceSet = false): Pr } }; -export const hasValidLicense = () => Boolean(license && valid); - -export const getUnmodifiedLicenseAndModules = () => { - if (valid && unmodifiedLicense) { - return { - license: unmodifiedLicense, - modules: getModules(), - }; - } -}; +export const hasValidLicense = () => Boolean(licenseData.license && licenseData.valid); export const getLicense = () => { - if (valid && license) { - return license; + if (licenseData.valid && licenseData.license) { + return licenseData.license; } }; - -export const startedFairPolicy = () => Boolean(inFairPolicy); diff --git a/ee/packages/license/src/modules.ts b/ee/packages/license/src/modules.ts index bfdb2bbc996b..0497c0c8f069 100644 --- a/ee/packages/license/src/modules.ts +++ b/ee/packages/license/src/modules.ts @@ -1,7 +1,7 @@ import type { LicenseModule } from './definition/LicenseModule'; import { moduleRemoved, moduleValidated } from './events/emitter'; -const modules = new Set(); +export const modules = new Set(); export const notifyValidatedModules = (licenseModules: LicenseModule[]) => { licenseModules.forEach((module) => { diff --git a/ee/packages/license/src/pendingLicense.ts b/ee/packages/license/src/pendingLicense.ts index 6c584761e5d4..6eb40eab2ac3 100644 --- a/ee/packages/license/src/pendingLicense.ts +++ b/ee/packages/license/src/pendingLicense.ts @@ -1,30 +1,29 @@ +import { licenseData } from './data'; import { setLicense } from './license'; import { logger } from './logger'; -let pendingLicense: string; - export const setPendingLicense = (encryptedLicense: string) => { - pendingLicense = encryptedLicense; - if (pendingLicense) { + licenseData.pendingLicense = encryptedLicense; + if (licenseData.pendingLicense) { logger.info('Storing license as pending validation.'); } }; export const applyPendingLicense = async () => { - if (pendingLicense) { + if (licenseData.pendingLicense) { logger.info('Applying pending license.'); - await setLicense(pendingLicense); + await setLicense(licenseData.pendingLicense); } }; -export const hasPendingLicense = () => Boolean(pendingLicense); +export const hasPendingLicense = () => Boolean(licenseData.pendingLicense); -export const isPendingLicense = (encryptedLicense: string) => !!pendingLicense && pendingLicense === encryptedLicense; +export const isPendingLicense = (encryptedLicense: string) => hasPendingLicense() && licenseData.pendingLicense === encryptedLicense; export const clearPendingLicense = () => { - if (pendingLicense) { + if (licenseData.pendingLicense) { logger.info('Removing pending license.'); } - pendingLicense = ''; + licenseData.pendingLicense = ''; }; diff --git a/ee/packages/license/src/tags.ts b/ee/packages/license/src/tags.ts index b115b164cd64..ca2639678475 100644 --- a/ee/packages/license/src/tags.ts +++ b/ee/packages/license/src/tags.ts @@ -1,6 +1,6 @@ import type { ILicenseTag } from './definition/ILicenseTag'; -const tags = new Set(); +export const tags = new Set(); export const addTag = (tag: ILicenseTag) => { // make sure to not add duplicated tag names diff --git a/ee/packages/license/src/validation/isReadyForValidation.ts b/ee/packages/license/src/validation/isReadyForValidation.ts new file mode 100644 index 000000000000..7f3c3c77736e --- /dev/null +++ b/ee/packages/license/src/validation/isReadyForValidation.ts @@ -0,0 +1,5 @@ +import { getWorkspaceUrl } from '../workspaceUrl'; +import { hasAllDataCounters } from './getCurrentValueForLicenseLimit'; + +// Can only validate licenses once the workspace URL and the data counter functions are set +export const isReadyForValidation = () => Boolean(getWorkspaceUrl() && hasAllDataCounters()); diff --git a/ee/packages/license/src/validation/processValidationResult.ts b/ee/packages/license/src/validation/processValidationResult.ts new file mode 100644 index 000000000000..0ddac8a6eef9 --- /dev/null +++ b/ee/packages/license/src/validation/processValidationResult.ts @@ -0,0 +1,31 @@ +import { licenseData } from '../data'; +import type { BehaviorWithContext } from '../definition/LicenseBehavior'; +import { licenseValidated } from '../events/emitter'; +import { logger } from '../logger'; +import { replaceModules } from '../modules'; +import { showLicense } from '../showLicense'; +import { replaceTags } from '../tags'; +import { getModulesToDisable } from './getModulesToDisable'; +import { isBehaviorsInResult } from './isBehaviorsInResult'; + +export const processValidationResult = (result: BehaviorWithContext[]) => { + if (!licenseData.license || isBehaviorsInResult(result, ['invalidate_license', 'prevent_installation'])) { + return; + } + + licenseData.valid = true; + licenseData.inFairPolicy = isBehaviorsInResult(result, ['start_fair_policy']); + + if (licenseData.license.information.tags) { + replaceTags(licenseData.license.information.tags); + } + + const disabledModules = getModulesToDisable(result); + const modulesToEnable = licenseData.license.grantedModules.filter(({ module }) => !disabledModules.includes(module)); + + replaceModules(modulesToEnable.map(({ module }) => module)); + logger.log({ msg: 'License validated', modules: modulesToEnable }); + + licenseValidated(); + showLicense(licenseData.license, licenseData.valid); +}; diff --git a/ee/packages/license/src/validation/validateLicense.ts b/ee/packages/license/src/validation/validateLicense.ts new file mode 100644 index 000000000000..b5828247dd17 --- /dev/null +++ b/ee/packages/license/src/validation/validateLicense.ts @@ -0,0 +1,19 @@ +import { licenseData } from '../data'; +import { isReadyForValidation } from './isReadyForValidation'; +import { processValidationResult } from './processValidationResult'; +import { runValidation } from './runValidation'; + +export const validateLicense = async () => { + if (!licenseData.license || !isReadyForValidation()) { + return; + } + + // #TODO: Only include 'prevent_installation' here if this is actually the initial installation of the license + const validationResult = await runValidation(licenseData.license, [ + 'invalidate_license', + 'prevent_installation', + 'start_fair_policy', + 'disable_modules', + ]); + processValidationResult(validationResult); +}; From 90f7e79aa4badeca7881cca4aa3bc1d5c2ebc716 Mon Sep 17 00:00:00 2001 From: Pierre Lehnen Date: Mon, 25 Sep 2023 20:37:40 -0300 Subject: [PATCH 26/38] reorganized license code into a class to facilitate writing tests --- apps/meteor/app/api/server/v1/federation.ts | 2 +- .../client/views/hooks/useUpgradeTabParams.ts | 4 +- .../ee/app/api-enterprise/server/index.ts | 2 +- .../authorization/server/validateUserRoles.js | 2 +- .../ee/app/canned-responses/server/index.ts | 2 +- .../ee/app/license/server/canEnableApp.ts | 6 +- .../ee/app/license/server/getStatistics.ts | 2 +- .../ee/app/license/server/lib/getAppCount.ts | 4 +- .../license/server/license.internalService.ts | 4 +- apps/meteor/ee/app/license/server/methods.ts | 6 +- apps/meteor/ee/app/license/server/settings.ts | 2 +- apps/meteor/ee/app/license/server/startup.ts | 2 +- .../server/business-hour/Helper.ts | 2 +- .../app/livechat-enterprise/server/index.ts | 2 +- .../server/lib/LivechatEnterprise.ts | 2 +- .../app/message-read-receipt/server/index.ts | 2 +- .../meteor/ee/app/settings/server/settings.ts | 4 +- .../server/services/voipService.ts | 2 +- .../ee/client/hooks/useHasLicenseModule.ts | 4 +- apps/meteor/ee/client/lib/onToggledFeature.ts | 4 +- apps/meteor/ee/server/api/api.ts | 2 +- apps/meteor/ee/server/api/chat.ts | 2 +- apps/meteor/ee/server/api/licenses.ts | 2 +- apps/meteor/ee/server/api/roles.ts | 2 +- apps/meteor/ee/server/api/sessions.ts | 2 +- .../endpoints/appsCountHandler.ts | 2 +- .../ee/server/apps/communication/rest.ts | 2 +- apps/meteor/ee/server/configuration/ldap.ts | 2 +- apps/meteor/ee/server/configuration/oauth.ts | 2 +- .../server/configuration/outlookCalendar.ts | 2 +- apps/meteor/ee/server/configuration/saml.ts | 2 +- .../server/configuration/videoConference.ts | 2 +- apps/meteor/ee/server/lib/syncUserRoles.ts | 4 +- .../ee/server/methods/getReadReceipts.ts | 2 +- apps/meteor/ee/server/models/startup.ts | 2 +- .../ee/server/startup/apps/trialExpiration.ts | 2 +- apps/meteor/ee/server/startup/audit.ts | 2 +- .../ee/server/startup/deviceManagement.ts | 2 +- .../ee/server/startup/engagementDashboard.ts | 2 +- .../ee/server/startup/maxRoomsPerGuest.ts | 4 +- apps/meteor/ee/server/startup/seatsCap.ts | 8 +- apps/meteor/ee/server/startup/services.ts | 2 +- apps/meteor/ee/server/startup/upsell.ts | 2 +- ...getInstallationSourceFromAppStorageItem.ts | 4 +- apps/meteor/server/startup/migrations/v278.ts | 2 +- ee/packages/license/src/actionBlockers.ts | 18 -- ee/packages/license/src/data.ts | 26 -- ee/packages/license/src/deprecated.ts | 13 +- ee/packages/license/src/encryptedLicense.ts | 8 - ee/packages/license/src/fairPolicy.ts | 3 - ee/packages/license/src/index.ts | 132 +++++++-- ee/packages/license/src/license.ts | 256 +++++++++++++----- ee/packages/license/src/pendingLicense.ts | 21 +- .../src/validation/processValidationResult.ts | 31 --- .../src/validation/shouldPreventAction.ts | 17 -- .../license/src/validation/validateLicense.ts | 19 -- 56 files changed, 377 insertions(+), 289 deletions(-) delete mode 100644 ee/packages/license/src/actionBlockers.ts delete mode 100644 ee/packages/license/src/data.ts delete mode 100644 ee/packages/license/src/encryptedLicense.ts delete mode 100644 ee/packages/license/src/fairPolicy.ts delete mode 100644 ee/packages/license/src/validation/processValidationResult.ts delete mode 100644 ee/packages/license/src/validation/shouldPreventAction.ts delete mode 100644 ee/packages/license/src/validation/validateLicense.ts diff --git a/apps/meteor/app/api/server/v1/federation.ts b/apps/meteor/app/api/server/v1/federation.ts index c28cb5ad0f61..7be5b1fc13fe 100644 --- a/apps/meteor/app/api/server/v1/federation.ts +++ b/apps/meteor/app/api/server/v1/federation.ts @@ -1,5 +1,5 @@ import { Federation, FederationEE } from '@rocket.chat/core-services'; -import * as License from '@rocket.chat/license'; +import { License } from '@rocket.chat/license'; import { isFederationVerifyMatrixIdProps } from '@rocket.chat/rest-typings'; import { API } from '../api'; diff --git a/apps/meteor/client/views/hooks/useUpgradeTabParams.ts b/apps/meteor/client/views/hooks/useUpgradeTabParams.ts index 88e21f668e02..65dd4cb1e396 100644 --- a/apps/meteor/client/views/hooks/useUpgradeTabParams.ts +++ b/apps/meteor/client/views/hooks/useUpgradeTabParams.ts @@ -1,4 +1,4 @@ -import type * as License from '@rocket.chat/license'; +import type { ILicenseV2, ILicenseV3 } from '@rocket.chat/license'; import { useSetting } from '@rocket.chat/ui-contexts'; import { format } from 'date-fns'; @@ -17,7 +17,7 @@ export const useUpgradeTabParams = (): { tabType: UpgradeTabVariant | false; tri const hasValidLicense = licensesData?.licenses.some((license) => license.modules.length > 0) ?? false; const hadExpiredTrials = cloudWorkspaceHadTrial ?? false; - const licenses = (licensesData?.licenses || []) as (Partial & { modules: string[] })[]; + const licenses = (licensesData?.licenses || []) as (Partial & { modules: string[] })[]; const trialLicense = licenses.find(({ meta, information }) => information?.trial ?? meta?.trial); const isTrial = Boolean(trialLicense); diff --git a/apps/meteor/ee/app/api-enterprise/server/index.ts b/apps/meteor/ee/app/api-enterprise/server/index.ts index 2efa48e7e2c8..7a528a4ec2f4 100644 --- a/apps/meteor/ee/app/api-enterprise/server/index.ts +++ b/apps/meteor/ee/app/api-enterprise/server/index.ts @@ -1,4 +1,4 @@ -import * as License from '@rocket.chat/license'; +import { License } from '@rocket.chat/license'; await License.onLicense('canned-responses', async () => { await import('./canned-responses'); diff --git a/apps/meteor/ee/app/authorization/server/validateUserRoles.js b/apps/meteor/ee/app/authorization/server/validateUserRoles.js index f13ff55ce726..c51482cf1386 100644 --- a/apps/meteor/ee/app/authorization/server/validateUserRoles.js +++ b/apps/meteor/ee/app/authorization/server/validateUserRoles.js @@ -1,4 +1,4 @@ -import * as License from '@rocket.chat/license'; +import { License } from '@rocket.chat/license'; import { Users } from '@rocket.chat/models'; import { Meteor } from 'meteor/meteor'; diff --git a/apps/meteor/ee/app/canned-responses/server/index.ts b/apps/meteor/ee/app/canned-responses/server/index.ts index 5976689f0b20..99254b037380 100644 --- a/apps/meteor/ee/app/canned-responses/server/index.ts +++ b/apps/meteor/ee/app/canned-responses/server/index.ts @@ -1,4 +1,4 @@ -import * as License from '@rocket.chat/license'; +import { License } from '@rocket.chat/license'; await License.onLicense('canned-responses', async () => { const { createSettings } = await import('./settings'); diff --git a/apps/meteor/ee/app/license/server/canEnableApp.ts b/apps/meteor/ee/app/license/server/canEnableApp.ts index b49f69d4bc08..72220e27acad 100644 --- a/apps/meteor/ee/app/license/server/canEnableApp.ts +++ b/apps/meteor/ee/app/license/server/canEnableApp.ts @@ -1,6 +1,6 @@ import type { IAppStorageItem } from '@rocket.chat/apps-engine/server/storage'; import { Apps } from '@rocket.chat/core-services'; -import * as License from '@rocket.chat/license'; +import { License } from '@rocket.chat/license'; import { getInstallationSourceFromAppStorageItem } from '../../../../lib/apps/getInstallationSourceFromAppStorageItem'; @@ -18,8 +18,8 @@ export const canEnableApp = async (app: IAppStorageItem): Promise => { const source = getInstallationSourceFromAppStorageItem(app); switch (source) { case 'private': - return !(await License.preventNewPrivateApps()); + return !(await License.shouldPreventAction('privateApps')); default: - return !(await License.preventNewMarketplaceApps()); + return !(await License.shouldPreventAction('marketplaceApps')); } }; diff --git a/apps/meteor/ee/app/license/server/getStatistics.ts b/apps/meteor/ee/app/license/server/getStatistics.ts index 66b05167a8fb..e8ff402ea1ca 100644 --- a/apps/meteor/ee/app/license/server/getStatistics.ts +++ b/apps/meteor/ee/app/license/server/getStatistics.ts @@ -1,7 +1,7 @@ import { log } from 'console'; import { Analytics } from '@rocket.chat/core-services'; -import * as License from '@rocket.chat/license'; +import { License } from '@rocket.chat/license'; import { CannedResponse, OmnichannelServiceLevelAgreements, LivechatRooms, LivechatTag, LivechatUnit, Users } from '@rocket.chat/models'; type ENTERPRISE_STATISTICS = GenericStats & Partial; diff --git a/apps/meteor/ee/app/license/server/lib/getAppCount.ts b/apps/meteor/ee/app/license/server/lib/getAppCount.ts index 9a87cb78a481..a05813f596bb 100644 --- a/apps/meteor/ee/app/license/server/lib/getAppCount.ts +++ b/apps/meteor/ee/app/license/server/lib/getAppCount.ts @@ -1,9 +1,9 @@ import { Apps } from '@rocket.chat/core-services'; -import type * as License from '@rocket.chat/license'; +import type { LicenseAppSources } from '@rocket.chat/license'; import { getInstallationSourceFromAppStorageItem } from '../../../../../lib/apps/getInstallationSourceFromAppStorageItem'; -export async function getAppCount(source: License.LicenseAppSources): Promise { +export async function getAppCount(source: LicenseAppSources): Promise { if (!(await Apps.isInitialized())) { return 0; } diff --git a/apps/meteor/ee/app/license/server/license.internalService.ts b/apps/meteor/ee/app/license/server/license.internalService.ts index b3e14fa7502c..9036a9b1848c 100644 --- a/apps/meteor/ee/app/license/server/license.internalService.ts +++ b/apps/meteor/ee/app/license/server/license.internalService.ts @@ -1,6 +1,6 @@ import type { ILicense } from '@rocket.chat/core-services'; import { api, ServiceClassInternal } from '@rocket.chat/core-services'; -import * as License from '@rocket.chat/license'; +import { License, type LicenseModule } from '@rocket.chat/license'; import { guestPermissions } from '../../authorization/lib/guestPermissions'; import { resetEnterprisePermissions } from '../../authorization/server/resetEnterprisePermissions'; @@ -34,7 +34,7 @@ export class LicenseService extends ServiceClassInternal implements ILicense { await resetEnterprisePermissions(); } - hasModule(feature: License.LicenseModule): boolean { + hasModule(feature: LicenseModule): boolean { return License.hasModule(feature); } diff --git a/apps/meteor/ee/app/license/server/methods.ts b/apps/meteor/ee/app/license/server/methods.ts index d5f82536bf37..39d14326a79a 100644 --- a/apps/meteor/ee/app/license/server/methods.ts +++ b/apps/meteor/ee/app/license/server/methods.ts @@ -1,4 +1,4 @@ -import * as License from '@rocket.chat/license'; +import { License, type ILicenseTag, type LicenseModule } from '@rocket.chat/license'; import type { ServerMethods } from '@rocket.chat/ui-contexts'; import { check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; @@ -8,7 +8,7 @@ declare module '@rocket.chat/ui-contexts' { interface ServerMethods { 'license:hasLicense'(feature: string): boolean; 'license:getModules'(): string[]; - 'license:getTags'(): License.ILicenseTag[]; + 'license:getTags'(): ILicenseTag[]; 'license:isEnterprise'(): boolean; } } @@ -17,7 +17,7 @@ Meteor.methods({ 'license:hasLicense'(feature: string) { check(feature, String); - return License.hasModule(feature as License.LicenseModule); + return License.hasModule(feature as LicenseModule); }, 'license:getModules'() { return License.getModules(); diff --git a/apps/meteor/ee/app/license/server/settings.ts b/apps/meteor/ee/app/license/server/settings.ts index 9b328ecb2031..c332142d286d 100644 --- a/apps/meteor/ee/app/license/server/settings.ts +++ b/apps/meteor/ee/app/license/server/settings.ts @@ -1,4 +1,4 @@ -import * as License from '@rocket.chat/license'; +import { License } from '@rocket.chat/license'; import { Settings } from '@rocket.chat/models'; import { Meteor } from 'meteor/meteor'; diff --git a/apps/meteor/ee/app/license/server/startup.ts b/apps/meteor/ee/app/license/server/startup.ts index 53f50d8048df..7da7917211be 100644 --- a/apps/meteor/ee/app/license/server/startup.ts +++ b/apps/meteor/ee/app/license/server/startup.ts @@ -1,4 +1,4 @@ -import * as License from '@rocket.chat/license'; +import { License } from '@rocket.chat/license'; import { Subscriptions, Users } from '@rocket.chat/models'; import { settings } from '../../../../app/settings/server'; diff --git a/apps/meteor/ee/app/livechat-enterprise/server/business-hour/Helper.ts b/apps/meteor/ee/app/livechat-enterprise/server/business-hour/Helper.ts index 6ef0364e4eed..a441e122ef99 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/business-hour/Helper.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/business-hour/Helper.ts @@ -1,6 +1,6 @@ import type { ILivechatBusinessHour } from '@rocket.chat/core-typings'; import { LivechatBusinessHourTypes } from '@rocket.chat/core-typings'; -import * as License from '@rocket.chat/license'; +import { License } from '@rocket.chat/license'; import { LivechatBusinessHours, LivechatDepartment, LivechatDepartmentAgents, Users } from '@rocket.chat/models'; import moment from 'moment-timezone'; diff --git a/apps/meteor/ee/app/livechat-enterprise/server/index.ts b/apps/meteor/ee/app/livechat-enterprise/server/index.ts index d80a112a9079..13676e7cbedb 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/index.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/index.ts @@ -1,4 +1,4 @@ -import * as License from '@rocket.chat/license'; +import { License } from '@rocket.chat/license'; import { Meteor } from 'meteor/meteor'; import './methods/addMonitor'; diff --git a/apps/meteor/ee/app/livechat-enterprise/server/lib/LivechatEnterprise.ts b/apps/meteor/ee/app/livechat-enterprise/server/lib/LivechatEnterprise.ts index b4de90489fc7..ec228e420abd 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/lib/LivechatEnterprise.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/lib/LivechatEnterprise.ts @@ -1,5 +1,5 @@ import type { IOmnichannelBusinessUnit, IOmnichannelServiceLevelAgreements, LivechatDepartmentDTO } from '@rocket.chat/core-typings'; -import * as License from '@rocket.chat/license'; +import { License } from '@rocket.chat/license'; import { Users, LivechatDepartment as LivechatDepartmentRaw, diff --git a/apps/meteor/ee/app/message-read-receipt/server/index.ts b/apps/meteor/ee/app/message-read-receipt/server/index.ts index 24e4057f1eb7..bb405c0eaffd 100644 --- a/apps/meteor/ee/app/message-read-receipt/server/index.ts +++ b/apps/meteor/ee/app/message-read-receipt/server/index.ts @@ -1,4 +1,4 @@ -import * as License from '@rocket.chat/license'; +import { License } from '@rocket.chat/license'; await License.onLicense('message-read-receipt', async () => { await import('./hooks'); diff --git a/apps/meteor/ee/app/settings/server/settings.ts b/apps/meteor/ee/app/settings/server/settings.ts index 4c90420bd50d..76ce7c15155a 100644 --- a/apps/meteor/ee/app/settings/server/settings.ts +++ b/apps/meteor/ee/app/settings/server/settings.ts @@ -1,5 +1,5 @@ import type { ISetting, SettingValue } from '@rocket.chat/core-typings'; -import * as License from '@rocket.chat/license'; +import { License, type LicenseModule } from '@rocket.chat/license'; import { Settings } from '@rocket.chat/models'; import { Meteor } from 'meteor/meteor'; @@ -20,7 +20,7 @@ export function changeSettingValue(record: ISetting): SettingValue { } for (const moduleName of record.modules) { - if (!License.hasModule(moduleName as License.LicenseModule)) { + if (!License.hasModule(moduleName as LicenseModule)) { return record.invalidValue; } } diff --git a/apps/meteor/ee/app/voip-enterprise/server/services/voipService.ts b/apps/meteor/ee/app/voip-enterprise/server/services/voipService.ts index d083fe882ef6..f5524ee026dc 100644 --- a/apps/meteor/ee/app/voip-enterprise/server/services/voipService.ts +++ b/apps/meteor/ee/app/voip-enterprise/server/services/voipService.ts @@ -1,5 +1,5 @@ import type { ILivechatAgent, ILivechatVisitor, IVoipRoomClosingInfo, IUser, IVoipRoom } from '@rocket.chat/core-typings'; -import * as License from '@rocket.chat/license'; +import { License } from '@rocket.chat/license'; import type { IOmniRoomClosingMessage } from '../../../../../server/services/omnichannel-voip/internalTypes'; import { OmnichannelVoipService } from '../../../../../server/services/omnichannel-voip/service'; diff --git a/apps/meteor/ee/client/hooks/useHasLicenseModule.ts b/apps/meteor/ee/client/hooks/useHasLicenseModule.ts index 012324ba9704..c7d76b093c3b 100644 --- a/apps/meteor/ee/client/hooks/useHasLicenseModule.ts +++ b/apps/meteor/ee/client/hooks/useHasLicenseModule.ts @@ -1,8 +1,8 @@ -import type * as License from '@rocket.chat/license'; +import type { LicenseModule } from '@rocket.chat/license'; import { useMethod, useUserId } from '@rocket.chat/ui-contexts'; import { useQuery } from '@tanstack/react-query'; -export const useHasLicenseModule = (licenseName: License.LicenseModule): 'loading' | boolean => { +export const useHasLicenseModule = (licenseName: LicenseModule): 'loading' | boolean => { const method = useMethod('license:getModules'); const uid = useUserId(); diff --git a/apps/meteor/ee/client/lib/onToggledFeature.ts b/apps/meteor/ee/client/lib/onToggledFeature.ts index 4319f2b268f8..ae2e4ad9f4a8 100644 --- a/apps/meteor/ee/client/lib/onToggledFeature.ts +++ b/apps/meteor/ee/client/lib/onToggledFeature.ts @@ -1,11 +1,11 @@ -import type * as License from '@rocket.chat/license'; +import type { LicenseModule } from '@rocket.chat/license'; import { QueryObserver } from '@tanstack/react-query'; import { queryClient } from '../../../client/lib/queryClient'; import { fetchFeatures } from './fetchFeatures'; export const onToggledFeature = ( - feature: License.LicenseModule, + feature: LicenseModule, { up, down, diff --git a/apps/meteor/ee/server/api/api.ts b/apps/meteor/ee/server/api/api.ts index 61e62440e1c7..ee2049bb70ae 100644 --- a/apps/meteor/ee/server/api/api.ts +++ b/apps/meteor/ee/server/api/api.ts @@ -1,4 +1,4 @@ -import * as License from '@rocket.chat/license'; +import { License } from '@rocket.chat/license'; import { API } from '../../../app/api/server/api'; import type { NonEnterpriseTwoFactorOptions, Options } from '../../../app/api/server/definition'; diff --git a/apps/meteor/ee/server/api/chat.ts b/apps/meteor/ee/server/api/chat.ts index eb63ee4e1ce1..2c8f8c5ca605 100644 --- a/apps/meteor/ee/server/api/chat.ts +++ b/apps/meteor/ee/server/api/chat.ts @@ -1,5 +1,5 @@ import type { IMessage, ReadReceipt } from '@rocket.chat/core-typings'; -import * as License from '@rocket.chat/license'; +import { License } from '@rocket.chat/license'; import { Meteor } from 'meteor/meteor'; import { API } from '../../../app/api/server/api'; diff --git a/apps/meteor/ee/server/api/licenses.ts b/apps/meteor/ee/server/api/licenses.ts index acdbabcbddd8..767a1c4bd7bb 100644 --- a/apps/meteor/ee/server/api/licenses.ts +++ b/apps/meteor/ee/server/api/licenses.ts @@ -1,4 +1,4 @@ -import * as License from '@rocket.chat/license'; +import { License } from '@rocket.chat/license'; import { Settings, Users } from '@rocket.chat/models'; import { check } from 'meteor/check'; diff --git a/apps/meteor/ee/server/api/roles.ts b/apps/meteor/ee/server/api/roles.ts index eb8f762d58c8..c10c32c3ee1a 100644 --- a/apps/meteor/ee/server/api/roles.ts +++ b/apps/meteor/ee/server/api/roles.ts @@ -1,5 +1,5 @@ import type { IRole } from '@rocket.chat/core-typings'; -import * as License from '@rocket.chat/license'; +import { License } from '@rocket.chat/license'; import { Roles } from '@rocket.chat/models'; import Ajv from 'ajv'; diff --git a/apps/meteor/ee/server/api/sessions.ts b/apps/meteor/ee/server/api/sessions.ts index 8330a647f51d..cdd454fd5bee 100644 --- a/apps/meteor/ee/server/api/sessions.ts +++ b/apps/meteor/ee/server/api/sessions.ts @@ -1,5 +1,5 @@ import type { IUser, ISession, DeviceManagementSession, DeviceManagementPopulatedSession } from '@rocket.chat/core-typings'; -import * as License from '@rocket.chat/license'; +import { License } from '@rocket.chat/license'; import { Users, Sessions } from '@rocket.chat/models'; import type { PaginatedResult, PaginatedRequest } from '@rocket.chat/rest-typings'; import { escapeRegExp } from '@rocket.chat/string-helpers'; diff --git a/apps/meteor/ee/server/apps/communication/endpoints/appsCountHandler.ts b/apps/meteor/ee/server/apps/communication/endpoints/appsCountHandler.ts index 634d85f73258..fc436b8229cf 100644 --- a/apps/meteor/ee/server/apps/communication/endpoints/appsCountHandler.ts +++ b/apps/meteor/ee/server/apps/communication/endpoints/appsCountHandler.ts @@ -1,5 +1,5 @@ import type { AppManager } from '@rocket.chat/apps-engine/server/AppManager'; -import * as License from '@rocket.chat/license'; +import { License } from '@rocket.chat/license'; import { API } from '../../../../../app/api/server'; import type { SuccessResult } from '../../../../../app/api/server/definition'; diff --git a/apps/meteor/ee/server/apps/communication/rest.ts b/apps/meteor/ee/server/apps/communication/rest.ts index a492e1f0abc4..f356f3e45a18 100644 --- a/apps/meteor/ee/server/apps/communication/rest.ts +++ b/apps/meteor/ee/server/apps/communication/rest.ts @@ -3,7 +3,7 @@ import type { IAppInfo } from '@rocket.chat/apps-engine/definition/metadata'; import type { AppManager } from '@rocket.chat/apps-engine/server/AppManager'; import { AppInstallationSource } from '@rocket.chat/apps-engine/server/storage'; import type { IUser, IMessage } from '@rocket.chat/core-typings'; -import * as License from '@rocket.chat/license'; +import { License } from '@rocket.chat/license'; import { Settings, Users } from '@rocket.chat/models'; import { serverFetch as fetch } from '@rocket.chat/server-fetch'; import { Meteor } from 'meteor/meteor'; diff --git a/apps/meteor/ee/server/configuration/ldap.ts b/apps/meteor/ee/server/configuration/ldap.ts index ea74a8e6ffa7..5f1a84557d70 100644 --- a/apps/meteor/ee/server/configuration/ldap.ts +++ b/apps/meteor/ee/server/configuration/ldap.ts @@ -1,6 +1,6 @@ import type { IImportUser, ILDAPEntry, IUser } from '@rocket.chat/core-typings'; import { cronJobs } from '@rocket.chat/cron'; -import * as License from '@rocket.chat/license'; +import { License } from '@rocket.chat/license'; import { Meteor } from 'meteor/meteor'; import { settings } from '../../../app/settings/server'; diff --git a/apps/meteor/ee/server/configuration/oauth.ts b/apps/meteor/ee/server/configuration/oauth.ts index c6571dcd7143..aa66a46caf69 100644 --- a/apps/meteor/ee/server/configuration/oauth.ts +++ b/apps/meteor/ee/server/configuration/oauth.ts @@ -1,5 +1,5 @@ import type { IUser } from '@rocket.chat/core-typings'; -import * as License from '@rocket.chat/license'; +import { License } from '@rocket.chat/license'; import { Logger } from '@rocket.chat/logger'; import { Roles } from '@rocket.chat/models'; import { capitalize } from '@rocket.chat/string-helpers'; diff --git a/apps/meteor/ee/server/configuration/outlookCalendar.ts b/apps/meteor/ee/server/configuration/outlookCalendar.ts index 451a8fadd466..67c8d7945030 100644 --- a/apps/meteor/ee/server/configuration/outlookCalendar.ts +++ b/apps/meteor/ee/server/configuration/outlookCalendar.ts @@ -1,5 +1,5 @@ import { Calendar } from '@rocket.chat/core-services'; -import * as License from '@rocket.chat/license'; +import { License } from '@rocket.chat/license'; import { Meteor } from 'meteor/meteor'; import { addSettings } from '../settings/outlookCalendar'; diff --git a/apps/meteor/ee/server/configuration/saml.ts b/apps/meteor/ee/server/configuration/saml.ts index cbb3350118ff..96dca07829c6 100644 --- a/apps/meteor/ee/server/configuration/saml.ts +++ b/apps/meteor/ee/server/configuration/saml.ts @@ -1,4 +1,4 @@ -import * as License from '@rocket.chat/license'; +import { License } from '@rocket.chat/license'; import { Roles, Users } from '@rocket.chat/models'; import type { ISAMLUser } from '../../../app/meteor-accounts-saml/server/definition/ISAMLUser'; diff --git a/apps/meteor/ee/server/configuration/videoConference.ts b/apps/meteor/ee/server/configuration/videoConference.ts index 9a59ff850e6e..035110904840 100644 --- a/apps/meteor/ee/server/configuration/videoConference.ts +++ b/apps/meteor/ee/server/configuration/videoConference.ts @@ -1,7 +1,7 @@ import { VideoConf } from '@rocket.chat/core-services'; import type { IRoom, IUser, VideoConference } from '@rocket.chat/core-typings'; import { VideoConferenceStatus } from '@rocket.chat/core-typings'; -import * as License from '@rocket.chat/license'; +import { License } from '@rocket.chat/license'; import { Rooms, Subscriptions } from '@rocket.chat/models'; import { Meteor } from 'meteor/meteor'; diff --git a/apps/meteor/ee/server/lib/syncUserRoles.ts b/apps/meteor/ee/server/lib/syncUserRoles.ts index 82d0590c30ea..f3a380a8f228 100644 --- a/apps/meteor/ee/server/lib/syncUserRoles.ts +++ b/apps/meteor/ee/server/lib/syncUserRoles.ts @@ -1,6 +1,6 @@ import { api } from '@rocket.chat/core-services'; import type { IUser, IRole, AtLeast } from '@rocket.chat/core-typings'; -import * as License from '@rocket.chat/license'; +import { License } from '@rocket.chat/license'; import { Users } from '@rocket.chat/models'; import { settings } from '../../../app/settings/server'; @@ -72,7 +72,7 @@ export async function syncUserRoles( } const wasGuest = existingRoles.length === 1 && existingRoles[0] === 'guest'; - if (wasGuest && (await License.preventNewUsers())) { + if (wasGuest && (await License.shouldPreventAction('activeUsers'))) { throw new Error('error-license-user-limit-reached'); } diff --git a/apps/meteor/ee/server/methods/getReadReceipts.ts b/apps/meteor/ee/server/methods/getReadReceipts.ts index 867333b0145d..78fe8a4d967e 100644 --- a/apps/meteor/ee/server/methods/getReadReceipts.ts +++ b/apps/meteor/ee/server/methods/getReadReceipts.ts @@ -1,5 +1,5 @@ import type { ReadReceipt as ReadReceiptType, IMessage } from '@rocket.chat/core-typings'; -import * as License from '@rocket.chat/license'; +import { License } from '@rocket.chat/license'; import { Messages } from '@rocket.chat/models'; import type { ServerMethods } from '@rocket.chat/ui-contexts'; import { check } from 'meteor/check'; diff --git a/apps/meteor/ee/server/models/startup.ts b/apps/meteor/ee/server/models/startup.ts index 5b4085db44cf..4fd8433358ca 100644 --- a/apps/meteor/ee/server/models/startup.ts +++ b/apps/meteor/ee/server/models/startup.ts @@ -1,4 +1,4 @@ -import * as License from '@rocket.chat/license'; +import { License } from '@rocket.chat/license'; // To facilitate our lives with the stream // Collection will be registered on CE too diff --git a/apps/meteor/ee/server/startup/apps/trialExpiration.ts b/apps/meteor/ee/server/startup/apps/trialExpiration.ts index de3bb13c4464..eec50e91b7dd 100644 --- a/apps/meteor/ee/server/startup/apps/trialExpiration.ts +++ b/apps/meteor/ee/server/startup/apps/trialExpiration.ts @@ -1,4 +1,4 @@ -import * as License from '@rocket.chat/license'; +import { License } from '@rocket.chat/license'; import { Meteor } from 'meteor/meteor'; import { Apps } from '../../apps'; diff --git a/apps/meteor/ee/server/startup/audit.ts b/apps/meteor/ee/server/startup/audit.ts index 637336e66dad..c38794a7582e 100644 --- a/apps/meteor/ee/server/startup/audit.ts +++ b/apps/meteor/ee/server/startup/audit.ts @@ -1,4 +1,4 @@ -import * as License from '@rocket.chat/license'; +import { License } from '@rocket.chat/license'; import { createPermissions } from '../lib/audit/startup'; diff --git a/apps/meteor/ee/server/startup/deviceManagement.ts b/apps/meteor/ee/server/startup/deviceManagement.ts index 1de5cd24a222..2ad5fd3b8a4f 100644 --- a/apps/meteor/ee/server/startup/deviceManagement.ts +++ b/apps/meteor/ee/server/startup/deviceManagement.ts @@ -1,4 +1,4 @@ -import * as License from '@rocket.chat/license'; +import { License } from '@rocket.chat/license'; import { addSettings } from '../settings/deviceManagement'; diff --git a/apps/meteor/ee/server/startup/engagementDashboard.ts b/apps/meteor/ee/server/startup/engagementDashboard.ts index c54d86528009..ca5dda577bb0 100644 --- a/apps/meteor/ee/server/startup/engagementDashboard.ts +++ b/apps/meteor/ee/server/startup/engagementDashboard.ts @@ -1,4 +1,4 @@ -import * as License from '@rocket.chat/license'; +import { License } from '@rocket.chat/license'; import { Meteor } from 'meteor/meteor'; License.onToggledFeature('engagement-dashboard', { diff --git a/apps/meteor/ee/server/startup/maxRoomsPerGuest.ts b/apps/meteor/ee/server/startup/maxRoomsPerGuest.ts index af28fa986055..5731ca0d1deb 100644 --- a/apps/meteor/ee/server/startup/maxRoomsPerGuest.ts +++ b/apps/meteor/ee/server/startup/maxRoomsPerGuest.ts @@ -1,4 +1,4 @@ -import * as License from '@rocket.chat/license'; +import { License } from '@rocket.chat/license'; import { Meteor } from 'meteor/meteor'; import { callbacks } from '../../../lib/callbacks'; @@ -8,7 +8,7 @@ callbacks.add( 'beforeAddedToRoom', async ({ user }) => { if (user.roles?.includes('guest')) { - if (await License.preventNewGuestSubscriptions(user._id)) { + if (await License.shouldPreventAction('roomsPerGuest', { userId: user._id })) { throw new Meteor.Error('error-max-rooms-per-guest-reached', i18n.t('error-max-rooms-per-guest-reached')); } } diff --git a/apps/meteor/ee/server/startup/seatsCap.ts b/apps/meteor/ee/server/startup/seatsCap.ts index 590245319f0d..3938daff485b 100644 --- a/apps/meteor/ee/server/startup/seatsCap.ts +++ b/apps/meteor/ee/server/startup/seatsCap.ts @@ -1,5 +1,5 @@ import type { IUser } from '@rocket.chat/core-typings'; -import * as License from '@rocket.chat/license'; +import { License } from '@rocket.chat/license'; import { Users } from '@rocket.chat/models'; import { Meteor } from 'meteor/meteor'; import { throttle } from 'underscore'; @@ -22,7 +22,7 @@ callbacks.add( return; } - if (await License.preventNewUsers()) { + if (await License.shouldPreventAction('activeUsers')) { throw new Meteor.Error('error-license-user-limit-reached', i18n.t('error-license-user-limit-reached')); } }, @@ -33,7 +33,7 @@ callbacks.add( callbacks.add( 'beforeUserImport', async ({ userCount }) => { - if (await License.preventNewUsers(userCount)) { + if (await License.shouldPreventAction('activeUsers', {}, userCount)) { throw new Meteor.Error('error-license-user-limit-reached', i18n.t('error-license-user-limit-reached')); } }, @@ -52,7 +52,7 @@ callbacks.add( return; } - if (await License.preventNewUsers()) { + if (await License.shouldPreventAction('activeUsers')) { throw new Meteor.Error('error-license-user-limit-reached', i18n.t('error-license-user-limit-reached')); } }, diff --git a/apps/meteor/ee/server/startup/services.ts b/apps/meteor/ee/server/startup/services.ts index ac82fe5e754f..37aec21bfe56 100644 --- a/apps/meteor/ee/server/startup/services.ts +++ b/apps/meteor/ee/server/startup/services.ts @@ -1,5 +1,5 @@ import { api } from '@rocket.chat/core-services'; -import * as License from '@rocket.chat/license'; +import { License } from '@rocket.chat/license'; import { isRunningMs } from '../../../server/lib/isRunningMs'; import { FederationService } from '../../../server/services/federation/service'; diff --git a/apps/meteor/ee/server/startup/upsell.ts b/apps/meteor/ee/server/startup/upsell.ts index ada354c4fb87..b31bf0635060 100644 --- a/apps/meteor/ee/server/startup/upsell.ts +++ b/apps/meteor/ee/server/startup/upsell.ts @@ -1,4 +1,4 @@ -import * as License from '@rocket.chat/license'; +import { License } from '@rocket.chat/license'; import { Settings } from '@rocket.chat/models'; import { Meteor } from 'meteor/meteor'; diff --git a/apps/meteor/lib/apps/getInstallationSourceFromAppStorageItem.ts b/apps/meteor/lib/apps/getInstallationSourceFromAppStorageItem.ts index f70a3c1456ee..8ac29d191576 100644 --- a/apps/meteor/lib/apps/getInstallationSourceFromAppStorageItem.ts +++ b/apps/meteor/lib/apps/getInstallationSourceFromAppStorageItem.ts @@ -1,5 +1,5 @@ import type { IAppStorageItem } from '@rocket.chat/apps-engine/server/storage'; -import type * as License from '@rocket.chat/license'; +import type { LicenseAppSources } from '@rocket.chat/license'; /** * There have been reports of apps not being correctly migrated from versions prior to 6.0 @@ -7,6 +7,6 @@ import type * as License from '@rocket.chat/license'; * This function is a workaround to get the installation source of an app from the app storage item * even if the installationSource property is not set. */ -export function getInstallationSourceFromAppStorageItem(item: IAppStorageItem): License.LicenseAppSources { +export function getInstallationSourceFromAppStorageItem(item: IAppStorageItem): LicenseAppSources { return item.installationSource || ('marketplaceInfo' in item ? 'marketplace' : 'private'); } diff --git a/apps/meteor/server/startup/migrations/v278.ts b/apps/meteor/server/startup/migrations/v278.ts index 3504e21a7cc1..068d86499ff9 100644 --- a/apps/meteor/server/startup/migrations/v278.ts +++ b/apps/meteor/server/startup/migrations/v278.ts @@ -1,4 +1,4 @@ -import * as License from '@rocket.chat/license'; +import { License } from '@rocket.chat/license'; import { Banners, Settings } from '@rocket.chat/models'; import { settings } from '../../../app/settings/server'; diff --git a/ee/packages/license/src/actionBlockers.ts b/ee/packages/license/src/actionBlockers.ts deleted file mode 100644 index ad170ad29250..000000000000 --- a/ee/packages/license/src/actionBlockers.ts +++ /dev/null @@ -1,18 +0,0 @@ -import type { IUser } from '@rocket.chat/core-typings'; - -import { shouldPreventAction } from './validation/shouldPreventAction'; - -export const preventNewUsers = async (userCount = 1) => shouldPreventAction('activeUsers', {}, userCount); -export const preventNewGuests = async (guestCount = 1) => shouldPreventAction('guestUsers', {}, guestCount); -export const preventNewPrivateApps = async (appCount = 1) => shouldPreventAction('privateApps', {}, appCount); -export const preventNewMarketplaceApps = async (appCount = 1) => shouldPreventAction('marketplaceApps', {}, appCount); -export const preventNewGuestSubscriptions = async (guest: IUser['_id'], roomCount = 1) => - shouldPreventAction('roomsPerGuest', { userId: guest }, roomCount); -export const preventNewActiveContacts = async (contactCount = 1) => shouldPreventAction('monthlyActiveContacts', {}, contactCount); - -export const userLimitReached = async () => preventNewUsers(0); -export const guestLimitReached = async () => preventNewGuests(0); -export const privateAppLimitReached = async () => preventNewPrivateApps(0); -export const marketplaceAppLimitReached = async () => preventNewMarketplaceApps(0); -export const guestSubscriptionLimitReached = async (guest: IUser['_id']) => preventNewGuestSubscriptions(guest, 0); -export const macLimitReached = async () => preventNewActiveContacts(0); diff --git a/ee/packages/license/src/data.ts b/ee/packages/license/src/data.ts deleted file mode 100644 index d640f1a26045..000000000000 --- a/ee/packages/license/src/data.ts +++ /dev/null @@ -1,26 +0,0 @@ -import type { ILicenseV2 } from './definition/ILicenseV2'; -import type { ILicenseV3 } from './definition/ILicenseV3'; - -export const licenseData: { - license: ILicenseV3 | undefined; - unmodifiedLicense: ILicenseV2 | ILicenseV3 | undefined; - valid: boolean | undefined; - inFairPolicy: boolean | undefined; - pendingLicense: string; - lockedLicense: string | undefined; -} = { - license: undefined, - unmodifiedLicense: undefined, - valid: undefined, - inFairPolicy: undefined, - pendingLicense: '', - lockedLicense: undefined, -}; - -export const clearLicenseData = () => { - licenseData.license = undefined; - licenseData.unmodifiedLicense = undefined; - licenseData.inFairPolicy = undefined; - licenseData.valid = false; - licenseData.pendingLicense = ''; -}; diff --git a/ee/packages/license/src/deprecated.ts b/ee/packages/license/src/deprecated.ts index 9ffc128dda83..7eff3d6ec52c 100644 --- a/ee/packages/license/src/deprecated.ts +++ b/ee/packages/license/src/deprecated.ts @@ -1,10 +1,11 @@ -import { licenseData } from './data'; import type { LicenseLimitKind } from './definition/ILicenseV3'; -import { getLicense } from './license'; +import { LicenseManager } from './license'; import { getModules } from './modules'; const getLicenseLimit = (kind: LicenseLimitKind) => { - const license = getLicense(); + const manager = LicenseManager.getLicenseManager(); + + const license = manager.getLicense(); if (!license) { return; } @@ -27,9 +28,11 @@ export const getAppsConfig = () => ({ }); export const getUnmodifiedLicenseAndModules = () => { - if (licenseData.valid && licenseData.unmodifiedLicense) { + const manager = LicenseManager.getLicenseManager(); + + if (manager.valid && manager.unmodifiedLicense) { return { - license: licenseData.unmodifiedLicense, + license: manager.unmodifiedLicense, modules: getModules(), }; } diff --git a/ee/packages/license/src/encryptedLicense.ts b/ee/packages/license/src/encryptedLicense.ts deleted file mode 100644 index b18b25ea8929..000000000000 --- a/ee/packages/license/src/encryptedLicense.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { licenseData } from './data'; - -export const lockLicense = (encryptedLicense: string) => { - licenseData.lockedLicense = encryptedLicense; -}; - -export const isLicenseDuplicate = (encryptedLicense: string) => - Boolean(licenseData.lockedLicense && licenseData.lockedLicense === encryptedLicense); diff --git a/ee/packages/license/src/fairPolicy.ts b/ee/packages/license/src/fairPolicy.ts deleted file mode 100644 index 66ac8a19ebd5..000000000000 --- a/ee/packages/license/src/fairPolicy.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { licenseData } from './data'; - -export const startedFairPolicy = () => Boolean(licenseData.inFairPolicy); diff --git a/ee/packages/license/src/index.ts b/ee/packages/license/src/index.ts index 075c2a496949..a6ef8eeeb806 100644 --- a/ee/packages/license/src/index.ts +++ b/ee/packages/license/src/index.ts @@ -1,9 +1,21 @@ -import { licenseData } from './data'; +import type { LicenseLimitKind } from './definition/ILicenseV3'; +import type { LimitContext } from './definition/LimitContext'; +import { getAppsConfig, getMaxActiveUsers, getUnmodifiedLicenseAndModules } from './deprecated'; +import { onLicense } from './events/deprecated'; +import { + onInvalidFeature, + onInvalidateLicense, + onLimitReached, + onModule, + onToggledFeature, + onValidFeature, + onValidateLicense, +} from './events/listeners'; import { overwriteClassOnLicense } from './events/overwriteClassOnLicense'; -import { getLicense, hasValidLicense, setLicense } from './license'; -import { modules as modulesData, hasModule, getModules } from './modules'; -import { tags as tagsData, getTags } from './tags'; -import { setLicenseLimitCounter, getCurrentValueForLicenseLimit } from './validation/getCurrentValueForLicenseLimit'; +import { LicenseManager } from './license'; +import { getModules, hasModule } from './modules'; +import { getTags } from './tags'; +import { getCurrentValueForLicenseLimit, setLicenseLimitCounter } from './validation/getCurrentValueForLicenseLimit'; import { validateFormat } from './validation/validateFormat'; import { setWorkspaceUrl } from './workspaceUrl'; @@ -16,28 +28,88 @@ export * from './definition/LicenseModule'; export * from './definition/LicensePeriod'; export * from './definition/LimitContext'; -export * from './events/deprecated'; -export * from './events/listeners'; -export * from './deprecated'; -export * from './actionBlockers'; - -const { modules, tags, data } = process.env.TEST_MODE - ? { modules: modulesData, tags: tagsData, data: licenseData } - : { modules: undefined, tags: undefined, data: undefined }; - -export { - setLicense, - validateFormat, - setWorkspaceUrl, - hasModule, - hasValidLicense, - getLicense, - modules, - getModules, - tags, - getTags, - overwriteClassOnLicense, - setLicenseLimitCounter, - getCurrentValueForLicenseLimit, - data, -}; +export class License extends LicenseManager { + public static validateFormat(...args: Parameters) { + return validateFormat(...args); + } + + public static async setWorkspaceUrl(...args: Parameters) { + return setWorkspaceUrl(...args); + } + + public static hasModule(...args: Parameters) { + return hasModule(...args); + } + + public static getModules(...args: Parameters) { + return getModules(...args); + } + + public static getTags(...args: Parameters) { + return getTags(...args); + } + + public static async overwriteClassOnLicense(...args: Parameters) { + return overwriteClassOnLicense(...args); + } + + public static setLicenseLimitCounter(...args: Parameters) { + return setLicenseLimitCounter(...args); + } + + public static async getCurrentValueForLicenseLimit(...args: Parameters) { + return getCurrentValueForLicenseLimit(...args); + } + + public static async isLimitReached(action: T, context?: Partial>) { + return this.shouldPreventAction(action, context, 0); + } + + public static onValidFeature(...args: Parameters) { + return onValidFeature(...args); + } + + public static onInvalidFeature(...args: Parameters) { + return onInvalidFeature(...args); + } + + public static onToggledFeature(...args: Parameters) { + return onToggledFeature(...args); + } + + public static onModule(...args: Parameters) { + return onModule(...args); + } + + public static onValidateLicense(...args: Parameters) { + return onValidateLicense(...args); + } + + public static onInvalidateLicense(...args: Parameters) { + return onInvalidateLicense(...args); + } + + public static onLimitReached(...args: Parameters) { + return onLimitReached(...args); + } + + // Deprecated: + public static onLicense(...args: Parameters) { + return onLicense(...args); + } + + // Deprecated: + public static getMaxActiveUsers() { + return getMaxActiveUsers(); + } + + // Deprecated: + public static getAppsConfig() { + return getAppsConfig(); + } + + // Deprecated: + public static getUnmodifiedLicenseAndModules() { + return getUnmodifiedLicenseAndModules(); + } +} diff --git a/ee/packages/license/src/license.ts b/ee/packages/license/src/license.ts index c134d168e27b..768f3684669c 100644 --- a/ee/packages/license/src/license.ts +++ b/ee/packages/license/src/license.ts @@ -1,93 +1,227 @@ -import { clearLicenseData, licenseData } from './data'; import decrypt from './decrypt'; import type { ILicenseV2 } from './definition/ILicenseV2'; -import type { ILicenseV3 } from './definition/ILicenseV3'; -import { isLicenseDuplicate, lockLicense } from './encryptedLicense'; -import { licenseRemoved } from './events/emitter'; +import type { ILicenseV3, LicenseLimitKind } from './definition/ILicenseV3'; +import type { BehaviorWithContext } from './definition/LicenseBehavior'; +import type { LimitContext } from './definition/LimitContext'; +import { licenseRemoved, licenseValidated } from './events/emitter'; import { logger } from './logger'; -import { invalidateAll } from './modules'; +import { invalidateAll, replaceModules } from './modules'; import { clearPendingLicense, hasPendingLicense, isPendingLicense, setPendingLicense } from './pendingLicense'; +import { showLicense } from './showLicense'; +import { replaceTags } from './tags'; import { convertToV3 } from './v2/convertToV3'; +import { getCurrentValueForLicenseLimit } from './validation/getCurrentValueForLicenseLimit'; +import { getModulesToDisable } from './validation/getModulesToDisable'; +import { isBehaviorsInResult } from './validation/isBehaviorsInResult'; import { isReadyForValidation } from './validation/isReadyForValidation'; +import { runValidation } from './validation/runValidation'; import { validateFormat } from './validation/validateFormat'; -import { validateLicense } from './validation/validateLicense'; - -const setLicenseV3 = async (newLicense: ILicenseV3, encryptedLicense: string, originalLicense?: ILicenseV2 | ILicenseV3) => { - const hadValidLicense = hasValidLicense(); - clearLicenseData(); - - try { - licenseData.unmodifiedLicense = originalLicense || newLicense; - licenseData.license = newLicense; - - await validateLicense(); - lockLicense(encryptedLicense); - } finally { - if (hadValidLicense && !hasValidLicense()) { - licenseRemoved(); - invalidateAll(); + +let licenseManager: LicenseManager | undefined; + +export class LicenseManager { + private _license: ILicenseV3 | undefined; + + private _unmodifiedLicense: ILicenseV2 | ILicenseV3 | undefined; + + private _valid: boolean | undefined; + + private _inFairPolicy: boolean | undefined; + + private _lockedLicense: string | undefined; + + public static getLicenseManager(): LicenseManager { + if (!licenseManager) { + licenseManager = new LicenseManager(); } + + return licenseManager; } -}; -const setLicenseV2 = async (newLicense: ILicenseV2, encryptedLicense: string) => - setLicenseV3(convertToV3(newLicense), encryptedLicense, newLicense); + public static async setLicense(encryptedLicense: string): Promise { + return this.getLicenseManager().setLicense(encryptedLicense); + } -export const setLicense = async (encryptedLicense: string, forceSet = false): Promise => { - if (!encryptedLicense || String(encryptedLicense).trim() === '') { - return false; + public static hasValidLicense(): boolean { + return this.getLicenseManager().hasValidLicense(); } - if (isLicenseDuplicate(encryptedLicense)) { - // If there is a pending license but the user is trying to revert to the license that is currently active - if (hasPendingLicense() && !isPendingLicense(encryptedLicense)) { - // simply remove the pending license - clearPendingLicense(); - return true; + public static getLicense(): ILicenseV3 | undefined { + return this.getLicenseManager().getLicense(); + } + + public static async shouldPreventAction( + action: T, + context?: Partial>, + newCount = 1, + ): Promise { + return this.getLicenseManager().shouldPreventAction(action, context, newCount); + } + + public get license(): ILicenseV3 | undefined { + return this._license; + } + + public get unmodifiedLicense(): ILicenseV2 | ILicenseV3 | undefined { + return this._unmodifiedLicense; + } + + public get valid(): boolean | undefined { + return this._valid; + } + + public get inFairPolicy(): boolean { + return Boolean(this._inFairPolicy); + } + + private clearLicenseData(): void { + this._license = undefined; + this._unmodifiedLicense = undefined; + this._inFairPolicy = undefined; + this._valid = false; + this._lockedLicense = undefined; + clearPendingLicense(); + } + + private async setLicenseV3(newLicense: ILicenseV3, encryptedLicense: string, originalLicense?: ILicenseV2 | ILicenseV3): Promise { + const hadValidLicense = this.hasValidLicense(); + this.clearLicenseData(); + + try { + this._unmodifiedLicense = originalLicense || newLicense; + this._license = newLicense; + + await this.validateLicense(); + this._lockedLicense = encryptedLicense; + } finally { + if (hadValidLicense && !this.hasValidLicense()) { + licenseRemoved(); + invalidateAll(); + } } + } - return false; + private async setLicenseV2(newLicense: ILicenseV2, encryptedLicense: string): Promise { + return this.setLicenseV3(convertToV3(newLicense), encryptedLicense, newLicense); } - if (!isReadyForValidation() && !forceSet) { - // If we can't validate the license data yet, but is a valid license string, store it to validate when we can - if (validateFormat(encryptedLicense)) { - setPendingLicense(encryptedLicense); - return true; + private isLicenseDuplicate(encryptedLicense: string): boolean { + return Boolean(this._lockedLicense && this._lockedLicense === encryptedLicense); + } + + private processValidationResult(result: BehaviorWithContext[]): void { + if (!this._license || isBehaviorsInResult(result, ['invalidate_license', 'prevent_installation'])) { + return; + } + + this._valid = true; + this._inFairPolicy = isBehaviorsInResult(result, ['start_fair_policy']); + + if (this._license.information.tags) { + replaceTags(this._license.information.tags); + } + + const disabledModules = getModulesToDisable(result); + const modulesToEnable = this._license.grantedModules.filter(({ module }) => !disabledModules.includes(module)); + + replaceModules(modulesToEnable.map(({ module }) => module)); + logger.log({ msg: 'License validated', modules: modulesToEnable }); + + licenseValidated(); + showLicense(this._license, this._valid); + } + + private async validateLicense(): Promise { + if (!this._license || !isReadyForValidation()) { + return; } - return false; + // #TODO: Only include 'prevent_installation' here if this is actually the initial installation of the license + const validationResult = await runValidation(this._license, [ + 'invalidate_license', + 'prevent_installation', + 'start_fair_policy', + 'disable_modules', + ]); + this.processValidationResult(validationResult); } - logger.info('New Enterprise License'); - try { - const decrypted = decrypt(encryptedLicense); - if (!decrypted) { + public async setLicense(encryptedLicense: string): Promise { + if (!encryptedLicense || String(encryptedLicense).trim() === '') { + return false; + } + + if (this.isLicenseDuplicate(encryptedLicense)) { + // If there is a pending license but the user is trying to revert to the license that is currently active + if (hasPendingLicense() && !isPendingLicense(encryptedLicense)) { + // simply remove the pending license + clearPendingLicense(); + return true; + } + return false; } - if (process.env.LICENSE_DEBUG && process.env.LICENSE_DEBUG !== 'false') { - logger.debug({ msg: 'license', decrypted }); + if (!isReadyForValidation()) { + // If we can't validate the license data yet, but is a valid license string, store it to validate when we can + if (validateFormat(encryptedLicense)) { + setPendingLicense(encryptedLicense); + return true; + } + + return false; + } + + logger.info('New Enterprise License'); + try { + const decrypted = decrypt(encryptedLicense); + if (!decrypted) { + return false; + } + + if (process.env.LICENSE_DEBUG && process.env.LICENSE_DEBUG !== 'false') { + logger.debug({ msg: 'license', decrypted }); + } + + encryptedLicense.startsWith('RCV3_') + ? await this.setLicenseV3(JSON.parse(decrypted), encryptedLicense) + : await this.setLicenseV2(JSON.parse(decrypted), encryptedLicense); + + return true; + } catch (e) { + logger.error('Invalid license'); + if (process.env.LICENSE_DEBUG && process.env.LICENSE_DEBUG !== 'false') { + logger.error({ msg: 'Invalid raw license', encryptedLicense, e }); + } + return false; } + } - encryptedLicense.startsWith('RCV3_') - ? await setLicenseV3(JSON.parse(decrypted), encryptedLicense) - : await setLicenseV2(JSON.parse(decrypted), encryptedLicense); + public hasValidLicense(): boolean { + return Boolean(this._license && this._valid); + } - return true; - } catch (e) { - logger.error('Invalid license'); - if (process.env.LICENSE_DEBUG && process.env.LICENSE_DEBUG !== 'false') { - logger.error({ msg: 'Invalid raw license', encryptedLicense, e }); + public getLicense(): ILicenseV3 | undefined { + if (this._valid && this._license) { + return this._license; } - return false; } -}; -export const hasValidLicense = () => Boolean(licenseData.license && licenseData.valid); + public async shouldPreventAction( + action: T, + context?: Partial>, + newCount = 1, + ): Promise { + const license = this.getLicense(); + if (!license) { + return false; + } -export const getLicense = () => { - if (licenseData.valid && licenseData.license) { - return licenseData.license; + const currentValue = (await getCurrentValueForLicenseLimit(action, context)) + newCount; + return Boolean( + license.limits[action] + ?.filter(({ behavior, max }) => behavior === 'prevent_action' && max >= 0) + .some(({ max }) => max < currentValue), + ); } -}; +} diff --git a/ee/packages/license/src/pendingLicense.ts b/ee/packages/license/src/pendingLicense.ts index 6eb40eab2ac3..9d8177b3dcb4 100644 --- a/ee/packages/license/src/pendingLicense.ts +++ b/ee/packages/license/src/pendingLicense.ts @@ -1,29 +1,30 @@ -import { licenseData } from './data'; -import { setLicense } from './license'; +import { LicenseManager } from './license'; import { logger } from './logger'; +let pendingLicense: string; + export const setPendingLicense = (encryptedLicense: string) => { - licenseData.pendingLicense = encryptedLicense; - if (licenseData.pendingLicense) { + pendingLicense = encryptedLicense; + if (pendingLicense) { logger.info('Storing license as pending validation.'); } }; export const applyPendingLicense = async () => { - if (licenseData.pendingLicense) { + if (pendingLicense) { logger.info('Applying pending license.'); - await setLicense(licenseData.pendingLicense); + LicenseManager.setLicense(pendingLicense); } }; -export const hasPendingLicense = () => Boolean(licenseData.pendingLicense); +export const hasPendingLicense = () => Boolean(pendingLicense); -export const isPendingLicense = (encryptedLicense: string) => hasPendingLicense() && licenseData.pendingLicense === encryptedLicense; +export const isPendingLicense = (encryptedLicense: string) => !!pendingLicense && pendingLicense === encryptedLicense; export const clearPendingLicense = () => { - if (licenseData.pendingLicense) { + if (pendingLicense) { logger.info('Removing pending license.'); } - licenseData.pendingLicense = ''; + pendingLicense = ''; }; diff --git a/ee/packages/license/src/validation/processValidationResult.ts b/ee/packages/license/src/validation/processValidationResult.ts deleted file mode 100644 index 0ddac8a6eef9..000000000000 --- a/ee/packages/license/src/validation/processValidationResult.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { licenseData } from '../data'; -import type { BehaviorWithContext } from '../definition/LicenseBehavior'; -import { licenseValidated } from '../events/emitter'; -import { logger } from '../logger'; -import { replaceModules } from '../modules'; -import { showLicense } from '../showLicense'; -import { replaceTags } from '../tags'; -import { getModulesToDisable } from './getModulesToDisable'; -import { isBehaviorsInResult } from './isBehaviorsInResult'; - -export const processValidationResult = (result: BehaviorWithContext[]) => { - if (!licenseData.license || isBehaviorsInResult(result, ['invalidate_license', 'prevent_installation'])) { - return; - } - - licenseData.valid = true; - licenseData.inFairPolicy = isBehaviorsInResult(result, ['start_fair_policy']); - - if (licenseData.license.information.tags) { - replaceTags(licenseData.license.information.tags); - } - - const disabledModules = getModulesToDisable(result); - const modulesToEnable = licenseData.license.grantedModules.filter(({ module }) => !disabledModules.includes(module)); - - replaceModules(modulesToEnable.map(({ module }) => module)); - logger.log({ msg: 'License validated', modules: modulesToEnable }); - - licenseValidated(); - showLicense(licenseData.license, licenseData.valid); -}; diff --git a/ee/packages/license/src/validation/shouldPreventAction.ts b/ee/packages/license/src/validation/shouldPreventAction.ts deleted file mode 100644 index 8b8cc83d67ab..000000000000 --- a/ee/packages/license/src/validation/shouldPreventAction.ts +++ /dev/null @@ -1,17 +0,0 @@ -import type { LicenseLimitKind } from '../definition/ILicenseV3'; -import type { LimitContext } from '../definition/LimitContext'; -import { getLicense } from '../license'; -import { getCurrentValueForLicenseLimit } from './getCurrentValueForLicenseLimit'; - -export const shouldPreventAction = async ( - action: T, - context?: Partial>, - newCount = 1, -): Promise => { - const license = getLicense(); - - const currentValue = (await getCurrentValueForLicenseLimit(action, context)) + newCount; - return Boolean( - license?.limits[action]?.filter(({ behavior, max }) => behavior === 'prevent_action' && max >= 0).some(({ max }) => max < currentValue), - ); -}; diff --git a/ee/packages/license/src/validation/validateLicense.ts b/ee/packages/license/src/validation/validateLicense.ts deleted file mode 100644 index b5828247dd17..000000000000 --- a/ee/packages/license/src/validation/validateLicense.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { licenseData } from '../data'; -import { isReadyForValidation } from './isReadyForValidation'; -import { processValidationResult } from './processValidationResult'; -import { runValidation } from './runValidation'; - -export const validateLicense = async () => { - if (!licenseData.license || !isReadyForValidation()) { - return; - } - - // #TODO: Only include 'prevent_installation' here if this is actually the initial installation of the license - const validationResult = await runValidation(licenseData.license, [ - 'invalidate_license', - 'prevent_installation', - 'start_fair_policy', - 'disable_modules', - ]); - processValidationResult(validationResult); -}; From f0d4caee917420c4f84b94180d520c4f18335726 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Tue, 26 Sep 2023 12:04:50 -0300 Subject: [PATCH 27/38] adjust packages --- ee/packages/license/jest.config.ts | 17 +++++++++++++++++ ee/packages/license/package.json | 8 ++++++++ packages/jwt/package.json | 3 +++ packages/jwt/tsconfig.json | 1 + yarn.lock | 5 +++++ 5 files changed, 34 insertions(+) create mode 100644 ee/packages/license/jest.config.ts diff --git a/ee/packages/license/jest.config.ts b/ee/packages/license/jest.config.ts new file mode 100644 index 000000000000..f23cf0851e4c --- /dev/null +++ b/ee/packages/license/jest.config.ts @@ -0,0 +1,17 @@ +export default { + preset: 'ts-jest', + errorOnDeprecated: true, + modulePathIgnorePatterns: ['/dist/'], + testMatch: ['**/**.spec.ts'], + transform: { + '^.+\\.(t|j)sx?$': '@swc/jest', + }, + // transformIgnorePatterns: ['!node_modules/jose'], + moduleNameMapper: { + '\\.css$': 'identity-obj-proxy', + '^jose$': require.resolve('jose'), + }, + collectCoverage: true, + coverageReporters: ['text', 'html'], + collectCoverageFrom: ['src/**/*.{js,jsx,ts,tsx}'], +}; diff --git a/ee/packages/license/package.json b/ee/packages/license/package.json index 5dc6b69078b8..8fc96ef0b3eb 100644 --- a/ee/packages/license/package.json +++ b/ee/packages/license/package.json @@ -3,9 +3,14 @@ "version": "0.0.1", "private": true, "devDependencies": { + "@swc/core": "^1.3.66", + "@swc/jest": "^0.2.26", "@types/jest": "~29.5.3", + "@types/ws": "^8.5.5", "eslint": "~8.45.0", "jest": "~29.6.1", + "jest-environment-jsdom": "~29.6.1", + "jest-websocket-mock": "^2.4.0", "ts-jest": "~29.0.5", "typescript": "~5.2.2" }, @@ -21,6 +26,9 @@ "files": [ "/dist" ], + "volta": { + "extends": "../../../package.json" + }, "dependencies": { "@rocket.chat/core-typings": "workspace:^", "@rocket.chat/jwt": "workspace:^", diff --git a/packages/jwt/package.json b/packages/jwt/package.json index 8ec0c5a2c104..1f556b32a445 100644 --- a/packages/jwt/package.json +++ b/packages/jwt/package.json @@ -21,6 +21,9 @@ "files": [ "/dist" ], + "volta": { + "extends": "../../package.json" + }, "dependencies": { "jose": "^4.14.4" } diff --git a/packages/jwt/tsconfig.json b/packages/jwt/tsconfig.json index 52e9dd8c4976..f236186070e8 100644 --- a/packages/jwt/tsconfig.json +++ b/packages/jwt/tsconfig.json @@ -2,6 +2,7 @@ "extends": "../../tsconfig.base.server.json", "compilerOptions": { "declaration": true, + "module": "commonjs", "rootDir": "./src", "outDir": "./dist" }, diff --git a/yarn.lock b/yarn.lock index 71f0730329cb..ba1d7d742c8c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8397,9 +8397,14 @@ __metadata: "@rocket.chat/core-typings": "workspace:^" "@rocket.chat/jwt": "workspace:^" "@rocket.chat/logger": "workspace:^" + "@swc/core": ^1.3.66 + "@swc/jest": ^0.2.26 "@types/jest": ~29.5.3 + "@types/ws": ^8.5.5 eslint: ~8.45.0 jest: ~29.6.1 + jest-environment-jsdom: ~29.6.1 + jest-websocket-mock: ^2.4.0 ts-jest: ~29.0.5 typescript: ~5.2.2 languageName: unknown From 7489a89b2a348b115d80a27d78201baedd65518f Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Tue, 26 Sep 2023 12:07:15 -0300 Subject: [PATCH 28/38] create custom errors --- ee/packages/license/src/errors/InvalidLicenseError.ts | 6 ++++++ ee/packages/license/src/errors/NotReadyForValidation.ts | 6 ++++++ 2 files changed, 12 insertions(+) create mode 100644 ee/packages/license/src/errors/InvalidLicenseError.ts create mode 100644 ee/packages/license/src/errors/NotReadyForValidation.ts diff --git a/ee/packages/license/src/errors/InvalidLicenseError.ts b/ee/packages/license/src/errors/InvalidLicenseError.ts new file mode 100644 index 000000000000..a1eb328acd46 --- /dev/null +++ b/ee/packages/license/src/errors/InvalidLicenseError.ts @@ -0,0 +1,6 @@ +export class InvalidLicenseError extends Error { + constructor(message = 'Invalid license') { + super(message); + this.name = 'InvalidLicenseError'; + } +} diff --git a/ee/packages/license/src/errors/NotReadyForValidation.ts b/ee/packages/license/src/errors/NotReadyForValidation.ts new file mode 100644 index 000000000000..ccb99e054500 --- /dev/null +++ b/ee/packages/license/src/errors/NotReadyForValidation.ts @@ -0,0 +1,6 @@ +export class NotReadyForValidation extends Error { + constructor(message = 'Not ready for validation') { + super(message); + this.name = 'NotReadyForValidation'; + } +} From 13079323b96f9ae9ba841c7750a73b227477dc05 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Tue, 26 Sep 2023 13:51:40 -0300 Subject: [PATCH 29/38] init refactor to support instances --- .../license/__tests__/setLicense.spec.ts | 68 +++++++++++ ee/packages/license/src/decrypt.ts | 9 +- ee/packages/license/src/deprecated.ts | 35 +++--- ee/packages/license/src/events/deprecated.ts | 10 +- ee/packages/license/src/events/emitter.ts | 43 ++----- ee/packages/license/src/events/listeners.ts | 59 +++++----- .../src/events/overwriteClassOnLicense.ts | 11 +- ee/packages/license/src/index.ts | 110 ++++++++---------- ee/packages/license/src/license.ts | 110 +++++++++--------- ee/packages/license/src/modules.ts | 51 ++++---- ee/packages/license/src/pendingLicense.ts | 36 +++--- ee/packages/license/src/showLicense.ts | 7 +- .../getCurrentValueForLicenseLimit.ts | 37 +++--- .../src/validation/isReadyForValidation.ts | 6 +- .../license/src/validation/runValidation.ts | 13 ++- .../license/src/validation/validateFormat.ts | 12 +- .../src/validation/validateLicenseLimits.ts | 10 +- .../src/validation/validateLicenseUrl.ts | 12 +- ee/packages/license/src/workspaceUrl.ts | 13 --- 19 files changed, 354 insertions(+), 298 deletions(-) create mode 100644 ee/packages/license/__tests__/setLicense.spec.ts delete mode 100644 ee/packages/license/src/workspaceUrl.ts diff --git a/ee/packages/license/__tests__/setLicense.spec.ts b/ee/packages/license/__tests__/setLicense.spec.ts new file mode 100644 index 000000000000..02daff3b07dd --- /dev/null +++ b/ee/packages/license/__tests__/setLicense.spec.ts @@ -0,0 +1,68 @@ +/** + * @jest-environment node + */ + +import { LicenseImp } from '../src'; +import { InvalidLicenseError } from '../src/errors/InvalidLicenseError'; +import { NotReadyForValidation } from '../src/errors/NotReadyForValidation'; + +const VALID_LICENSE = + 'WMa5i+/t/LZbYOj8u3XUkivRhWBtWO6ycUjaZoVAw2DxMfdyBIAa2gMMI4x7Z2BrTZIZhFEImfOxcXcgD0QbXHGBJaMI+eYG+eofnVWi2VA7RWbpvWTULgPFgyJ4UEFeCOzVjcBLTQbmMSam3u0RlekWJkfAO0KnmLtsaEYNNA2rz1U+CLI/CdNGfdqrBu5PZZbGkH0KEzyIZMaykOjzvX+C6vd7fRxh23HecwhkBbqE8eQsCBt2ad0qC4MoVXsDaSOmSzGW+aXjuXt/9zjvrLlsmWQTSlkrEHdNkdywm0UkGxqz3+CP99n0WggUBioUiChjMuNMoceWvDvmxYP9Ml2NpYU7SnfhjmMFyXOah8ofzv8w509Y7XODvQBz+iB4Co9YnF3vT96HDDQyAV5t4jATE+0t37EAXmwjTi3qqyP7DLGK/revl+mlcwJ5kS4zZBsm1E4519FkXQOZSyWRnPdjqvh4mCLqoispZ49wKvklDvjPxCSP9us6cVXLDg7NTJr/4pfxLPOkvv7qCgugDvlDx17bXpQFPSDxmpw66FLzvb5Id0dkWjOzrRYSXb0bFWoUQjtHFzmcpFkyVhOKrQ9zA9+Zm7vXmU9Y2l2dK79EloOuHMSYAqsPEag8GMW6vI/cT4iIjHGGDePKnD0HblvTEKzql11cfT/abf2IiaY='; + +describe('License set license procedures', () => { + it('by default it should have no license', async () => { + const license = new LicenseImp(); + + expect(license.hasValidLicense()).toBe(false); + expect(license.getLicense()).toBeUndefined(); + }); + + it('should throw an error if the license applied is empty', async () => { + const license = new LicenseImp(); + await expect(async () => { + await license.setLicense(''); + }).rejects.toThrow(InvalidLicenseError); + }); + + it('should throw an error if the license applied is invalid', async () => { + const license = new LicenseImp(); + await expect(async () => { + await license.setLicense('invalid'); + }).rejects.toThrow(InvalidLicenseError); + }); + + describe('pending cases', () => { + it('should return an error if the license is not ready for validation yet - missing workspace url', async () => { + const license = new LicenseImp(); + await expect(async () => { + await license.setLicense(VALID_LICENSE); + }).rejects.toThrow(NotReadyForValidation); + }); + + it('should return an error if the license is not ready for validation yet - missing counters', async () => { + const license = new LicenseImp(); + await license.setWorkspaceUrl('http://localhost:3000'); + + expect(license.getWorkspaceUrl()).toBe('localhost:3000'); + + await expect(async () => { + await license.setLicense(VALID_LICENSE); + }).rejects.toThrow(NotReadyForValidation); + }); + + it('should return a valid license if the license is ready for validation', async () => { + const license = new LicenseImp(); + await license.setWorkspaceUrl('http://localhost:3000'); + + license.setLicenseLimitCounter('activeUsers', () => 0); + license.setLicenseLimitCounter('guestUsers', () => 0); + license.setLicenseLimitCounter('roomsPerGuest', async () => 0); + license.setLicenseLimitCounter('privateApps', () => 0); + license.setLicenseLimitCounter('marketplaceApps', () => 0); + license.setLicenseLimitCounter('monthlyActiveContacts', async () => 0); + + expect(license.getWorkspaceUrl()).toBe('localhost:3000'); + await expect(license.setLicense(VALID_LICENSE)).resolves.toBe(true); + }); + }); +}); diff --git a/ee/packages/license/src/decrypt.ts b/ee/packages/license/src/decrypt.ts index 87fd55b507d5..1159e6de82dc 100644 --- a/ee/packages/license/src/decrypt.ts +++ b/ee/packages/license/src/decrypt.ts @@ -6,17 +6,14 @@ const publicKey = 'LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUlJQ0lqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FnOEFNSUlDQ2dLQ0FnRUFxV1Nza2Q5LzZ6Ung4a3lQY2ljcwpiMzJ3Mnd4VnV3N3lCVDk2clEvOEQreU1lQ01POXdTU3BIYS85bkZ5d293RXRpZ3B0L3dyb1BOK1ZHU3didHdQCkZYQmVxRWxCbmRHRkFsODZlNStFbGlIOEt6L2hHbkNtSk5tWHB4RUsyUkUwM1g0SXhzWVg3RERCN010eC9pcXMKY2pCL091dlNCa2ppU2xlUzdibE5JVC9kQTdLNC9DSjNvaXUwMmJMNEV4Y2xDSGVwenFOTWVQM3dVWmdweE9uZgpOT3VkOElYWUs3M3pTY3VFOEUxNTdZd3B6Q0twVmFIWDdaSmY4UXVOc09PNVcvYUlqS2wzTDYyNjkrZUlPRXJHCndPTm1hSG56Zmc5RkxwSmh6Z3BPMzhhVm43NnZENUtLakJhaldza1krNGEyZ1NRbUtOZUZxYXFPb3p5RUZNMGUKY0ZXWlZWWjNMZWg0dkVNb1lWUHlJeng5Nng4ZjIveW1QbmhJdXZRdjV3TjRmeWVwYTdFWTVVQ2NwNzF6OGtmUAo0RmNVelBBMElEV3lNaWhYUi9HNlhnUVFaNEdiL3FCQmh2cnZpSkNGemZZRGNKZ0w3RmVnRllIUDNQR0wwN1FnCnZMZXZNSytpUVpQcnhyYnh5U3FkUE9rZ3VyS2pWclhUVXI0QTlUZ2lMeUlYNVVsSnEzRS9SVjdtZk9xWm5MVGEKU0NWWEhCaHVQbG5DR1pSMDFUb1RDZktoTUcxdTBDRm5MMisxNWhDOWZxT21XdjlRa2U0M3FsSjBQZ0YzVkovWAp1eC9tVHBuazlnbmJHOUpIK21mSDM5Um9GdlROaW5Zd1NNdll6dXRWT242OXNPemR3aERsYTkwbDNBQ2g0eENWCks3Sk9YK3VIa29OdTNnMmlWeGlaVU0wQ0F3RUFBUT09Ci0tLS0tRU5EIFBVQkxJQyBLRVktLS0tLQo='; // #TODO: use async/await -export default function decrypt(encrypted: string): string { +export default async function decrypt(encrypted: string): Promise { // handle V3 if (encrypted.startsWith('RCV3_')) { - let decrypted = ''; const jwt = encrypted.substring(5); - verify(jwt, publicKey).then(([payload, _header]) => { - decrypted = JSON.stringify(payload); + return verify(jwt, publicKey).then(([payload, _header]) => { + return JSON.stringify(payload); }); - - return decrypted; } const decrypted = crypto.publicDecrypt(Buffer.from(publicKey, 'base64').toString('utf-8'), Buffer.from(encrypted, 'base64')); diff --git a/ee/packages/license/src/deprecated.ts b/ee/packages/license/src/deprecated.ts index 7eff3d6ec52c..65851a79c7eb 100644 --- a/ee/packages/license/src/deprecated.ts +++ b/ee/packages/license/src/deprecated.ts @@ -1,11 +1,8 @@ -import type { LicenseLimitKind } from './definition/ILicenseV3'; -import { LicenseManager } from './license'; +import type { ILicenseV3, LicenseLimitKind } from './definition/ILicenseV3'; +import type { LicenseManager } from './license'; import { getModules } from './modules'; -const getLicenseLimit = (kind: LicenseLimitKind) => { - const manager = LicenseManager.getLicenseManager(); - - const license = manager.getLicense(); +const getLicenseLimit = (license: ILicenseV3 | undefined, kind: LicenseLimitKind) => { if (!license) { return; } @@ -20,20 +17,22 @@ const getLicenseLimit = (kind: LicenseLimitKind) => { // #TODO: Remove references to those functions -export const getMaxActiveUsers = () => getLicenseLimit('activeUsers') ?? 0; - -export const getAppsConfig = () => ({ - maxPrivateApps: getLicenseLimit('privateApps') ?? -1, - maxMarketplaceApps: getLicenseLimit('marketplaceApps') ?? -1, -}); +export function getMaxActiveUsers(this: LicenseManager) { + return getLicenseLimit(this.getLicense(), 'activeUsers') ?? 0; +} -export const getUnmodifiedLicenseAndModules = () => { - const manager = LicenseManager.getLicenseManager(); +export function getAppsConfig(this: LicenseManager) { + return { + maxPrivateApps: getLicenseLimit(this.getLicense(), 'privateApps') ?? -1, + maxMarketplaceApps: getLicenseLimit(this.getLicense(), 'marketplaceApps') ?? -1, + }; +} - if (manager.valid && manager.unmodifiedLicense) { +export function getUnmodifiedLicenseAndModules(this: LicenseManager) { + if (this.valid && this.unmodifiedLicense) { return { - license: manager.unmodifiedLicense, - modules: getModules(), + license: this.unmodifiedLicense, + modules: getModules.call(this), }; } -}; +} diff --git a/ee/packages/license/src/events/deprecated.ts b/ee/packages/license/src/events/deprecated.ts index 0eebeb2173d9..cc507fb0345c 100644 --- a/ee/packages/license/src/events/deprecated.ts +++ b/ee/packages/license/src/events/deprecated.ts @@ -1,12 +1,12 @@ import type { LicenseModule } from '../definition/LicenseModule'; +import type { LicenseManager } from '../license'; import { hasModule } from '../modules'; -import { EnterpriseLicenses } from './emitter'; // #TODO: Remove this onLicense handler -export const onLicense = (feature: LicenseModule, cb: (...args: any[]) => void): void | Promise => { - if (hasModule(feature)) { +export function onLicense(this: LicenseManager, feature: LicenseModule, cb: (...args: any[]) => void): void | Promise { + if (hasModule.call(this, feature)) { return cb(); } - EnterpriseLicenses.once(`valid:${feature}`, cb); -}; + this.on(`valid:${feature}`, cb); +} diff --git a/ee/packages/license/src/events/emitter.ts b/ee/packages/license/src/events/emitter.ts index 85080c123c1d..52256d0fe6f5 100644 --- a/ee/packages/license/src/events/emitter.ts +++ b/ee/packages/license/src/events/emitter.ts @@ -1,49 +1,30 @@ -import { EventEmitter } from 'events'; - import type { LicenseLimitKind } from '../definition/ILicenseV3'; import type { LicenseModule } from '../definition/LicenseModule'; +import type { LicenseManager } from '../license'; import { logger } from '../logger'; -export const EnterpriseLicenses = new EventEmitter(); - -export const licenseValidated = () => { - try { - EnterpriseLicenses.emit('validate'); - } catch (error) { - logger.error({ msg: 'Error running license validated event', error }); - } -}; - -export const licenseRemoved = () => { - try { - EnterpriseLicenses.emit('invalidate'); - } catch (error) { - logger.error({ msg: 'Error running license invalidated event', error }); - } -}; - -export const moduleValidated = (module: LicenseModule) => { +export function moduleValidated(this: LicenseManager, module: LicenseModule) { try { - EnterpriseLicenses.emit('module', { module, valid: true }); - EnterpriseLicenses.emit(`valid:${module}`); + this.emit('module', { module, valid: true }); + // this.emit(`valid:${module}`); } catch (error) { logger.error({ msg: 'Error running module added event', error }); } -}; +} -export const moduleRemoved = (module: LicenseModule) => { +export function moduleRemoved(this: LicenseManager, module: LicenseModule) { try { - EnterpriseLicenses.emit('module', { module, valid: false }); - EnterpriseLicenses.emit(`invalid:${module}`); + this.emit('module', { module, valid: false }); + this.emit(`invalid:${module}`); } catch (error) { logger.error({ msg: 'Error running module removed event', error }); } -}; +} -export const limitReached = (limitKind: LicenseLimitKind) => { +export function limitReached(this: LicenseManager, limitKind: LicenseLimitKind) { try { - EnterpriseLicenses.emit(`limitReached:${limitKind}`); + this.emit(`limitReached:${limitKind}`); } 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 08beaf30fe3a..d6e9fb016f2c 100644 --- a/ee/packages/license/src/events/listeners.ts +++ b/ee/packages/license/src/events/listeners.ts @@ -1,46 +1,47 @@ import type { LicenseLimitKind } from '../definition/ILicenseV3'; import type { LicenseModule } from '../definition/LicenseModule'; +import type { LicenseManager } from '../license'; import { hasModule } from '../modules'; -import { EnterpriseLicenses } from './emitter'; -export const onValidFeature = (feature: LicenseModule, cb: () => void) => { - EnterpriseLicenses.on(`valid:${feature}`, cb); +export function onValidFeature(this: LicenseManager, feature: LicenseModule, cb: () => void) { + this.on(`valid:${feature}`, cb); - if (hasModule(feature)) { + if (hasModule.call(this, feature)) { cb(); } return (): void => { - EnterpriseLicenses.off(`valid:${feature}`, cb); + this.off(`valid:${feature}`, cb); }; -}; +} -export const onInvalidFeature = (feature: LicenseModule, cb: () => void) => { - EnterpriseLicenses.on(`invalid:${feature}`, cb); +export function onInvalidFeature(this: LicenseManager, feature: LicenseModule, cb: () => void) { + this.on(`invalid:${feature}`, cb); - if (!hasModule(feature)) { + if (!hasModule.call(this, feature)) { cb(); } return (): void => { - EnterpriseLicenses.off(`invalid:${feature}`, cb); + this.off(`invalid:${feature}`, cb); }; -}; +} -export const onToggledFeature = ( +export function onToggledFeature( + this: LicenseManager, feature: LicenseModule, { up, down }: { up?: () => Promise | void; down?: () => Promise | void }, -): (() => void) => { - let enabled = hasModule(feature); +): () => void { + let enabled = hasModule.call(this, feature); - const offValidFeature = onValidFeature(feature, () => { + const offValidFeature = onValidFeature.bind(this)(feature, () => { if (!enabled) { void up?.(); enabled = true; } }); - const offInvalidFeature = onInvalidFeature(feature, () => { + const offInvalidFeature = onInvalidFeature.bind(this)(feature, () => { if (enabled) { void down?.(); enabled = false; @@ -55,20 +56,20 @@ export const onToggledFeature = ( offValidFeature(); offInvalidFeature(); }; -}; +} -export const onModule = (cb: (...args: any[]) => void) => { - EnterpriseLicenses.on('module', cb); -}; +export function onModule(this: LicenseManager, cb: (...args: any[]) => void) { + this.on('module', cb); +} -export const onValidateLicense = (cb: (...args: any[]) => void) => { - EnterpriseLicenses.on('validate', cb); -}; +export function onValidateLicense(this: LicenseManager, cb: (...args: any[]) => void) { + this.on('validate', cb); +} -export const onInvalidateLicense = (cb: (...args: any[]) => void) => { - EnterpriseLicenses.on('invalidate', cb); -}; +export function onInvalidateLicense(this: LicenseManager, cb: (...args: any[]) => void) { + this.on('invalidate', cb); +} -export const onLimitReached = (limitKind: LicenseLimitKind, cb: (...args: any[]) => void) => { - EnterpriseLicenses.on(`limitReached:${limitKind}`, cb); -}; +export function onLimitReached(this: LicenseManager, limitKind: LicenseLimitKind, cb: (...args: any[]) => void) { + this.on(`limitReached:${limitKind}`, cb); +} diff --git a/ee/packages/license/src/events/overwriteClassOnLicense.ts b/ee/packages/license/src/events/overwriteClassOnLicense.ts index 27d9133a7a33..00a690d8f413 100644 --- a/ee/packages/license/src/events/overwriteClassOnLicense.ts +++ b/ee/packages/license/src/events/overwriteClassOnLicense.ts @@ -1,4 +1,5 @@ import type { LicenseModule } from '../definition/LicenseModule'; +import type { LicenseManager } from '../license'; import { onLicense } from './deprecated'; interface IOverrideClassProperties { @@ -7,8 +8,14 @@ interface IOverrideClassProperties { type Class = { new (...args: any[]): any }; -export async function overwriteClassOnLicense(license: LicenseModule, original: Class, overwrite: IOverrideClassProperties): Promise { - await onLicense(license, () => { +export async function overwriteClassOnLicense( + this: LicenseManager, + + license: LicenseModule, + original: Class, + overwrite: IOverrideClassProperties, +): Promise { + await onLicense.call(this, license, () => { Object.entries(overwrite).forEach(([key, value]) => { const originalFn = original.prototype[key]; original.prototype[key] = function (...args: any[]): any { diff --git a/ee/packages/license/src/index.ts b/ee/packages/license/src/index.ts index a6ef8eeeb806..433b4b195bf3 100644 --- a/ee/packages/license/src/index.ts +++ b/ee/packages/license/src/index.ts @@ -17,7 +17,6 @@ import { getModules, hasModule } from './modules'; import { getTags } from './tags'; import { getCurrentValueForLicenseLimit, setLicenseLimitCounter } from './validation/getCurrentValueForLicenseLimit'; import { validateFormat } from './validation/validateFormat'; -import { setWorkspaceUrl } from './workspaceUrl'; export * from './definition/ILicenseTag'; export * from './definition/ILicenseV2'; @@ -28,88 +27,79 @@ export * from './definition/LicenseModule'; export * from './definition/LicensePeriod'; export * from './definition/LimitContext'; -export class License extends LicenseManager { - public static validateFormat(...args: Parameters) { - return validateFormat(...args); - } +// eslint-disable-next-line @typescript-eslint/naming-convention +interface License { + validateFormat: typeof validateFormat; + hasModule: typeof hasModule; + getModules: typeof getModules; + getTags: typeof getTags; + overwriteClassOnLicense: typeof overwriteClassOnLicense; + setLicenseLimitCounter: typeof setLicenseLimitCounter; + getCurrentValueForLicenseLimit: typeof getCurrentValueForLicenseLimit; + isLimitReached: (action: T, context?: Partial>) => Promise; + onValidFeature: typeof onValidFeature; + onInvalidFeature: typeof onInvalidFeature; + onToggledFeature: typeof onToggledFeature; + onModule: typeof onModule; + onValidateLicense: typeof onValidateLicense; + onInvalidateLicense: typeof onInvalidateLicense; + onLimitReached: typeof onLimitReached; + // Deprecated: + onLicense: typeof onLicense; + // Deprecated: + getMaxActiveUsers: typeof getMaxActiveUsers; + // Deprecated: + getAppsConfig: typeof getAppsConfig; + // Deprecated: + getUnmodifiedLicenseAndModules: typeof getUnmodifiedLicenseAndModules; +} - public static async setWorkspaceUrl(...args: Parameters) { - return setWorkspaceUrl(...args); - } +export class LicenseImp extends LicenseManager implements License { + validateFormat = validateFormat; - public static hasModule(...args: Parameters) { - return hasModule(...args); - } + hasModule = hasModule; - public static getModules(...args: Parameters) { - return getModules(...args); - } + getModules = getModules; - public static getTags(...args: Parameters) { - return getTags(...args); - } + getTags = getTags; - public static async overwriteClassOnLicense(...args: Parameters) { - return overwriteClassOnLicense(...args); - } + overwriteClassOnLicense = overwriteClassOnLicense; - public static setLicenseLimitCounter(...args: Parameters) { - return setLicenseLimitCounter(...args); - } + public setLicenseLimitCounter = setLicenseLimitCounter; - public static async getCurrentValueForLicenseLimit(...args: Parameters) { - return getCurrentValueForLicenseLimit(...args); - } + getCurrentValueForLicenseLimit = getCurrentValueForLicenseLimit; - public static async isLimitReached(action: T, context?: Partial>) { + public async isLimitReached(action: T, context?: Partial>) { return this.shouldPreventAction(action, context, 0); } - public static onValidFeature(...args: Parameters) { - return onValidFeature(...args); - } + onValidFeature = onValidFeature; - public static onInvalidFeature(...args: Parameters) { - return onInvalidFeature(...args); - } + onInvalidFeature = onInvalidFeature; - public static onToggledFeature(...args: Parameters) { - return onToggledFeature(...args); - } + onToggledFeature = onToggledFeature; - public static onModule(...args: Parameters) { - return onModule(...args); - } + onModule = onModule; - public static onValidateLicense(...args: Parameters) { - return onValidateLicense(...args); - } + onValidateLicense = onValidateLicense; - public static onInvalidateLicense(...args: Parameters) { - return onInvalidateLicense(...args); - } + onInvalidateLicense = onInvalidateLicense; - public static onLimitReached(...args: Parameters) { - return onLimitReached(...args); - } + onLimitReached = onLimitReached; // Deprecated: - public static onLicense(...args: Parameters) { - return onLicense(...args); - } + onLicense = onLicense; // Deprecated: - public static getMaxActiveUsers() { - return getMaxActiveUsers(); - } + getMaxActiveUsers = getMaxActiveUsers; // Deprecated: - public static getAppsConfig() { - return getAppsConfig(); - } + getAppsConfig = getAppsConfig; // Deprecated: - public static getUnmodifiedLicenseAndModules() { - return getUnmodifiedLicenseAndModules(); - } + getUnmodifiedLicenseAndModules = getUnmodifiedLicenseAndModules; } + +const license = new LicenseImp(); + +export { license as License }; diff --git a/ee/packages/license/src/license.ts b/ee/packages/license/src/license.ts index 768f3684669c..7fbec4bfea18 100644 --- a/ee/packages/license/src/license.ts +++ b/ee/packages/license/src/license.ts @@ -1,12 +1,16 @@ +import { Emitter } from '@rocket.chat/emitter'; + import decrypt from './decrypt'; import type { ILicenseV2 } from './definition/ILicenseV2'; import type { ILicenseV3, LicenseLimitKind } from './definition/ILicenseV3'; import type { BehaviorWithContext } from './definition/LicenseBehavior'; +import type { LicenseModule } from './definition/LicenseModule'; import type { LimitContext } from './definition/LimitContext'; -import { licenseRemoved, licenseValidated } from './events/emitter'; +import { InvalidLicenseError } from './errors/InvalidLicenseError'; +import { NotReadyForValidation } from './errors/NotReadyForValidation'; import { logger } from './logger'; import { invalidateAll, replaceModules } from './modules'; -import { clearPendingLicense, hasPendingLicense, isPendingLicense, setPendingLicense } from './pendingLicense'; +import { applyPendingLicense, clearPendingLicense, hasPendingLicense, isPendingLicense, setPendingLicense } from './pendingLicense'; import { showLicense } from './showLicense'; import { replaceTags } from './tags'; import { convertToV3 } from './v2/convertToV3'; @@ -17,9 +21,21 @@ import { isReadyForValidation } from './validation/isReadyForValidation'; import { runValidation } from './validation/runValidation'; import { validateFormat } from './validation/validateFormat'; -let licenseManager: LicenseManager | undefined; +export class LicenseManager extends Emitter< + Record<`limitReached:${LicenseLimitKind}` | `${'invalid' | 'valid'}:${LicenseModule}`, undefined> & { + validate: undefined; + invalidate: undefined; + module: { module: LicenseModule; valid: boolean }; + } +> { + dataCounters = new Map) => Promise>(); + + pendingLicense = ''; + + modules = new Set(); + + private workspaceUrl: string | undefined; -export class LicenseManager { private _license: ILicenseV3 | undefined; private _unmodifiedLicense: ILicenseV2 | ILicenseV3 | undefined; @@ -30,34 +46,6 @@ export class LicenseManager { private _lockedLicense: string | undefined; - public static getLicenseManager(): LicenseManager { - if (!licenseManager) { - licenseManager = new LicenseManager(); - } - - return licenseManager; - } - - public static async setLicense(encryptedLicense: string): Promise { - return this.getLicenseManager().setLicense(encryptedLicense); - } - - public static hasValidLicense(): boolean { - return this.getLicenseManager().hasValidLicense(); - } - - public static getLicense(): ILicenseV3 | undefined { - return this.getLicenseManager().getLicense(); - } - - public static async shouldPreventAction( - action: T, - context?: Partial>, - newCount = 1, - ): Promise { - return this.getLicenseManager().shouldPreventAction(action, context, newCount); - } - public get license(): ILicenseV3 | undefined { return this._license; } @@ -74,13 +62,25 @@ export class LicenseManager { return Boolean(this._inFairPolicy); } + public async setWorkspaceUrl(url: string) { + this.workspaceUrl = url.replace(/\/$/, '').replace(/^https?:\/\/(.*)$/, '$1'); + + if (hasPendingLicense.call(this)) { + await applyPendingLicense.call(this); + } + } + + public getWorkspaceUrl() { + return this.workspaceUrl; + } + private clearLicenseData(): void { this._license = undefined; this._unmodifiedLicense = undefined; this._inFairPolicy = undefined; this._valid = false; this._lockedLicense = undefined; - clearPendingLicense(); + clearPendingLicense.call(this); } private async setLicenseV3(newLicense: ILicenseV3, encryptedLicense: string, originalLicense?: ILicenseV2 | ILicenseV3): Promise { @@ -92,11 +92,12 @@ export class LicenseManager { this._license = newLicense; await this.validateLicense(); + this._lockedLicense = encryptedLicense; } finally { if (hadValidLicense && !this.hasValidLicense()) { - licenseRemoved(); - invalidateAll(); + this.emit('invalidate'); + invalidateAll.call(this); } } } @@ -124,20 +125,23 @@ export class LicenseManager { const disabledModules = getModulesToDisable(result); const modulesToEnable = this._license.grantedModules.filter(({ module }) => !disabledModules.includes(module)); - replaceModules(modulesToEnable.map(({ module }) => module)); + replaceModules.call( + this, + modulesToEnable.map(({ module }) => module), + ); logger.log({ msg: 'License validated', modules: modulesToEnable }); - licenseValidated(); - showLicense(this._license, this._valid); + this.emit('validate'); + showLicense.call(this, this._license, this._valid); } private async validateLicense(): Promise { - if (!this._license || !isReadyForValidation()) { + if (!this._license || !isReadyForValidation.call(this)) { return; } // #TODO: Only include 'prevent_installation' here if this is actually the initial installation of the license - const validationResult = await runValidation(this._license, [ + const validationResult = await runValidation.bind(this)(this._license, [ 'invalidate_license', 'prevent_installation', 'start_fair_policy', @@ -147,34 +151,30 @@ export class LicenseManager { } public async setLicense(encryptedLicense: string): Promise { - if (!encryptedLicense || String(encryptedLicense).trim() === '') { - return false; + if (!(await validateFormat(encryptedLicense))) { + throw new InvalidLicenseError(); } if (this.isLicenseDuplicate(encryptedLicense)) { // If there is a pending license but the user is trying to revert to the license that is currently active - if (hasPendingLicense() && !isPendingLicense(encryptedLicense)) { + if (hasPendingLicense.call(this) && !isPendingLicense.call(this, encryptedLicense)) { // simply remove the pending license - clearPendingLicense(); + clearPendingLicense.call(this); return true; } return false; } - if (!isReadyForValidation()) { + if (!isReadyForValidation.call(this)) { // If we can't validate the license data yet, but is a valid license string, store it to validate when we can - if (validateFormat(encryptedLicense)) { - setPendingLicense(encryptedLicense); - return true; - } - - return false; + setPendingLicense.call(this, encryptedLicense); + throw new NotReadyForValidation(); } logger.info('New Enterprise License'); try { - const decrypted = decrypt(encryptedLicense); + const decrypted = await decrypt(encryptedLicense); if (!decrypted) { return false; } @@ -193,12 +193,12 @@ export class LicenseManager { if (process.env.LICENSE_DEBUG && process.env.LICENSE_DEBUG !== 'false') { logger.error({ msg: 'Invalid raw license', encryptedLicense, e }); } - return false; + throw new InvalidLicenseError(); } } public hasValidLicense(): boolean { - return Boolean(this._license && this._valid); + return Boolean(this.getLicense()); } public getLicense(): ILicenseV3 | undefined { @@ -217,7 +217,7 @@ export class LicenseManager { return false; } - const currentValue = (await getCurrentValueForLicenseLimit(action, context)) + newCount; + const currentValue = (await getCurrentValueForLicenseLimit.call(this, action, context)) + newCount; return Boolean( license.limits[action] ?.filter(({ behavior, max }) => behavior === 'prevent_action' && max >= 0) diff --git a/ee/packages/license/src/modules.ts b/ee/packages/license/src/modules.ts index 0497c0c8f069..7570ec525fc7 100644 --- a/ee/packages/license/src/modules.ts +++ b/ee/packages/license/src/modules.ts @@ -1,47 +1,50 @@ import type { LicenseModule } from './definition/LicenseModule'; import { moduleRemoved, moduleValidated } from './events/emitter'; +import type { LicenseManager } from './license'; -export const modules = new Set(); - -export const notifyValidatedModules = (licenseModules: LicenseModule[]) => { +export function notifyValidatedModules(this: LicenseManager, licenseModules: LicenseModule[]) { licenseModules.forEach((module) => { - modules.add(module); - moduleValidated(module); + this.modules.add(module); + moduleValidated.call(this, module); }); -}; +} -export const notifyInvalidatedModules = (licenseModules: LicenseModule[]) => { +export function notifyInvalidatedModules(this: LicenseManager, licenseModules: LicenseModule[]) { licenseModules.forEach((module) => { - moduleRemoved(module); - modules.delete(module); + moduleRemoved.call(this, module); + this.modules.delete(module); }); -}; +} -export const invalidateAll = () => { - notifyInvalidatedModules([...modules]); - modules.clear(); -}; +export function invalidateAll(this: LicenseManager) { + notifyInvalidatedModules.call(this, [...this.modules]); + this.modules.clear(); +} -export const getModules = () => [...modules]; +export function getModules(this: LicenseManager) { + return [...this.modules]; +} -export const hasModule = (module: LicenseModule) => modules.has(module); +export function hasModule(this: LicenseManager, module: LicenseModule) { + return this.modules.has(module); +} -export const replaceModules = (newModules: LicenseModule[]) => { +export function replaceModules(this: LicenseManager, newModules: LicenseModule[]) { for (const moduleName of newModules) { - if (modules.has(moduleName)) { + if (this.modules.has(moduleName)) { continue; } - modules.add(moduleName); - moduleValidated(moduleName); + this.modules.add(moduleName); + moduleValidated.call(this, moduleName); } - for (const moduleName of modules) { + for (const moduleName of this.modules) { if (newModules.includes(moduleName)) { continue; } - moduleRemoved(moduleName); - modules.delete(moduleName); + moduleRemoved.call(this, moduleName); + this.modules.delete(moduleName); } -}; +} diff --git a/ee/packages/license/src/pendingLicense.ts b/ee/packages/license/src/pendingLicense.ts index 9d8177b3dcb4..2c2140044336 100644 --- a/ee/packages/license/src/pendingLicense.ts +++ b/ee/packages/license/src/pendingLicense.ts @@ -1,30 +1,32 @@ -import { LicenseManager } from './license'; +import type { LicenseManager } from './license'; import { logger } from './logger'; -let pendingLicense: string; - -export const setPendingLicense = (encryptedLicense: string) => { - pendingLicense = encryptedLicense; - if (pendingLicense) { +export function setPendingLicense(this: LicenseManager, encryptedLicense: string) { + this.pendingLicense = encryptedLicense; + if (this.pendingLicense) { logger.info('Storing license as pending validation.'); } -}; +} -export const applyPendingLicense = async () => { - if (pendingLicense) { +export function applyPendingLicense(this: LicenseManager) { + if (this.pendingLicense) { logger.info('Applying pending license.'); - LicenseManager.setLicense(pendingLicense); + this.setLicense(this.pendingLicense); } -}; +} -export const hasPendingLicense = () => Boolean(pendingLicense); +export function hasPendingLicense(this: LicenseManager) { + return Boolean(this.pendingLicense); +} -export const isPendingLicense = (encryptedLicense: string) => !!pendingLicense && pendingLicense === encryptedLicense; +export function isPendingLicense(this: LicenseManager, encryptedLicense: string) { + return !!this.pendingLicense && this.pendingLicense === encryptedLicense; +} -export const clearPendingLicense = () => { - if (pendingLicense) { +export function clearPendingLicense(this: LicenseManager) { + if (this.pendingLicense) { logger.info('Removing pending license.'); } - pendingLicense = ''; -}; + this.pendingLicense = ''; +} diff --git a/ee/packages/license/src/showLicense.ts b/ee/packages/license/src/showLicense.ts index b7d131e8e438..3dda60a43b76 100644 --- a/ee/packages/license/src/showLicense.ts +++ b/ee/packages/license/src/showLicense.ts @@ -1,7 +1,8 @@ import type { ILicenseV3 } from './definition/ILicenseV3'; +import type { LicenseManager } from './license'; import { getModules } from './modules'; -export const showLicense = (license: ILicenseV3 | undefined, valid: boolean | undefined) => { +export function showLicense(this: LicenseManager, license: ILicenseV3 | undefined, valid: boolean | undefined) { if (!process.env.LICENSE_DEBUG || process.env.LICENSE_DEBUG === 'false') { return; } @@ -15,7 +16,7 @@ export const showLicense = (license: ILicenseV3 | undefined, valid: boolean | un limits, } = license; - const modules = getModules(); + const modules = getModules.call(this); console.log('---- License enabled ----'); console.log(' url ->', JSON.stringify(serverUrls)); @@ -23,4 +24,4 @@ export const showLicense = (license: ILicenseV3 | undefined, valid: boolean | un console.log(' limits ->', JSON.stringify(limits)); console.log(' modules ->', modules.join(', ')); console.log('-------------------------'); -}; +} diff --git a/ee/packages/license/src/validation/getCurrentValueForLicenseLimit.ts b/ee/packages/license/src/validation/getCurrentValueForLicenseLimit.ts index 2e8edfa6e2d5..1c3ca31807c2 100644 --- a/ee/packages/license/src/validation/getCurrentValueForLicenseLimit.ts +++ b/ee/packages/license/src/validation/getCurrentValueForLicenseLimit.ts @@ -1,35 +1,40 @@ import type { IUser } from '@rocket.chat/core-typings'; import type { LicenseLimitKind } from '../definition/ILicenseV3'; +import type { LicenseManager } from '../license'; import { logger } from '../logger'; import { applyPendingLicense, hasPendingLicense } from '../pendingLicense'; type LimitContext = T extends 'roomsPerGuest' ? { userId: IUser['_id'] } : Record; -const dataCounters = new Map) => Promise>(); - -export const setLicenseLimitCounter = (limitKey: T, fn: (context?: LimitContext) => Promise) => { - dataCounters.set(limitKey, fn as (context?: LimitContext) => Promise); +export function setLicenseLimitCounter( + this: LicenseManager, + limitKey: T, + fn: (context?: LimitContext) => Promise | number, +) { + this.dataCounters.set(limitKey, fn as (context?: LimitContext) => Promise); - if (hasPendingLicense() && hasAllDataCounters()) { - void applyPendingLicense(); + if (hasPendingLicense.call(this) && hasAllDataCounters.call(this)) { + void applyPendingLicense.call(this); } -}; +} -export const getCurrentValueForLicenseLimit = async ( +export async function getCurrentValueForLicenseLimit( + this: LicenseManager, limitKey: T, context?: Partial>, -): Promise => { - if (dataCounters.has(limitKey)) { - return dataCounters.get(limitKey)?.(context as LimitContext | undefined) ?? 0; +): Promise { + if (this.dataCounters.has(limitKey)) { + return this.dataCounters.get(limitKey)?.(context as LimitContext | undefined) ?? 0; } logger.error({ msg: 'Unable to validate license limit due to missing data counter.', limitKey }); return 0; -}; +} -export const hasAllDataCounters = () => - (['activeUsers', 'guestUsers', 'roomsPerGuest', 'privateApps', 'marketplaceApps', 'monthlyActiveContacts'] as LicenseLimitKind[]).every( - (limitKey) => dataCounters.has(limitKey), - ); +export function hasAllDataCounters(this: LicenseManager) { + return ( + ['activeUsers', 'guestUsers', 'roomsPerGuest', 'privateApps', 'marketplaceApps', 'monthlyActiveContacts'] as LicenseLimitKind[] + ).every((limitKey) => this.dataCounters.has(limitKey)); +} diff --git a/ee/packages/license/src/validation/isReadyForValidation.ts b/ee/packages/license/src/validation/isReadyForValidation.ts index 7f3c3c77736e..aa763bf7f353 100644 --- a/ee/packages/license/src/validation/isReadyForValidation.ts +++ b/ee/packages/license/src/validation/isReadyForValidation.ts @@ -1,5 +1,7 @@ -import { getWorkspaceUrl } from '../workspaceUrl'; +import type { LicenseManager } from '../license'; import { hasAllDataCounters } from './getCurrentValueForLicenseLimit'; // Can only validate licenses once the workspace URL and the data counter functions are set -export const isReadyForValidation = () => Boolean(getWorkspaceUrl() && hasAllDataCounters()); +export function isReadyForValidation(this: LicenseManager) { + return Boolean(this.getWorkspaceUrl() && hasAllDataCounters.call(this)); +} diff --git a/ee/packages/license/src/validation/runValidation.ts b/ee/packages/license/src/validation/runValidation.ts index 6a75917c311d..639492b431ff 100644 --- a/ee/packages/license/src/validation/runValidation.ts +++ b/ee/packages/license/src/validation/runValidation.ts @@ -1,17 +1,22 @@ import type { ILicenseV3 } from '../definition/ILicenseV3'; import type { LicenseBehavior, BehaviorWithContext } from '../definition/LicenseBehavior'; +import type { LicenseManager } from '../license'; import { validateLicenseLimits } from './validateLicenseLimits'; import { validateLicensePeriods } from './validateLicensePeriods'; import { validateLicenseUrl } from './validateLicenseUrl'; -export const runValidation = async (license: ILicenseV3, behaviorsToValidate: LicenseBehavior[] = []): Promise => { +export async function runValidation( + this: LicenseManager, + license: ILicenseV3, + behaviorsToValidate: LicenseBehavior[] = [], +): Promise { const shouldValidateBehavior = (behavior: LicenseBehavior) => !behaviorsToValidate?.length || behaviorsToValidate.includes(behavior); return [ ...new Set([ - ...validateLicenseUrl(license, shouldValidateBehavior), + ...validateLicenseUrl.call(this, license, shouldValidateBehavior), ...validateLicensePeriods(license, shouldValidateBehavior), - ...(await validateLicenseLimits(license, shouldValidateBehavior)), + ...(await validateLicenseLimits.call(this, license, shouldValidateBehavior)), ]), ]; -}; +} diff --git a/ee/packages/license/src/validation/validateFormat.ts b/ee/packages/license/src/validation/validateFormat.ts index 8d57a5509c5f..0b8f5ae344a1 100644 --- a/ee/packages/license/src/validation/validateFormat.ts +++ b/ee/packages/license/src/validation/validateFormat.ts @@ -1,13 +1,15 @@ import decrypt from '../decrypt'; +import { InvalidLicenseError } from '../errors/InvalidLicenseError'; -export const validateFormat = (encryptedLicense: string): boolean => { +export const validateFormat = async (encryptedLicense: string): Promise => { if (!encryptedLicense || String(encryptedLicense).trim() === '') { - return false; + throw new InvalidLicenseError('Empty license'); } - const decrypted = decrypt(encryptedLicense); - if (!decrypted) { - return false; + try { + await decrypt(encryptedLicense); + } catch (e) { + throw new InvalidLicenseError(); } return true; diff --git a/ee/packages/license/src/validation/validateLicenseLimits.ts b/ee/packages/license/src/validation/validateLicenseLimits.ts index 32b0a1229d9a..168effe6a250 100644 --- a/ee/packages/license/src/validation/validateLicenseLimits.ts +++ b/ee/packages/license/src/validation/validateLicenseLimits.ts @@ -1,13 +1,15 @@ import type { ILicenseV3 } from '../definition/ILicenseV3'; import type { BehaviorWithContext, LicenseBehavior } from '../definition/LicenseBehavior'; +import type { LicenseManager } from '../license'; import { logger } from '../logger'; import { getCurrentValueForLicenseLimit } from './getCurrentValueForLicenseLimit'; import { getResultingBehavior } from './getResultingBehavior'; -export const validateLicenseLimits = async ( +export async function validateLicenseLimits( + this: LicenseManager, license: ILicenseV3, behaviorFilter: (behavior: LicenseBehavior) => boolean, -): Promise => { +): Promise { const { limits } = license; const limitKeys = Object.keys(limits) as (keyof ILicenseV3['limits'])[]; @@ -20,7 +22,7 @@ export const validateLicenseLimits = async ( return []; } - const currentValue = await getCurrentValueForLicenseLimit(limitKey); + const currentValue = await getCurrentValueForLicenseLimit.call(this, limitKey); return limitList .filter(({ max }) => max < currentValue) .map((limit) => { @@ -34,4 +36,4 @@ export const validateLicenseLimits = async ( }), ) ).flat(); -}; +} diff --git a/ee/packages/license/src/validation/validateLicenseUrl.ts b/ee/packages/license/src/validation/validateLicenseUrl.ts index db7e91b4f870..55cd076c4378 100644 --- a/ee/packages/license/src/validation/validateLicenseUrl.ts +++ b/ee/packages/license/src/validation/validateLicenseUrl.ts @@ -1,7 +1,7 @@ import type { ILicenseV3 } from '../definition/ILicenseV3'; import type { BehaviorWithContext, LicenseBehavior } from '../definition/LicenseBehavior'; +import type { LicenseManager } from '../license'; import { logger } from '../logger'; -import { getWorkspaceUrl } from '../workspaceUrl'; import { getResultingBehavior } from './getResultingBehavior'; export const validateUrl = (licenseURL: string, url: string) => { @@ -13,7 +13,11 @@ export const validateUrl = (licenseURL: string, url: string) => { return !!regex.exec(url); }; -export const validateLicenseUrl = (license: ILicenseV3, behaviorFilter: (behavior: LicenseBehavior) => boolean): BehaviorWithContext[] => { +export function validateLicenseUrl( + this: LicenseManager, + license: ILicenseV3, + behaviorFilter: (behavior: LicenseBehavior) => boolean, +): BehaviorWithContext[] { if (!behaviorFilter('invalidate_license')) { return []; } @@ -22,7 +26,7 @@ export const validateLicenseUrl = (license: ILicenseV3, behaviorFilter: (behavio validation: { serverUrls }, } = license; - const workspaceUrl = getWorkspaceUrl(); + const workspaceUrl = this.getWorkspaceUrl(); if (!workspaceUrl) { logger.error('Unable to validate license URL without knowing the workspace URL.'); @@ -52,4 +56,4 @@ export const validateLicenseUrl = (license: ILicenseV3, behaviorFilter: (behavio }); return getResultingBehavior({ behavior: 'invalidate_license' }); }); -}; +} diff --git a/ee/packages/license/src/workspaceUrl.ts b/ee/packages/license/src/workspaceUrl.ts deleted file mode 100644 index 8d384b2d453d..000000000000 --- a/ee/packages/license/src/workspaceUrl.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { applyPendingLicense, hasPendingLicense } from './pendingLicense'; - -let workspaceUrl: string | undefined; - -export const setWorkspaceUrl = async (url: string) => { - workspaceUrl = url.replace(/\/$/, '').replace(/^https?:\/\/(.*)$/, '$1'); - - if (hasPendingLicense()) { - await applyPendingLicense(); - } -}; - -export const getWorkspaceUrl = () => workspaceUrl; From fa3500b767284e0a9c3f9e7d06e4b6ea23360852 Mon Sep 17 00:00:00 2001 From: Pierre Lehnen Date: Tue, 26 Sep 2023 13:57:23 -0300 Subject: [PATCH 30/38] fixed invalid reference --- .../server/{validateUserRoles.js => validateUserRoles.ts} | 7 ++++--- apps/meteor/ee/server/startup/seatsCap.ts | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) rename apps/meteor/ee/app/authorization/server/{validateUserRoles.js => validateUserRoles.ts} (79%) diff --git a/apps/meteor/ee/app/authorization/server/validateUserRoles.js b/apps/meteor/ee/app/authorization/server/validateUserRoles.ts similarity index 79% rename from apps/meteor/ee/app/authorization/server/validateUserRoles.js rename to apps/meteor/ee/app/authorization/server/validateUserRoles.ts index c51482cf1386..a07165b8c5d8 100644 --- a/apps/meteor/ee/app/authorization/server/validateUserRoles.js +++ b/apps/meteor/ee/app/authorization/server/validateUserRoles.ts @@ -1,10 +1,11 @@ +import type { IUser } from '@rocket.chat/core-typings'; import { License } from '@rocket.chat/license'; import { Users } from '@rocket.chat/models'; import { Meteor } from 'meteor/meteor'; import { i18n } from '../../../../server/lib/i18n'; -export const validateUserRoles = async function (userId, userData) { +export const validateUserRoles = async function (userData: Partial) { if (!License.hasValidLicense()) { return; } @@ -22,7 +23,7 @@ export const validateUserRoles = async function (userId, userData) { return; } - if (await License.preventNewGuests()) { + if (await License.shouldPreventAction('guestUsers')) { throw new Meteor.Error('error-max-guests-number-reached', 'Maximum number of guests reached.', { method: 'insertOrUpdateUser', field: 'Assign_role', @@ -36,7 +37,7 @@ export const validateUserRoles = async function (userId, userData) { return; } - if (await License.preventNewUsers()) { + if (await License.shouldPreventAction('activeUsers')) { throw new Meteor.Error('error-license-user-limit-reached', i18n.t('error-license-user-limit-reached')); } }; diff --git a/apps/meteor/ee/server/startup/seatsCap.ts b/apps/meteor/ee/server/startup/seatsCap.ts index 3938daff485b..f6d42823cb97 100644 --- a/apps/meteor/ee/server/startup/seatsCap.ts +++ b/apps/meteor/ee/server/startup/seatsCap.ts @@ -62,7 +62,7 @@ callbacks.add( callbacks.add( 'validateUserRoles', - async (userData: Partial) => validateUserRoles(Meteor.userId(), userData), + async (userData: Partial) => validateUserRoles(userData), callbacks.priority.MEDIUM, 'check-max-user-seats', ); From 80bddfa51a214f1fa0eb77075a81a5b7df244755 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Tue, 26 Sep 2023 14:17:16 -0300 Subject: [PATCH 31/38] add a test for duplicated license one --- .../license/__tests__/setLicense.spec.ts | 74 +++++++++++-------- .../src/errors/DuplicatedLicenseError.ts | 6 ++ ee/packages/license/src/license.ts | 15 ++-- 3 files changed, 53 insertions(+), 42 deletions(-) create mode 100644 ee/packages/license/src/errors/DuplicatedLicenseError.ts diff --git a/ee/packages/license/__tests__/setLicense.spec.ts b/ee/packages/license/__tests__/setLicense.spec.ts index 02daff3b07dd..9f783deca658 100644 --- a/ee/packages/license/__tests__/setLicense.spec.ts +++ b/ee/packages/license/__tests__/setLicense.spec.ts @@ -3,40 +3,51 @@ */ import { LicenseImp } from '../src'; +import { DuplicatedLicenseError } from '../src/errors/DuplicatedLicenseError'; import { InvalidLicenseError } from '../src/errors/InvalidLicenseError'; import { NotReadyForValidation } from '../src/errors/NotReadyForValidation'; const VALID_LICENSE = 'WMa5i+/t/LZbYOj8u3XUkivRhWBtWO6ycUjaZoVAw2DxMfdyBIAa2gMMI4x7Z2BrTZIZhFEImfOxcXcgD0QbXHGBJaMI+eYG+eofnVWi2VA7RWbpvWTULgPFgyJ4UEFeCOzVjcBLTQbmMSam3u0RlekWJkfAO0KnmLtsaEYNNA2rz1U+CLI/CdNGfdqrBu5PZZbGkH0KEzyIZMaykOjzvX+C6vd7fRxh23HecwhkBbqE8eQsCBt2ad0qC4MoVXsDaSOmSzGW+aXjuXt/9zjvrLlsmWQTSlkrEHdNkdywm0UkGxqz3+CP99n0WggUBioUiChjMuNMoceWvDvmxYP9Ml2NpYU7SnfhjmMFyXOah8ofzv8w509Y7XODvQBz+iB4Co9YnF3vT96HDDQyAV5t4jATE+0t37EAXmwjTi3qqyP7DLGK/revl+mlcwJ5kS4zZBsm1E4519FkXQOZSyWRnPdjqvh4mCLqoispZ49wKvklDvjPxCSP9us6cVXLDg7NTJr/4pfxLPOkvv7qCgugDvlDx17bXpQFPSDxmpw66FLzvb5Id0dkWjOzrRYSXb0bFWoUQjtHFzmcpFkyVhOKrQ9zA9+Zm7vXmU9Y2l2dK79EloOuHMSYAqsPEag8GMW6vI/cT4iIjHGGDePKnD0HblvTEKzql11cfT/abf2IiaY='; +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); + license.setLicenseLimitCounter('roomsPerGuest', async () => 0); + license.setLicenseLimitCounter('privateApps', () => 0); + license.setLicenseLimitCounter('marketplaceApps', () => 0); + license.setLicenseLimitCounter('monthlyActiveContacts', async () => 0); + return license; +}; + describe('License set license procedures', () => { - it('by default it should have no license', async () => { - const license = new LicenseImp(); + describe('Invalid formats', () => { + it('by default it should have no license', async () => { + const license = new LicenseImp(); - expect(license.hasValidLicense()).toBe(false); - expect(license.getLicense()).toBeUndefined(); - }); + expect(license.hasValidLicense()).toBe(false); + expect(license.getLicense()).toBeUndefined(); + }); - it('should throw an error if the license applied is empty', async () => { - const license = new LicenseImp(); - await expect(async () => { - await license.setLicense(''); - }).rejects.toThrow(InvalidLicenseError); - }); + it('should throw an error if the license applied is empty', async () => { + const license = new LicenseImp(); + await expect(license.setLicense('')).rejects.toThrow(InvalidLicenseError); + }); - it('should throw an error if the license applied is invalid', async () => { - const license = new LicenseImp(); - await expect(async () => { - await license.setLicense('invalid'); - }).rejects.toThrow(InvalidLicenseError); + it('should throw an error if the license applied is invalid', async () => { + const license = new LicenseImp(); + await expect(license.setLicense('invalid')).rejects.toThrow(InvalidLicenseError); + }); }); - describe('pending cases', () => { + describe('Pending cases', () => { it('should return an error if the license is not ready for validation yet - missing workspace url', async () => { const license = new LicenseImp(); - await expect(async () => { - await license.setLicense(VALID_LICENSE); - }).rejects.toThrow(NotReadyForValidation); + await expect(license.setLicense(VALID_LICENSE)).rejects.toThrow(NotReadyForValidation); }); it('should return an error if the license is not ready for validation yet - missing counters', async () => { @@ -45,24 +56,23 @@ describe('License set license procedures', () => { expect(license.getWorkspaceUrl()).toBe('localhost:3000'); - await expect(async () => { - await license.setLicense(VALID_LICENSE); - }).rejects.toThrow(NotReadyForValidation); + await expect(license.setLicense(VALID_LICENSE)).rejects.toThrow(NotReadyForValidation); + + await expect(license.hasValidLicense()).toBe(false); }); it('should return a valid license if the license is ready for validation', async () => { - const license = new LicenseImp(); - await license.setWorkspaceUrl('http://localhost:3000'); + const license = await getReadyLicenseManager(); - license.setLicenseLimitCounter('activeUsers', () => 0); - license.setLicenseLimitCounter('guestUsers', () => 0); - license.setLicenseLimitCounter('roomsPerGuest', async () => 0); - license.setLicenseLimitCounter('privateApps', () => 0); - license.setLicenseLimitCounter('marketplaceApps', () => 0); - license.setLicenseLimitCounter('monthlyActiveContacts', async () => 0); + await expect(license.setLicense(VALID_LICENSE)).resolves.toBe(true); + await expect(license.hasValidLicense()).toBe(true); + }); + + it('should throw an error if the license is duplicated', async () => { + const license = await getReadyLicenseManager(); - expect(license.getWorkspaceUrl()).toBe('localhost:3000'); await expect(license.setLicense(VALID_LICENSE)).resolves.toBe(true); + await expect(license.setLicense(VALID_LICENSE)).rejects.toThrow(DuplicatedLicenseError); }); }); }); diff --git a/ee/packages/license/src/errors/DuplicatedLicenseError.ts b/ee/packages/license/src/errors/DuplicatedLicenseError.ts new file mode 100644 index 000000000000..70b962d53105 --- /dev/null +++ b/ee/packages/license/src/errors/DuplicatedLicenseError.ts @@ -0,0 +1,6 @@ +export class DuplicatedLicenseError extends Error { + constructor(message = 'Duplicated license') { + super(message); + this.name = 'DuplicatedLicense'; + } +} diff --git a/ee/packages/license/src/license.ts b/ee/packages/license/src/license.ts index 7fbec4bfea18..0a632e56aa3f 100644 --- a/ee/packages/license/src/license.ts +++ b/ee/packages/license/src/license.ts @@ -6,6 +6,7 @@ import type { ILicenseV3, LicenseLimitKind } from './definition/ILicenseV3'; import type { BehaviorWithContext } 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 { logger } from './logger'; @@ -163,7 +164,7 @@ export class LicenseManager extends Emitter< return true; } - return false; + throw new DuplicatedLicenseError(); } if (!isReadyForValidation.call(this)) { @@ -175,13 +176,8 @@ export class LicenseManager extends Emitter< logger.info('New Enterprise License'); try { const decrypted = await decrypt(encryptedLicense); - if (!decrypted) { - return false; - } - if (process.env.LICENSE_DEBUG && process.env.LICENSE_DEBUG !== 'false') { - logger.debug({ msg: 'license', decrypted }); - } + logger.debug({ msg: 'license', decrypted }); encryptedLicense.startsWith('RCV3_') ? await this.setLicenseV3(JSON.parse(decrypted), encryptedLicense) @@ -190,9 +186,8 @@ export class LicenseManager extends Emitter< return true; } catch (e) { logger.error('Invalid license'); - if (process.env.LICENSE_DEBUG && process.env.LICENSE_DEBUG !== 'false') { - logger.error({ msg: 'Invalid raw license', encryptedLicense, e }); - } + + logger.error({ msg: 'Invalid raw license', encryptedLicense, e }); throw new InvalidLicenseError(); } } From 74b31495a90d4c772dc779440902602f6e392e82 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Tue, 26 Sep 2023 14:42:22 -0300 Subject: [PATCH 32/38] lint --- apps/meteor/ee/server/api/licenses.ts | 2 +- .../license/__tests__/setLicense.spec.ts | 24 +++++++++++++------ ee/packages/license/src/license.ts | 11 ++++++--- 3 files changed, 26 insertions(+), 11 deletions(-) diff --git a/apps/meteor/ee/server/api/licenses.ts b/apps/meteor/ee/server/api/licenses.ts index 767a1c4bd7bb..cfd657a1f0e9 100644 --- a/apps/meteor/ee/server/api/licenses.ts +++ b/apps/meteor/ee/server/api/licenses.ts @@ -36,7 +36,7 @@ API.v1.addRoute( } const { license } = this.bodyParams; - if (!License.validateFormat(license)) { + if (!(await License.validateFormat(license))) { return API.v1.failure('Invalid license'); } diff --git a/ee/packages/license/__tests__/setLicense.spec.ts b/ee/packages/license/__tests__/setLicense.spec.ts index 9f783deca658..349ac09e8d94 100644 --- a/ee/packages/license/__tests__/setLicense.spec.ts +++ b/ee/packages/license/__tests__/setLicense.spec.ts @@ -44,6 +44,23 @@ describe('License set license procedures', () => { }); }); + it('should throw an error if the license is duplicated', async () => { + const license = await getReadyLicenseManager(); + + await expect(license.setLicense(VALID_LICENSE)).resolves.toBe(true); + await expect(license.setLicense(VALID_LICENSE)).rejects.toThrow(DuplicatedLicenseError); + }); + + it('should keep a valid license if a new invalid license is applied', async () => { + const license = await getReadyLicenseManager(); + + await expect(license.setLicense(VALID_LICENSE)).resolves.toBe(true); + await expect(license.hasValidLicense()).toBe(true); + + await expect(license.setLicense('invalid')).rejects.toThrow(InvalidLicenseError); + await expect(license.hasValidLicense()).toBe(true); + }); + describe('Pending cases', () => { it('should return an error if the license is not ready for validation yet - missing workspace url', async () => { const license = new LicenseImp(); @@ -67,12 +84,5 @@ describe('License set license procedures', () => { await expect(license.setLicense(VALID_LICENSE)).resolves.toBe(true); await expect(license.hasValidLicense()).toBe(true); }); - - it('should throw an error if the license is duplicated', async () => { - const license = await getReadyLicenseManager(); - - await expect(license.setLicense(VALID_LICENSE)).resolves.toBe(true); - await expect(license.setLicense(VALID_LICENSE)).rejects.toThrow(DuplicatedLicenseError); - }); }); }); diff --git a/ee/packages/license/src/license.ts b/ee/packages/license/src/license.ts index 0a632e56aa3f..a2463227f1b2 100644 --- a/ee/packages/license/src/license.ts +++ b/ee/packages/license/src/license.ts @@ -137,17 +137,22 @@ export class LicenseManager extends Emitter< } private async validateLicense(): Promise { - if (!this._license || !isReadyForValidation.call(this)) { - return; + if (!this._license) { + throw new InvalidLicenseError(); + } + + if (!isReadyForValidation.call(this)) { + throw new NotReadyForValidation(); } // #TODO: Only include 'prevent_installation' here if this is actually the initial installation of the license - const validationResult = await runValidation.bind(this)(this._license, [ + const validationResult = await runValidation.call(this, this._license, [ 'invalidate_license', 'prevent_installation', 'start_fair_policy', 'disable_modules', ]); + this.processValidationResult(validationResult); } From ef1011822abe05cb387aba27e9cb02ca3a1599e4 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Tue, 26 Sep 2023 14:44:45 -0300 Subject: [PATCH 33/38] enable testunit --- packages/jwt/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/jwt/package.json b/packages/jwt/package.json index 1f556b32a445..b6be368917c3 100644 --- a/packages/jwt/package.json +++ b/packages/jwt/package.json @@ -13,6 +13,7 @@ "lint": "eslint --ext .js,.jsx,.ts,.tsx .", "lint:fix": "eslint --ext .js,.jsx,.ts,.tsx . --fix", "test": "jest", + "testunit": "jest", "build": "rm -rf dist && tsc -p tsconfig.json", "dev": "tsc -p tsconfig.json --watch --preserveWatchOutput" }, From 819a7ce2f647c29a594141f982f530a39d543dc3 Mon Sep 17 00:00:00 2001 From: Luis Mauro Date: Tue, 26 Sep 2023 11:32:23 -0600 Subject: [PATCH 34/38] add changeset --- .changeset/twelve-files-deny.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 .changeset/twelve-files-deny.md diff --git a/.changeset/twelve-files-deny.md b/.changeset/twelve-files-deny.md new file mode 100644 index 000000000000..04700f86c002 --- /dev/null +++ b/.changeset/twelve-files-deny.md @@ -0,0 +1,21 @@ +--- +'@rocket.chat/license': minor +'@rocket.chat/jwt': minor +'@rocket.chat/omnichannel-services': minor +'@rocket.chat/omnichannel-transcript': minor +'@rocket.chat/authorization-service': minor +'@rocket.chat/stream-hub-service': minor +'@rocket.chat/presence-service': minor +'@rocket.chat/account-service': minor +'@rocket.chat/core-services': minor +'@rocket.chat/model-typings': minor +'@rocket.chat/core-typings': minor +'@rocket.chat/rest-typings': minor +'@rocket.chat/ddp-streamer': minor +'@rocket.chat/queue-worker': minor +'@rocket.chat/presence': minor +'@rocket.chat/meteor': minor +--- + +feat new `package/jwt` with `sign` and `veriy` functions to create a JWT given a JS object. By default uses algorithm RS256 for the public/private keys. +feat new `ee/package/license`. Contains new V3 definition and previous License definition renamed as LicenseV2. Package handles both versions. From d1b85f4863360014745199b070bf24307990e3e3 Mon Sep 17 00:00:00 2001 From: Luis Mauro Date: Tue, 26 Sep 2023 12:24:10 -0600 Subject: [PATCH 35/38] uses async/await on decrypt --- ee/packages/license/src/decrypt.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/ee/packages/license/src/decrypt.ts b/ee/packages/license/src/decrypt.ts index 1159e6de82dc..6168c0e50b5a 100644 --- a/ee/packages/license/src/decrypt.ts +++ b/ee/packages/license/src/decrypt.ts @@ -5,15 +5,13 @@ import { verify } from '@rocket.chat/jwt'; const publicKey = 'LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUlJQ0lqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FnOEFNSUlDQ2dLQ0FnRUFxV1Nza2Q5LzZ6Ung4a3lQY2ljcwpiMzJ3Mnd4VnV3N3lCVDk2clEvOEQreU1lQ01POXdTU3BIYS85bkZ5d293RXRpZ3B0L3dyb1BOK1ZHU3didHdQCkZYQmVxRWxCbmRHRkFsODZlNStFbGlIOEt6L2hHbkNtSk5tWHB4RUsyUkUwM1g0SXhzWVg3RERCN010eC9pcXMKY2pCL091dlNCa2ppU2xlUzdibE5JVC9kQTdLNC9DSjNvaXUwMmJMNEV4Y2xDSGVwenFOTWVQM3dVWmdweE9uZgpOT3VkOElYWUs3M3pTY3VFOEUxNTdZd3B6Q0twVmFIWDdaSmY4UXVOc09PNVcvYUlqS2wzTDYyNjkrZUlPRXJHCndPTm1hSG56Zmc5RkxwSmh6Z3BPMzhhVm43NnZENUtLakJhaldza1krNGEyZ1NRbUtOZUZxYXFPb3p5RUZNMGUKY0ZXWlZWWjNMZWg0dkVNb1lWUHlJeng5Nng4ZjIveW1QbmhJdXZRdjV3TjRmeWVwYTdFWTVVQ2NwNzF6OGtmUAo0RmNVelBBMElEV3lNaWhYUi9HNlhnUVFaNEdiL3FCQmh2cnZpSkNGemZZRGNKZ0w3RmVnRllIUDNQR0wwN1FnCnZMZXZNSytpUVpQcnhyYnh5U3FkUE9rZ3VyS2pWclhUVXI0QTlUZ2lMeUlYNVVsSnEzRS9SVjdtZk9xWm5MVGEKU0NWWEhCaHVQbG5DR1pSMDFUb1RDZktoTUcxdTBDRm5MMisxNWhDOWZxT21XdjlRa2U0M3FsSjBQZ0YzVkovWAp1eC9tVHBuazlnbmJHOUpIK21mSDM5Um9GdlROaW5Zd1NNdll6dXRWT242OXNPemR3aERsYTkwbDNBQ2g0eENWCks3Sk9YK3VIa29OdTNnMmlWeGlaVU0wQ0F3RUFBUT09Ci0tLS0tRU5EIFBVQkxJQyBLRVktLS0tLQo='; -// #TODO: use async/await export default async function decrypt(encrypted: string): Promise { // handle V3 if (encrypted.startsWith('RCV3_')) { const jwt = encrypted.substring(5); + const [payload] = await verify(jwt, publicKey); - return verify(jwt, publicKey).then(([payload, _header]) => { - return JSON.stringify(payload); - }); + return JSON.stringify(payload); } const decrypted = crypto.publicDecrypt(Buffer.from(publicKey, 'base64').toString('utf-8'), Buffer.from(encrypted, 'base64')); From 21d9d7209c19227ed9cf0ee1b5bd7421a024d7b5 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Thu, 28 Sep 2023 14:19:49 -0300 Subject: [PATCH 36/38] chore: unit tests (#30504) --- apps/meteor/ee/app/license/server/settings.ts | 16 +- apps/meteor/ee/app/license/server/startup.ts | 6 +- apps/meteor/ee/server/lib/EnterpriseCheck.ts | 2 +- .../license/__tests__/MockedLicenseBuilder.ts | 209 ++++++ ee/packages/license/__tests__/emitter.spec.ts | 66 ++ .../license/__tests__/setLicense.spec.ts | 43 +- ee/packages/license/babel.config.json | 11 + ee/packages/license/package.json | 14 +- ee/packages/license/src/events/deprecated.ts | 2 +- ee/packages/license/src/events/emitter.ts | 2 +- ee/packages/license/src/index.ts | 1 + ee/packages/license/src/license.spec.ts | 42 ++ ee/packages/license/src/license.ts | 69 +- .../license/src/{decrypt.ts => token.ts} | 49 +- ee/packages/license/src/v2/convertToV3.ts | 2 +- .../getCurrentValueForLicenseLimit.ts | 10 +- .../src/validation/runValidation.spec.ts | 38 ++ .../license/src/validation/runValidation.ts | 2 +- .../license/src/validation/validateFormat.ts | 2 +- .../src/validation/validateLicensePeriods.ts | 2 +- packages/jwt/src/index.ts | 13 +- yarn.lock | 593 ++++++++++-------- 22 files changed, 868 insertions(+), 326 deletions(-) create mode 100644 ee/packages/license/__tests__/MockedLicenseBuilder.ts create mode 100644 ee/packages/license/__tests__/emitter.spec.ts create mode 100644 ee/packages/license/babel.config.json create mode 100644 ee/packages/license/src/license.spec.ts rename ee/packages/license/src/{decrypt.ts => token.ts} (50%) create mode 100644 ee/packages/license/src/validation/runValidation.spec.ts diff --git a/apps/meteor/ee/app/license/server/settings.ts b/apps/meteor/ee/app/license/server/settings.ts index c332142d286d..1bec7126ae85 100644 --- a/apps/meteor/ee/app/license/server/settings.ts +++ b/apps/meteor/ee/app/license/server/settings.ts @@ -29,16 +29,24 @@ settings.watch('Enterprise_License', async (license) => { return; } - if (!(await License.setLicense(license))) { - await Settings.updateValueById('Enterprise_License_Status', 'Invalid'); - return; + try { + if (!(await License.setLicense(license))) { + await Settings.updateValueById('Enterprise_License_Status', 'Invalid'); + return; + } + } catch (_error) { + // do nothing } await Settings.updateValueById('Enterprise_License_Status', 'Valid'); }); if (process.env.ROCKETCHAT_LICENSE) { - await License.setLicense(process.env.ROCKETCHAT_LICENSE); + try { + await License.setLicense(process.env.ROCKETCHAT_LICENSE); + } catch (_error) { + // do nothing + } Meteor.startup(async () => { if (settings.get('Enterprise_License')) { diff --git a/apps/meteor/ee/app/license/server/startup.ts b/apps/meteor/ee/app/license/server/startup.ts index 7da7917211be..d3523282d1e8 100644 --- a/apps/meteor/ee/app/license/server/startup.ts +++ b/apps/meteor/ee/app/license/server/startup.ts @@ -12,7 +12,11 @@ settings.watch('Site_Url', (value) => { }); callbacks.add('workspaceLicenseChanged', async (updatedLicense) => { - await License.setLicense(updatedLicense); + try { + await License.setLicense(updatedLicense); + } catch (_error) { + // Ignore + } }); License.setLicenseLimitCounter('activeUsers', () => Users.getActiveLocalUserCount()); diff --git a/apps/meteor/ee/server/lib/EnterpriseCheck.ts b/apps/meteor/ee/server/lib/EnterpriseCheck.ts index 8bccfed59071..ca8cd1e25b10 100644 --- a/apps/meteor/ee/server/lib/EnterpriseCheck.ts +++ b/apps/meteor/ee/server/lib/EnterpriseCheck.ts @@ -41,7 +41,7 @@ export const EnterpriseCheck: ServiceSchema = { async started(): Promise { setInterval(async () => { try { - const hasLicense = await this.broker.call('license.hasLicense', ['scalability']); + const hasLicense = await this.broker.call('license.hasValidLicense', ['scalability']); if (hasLicense) { checkFails = 0; return; diff --git a/ee/packages/license/__tests__/MockedLicenseBuilder.ts b/ee/packages/license/__tests__/MockedLicenseBuilder.ts new file mode 100644 index 000000000000..316261744da5 --- /dev/null +++ b/ee/packages/license/__tests__/MockedLicenseBuilder.ts @@ -0,0 +1,209 @@ +import { LicenseImp } from '../src'; +import type { ILicenseTag } from '../src/definition/ILicenseTag'; +import type { ILicenseV3 } from '../src/definition/ILicenseV3'; +import type { LicenseLimit } from '../src/definition/LicenseLimit'; +import type { LicenseModule } from '../src/definition/LicenseModule'; +import type { LicensePeriod, Timestamp } from '../src/definition/LicensePeriod'; +import { encrypt } from '../src/token'; + +export class MockedLicenseBuilder { + information: { + id?: string; + autoRenew: boolean; + visualExpiration: Timestamp; + notifyAdminsAt?: Timestamp; + notifyUsersAt?: Timestamp; + trial: boolean; + offline: boolean; + createdAt: Timestamp; + grantedBy: { + method: 'manual' | 'self-service' | 'sales' | 'support' | 'reseller'; + seller?: string; + }; + grantedTo?: { + name?: string; + company?: string; + email?: string; + }; + legalText?: string; + notes?: string; + tags?: ILicenseTag[]; + }; + + validation: { + serverUrls: { + value: string; + type: 'url' | 'regex' | 'hash'; + }[]; + + serverVersions?: { + value: string; + }[]; + + serverUniqueId?: string; + cloudWorkspaceId?: string; + validPeriods: LicensePeriod[]; + legalTextAgreement?: { + type: 'required' | 'not-required' | 'accepted'; + acceptedVia?: 'cloud'; + }; + + statisticsReport: { + required: boolean; + allowedStaleInDays?: number; + }; + }; + + constructor() { + this.information = { + autoRenew: true, + // expires in 1 year + visualExpiration: new Date(new Date().setFullYear(new Date().getFullYear() + 1)).toISOString(), + // 15 days before expiration + notifyAdminsAt: new Date(new Date().setDate(new Date().getDate() + 15)).toISOString(), + // 30 days before expiration + notifyUsersAt: new Date(new Date().setDate(new Date().getDate() + 30)).toISOString(), + trial: false, + offline: false, + createdAt: new Date().toISOString(), + grantedBy: { + method: 'manual', + seller: 'Rocket.Cat', + }, + tags: [ + { + name: 'Test', + color: 'blue', + }, + ], + }; + + this.validation = { + serverUrls: [ + { + value: 'localhost:3000', + type: 'url', + }, + ], + serverVersions: [ + { + value: '3.0.0', + }, + ], + + serverUniqueId: '1234567890', + cloudWorkspaceId: '1234567890', + + validPeriods: [ + { + invalidBehavior: 'disable_modules', + modules: ['livechat-enterprise'], + validFrom: new Date(new Date().setFullYear(new Date().getFullYear() - 1)).toISOString(), + validUntil: new Date(new Date().setFullYear(new Date().getFullYear() + 1)).toISOString(), + }, + ], + + statisticsReport: { + required: true, + allowedStaleInDays: 30, + }, + }; + } + + public resetValidPeriods(): this { + this.validation.validPeriods = []; + return this; + } + + public withValidPeriod(period: LicensePeriod): this { + this.validation.validPeriods.push(period); + return this; + } + + public withGrantedTo(grantedTo: { name?: string; company?: string; email?: string }): this { + this.information.grantedTo = grantedTo; + return this; + } + + grantedModules: { + module: LicenseModule; + }[]; + + limits: { + activeUsers?: LicenseLimit[]; + guestUsers?: LicenseLimit[]; + roomsPerGuest?: LicenseLimit<'prevent_action'>[]; + privateApps?: LicenseLimit[]; + marketplaceApps?: LicenseLimit[]; + monthlyActiveContacts?: LicenseLimit[]; + }; + + cloudMeta?: Record; + + public withServerUrls(urls: { value: string; type: 'url' | 'regex' | 'hash' }): this { + this.validation.serverUrls = this.validation.serverUrls ?? []; + this.validation.serverUrls.push(urls); + return this; + } + + public withServerVersions(versions: { value: string }): this { + this.validation.serverVersions = this.validation.serverVersions ?? []; + this.validation.serverVersions.push(versions); + return this; + } + + public withGratedModules(modules: LicenseModule[]): this { + this.grantedModules = this.grantedModules ?? []; + this.grantedModules.push(...modules.map((module) => ({ module }))); + return this; + } + + withNoGratedModules(modules: LicenseModule[]): this { + this.grantedModules = this.grantedModules ?? []; + this.grantedModules = this.grantedModules.filter(({ module }) => !modules.includes(module)); + return this; + } + + public withLimits(key: K, value: ILicenseV3['limits'][K]): this { + this.limits = this.limits ?? {}; + this.limits[key] = value; + return this; + } + + public build(): ILicenseV3 { + return { + version: '3.0', + information: this.information, + validation: this.validation, + grantedModules: [...new Set(this.grantedModules)], + limits: { + activeUsers: [], + guestUsers: [], + roomsPerGuest: [], + privateApps: [], + marketplaceApps: [], + monthlyActiveContacts: [], + ...this.limits, + }, + cloudMeta: this.cloudMeta, + }; + } + + public sign(): Promise { + return encrypt(this.build()); + } +} + +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); + license.setLicenseLimitCounter('roomsPerGuest', async () => 0); + license.setLicenseLimitCounter('privateApps', () => 0); + license.setLicenseLimitCounter('marketplaceApps', () => 0); + license.setLicenseLimitCounter('monthlyActiveContacts', async () => 0); + return license; +}; diff --git a/ee/packages/license/__tests__/emitter.spec.ts b/ee/packages/license/__tests__/emitter.spec.ts new file mode 100644 index 000000000000..4c7c5a8255d1 --- /dev/null +++ b/ee/packages/license/__tests__/emitter.spec.ts @@ -0,0 +1,66 @@ +/** + * @jest-environment node + */ + +import { MockedLicenseBuilder, getReadyLicenseManager } from './MockedLicenseBuilder'; + +describe('Event License behaviors', () => { + it('should call the module as they are enabled/disabled', async () => { + const license = await getReadyLicenseManager(); + const validFn = jest.fn(); + const invalidFn = jest.fn(); + + license.onValidFeature('livechat-enterprise', validFn); + license.onInvalidFeature('livechat-enterprise', invalidFn); + + const mocked = await new MockedLicenseBuilder(); + const oldToken = await mocked.sign(); + + const newToken = await mocked.withGratedModules(['livechat-enterprise']).sign(); + + // apply license + await expect(license.setLicense(oldToken)).resolves.toBe(true); + await expect(license.hasValidLicense()).toBe(true); + + await expect(license.hasModule('livechat-enterprise')).toBe(false); + + await expect(validFn).not.toBeCalled(); + await expect(invalidFn).toBeCalledTimes(1); + + // apply license containing livechat-enterprise module + + validFn.mockClear(); + invalidFn.mockClear(); + + await expect(license.setLicense(newToken)).resolves.toBe(true); + await expect(license.hasValidLicense()).toBe(true); + await expect(license.hasModule('livechat-enterprise')).toBe(true); + + await expect(validFn).toBeCalledTimes(1); + await expect(invalidFn).toBeCalledTimes(0); + + // apply the old license again + + validFn.mockClear(); + invalidFn.mockClear(); + await expect(license.setLicense(oldToken)).resolves.toBe(true); + await expect(license.hasValidLicense()).toBe(true); + await expect(license.hasModule('livechat-enterprise')).toBe(false); + await expect(validFn).toBeCalledTimes(0); + await expect(invalidFn).toBeCalledTimes(1); + }); + + it('should call `onValidateLicense` when a valid license is applied', async () => { + const license = await getReadyLicenseManager(); + const fn = jest.fn(); + + license.onValidateLicense(fn); + + const mocked = await new MockedLicenseBuilder(); + const token = await mocked.sign(); + + await expect(license.setLicense(token)).resolves.toBe(true); + await expect(license.hasValidLicense()).toBe(true); + await expect(fn).toBeCalledTimes(1); + }); +}); diff --git a/ee/packages/license/__tests__/setLicense.spec.ts b/ee/packages/license/__tests__/setLicense.spec.ts index 349ac09e8d94..962f591750ad 100644 --- a/ee/packages/license/__tests__/setLicense.spec.ts +++ b/ee/packages/license/__tests__/setLicense.spec.ts @@ -6,24 +6,12 @@ import { LicenseImp } from '../src'; import { DuplicatedLicenseError } from '../src/errors/DuplicatedLicenseError'; import { InvalidLicenseError } from '../src/errors/InvalidLicenseError'; import { NotReadyForValidation } from '../src/errors/NotReadyForValidation'; +import { MockedLicenseBuilder, getReadyLicenseManager } from './MockedLicenseBuilder'; +// Same license used on ci tasks so no I didnt leak it const VALID_LICENSE = 'WMa5i+/t/LZbYOj8u3XUkivRhWBtWO6ycUjaZoVAw2DxMfdyBIAa2gMMI4x7Z2BrTZIZhFEImfOxcXcgD0QbXHGBJaMI+eYG+eofnVWi2VA7RWbpvWTULgPFgyJ4UEFeCOzVjcBLTQbmMSam3u0RlekWJkfAO0KnmLtsaEYNNA2rz1U+CLI/CdNGfdqrBu5PZZbGkH0KEzyIZMaykOjzvX+C6vd7fRxh23HecwhkBbqE8eQsCBt2ad0qC4MoVXsDaSOmSzGW+aXjuXt/9zjvrLlsmWQTSlkrEHdNkdywm0UkGxqz3+CP99n0WggUBioUiChjMuNMoceWvDvmxYP9Ml2NpYU7SnfhjmMFyXOah8ofzv8w509Y7XODvQBz+iB4Co9YnF3vT96HDDQyAV5t4jATE+0t37EAXmwjTi3qqyP7DLGK/revl+mlcwJ5kS4zZBsm1E4519FkXQOZSyWRnPdjqvh4mCLqoispZ49wKvklDvjPxCSP9us6cVXLDg7NTJr/4pfxLPOkvv7qCgugDvlDx17bXpQFPSDxmpw66FLzvb5Id0dkWjOzrRYSXb0bFWoUQjtHFzmcpFkyVhOKrQ9zA9+Zm7vXmU9Y2l2dK79EloOuHMSYAqsPEag8GMW6vI/cT4iIjHGGDePKnD0HblvTEKzql11cfT/abf2IiaY='; -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); - license.setLicenseLimitCounter('roomsPerGuest', async () => 0); - license.setLicenseLimitCounter('privateApps', () => 0); - license.setLicenseLimitCounter('marketplaceApps', () => 0); - license.setLicenseLimitCounter('monthlyActiveContacts', async () => 0); - return license; -}; - describe('License set license procedures', () => { describe('Invalid formats', () => { it('by default it should have no license', async () => { @@ -85,4 +73,31 @@ describe('License set license procedures', () => { await expect(license.hasValidLicense()).toBe(true); }); }); + + describe('License V3', () => { + it('should return a valid license if the license is ready for validation', async () => { + const license = await getReadyLicenseManager(); + const token = await new MockedLicenseBuilder().sign(); + + await expect(license.setLicense(token)).resolves.toBe(true); + await expect(license.hasValidLicense()).toBe(true); + }); + + it('should accept new licenses', async () => { + const license = await getReadyLicenseManager(); + const mocked = await new MockedLicenseBuilder(); + const oldToken = await mocked.sign(); + + const newToken = await mocked.withGratedModules(['livechat-enterprise']).sign(); + + await expect(license.setLicense(oldToken)).resolves.toBe(true); + await expect(license.hasValidLicense()).toBe(true); + + await expect(license.hasModule('livechat-enterprise')).toBe(false); + + await expect(license.setLicense(newToken)).resolves.toBe(true); + await expect(license.hasValidLicense()).toBe(true); + await expect(license.hasModule('livechat-enterprise')).toBe(true); + }); + }); }); diff --git a/ee/packages/license/babel.config.json b/ee/packages/license/babel.config.json new file mode 100644 index 000000000000..e154c0813530 --- /dev/null +++ b/ee/packages/license/babel.config.json @@ -0,0 +1,11 @@ +{ + "presets": ["@babel/preset-typescript"], + "plugins": [ + [ + "transform-inline-environment-variables", + { + "include": ["LICENSE_PUBLIC_KEY_V3"] + } + ] + ] +} diff --git a/ee/packages/license/package.json b/ee/packages/license/package.json index 8fc96ef0b3eb..f6a1e7a2b7d5 100644 --- a/ee/packages/license/package.json +++ b/ee/packages/license/package.json @@ -3,22 +3,32 @@ "version": "0.0.1", "private": true, "devDependencies": { + "@babel/cli": "^7.23.0", + "@babel/core": "^7.23.0", + "@babel/preset-env": "^7.22.20", + "@babel/preset-typescript": "^7.23.0", "@swc/core": "^1.3.66", "@swc/jest": "^0.2.26", + "@types/babel__core": "^7", + "@types/babel__preset-env": "^7", "@types/jest": "~29.5.3", "@types/ws": "^8.5.5", + "babel-plugin-transform-inline-environment-variables": "^0.4.4", "eslint": "~8.45.0", "jest": "~29.6.1", "jest-environment-jsdom": "~29.6.1", "jest-websocket-mock": "^2.4.0", "ts-jest": "~29.0.5", - "typescript": "~5.2.2" + "typescript": "^5.2.2" }, "scripts": { "lint": "eslint --ext .js,.jsx,.ts,.tsx .", "lint:fix": "eslint --ext .js,.jsx,.ts,.tsx . --fix", "test": "jest", - "build": "rm -rf dist && tsc -p tsconfig.json", + "testunit": "jest", + "build": "npm run build:types && npm run build:js", + "build:types": "tsc --emitDeclarationOnly", + "build:js": "babel src --out-dir dist --extensions \".ts,.tsx\" --source-maps inline", "dev": "tsc -p tsconfig.json --watch --preserveWatchOutput" }, "main": "./dist/index.js", diff --git a/ee/packages/license/src/events/deprecated.ts b/ee/packages/license/src/events/deprecated.ts index cc507fb0345c..8ebfe4729292 100644 --- a/ee/packages/license/src/events/deprecated.ts +++ b/ee/packages/license/src/events/deprecated.ts @@ -8,5 +8,5 @@ export function onLicense(this: LicenseManager, feature: LicenseModule, cb: (... return cb(); } - this.on(`valid:${feature}`, cb); + this.once(`valid:${feature}`, cb); } diff --git a/ee/packages/license/src/events/emitter.ts b/ee/packages/license/src/events/emitter.ts index 52256d0fe6f5..9d4025e4bce3 100644 --- a/ee/packages/license/src/events/emitter.ts +++ b/ee/packages/license/src/events/emitter.ts @@ -6,7 +6,7 @@ import { logger } from '../logger'; export function moduleValidated(this: LicenseManager, module: LicenseModule) { try { this.emit('module', { module, valid: true }); - // this.emit(`valid:${module}`); + this.emit(`valid:${module}`); } catch (error) { logger.error({ msg: 'Error running module added event', error }); } diff --git a/ee/packages/license/src/index.ts b/ee/packages/license/src/index.ts index 433b4b195bf3..9dbd94db53ed 100644 --- a/ee/packages/license/src/index.ts +++ b/ee/packages/license/src/index.ts @@ -44,6 +44,7 @@ interface License { onValidateLicense: typeof onValidateLicense; onInvalidateLicense: typeof onInvalidateLicense; onLimitReached: typeof onLimitReached; + // Deprecated: onLicense: typeof onLicense; // Deprecated: diff --git a/ee/packages/license/src/license.spec.ts b/ee/packages/license/src/license.spec.ts new file mode 100644 index 000000000000..36744585d59f --- /dev/null +++ b/ee/packages/license/src/license.spec.ts @@ -0,0 +1,42 @@ +import { MockedLicenseBuilder, getReadyLicenseManager } from '../__tests__/MockedLicenseBuilder'; + +it('should not prevent if there is no license', async () => { + const license = await getReadyLicenseManager(); + const result = await license.shouldPreventAction('activeUsers'); + expect(result).toBe(false); +}); + +it('should not prevent if the counter is under the limit', async () => { + const licenseManager = await getReadyLicenseManager(); + + 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', () => 5); + await expect(licenseManager.shouldPreventAction('activeUsers')).resolves.toBe(false); +}); + +it('should prevent if the counter is equal or over the limit', async () => { + const licenseManager = await getReadyLicenseManager(); + + 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); + + licenseManager.setLicenseLimitCounter('activeUsers', () => 11); + await expect(licenseManager.shouldPreventAction('activeUsers')).resolves.toBe(true); +}); diff --git a/ee/packages/license/src/license.ts b/ee/packages/license/src/license.ts index a2463227f1b2..2fb25b0e3b4f 100644 --- a/ee/packages/license/src/license.ts +++ b/ee/packages/license/src/license.ts @@ -1,6 +1,5 @@ import { Emitter } from '@rocket.chat/emitter'; -import decrypt from './decrypt'; import type { ILicenseV2 } from './definition/ILicenseV2'; import type { ILicenseV3, LicenseLimitKind } from './definition/ILicenseV3'; import type { BehaviorWithContext } from './definition/LicenseBehavior'; @@ -14,6 +13,7 @@ import { invalidateAll, replaceModules } from './modules'; import { applyPendingLicense, clearPendingLicense, hasPendingLicense, isPendingLicense, setPendingLicense } from './pendingLicense'; import { showLicense } from './showLicense'; import { replaceTags } from './tags'; +import { decrypt } from './token'; import { convertToV3 } from './v2/convertToV3'; import { getCurrentValueForLicenseLimit } from './validation/getCurrentValueForLicenseLimit'; import { getModulesToDisable } from './validation/getModulesToDisable'; @@ -107,35 +107,10 @@ export class LicenseManager extends Emitter< return this.setLicenseV3(convertToV3(newLicense), encryptedLicense, newLicense); } - private isLicenseDuplicate(encryptedLicense: string): boolean { + private isLicenseDuplicated(encryptedLicense: string): boolean { return Boolean(this._lockedLicense && this._lockedLicense === encryptedLicense); } - private processValidationResult(result: BehaviorWithContext[]): void { - if (!this._license || isBehaviorsInResult(result, ['invalidate_license', 'prevent_installation'])) { - return; - } - - this._valid = true; - this._inFairPolicy = isBehaviorsInResult(result, ['start_fair_policy']); - - if (this._license.information.tags) { - replaceTags(this._license.information.tags); - } - - const disabledModules = getModulesToDisable(result); - const modulesToEnable = this._license.grantedModules.filter(({ module }) => !disabledModules.includes(module)); - - replaceModules.call( - this, - modulesToEnable.map(({ module }) => module), - ); - logger.log({ msg: 'License validated', modules: modulesToEnable }); - - this.emit('validate'); - showLicense.call(this, this._license, this._valid); - } - private async validateLicense(): Promise { if (!this._license) { throw new InvalidLicenseError(); @@ -161,12 +136,12 @@ export class LicenseManager extends Emitter< throw new InvalidLicenseError(); } - if (this.isLicenseDuplicate(encryptedLicense)) { + if (this.isLicenseDuplicated(encryptedLicense)) { // If there is a pending license but the user is trying to revert to the license that is currently active if (hasPendingLicense.call(this) && !isPendingLicense.call(this, encryptedLicense)) { // simply remove the pending license clearPendingLicense.call(this); - return true; + throw new Error('Invalid license 1'); } throw new DuplicatedLicenseError(); @@ -180,23 +155,51 @@ export class LicenseManager extends Emitter< logger.info('New Enterprise License'); try { - const decrypted = await decrypt(encryptedLicense); + const decrypted = JSON.parse(await decrypt(encryptedLicense)); logger.debug({ msg: 'license', decrypted }); - encryptedLicense.startsWith('RCV3_') - ? await this.setLicenseV3(JSON.parse(decrypted), encryptedLicense) - : await this.setLicenseV2(JSON.parse(decrypted), encryptedLicense); + if (!encryptedLicense.startsWith('RCV3_')) { + await this.setLicenseV2(decrypted, encryptedLicense); + return true; + } + await this.setLicenseV3(decrypted, encryptedLicense); return true; } catch (e) { logger.error('Invalid license'); logger.error({ msg: 'Invalid raw license', encryptedLicense, e }); + throw new InvalidLicenseError(); } } + private processValidationResult(result: BehaviorWithContext[]): void { + if (!this._license || isBehaviorsInResult(result, ['invalidate_license', 'prevent_installation'])) { + return; + } + + this._valid = true; + this._inFairPolicy = isBehaviorsInResult(result, ['start_fair_policy']); + + if (this._license.information.tags) { + replaceTags(this._license.information.tags); + } + + const disabledModules = getModulesToDisable(result); + const modulesToEnable = this._license.grantedModules.filter(({ module }) => !disabledModules.includes(module)); + + replaceModules.call( + this, + modulesToEnable.map(({ module }) => module), + ); + logger.log({ msg: 'License validated', modules: modulesToEnable }); + + this.emit('validate'); + showLicense.call(this, this._license, this._valid); + } + public hasValidLicense(): boolean { return Boolean(this.getLicense()); } diff --git a/ee/packages/license/src/decrypt.ts b/ee/packages/license/src/token.ts similarity index 50% rename from ee/packages/license/src/decrypt.ts rename to ee/packages/license/src/token.ts index 6168c0e50b5a..80ecc29b4a3f 100644 --- a/ee/packages/license/src/decrypt.ts +++ b/ee/packages/license/src/token.ts @@ -1,20 +1,59 @@ import crypto from 'crypto'; -import { verify } from '@rocket.chat/jwt'; +import { verify, sign, getPairs } from '@rocket.chat/jwt'; -const publicKey = +import type { ILicenseV3 } from './definition/ILicenseV3'; + +const PUBLIC_KEY_V2 = 'LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUlJQ0lqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FnOEFNSUlDQ2dLQ0FnRUFxV1Nza2Q5LzZ6Ung4a3lQY2ljcwpiMzJ3Mnd4VnV3N3lCVDk2clEvOEQreU1lQ01POXdTU3BIYS85bkZ5d293RXRpZ3B0L3dyb1BOK1ZHU3didHdQCkZYQmVxRWxCbmRHRkFsODZlNStFbGlIOEt6L2hHbkNtSk5tWHB4RUsyUkUwM1g0SXhzWVg3RERCN010eC9pcXMKY2pCL091dlNCa2ppU2xlUzdibE5JVC9kQTdLNC9DSjNvaXUwMmJMNEV4Y2xDSGVwenFOTWVQM3dVWmdweE9uZgpOT3VkOElYWUs3M3pTY3VFOEUxNTdZd3B6Q0twVmFIWDdaSmY4UXVOc09PNVcvYUlqS2wzTDYyNjkrZUlPRXJHCndPTm1hSG56Zmc5RkxwSmh6Z3BPMzhhVm43NnZENUtLakJhaldza1krNGEyZ1NRbUtOZUZxYXFPb3p5RUZNMGUKY0ZXWlZWWjNMZWg0dkVNb1lWUHlJeng5Nng4ZjIveW1QbmhJdXZRdjV3TjRmeWVwYTdFWTVVQ2NwNzF6OGtmUAo0RmNVelBBMElEV3lNaWhYUi9HNlhnUVFaNEdiL3FCQmh2cnZpSkNGemZZRGNKZ0w3RmVnRllIUDNQR0wwN1FnCnZMZXZNSytpUVpQcnhyYnh5U3FkUE9rZ3VyS2pWclhUVXI0QTlUZ2lMeUlYNVVsSnEzRS9SVjdtZk9xWm5MVGEKU0NWWEhCaHVQbG5DR1pSMDFUb1RDZktoTUcxdTBDRm5MMisxNWhDOWZxT21XdjlRa2U0M3FsSjBQZ0YzVkovWAp1eC9tVHBuazlnbmJHOUpIK21mSDM5Um9GdlROaW5Zd1NNdll6dXRWT242OXNPemR3aERsYTkwbDNBQ2g0eENWCks3Sk9YK3VIa29OdTNnMmlWeGlaVU0wQ0F3RUFBUT09Ci0tLS0tRU5EIFBVQkxJQyBLRVktLS0tLQo='; -export default async function decrypt(encrypted: string): Promise { +const PUBLIC_KEY_V3 = ''; + +let TEST_KEYS: [string, string] | undefined = undefined; + +export async function decrypt(encrypted: string): Promise { + if (process.env.NODE_ENV === 'test') { + if (encrypted.startsWith('RCV3_')) { + const jwt = encrypted.substring(5); + + TEST_KEYS = TEST_KEYS ?? (await getPairs()); + + if (!TEST_KEYS) { + throw new Error('Missing LICENSE_PUBLIC_KEY_V3'); + } + + const [spki] = TEST_KEYS; + + const [payload] = await verify(jwt, spki); + return JSON.stringify(payload); + } + } + // handle V3 if (encrypted.startsWith('RCV3_')) { const jwt = encrypted.substring(5); - const [payload] = await verify(jwt, publicKey); + const [payload] = await verify(jwt, PUBLIC_KEY_V3); return JSON.stringify(payload); } - const decrypted = crypto.publicDecrypt(Buffer.from(publicKey, 'base64').toString('utf-8'), Buffer.from(encrypted, 'base64')); + const decrypted = crypto.publicDecrypt(Buffer.from(PUBLIC_KEY_V2, 'base64').toString('utf-8'), Buffer.from(encrypted, 'base64')); return decrypted.toString('utf-8'); } + +export async function encrypt(license: ILicenseV3): Promise { + if (process.env.NODE_ENV !== 'test') { + throw new Error('This function should only be used in tests'); + } + + TEST_KEYS = TEST_KEYS ?? (await getPairs()); + + if (!TEST_KEYS) { + throw new Error('Missing LICENSE_PUBLIC_KEY_V3'); + } + + const [, pkcs8] = TEST_KEYS; + + return `RCV3_${await sign(license, pkcs8)}`; +} diff --git a/ee/packages/license/src/v2/convertToV3.ts b/ee/packages/license/src/v2/convertToV3.ts index a4d5cf118573..7586f54c8c54 100644 --- a/ee/packages/license/src/v2/convertToV3.ts +++ b/ee/packages/license/src/v2/convertToV3.ts @@ -46,7 +46,7 @@ export const convertToV3 = (v2: ILicenseV2): ILicenseV3 => { }, ], statisticsReport: { - required: false, + required: true, }, }, grantedModules: [ diff --git a/ee/packages/license/src/validation/getCurrentValueForLicenseLimit.ts b/ee/packages/license/src/validation/getCurrentValueForLicenseLimit.ts index 1c3ca31807c2..88cedc6c7bc9 100644 --- a/ee/packages/license/src/validation/getCurrentValueForLicenseLimit.ts +++ b/ee/packages/license/src/validation/getCurrentValueForLicenseLimit.ts @@ -24,13 +24,13 @@ export async function getCurrentValueForLicenseLimit limitKey: T, context?: Partial>, ): Promise { - if (this.dataCounters.has(limitKey)) { - return this.dataCounters.get(limitKey)?.(context as LimitContext | undefined) ?? 0; + const counterFn = this.dataCounters.get(limitKey); + if (!counterFn) { + logger.error({ msg: 'Unable to validate license limit due to missing data counter.', limitKey }); + throw new Error('Unable to validate license limit due to missing data counter.'); } - logger.error({ msg: 'Unable to validate license limit due to missing data counter.', limitKey }); - - return 0; + return counterFn(context as LimitContext | undefined); } export function hasAllDataCounters(this: LicenseManager) { diff --git a/ee/packages/license/src/validation/runValidation.spec.ts b/ee/packages/license/src/validation/runValidation.spec.ts new file mode 100644 index 000000000000..98797c86cd27 --- /dev/null +++ b/ee/packages/license/src/validation/runValidation.spec.ts @@ -0,0 +1,38 @@ +/** + * @jest-environment node + */ + +import { MockedLicenseBuilder, getReadyLicenseManager } from '../../__tests__/MockedLicenseBuilder'; +import { runValidation } from './runValidation'; + +describe('Validation behaviors', () => { + it('should return a behavior if the license period is invalid', async () => { + const licenseManager = await getReadyLicenseManager(); + + // two days ago + const validFrom = new Date(new Date().setDate(new Date().getDate() - 2)); + // one day ago + const validUntil = new Date(new Date().setDate(new Date().getDate() - 1)); + + const license = await new MockedLicenseBuilder().resetValidPeriods().withValidPeriod({ + validFrom: validFrom.toISOString(), + validUntil: validUntil.toISOString(), + invalidBehavior: 'disable_modules', + modules: ['livechat-enterprise'], + }); + + await expect( + runValidation.call(licenseManager, await license.build(), [ + 'invalidate_license', + 'prevent_installation', + 'start_fair_policy', + 'disable_modules', + ]), + ).resolves.toStrictEqual([ + { + behavior: 'disable_modules', + modules: ['livechat-enterprise'], + }, + ]); + }); +}); diff --git a/ee/packages/license/src/validation/runValidation.ts b/ee/packages/license/src/validation/runValidation.ts index 639492b431ff..9cb623b8eae0 100644 --- a/ee/packages/license/src/validation/runValidation.ts +++ b/ee/packages/license/src/validation/runValidation.ts @@ -10,7 +10,7 @@ export async function runValidation( license: ILicenseV3, behaviorsToValidate: LicenseBehavior[] = [], ): Promise { - const shouldValidateBehavior = (behavior: LicenseBehavior) => !behaviorsToValidate?.length || behaviorsToValidate.includes(behavior); + const shouldValidateBehavior = (behavior: LicenseBehavior) => !behaviorsToValidate.length || behaviorsToValidate.includes(behavior); return [ ...new Set([ diff --git a/ee/packages/license/src/validation/validateFormat.ts b/ee/packages/license/src/validation/validateFormat.ts index 0b8f5ae344a1..a8c2488cd9fc 100644 --- a/ee/packages/license/src/validation/validateFormat.ts +++ b/ee/packages/license/src/validation/validateFormat.ts @@ -1,5 +1,5 @@ -import decrypt from '../decrypt'; import { InvalidLicenseError } from '../errors/InvalidLicenseError'; +import { decrypt } from '../token'; export const validateFormat = async (encryptedLicense: string): Promise => { if (!encryptedLicense || String(encryptedLicense).trim() === '') { diff --git a/ee/packages/license/src/validation/validateLicensePeriods.ts b/ee/packages/license/src/validation/validateLicensePeriods.ts index b0967ecba49b..5b3fae433e38 100644 --- a/ee/packages/license/src/validation/validateLicensePeriods.ts +++ b/ee/packages/license/src/validation/validateLicensePeriods.ts @@ -4,7 +4,7 @@ import type { Timestamp } from '../definition/LicensePeriod'; import { logger } from '../logger'; import { getResultingBehavior } from './getResultingBehavior'; -export const isPeriodInvalid = (from?: Timestamp, until?: Timestamp) => { +export const isPeriodInvalid = (from: Timestamp | undefined, until: Timestamp | undefined) => { const now = new Date(); if (from && now < new Date(from)) { diff --git a/packages/jwt/src/index.ts b/packages/jwt/src/index.ts index eb43f8b2e75c..3508471f9d81 100644 --- a/packages/jwt/src/index.ts +++ b/packages/jwt/src/index.ts @@ -1,4 +1,4 @@ -import { SignJWT, importPKCS8, jwtVerify, importSPKI } from 'jose'; +import { SignJWT, importPKCS8, jwtVerify, importSPKI, generateKeyPair, exportSPKI, exportPKCS8 } from 'jose'; import type { JWTPayload } from 'jose'; export async function sign(keyObject: object, pkcs8: string, alg = 'RS256') { @@ -16,3 +16,14 @@ export async function verify(jwt: string, spki: string, alg = 'RS256') { return [payload, protectedHeader]; } + +export async function getPairs(): Promise<[string, string]> { + if (process.env.NODE_ENV !== 'test') { + throw new Error('This function should only be used in tests'); + } + const { publicKey, privateKey } = await generateKeyPair('RS256'); + const spki = await exportSPKI(publicKey); + const pkcs8 = await exportPKCS8(privateKey); + + return [spki, pkcs8]; +} diff --git a/yarn.lock b/yarn.lock index ba1d7d742c8c..a3d8d34c8906 100644 --- a/yarn.lock +++ b/yarn.lock @@ -966,6 +966,33 @@ __metadata: languageName: node linkType: hard +"@babel/cli@npm:^7.23.0": + version: 7.23.0 + resolution: "@babel/cli@npm:7.23.0" + dependencies: + "@jridgewell/trace-mapping": ^0.3.17 + "@nicolo-ribaudo/chokidar-2": 2.1.8-no-fsevents.3 + chokidar: ^3.4.0 + commander: ^4.0.1 + convert-source-map: ^2.0.0 + fs-readdir-recursive: ^1.1.0 + glob: ^7.2.0 + make-dir: ^2.1.0 + slash: ^2.0.0 + peerDependencies: + "@babel/core": ^7.0.0-0 + dependenciesMeta: + "@nicolo-ribaudo/chokidar-2": + optional: true + chokidar: + optional: true + bin: + babel: ./bin/babel.js + babel-external-helpers: ./bin/babel-external-helpers.js + checksum: beeb189560bf9c4ea951ef637eefa5214654678fb09c4aaa6695921037059c1e1553c610fe95fbd19a9cdfd9f5598a812fc13df40a6b9a9ea899e43fc6c42052 + languageName: node + linkType: hard + "@babel/code-frame@npm:7.12.11": version: 7.12.11 resolution: "@babel/code-frame@npm:7.12.11" @@ -975,20 +1002,20 @@ __metadata: languageName: node linkType: hard -"@babel/code-frame@npm:^7.0.0, @babel/code-frame@npm:^7.10.4, @babel/code-frame@npm:^7.12.13, @babel/code-frame@npm:^7.22.10, @babel/code-frame@npm:^7.22.5, @babel/code-frame@npm:^7.5.5, @babel/code-frame@npm:^7.8.3": - version: 7.22.10 - resolution: "@babel/code-frame@npm:7.22.10" +"@babel/code-frame@npm:^7.0.0, @babel/code-frame@npm:^7.10.4, @babel/code-frame@npm:^7.12.13, @babel/code-frame@npm:^7.22.10, @babel/code-frame@npm:^7.22.13, @babel/code-frame@npm:^7.5.5, @babel/code-frame@npm:^7.8.3": + version: 7.22.13 + resolution: "@babel/code-frame@npm:7.22.13" dependencies: - "@babel/highlight": ^7.22.10 + "@babel/highlight": ^7.22.13 chalk: ^2.4.2 - checksum: 89a06534ad19759da6203a71bad120b1d7b2ddc016c8e07d4c56b35dea25e7396c6da60a754e8532a86733092b131ae7f661dbe6ba5d165ea777555daa2ed3c9 + checksum: 22e342c8077c8b77eeb11f554ecca2ba14153f707b85294fcf6070b6f6150aae88a7b7436dd88d8c9289970585f3fe5b9b941c5aa3aa26a6d5a8ef3f292da058 languageName: node linkType: hard -"@babel/compat-data@npm:^7.20.5, @babel/compat-data@npm:^7.22.5, @babel/compat-data@npm:^7.22.6, @babel/compat-data@npm:^7.22.9": - version: 7.22.9 - resolution: "@babel/compat-data@npm:7.22.9" - checksum: bed77d9044ce948b4327b30dd0de0779fa9f3a7ed1f2d31638714ed00229fa71fc4d1617ae0eb1fad419338d3658d0e9a5a083297451e09e73e078d0347ff808 +"@babel/compat-data@npm:^7.20.5, @babel/compat-data@npm:^7.22.20, @babel/compat-data@npm:^7.22.6, @babel/compat-data@npm:^7.22.9": + version: 7.22.20 + resolution: "@babel/compat-data@npm:7.22.20" + checksum: efedd1d18878c10fde95e4d82b1236a9aba41395ef798cbb651f58dbf5632dbff475736c507b8d13d4c8f44809d41c0eb2ef0d694283af9ba5dd8339b6dab451 languageName: node linkType: hard @@ -1016,7 +1043,30 @@ __metadata: languageName: node linkType: hard -"@babel/core@npm:^7.1.0, @babel/core@npm:^7.11.6, @babel/core@npm:^7.12.10, @babel/core@npm:^7.12.3, @babel/core@npm:^7.20.7, @babel/core@npm:^7.21.4, @babel/core@npm:^7.7.5, @babel/core@npm:~7.22.10, @babel/core@npm:~7.22.9": +"@babel/core@npm:^7.1.0, @babel/core@npm:^7.11.6, @babel/core@npm:^7.12.10, @babel/core@npm:^7.12.3, @babel/core@npm:^7.20.7, @babel/core@npm:^7.21.4, @babel/core@npm:^7.23.0, @babel/core@npm:^7.7.5": + version: 7.23.0 + resolution: "@babel/core@npm:7.23.0" + dependencies: + "@ampproject/remapping": ^2.2.0 + "@babel/code-frame": ^7.22.13 + "@babel/generator": ^7.23.0 + "@babel/helper-compilation-targets": ^7.22.15 + "@babel/helper-module-transforms": ^7.23.0 + "@babel/helpers": ^7.23.0 + "@babel/parser": ^7.23.0 + "@babel/template": ^7.22.15 + "@babel/traverse": ^7.23.0 + "@babel/types": ^7.23.0 + convert-source-map: ^2.0.0 + debug: ^4.1.0 + gensync: ^1.0.0-beta.2 + json5: ^2.2.3 + semver: ^6.3.1 + checksum: cebd9b48dbc970a7548522f207f245c69567e5ea17ebb1a4e4de563823cf20a01177fe8d2fe19b6e1461361f92fa169fd0b29f8ee9d44eeec84842be1feee5f2 + languageName: node + linkType: hard + +"@babel/core@npm:~7.22.10, @babel/core@npm:~7.22.9": version: 7.22.10 resolution: "@babel/core@npm:7.22.10" dependencies: @@ -1053,15 +1103,15 @@ __metadata: languageName: node linkType: hard -"@babel/generator@npm:^7.12.11, @babel/generator@npm:^7.12.5, @babel/generator@npm:^7.22.10, @babel/generator@npm:^7.7.2": - version: 7.22.10 - resolution: "@babel/generator@npm:7.22.10" +"@babel/generator@npm:^7.12.11, @babel/generator@npm:^7.12.5, @babel/generator@npm:^7.22.10, @babel/generator@npm:^7.23.0, @babel/generator@npm:^7.7.2": + version: 7.23.0 + resolution: "@babel/generator@npm:7.23.0" dependencies: - "@babel/types": ^7.22.10 + "@babel/types": ^7.23.0 "@jridgewell/gen-mapping": ^0.3.2 "@jridgewell/trace-mapping": ^0.3.17 jsesc: ^2.5.1 - checksum: 59a79730abdff9070692834bd3af179e7a9413fa2ff7f83dff3eb888765aeaeb2bfc7b0238a49613ed56e1af05956eff303cc139f2407eda8df974813e486074 + checksum: 8efe24adad34300f1f8ea2add420b28171a646edc70f2a1b3e1683842f23b8b7ffa7e35ef0119294e1901f45bfea5b3dc70abe1f10a1917ccdfb41bed69be5f1 languageName: node linkType: hard @@ -1083,35 +1133,35 @@ __metadata: languageName: node linkType: hard -"@babel/helper-compilation-targets@npm:^7.13.0, @babel/helper-compilation-targets@npm:^7.20.7, @babel/helper-compilation-targets@npm:^7.22.10, @babel/helper-compilation-targets@npm:^7.22.5, @babel/helper-compilation-targets@npm:^7.22.6": - version: 7.22.10 - resolution: "@babel/helper-compilation-targets@npm:7.22.10" +"@babel/helper-compilation-targets@npm:^7.13.0, @babel/helper-compilation-targets@npm:^7.20.7, @babel/helper-compilation-targets@npm:^7.22.10, @babel/helper-compilation-targets@npm:^7.22.15, @babel/helper-compilation-targets@npm:^7.22.5, @babel/helper-compilation-targets@npm:^7.22.6": + version: 7.22.15 + resolution: "@babel/helper-compilation-targets@npm:7.22.15" dependencies: "@babel/compat-data": ^7.22.9 - "@babel/helper-validator-option": ^7.22.5 + "@babel/helper-validator-option": ^7.22.15 browserslist: ^4.21.9 lru-cache: ^5.1.1 semver: ^6.3.1 - checksum: f6f1896816392bcff671bbe6e277307729aee53befb4a66ea126e2a91eda78d819a70d06fa384c74ef46c1595544b94dca50bef6c78438d9ffd31776dafbd435 + checksum: ce85196769e091ae54dd39e4a80c2a9df1793da8588e335c383d536d54f06baf648d0a08fc873044f226398c4ded15c4ae9120ee18e7dfd7c639a68e3cdc9980 languageName: node linkType: hard -"@babel/helper-create-class-features-plugin@npm:^7.17.6, @babel/helper-create-class-features-plugin@npm:^7.18.6, @babel/helper-create-class-features-plugin@npm:^7.21.0, @babel/helper-create-class-features-plugin@npm:^7.22.5": - version: 7.22.5 - resolution: "@babel/helper-create-class-features-plugin@npm:7.22.5" +"@babel/helper-create-class-features-plugin@npm:^7.17.6, @babel/helper-create-class-features-plugin@npm:^7.18.6, @babel/helper-create-class-features-plugin@npm:^7.21.0, @babel/helper-create-class-features-plugin@npm:^7.22.11, @babel/helper-create-class-features-plugin@npm:^7.22.15, @babel/helper-create-class-features-plugin@npm:^7.22.5": + version: 7.22.15 + resolution: "@babel/helper-create-class-features-plugin@npm:7.22.15" dependencies: "@babel/helper-annotate-as-pure": ^7.22.5 "@babel/helper-environment-visitor": ^7.22.5 "@babel/helper-function-name": ^7.22.5 - "@babel/helper-member-expression-to-functions": ^7.22.5 + "@babel/helper-member-expression-to-functions": ^7.22.15 "@babel/helper-optimise-call-expression": ^7.22.5 - "@babel/helper-replace-supers": ^7.22.5 + "@babel/helper-replace-supers": ^7.22.9 "@babel/helper-skip-transparent-expression-wrappers": ^7.22.5 - "@babel/helper-split-export-declaration": ^7.22.5 - semver: ^6.3.0 + "@babel/helper-split-export-declaration": ^7.22.6 + semver: ^6.3.1 peerDependencies: "@babel/core": ^7.0.0 - checksum: f1e91deae06dbee6dd956c0346bca600adfbc7955427795d9d8825f0439a3c3290c789ba2b4a02a1cdf6c1a1bd163dfa16d3d5e96b02a8efb639d2a774e88ed9 + checksum: 52c500d8d164abb3a360b1b7c4b8fff77bc4a5920d3a2b41ae6e1d30617b0dc0b972c1f5db35b1752007e04a748908b4a99bc872b73549ae837e87dcdde005a3 languageName: node linkType: hard @@ -1161,20 +1211,20 @@ __metadata: languageName: node linkType: hard -"@babel/helper-environment-visitor@npm:^7.22.5": - version: 7.22.5 - resolution: "@babel/helper-environment-visitor@npm:7.22.5" - checksum: 248532077d732a34cd0844eb7b078ff917c3a8ec81a7f133593f71a860a582f05b60f818dc5049c2212e5baa12289c27889a4b81d56ef409b4863db49646c4b1 +"@babel/helper-environment-visitor@npm:^7.22.20, @babel/helper-environment-visitor@npm:^7.22.5": + version: 7.22.20 + resolution: "@babel/helper-environment-visitor@npm:7.22.20" + checksum: d80ee98ff66f41e233f36ca1921774c37e88a803b2f7dca3db7c057a5fea0473804db9fb6729e5dbfd07f4bed722d60f7852035c2c739382e84c335661590b69 languageName: node linkType: hard -"@babel/helper-function-name@npm:^7.22.5": - version: 7.22.5 - resolution: "@babel/helper-function-name@npm:7.22.5" +"@babel/helper-function-name@npm:^7.22.5, @babel/helper-function-name@npm:^7.23.0": + version: 7.23.0 + resolution: "@babel/helper-function-name@npm:7.23.0" dependencies: - "@babel/template": ^7.22.5 - "@babel/types": ^7.22.5 - checksum: 6b1f6ce1b1f4e513bf2c8385a557ea0dd7fa37971b9002ad19268ca4384bbe90c09681fe4c076013f33deabc63a53b341ed91e792de741b4b35e01c00238177a + "@babel/template": ^7.22.15 + "@babel/types": ^7.23.0 + checksum: e44542257b2d4634a1f979244eb2a4ad8e6d75eb6761b4cfceb56b562f7db150d134bc538c8e6adca3783e3bc31be949071527aa8e3aab7867d1ad2d84a26e10 languageName: node linkType: hard @@ -1187,36 +1237,36 @@ __metadata: languageName: node linkType: hard -"@babel/helper-member-expression-to-functions@npm:^7.22.5": - version: 7.22.5 - resolution: "@babel/helper-member-expression-to-functions@npm:7.22.5" +"@babel/helper-member-expression-to-functions@npm:^7.22.15": + version: 7.23.0 + resolution: "@babel/helper-member-expression-to-functions@npm:7.23.0" dependencies: - "@babel/types": ^7.22.5 - checksum: 4bd5791529c280c00743e8bdc669ef0d4cd1620d6e3d35e0d42b862f8262bc2364973e5968007f960780344c539a4b9cf92ab41f5b4f94560a9620f536de2a39 + "@babel/types": ^7.23.0 + checksum: 494659361370c979ada711ca685e2efe9460683c36db1b283b446122596602c901e291e09f2f980ecedfe6e0f2bd5386cb59768285446530df10c14df1024e75 languageName: node linkType: hard -"@babel/helper-module-imports@npm:^7.12.13, @babel/helper-module-imports@npm:^7.22.5": - version: 7.22.5 - resolution: "@babel/helper-module-imports@npm:7.22.5" +"@babel/helper-module-imports@npm:^7.12.13, @babel/helper-module-imports@npm:^7.22.15, @babel/helper-module-imports@npm:^7.22.5": + version: 7.22.15 + resolution: "@babel/helper-module-imports@npm:7.22.15" dependencies: - "@babel/types": ^7.22.5 - checksum: 9ac2b0404fa38b80bdf2653fbeaf8e8a43ccb41bd505f9741d820ed95d3c4e037c62a1bcdcb6c9527d7798d2e595924c4d025daed73283badc180ada2c9c49ad + "@babel/types": ^7.22.15 + checksum: ecd7e457df0a46f889228f943ef9b4a47d485d82e030676767e6a2fdcbdaa63594d8124d4b55fd160b41c201025aec01fc27580352b1c87a37c9c6f33d116702 languageName: node linkType: hard -"@babel/helper-module-transforms@npm:^7.12.1, @babel/helper-module-transforms@npm:^7.22.5, @babel/helper-module-transforms@npm:^7.22.9": - version: 7.22.9 - resolution: "@babel/helper-module-transforms@npm:7.22.9" +"@babel/helper-module-transforms@npm:^7.12.1, @babel/helper-module-transforms@npm:^7.22.5, @babel/helper-module-transforms@npm:^7.22.9, @babel/helper-module-transforms@npm:^7.23.0": + version: 7.23.0 + resolution: "@babel/helper-module-transforms@npm:7.23.0" dependencies: - "@babel/helper-environment-visitor": ^7.22.5 - "@babel/helper-module-imports": ^7.22.5 + "@babel/helper-environment-visitor": ^7.22.20 + "@babel/helper-module-imports": ^7.22.15 "@babel/helper-simple-access": ^7.22.5 "@babel/helper-split-export-declaration": ^7.22.6 - "@babel/helper-validator-identifier": ^7.22.5 + "@babel/helper-validator-identifier": ^7.22.20 peerDependencies: "@babel/core": ^7.0.0 - checksum: 2751f77660518cf4ff027514d6f4794f04598c6393be7b04b8e46c6e21606e11c19f3f57ab6129a9c21bacdf8b3ffe3af87bb401d972f34af2d0ffde02ac3001 + checksum: 6e2afffb058cf3f8ce92f5116f710dda4341c81cfcd872f9a0197ea594f7ce0ab3cb940b0590af2fe99e60d2e5448bfba6bca8156ed70a2ed4be2adc8586c891 languageName: node linkType: hard @@ -1256,17 +1306,16 @@ __metadata: languageName: node linkType: hard -"@babel/helper-replace-supers@npm:^7.16.7, @babel/helper-replace-supers@npm:^7.22.5": - version: 7.22.5 - resolution: "@babel/helper-replace-supers@npm:7.22.5" +"@babel/helper-replace-supers@npm:^7.16.7, @babel/helper-replace-supers@npm:^7.22.5, @babel/helper-replace-supers@npm:^7.22.9": + version: 7.22.20 + resolution: "@babel/helper-replace-supers@npm:7.22.20" dependencies: - "@babel/helper-environment-visitor": ^7.22.5 - "@babel/helper-member-expression-to-functions": ^7.22.5 + "@babel/helper-environment-visitor": ^7.22.20 + "@babel/helper-member-expression-to-functions": ^7.22.15 "@babel/helper-optimise-call-expression": ^7.22.5 - "@babel/template": ^7.22.5 - "@babel/traverse": ^7.22.5 - "@babel/types": ^7.22.5 - checksum: af29deff6c6dc3fa2d1a517390716aa3f4d329855e8689f1d5c3cb07c1b898e614a5e175f1826bb58e9ff1480e6552885a71a9a0ba5161787aaafa2c79b216cc + peerDependencies: + "@babel/core": ^7.0.0 + checksum: a0008332e24daedea2e9498733e3c39b389d6d4512637e000f96f62b797e702ee24a407ccbcd7a236a551590a38f31282829a8ef35c50a3c0457d88218cae639 languageName: node linkType: hard @@ -1288,7 +1337,7 @@ __metadata: languageName: node linkType: hard -"@babel/helper-split-export-declaration@npm:^7.22.5, @babel/helper-split-export-declaration@npm:^7.22.6": +"@babel/helper-split-export-declaration@npm:^7.22.6": version: 7.22.6 resolution: "@babel/helper-split-export-declaration@npm:7.22.6" dependencies: @@ -1304,17 +1353,17 @@ __metadata: languageName: node linkType: hard -"@babel/helper-validator-identifier@npm:^7.22.5": - version: 7.22.5 - resolution: "@babel/helper-validator-identifier@npm:7.22.5" - checksum: 7f0f30113474a28298c12161763b49de5018732290ca4de13cdaefd4fd0d635a6fe3f6686c37a02905fb1e64f21a5ee2b55140cf7b070e729f1bd66866506aea +"@babel/helper-validator-identifier@npm:^7.22.20": + version: 7.22.20 + resolution: "@babel/helper-validator-identifier@npm:7.22.20" + checksum: 136412784d9428266bcdd4d91c32bcf9ff0e8d25534a9d94b044f77fe76bc50f941a90319b05aafd1ec04f7d127cd57a179a3716009ff7f3412ef835ada95bdc languageName: node linkType: hard -"@babel/helper-validator-option@npm:^7.16.7, @babel/helper-validator-option@npm:^7.22.5": - version: 7.22.5 - resolution: "@babel/helper-validator-option@npm:7.22.5" - checksum: bbeca8a85ee86990215c0424997438b388b8d642d69b9f86c375a174d3cdeb270efafd1ff128bc7a1d370923d13b6e45829ba8581c027620e83e3a80c5c414b3 +"@babel/helper-validator-option@npm:^7.16.7, @babel/helper-validator-option@npm:^7.22.15, @babel/helper-validator-option@npm:^7.22.5": + version: 7.22.15 + resolution: "@babel/helper-validator-option@npm:7.22.15" + checksum: 68da52b1e10002a543161494c4bc0f4d0398c8fdf361d5f7f4272e95c45d5b32d974896d44f6a0ea7378c9204988879d73613ca683e13bd1304e46d25ff67a8d languageName: node linkType: hard @@ -1329,58 +1378,58 @@ __metadata: languageName: node linkType: hard -"@babel/helpers@npm:^7.12.5, @babel/helpers@npm:^7.22.10": - version: 7.22.10 - resolution: "@babel/helpers@npm:7.22.10" +"@babel/helpers@npm:^7.12.5, @babel/helpers@npm:^7.22.10, @babel/helpers@npm:^7.23.0": + version: 7.23.1 + resolution: "@babel/helpers@npm:7.23.1" dependencies: - "@babel/template": ^7.22.5 - "@babel/traverse": ^7.22.10 - "@babel/types": ^7.22.10 - checksum: 3b1219e362df390b6c5d94b75a53fc1c2eb42927ced0b8022d6a29b833a839696206b9bdad45b4805d05591df49fc16b6fb7db758c9c2ecfe99e3e94cb13020f + "@babel/template": ^7.22.15 + "@babel/traverse": ^7.23.0 + "@babel/types": ^7.23.0 + checksum: acfc345102045c24ea2a4d60e00dcf8220e215af3add4520e2167700661338e6a80bd56baf44bb764af05ec6621101c9afc315dc107e18c61fa6da8acbdbb893 languageName: node linkType: hard -"@babel/highlight@npm:^7.10.4, @babel/highlight@npm:^7.22.10": - version: 7.22.10 - resolution: "@babel/highlight@npm:7.22.10" +"@babel/highlight@npm:^7.10.4, @babel/highlight@npm:^7.22.13": + version: 7.22.20 + resolution: "@babel/highlight@npm:7.22.20" dependencies: - "@babel/helper-validator-identifier": ^7.22.5 + "@babel/helper-validator-identifier": ^7.22.20 chalk: ^2.4.2 js-tokens: ^4.0.0 - checksum: f714a1e1a72dd9d72f6383f4f30fd342e21a8df32d984a4ea8f5eab691bb6ba6db2f8823d4b4cf135d98869e7a98925b81306aa32ee3c429f8cfa52c75889e1b + checksum: 84bd034dca309a5e680083cd827a766780ca63cef37308404f17653d32366ea76262bd2364b2d38776232f2d01b649f26721417d507e8b4b6da3e4e739f6d134 languageName: node linkType: hard -"@babel/parser@npm:^7.1.0, @babel/parser@npm:^7.12.11, @babel/parser@npm:^7.12.7, @babel/parser@npm:^7.14.7, @babel/parser@npm:^7.20.7, @babel/parser@npm:^7.22.10, @babel/parser@npm:^7.22.5": - version: 7.22.10 - resolution: "@babel/parser@npm:7.22.10" +"@babel/parser@npm:^7.1.0, @babel/parser@npm:^7.12.11, @babel/parser@npm:^7.12.7, @babel/parser@npm:^7.14.7, @babel/parser@npm:^7.20.7, @babel/parser@npm:^7.22.10, @babel/parser@npm:^7.22.15, @babel/parser@npm:^7.23.0": + version: 7.23.0 + resolution: "@babel/parser@npm:7.23.0" bin: parser: ./bin/babel-parser.js - checksum: af51567b7d3cdf523bc608eae057397486c7fa6c2e5753027c01fe5c36f0767b2d01ce3049b222841326cc5b8c7fda1d810ac1a01af0a97bb04679e2ef9f7049 + checksum: 453fdf8b9e2c2b7d7b02139e0ce003d1af21947bbc03eb350fb248ee335c9b85e4ab41697ddbdd97079698de825a265e45a0846bb2ed47a2c7c1df833f42a354 languageName: node linkType: hard -"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@npm:^7.22.5": - version: 7.22.5 - resolution: "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@npm:7.22.5" +"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@npm:^7.22.15": + version: 7.22.15 + resolution: "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@npm:7.22.15" dependencies: "@babel/helper-plugin-utils": ^7.22.5 peerDependencies: "@babel/core": ^7.0.0 - checksum: 1e353a060fb2cd8f1256d28cd768f16fb02513f905b9b6d656fb0242c96c341a196fa188b27c2701506a6e27515359fbcc1a5ca7fa8b9b530cf88fbd137baefc + checksum: 8910ca21a7ec7c06f7b247d4b86c97c5aa15ef321518f44f6f490c5912fdf82c605aaa02b90892e375d82ccbedeadfdeadd922c1b836c9dd4c596871bf654753 languageName: node linkType: hard -"@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@npm:^7.22.5": - version: 7.22.5 - resolution: "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@npm:7.22.5" +"@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@npm:^7.22.15": + version: 7.22.15 + resolution: "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@npm:7.22.15" dependencies: "@babel/helper-plugin-utils": ^7.22.5 "@babel/helper-skip-transparent-expression-wrappers": ^7.22.5 - "@babel/plugin-transform-optional-chaining": ^7.22.5 + "@babel/plugin-transform-optional-chaining": ^7.22.15 peerDependencies: "@babel/core": ^7.13.0 - checksum: 16e7a5f3bf2f2ac0ca032a70bf0ebd7e886d84dbb712b55c0643c04c495f0f221fbcbca14b5f8f8027fa6c87a3dafae0934022ad2b409384af6c5c356495b7bd + checksum: fbefedc0da014c37f1a50a8094ce7dbbf2181ae93243f23d6ecba2499b5b20196c2124d6a4dfe3e9e0125798e80593103e456352a4beb4e5c6f7c75efb80fdac languageName: node linkType: hard @@ -1798,9 +1847,9 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-async-generator-functions@npm:^7.22.10": - version: 7.22.10 - resolution: "@babel/plugin-transform-async-generator-functions@npm:7.22.10" +"@babel/plugin-transform-async-generator-functions@npm:^7.22.15": + version: 7.22.15 + resolution: "@babel/plugin-transform-async-generator-functions@npm:7.22.15" dependencies: "@babel/helper-environment-visitor": ^7.22.5 "@babel/helper-plugin-utils": ^7.22.5 @@ -1808,7 +1857,7 @@ __metadata: "@babel/plugin-syntax-async-generators": ^7.8.4 peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 87d77b66fda05b42450aa285fa031aa3963c52aab00190f95f6c3ddefbed683035c1f314347c888f8406fba5d436b888ff75b5e36b8ab23afd4ca4c3f086f88c + checksum: fad98786b446ce63bde0d14a221e2617eef5a7bbca62b49d96f16ab5e1694521234cfba6145b830fbf9af16d60a8a3dbf148e8694830bd91796fe333b0599e73 languageName: node linkType: hard @@ -1836,14 +1885,14 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-block-scoping@npm:^7.12.12, @babel/plugin-transform-block-scoping@npm:^7.22.10": - version: 7.22.10 - resolution: "@babel/plugin-transform-block-scoping@npm:7.22.10" +"@babel/plugin-transform-block-scoping@npm:^7.12.12, @babel/plugin-transform-block-scoping@npm:^7.22.15": + version: 7.23.0 + resolution: "@babel/plugin-transform-block-scoping@npm:7.23.0" dependencies: "@babel/helper-plugin-utils": ^7.22.5 peerDependencies: "@babel/core": ^7.0.0-0 - checksum: b1d06f358dedcb748a57e5feea4b9285c60593fb2912b921f22898c57c552c78fe18128678c8f84dd4ea1d4e5aebede8783830b24cd63f22c30261156d78bc77 + checksum: 0cfe925cc3b5a3ad407e2253fab3ceeaa117a4b291c9cb245578880872999bca91bd83ffa0128ae9ca356330702e1ef1dcb26804f28d2cef678239caf629f73e languageName: node linkType: hard @@ -1859,35 +1908,35 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-class-static-block@npm:^7.22.5": - version: 7.22.5 - resolution: "@babel/plugin-transform-class-static-block@npm:7.22.5" +"@babel/plugin-transform-class-static-block@npm:^7.22.11": + version: 7.22.11 + resolution: "@babel/plugin-transform-class-static-block@npm:7.22.11" dependencies: - "@babel/helper-create-class-features-plugin": ^7.22.5 + "@babel/helper-create-class-features-plugin": ^7.22.11 "@babel/helper-plugin-utils": ^7.22.5 "@babel/plugin-syntax-class-static-block": ^7.14.5 peerDependencies: "@babel/core": ^7.12.0 - checksum: bc48b92dbaf625a14f2bf62382384eef01e0515802426841636ae9146e27395d068c7a8a45e9e15699491b0a01d990f38f179cbc9dc89274a393f85648772f12 + checksum: 69f040506fad66f1c6918d288d0e0edbc5c8a07c8b4462c1184ad2f9f08995d68b057126c213871c0853ae0c72afc60ec87492049dfacb20902e32346a448bcb languageName: node linkType: hard -"@babel/plugin-transform-classes@npm:^7.12.1, @babel/plugin-transform-classes@npm:^7.22.6": - version: 7.22.6 - resolution: "@babel/plugin-transform-classes@npm:7.22.6" +"@babel/plugin-transform-classes@npm:^7.12.1, @babel/plugin-transform-classes@npm:^7.22.15": + version: 7.22.15 + resolution: "@babel/plugin-transform-classes@npm:7.22.15" dependencies: "@babel/helper-annotate-as-pure": ^7.22.5 - "@babel/helper-compilation-targets": ^7.22.6 + "@babel/helper-compilation-targets": ^7.22.15 "@babel/helper-environment-visitor": ^7.22.5 "@babel/helper-function-name": ^7.22.5 "@babel/helper-optimise-call-expression": ^7.22.5 "@babel/helper-plugin-utils": ^7.22.5 - "@babel/helper-replace-supers": ^7.22.5 + "@babel/helper-replace-supers": ^7.22.9 "@babel/helper-split-export-declaration": ^7.22.6 globals: ^11.1.0 peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 8380e855c01033dbc7460d9acfbc1fc37c880350fa798c2de8c594ef818ade0e4c96173ec72f05f2a4549d8d37135e18cb62548352d51557b45a0fb4388d2f3f + checksum: d3f4d0c107dd8a3557ea3575cc777fab27efa92958b41e4a9822f7499725c1f554beae58855de16ddec0a7b694e45f59a26cea8fbde4275563f72f09c6e039a0 languageName: node linkType: hard @@ -1903,14 +1952,14 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-destructuring@npm:^7.12.1, @babel/plugin-transform-destructuring@npm:^7.22.10": - version: 7.22.10 - resolution: "@babel/plugin-transform-destructuring@npm:7.22.10" +"@babel/plugin-transform-destructuring@npm:^7.12.1, @babel/plugin-transform-destructuring@npm:^7.22.15": + version: 7.23.0 + resolution: "@babel/plugin-transform-destructuring@npm:7.23.0" dependencies: "@babel/helper-plugin-utils": ^7.22.5 peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 011707801bd0029fd4f0523d24d06fdc0cbe8c9da280d75728f76713d639c4dc976e1b56a1ba7bff25468f86867efb71c9b4cac81140adbdd0abf2324b19a8bb + checksum: cd6dd454ccc2766be551e4f8a04b1acc2aa539fa19e5c7501c56cc2f8cc921dd41a7ffb78455b4c4b2f954fcab8ca4561ba7c9c7bd5af9f19465243603d18cc3 languageName: node linkType: hard @@ -1937,15 +1986,15 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-dynamic-import@npm:^7.22.5": - version: 7.22.5 - resolution: "@babel/plugin-transform-dynamic-import@npm:7.22.5" +"@babel/plugin-transform-dynamic-import@npm:^7.22.11": + version: 7.22.11 + resolution: "@babel/plugin-transform-dynamic-import@npm:7.22.11" dependencies: "@babel/helper-plugin-utils": ^7.22.5 "@babel/plugin-syntax-dynamic-import": ^7.8.3 peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 186a6d59f36eb3c5824739fc9c22ed0f4ca68e001662aa3a302634346a8b785cb9579b23b0c158f4570604d697d19598ca09b58c60a7fa2894da1163c4eb1907 + checksum: 78fc9c532210bf9e8f231747f542318568ac360ee6c27e80853962c984283c73da3f8f8aebe83c2096090a435b356b092ed85de617a156cbe0729d847632be45 languageName: node linkType: hard @@ -1961,15 +2010,15 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-export-namespace-from@npm:^7.22.5": - version: 7.22.5 - resolution: "@babel/plugin-transform-export-namespace-from@npm:7.22.5" +"@babel/plugin-transform-export-namespace-from@npm:^7.22.11": + version: 7.22.11 + resolution: "@babel/plugin-transform-export-namespace-from@npm:7.22.11" dependencies: "@babel/helper-plugin-utils": ^7.22.5 "@babel/plugin-syntax-export-namespace-from": ^7.8.3 peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 3d197b788758044983c96b9c49bed4b456055f35a388521a405968db0f6e2ffb6fd59110e3931f4dcc5e126ae9e5e00e154a0afb47a7ea359d8d0dea79f480d7 + checksum: 73af5883a321ed56a4bfd43c8a7de0164faebe619287706896fc6ee2f7a4e69042adaa1338c0b8b4bdb9f7e5fdceb016fb1d40694cb43ca3b8827429e8aac4bf languageName: node linkType: hard @@ -1985,14 +2034,14 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-for-of@npm:^7.12.1, @babel/plugin-transform-for-of@npm:^7.22.5": - version: 7.22.5 - resolution: "@babel/plugin-transform-for-of@npm:7.22.5" +"@babel/plugin-transform-for-of@npm:^7.12.1, @babel/plugin-transform-for-of@npm:^7.22.15": + version: 7.22.15 + resolution: "@babel/plugin-transform-for-of@npm:7.22.15" dependencies: "@babel/helper-plugin-utils": ^7.22.5 peerDependencies: "@babel/core": ^7.0.0-0 - checksum: d7b8d4db010bce7273674caa95c4e6abd909362866ce297e86a2ecaa9ae636e05d525415811db9b3c942155df7f3651d19b91dd6c41f142f7308a97c7cb06023 + checksum: f395ae7bce31e14961460f56cf751b5d6e37dd27d7df5b1f4e49fec1c11b6f9cf71991c7ffbe6549878591e87df0d66af798cf26edfa4bfa6b4c3dba1fb2f73a languageName: node linkType: hard @@ -2009,15 +2058,15 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-json-strings@npm:^7.22.5": - version: 7.22.5 - resolution: "@babel/plugin-transform-json-strings@npm:7.22.5" +"@babel/plugin-transform-json-strings@npm:^7.22.11": + version: 7.22.11 + resolution: "@babel/plugin-transform-json-strings@npm:7.22.11" dependencies: "@babel/helper-plugin-utils": ^7.22.5 "@babel/plugin-syntax-json-strings": ^7.8.3 peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 4e00b902487a670b6c8948f33f9108133fd745cf9d1478aca515fb460b9b2f12e137988ebc1663630fb82070a870aed8b0c1aa4d007a841c18004619798f255c + checksum: 50665e5979e66358c50e90a26db53c55917f78175127ac2fa05c7888d156d418ffb930ec0a109353db0a7c5f57c756ce01bfc9825d24cbfd2b3ec453f2ed8cba languageName: node linkType: hard @@ -2032,15 +2081,15 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-logical-assignment-operators@npm:^7.22.5": - version: 7.22.5 - resolution: "@babel/plugin-transform-logical-assignment-operators@npm:7.22.5" +"@babel/plugin-transform-logical-assignment-operators@npm:^7.22.11": + version: 7.22.11 + resolution: "@babel/plugin-transform-logical-assignment-operators@npm:7.22.11" dependencies: "@babel/helper-plugin-utils": ^7.22.5 "@babel/plugin-syntax-logical-assignment-operators": ^7.10.4 peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 18748e953c08f64885f18c224eac58df10a13eac4d845d16b5d9b6276907da7ca2530dfebe6ed41cdc5f8a75d9db3e36d8eb54ddce7cd0364af1cab09b435302 + checksum: c664e9798e85afa7f92f07b867682dee7392046181d82f5d21bae6f2ca26dfe9c8375cdc52b7483c3fc09a983c1989f60eff9fbc4f373b0c0a74090553d05739 languageName: node linkType: hard @@ -2067,30 +2116,30 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-modules-commonjs@npm:^7.22.5": - version: 7.22.5 - resolution: "@babel/plugin-transform-modules-commonjs@npm:7.22.5" +"@babel/plugin-transform-modules-commonjs@npm:^7.22.15, @babel/plugin-transform-modules-commonjs@npm:^7.22.5, @babel/plugin-transform-modules-commonjs@npm:^7.23.0": + version: 7.23.0 + resolution: "@babel/plugin-transform-modules-commonjs@npm:7.23.0" dependencies: - "@babel/helper-module-transforms": ^7.22.5 + "@babel/helper-module-transforms": ^7.23.0 "@babel/helper-plugin-utils": ^7.22.5 "@babel/helper-simple-access": ^7.22.5 peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 2067aca8f6454d54ffcce69b02c457cfa61428e11372f6a1d99ff4fcfbb55c396ed2ca6ca886bf06c852e38c1a205b8095921b2364fd0243f3e66bc1dda61caa + checksum: 7fb25997194053e167c4207c319ff05362392da841bd9f42ddb3caf9c8798a5d203bd926d23ddf5830fdf05eddc82c2810f40d1287e3a4f80b07eff13d1024b5 languageName: node linkType: hard -"@babel/plugin-transform-modules-systemjs@npm:^7.22.5": - version: 7.22.5 - resolution: "@babel/plugin-transform-modules-systemjs@npm:7.22.5" +"@babel/plugin-transform-modules-systemjs@npm:^7.22.11": + version: 7.23.0 + resolution: "@babel/plugin-transform-modules-systemjs@npm:7.23.0" dependencies: "@babel/helper-hoist-variables": ^7.22.5 - "@babel/helper-module-transforms": ^7.22.5 + "@babel/helper-module-transforms": ^7.23.0 "@babel/helper-plugin-utils": ^7.22.5 - "@babel/helper-validator-identifier": ^7.22.5 + "@babel/helper-validator-identifier": ^7.22.20 peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 04f4178589543396b3c24330a67a59c5e69af5e96119c9adda730c0f20122deaff54671ebbc72ad2df6495a5db8a758bd96942de95fba7ad427de9c80b1b38c8 + checksum: 2d481458b22605046badea2317d5cc5c94ac3031c2293e34c96f02063f5b02af0979c4da6a8fbc67cc249541575dc9c6d710db6b919ede70b7337a22d9fd57a7 languageName: node linkType: hard @@ -2129,42 +2178,42 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-nullish-coalescing-operator@npm:^7.22.5": - version: 7.22.5 - resolution: "@babel/plugin-transform-nullish-coalescing-operator@npm:7.22.5" +"@babel/plugin-transform-nullish-coalescing-operator@npm:^7.22.11": + version: 7.22.11 + resolution: "@babel/plugin-transform-nullish-coalescing-operator@npm:7.22.11" dependencies: "@babel/helper-plugin-utils": ^7.22.5 "@babel/plugin-syntax-nullish-coalescing-operator": ^7.8.3 peerDependencies: "@babel/core": ^7.0.0-0 - checksum: e6a059169d257fc61322d0708edae423072449b7c33de396261e68dee582aec5396789a1c22bce84e5bd88a169623c2e750b513fc222930979e6accd52a44bf2 + checksum: 167babecc8b8fe70796a7b7d34af667ebbf43da166c21689502e5e8cc93180b7a85979c77c9f64b7cce431b36718bd0a6df9e5e0ffea4ae22afb22cfef886372 languageName: node linkType: hard -"@babel/plugin-transform-numeric-separator@npm:^7.22.5": - version: 7.22.5 - resolution: "@babel/plugin-transform-numeric-separator@npm:7.22.5" +"@babel/plugin-transform-numeric-separator@npm:^7.22.11": + version: 7.22.11 + resolution: "@babel/plugin-transform-numeric-separator@npm:7.22.11" dependencies: "@babel/helper-plugin-utils": ^7.22.5 "@babel/plugin-syntax-numeric-separator": ^7.10.4 peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 9e7837d4eae04f211ebaa034fe5003d2927b6bf6d5b9dc09f2b1183c01482cdde5a75b8bd5c7ff195c2abc7b923339eb0b2a9d27cb78359d38248a3b2c2367c4 + checksum: af064d06a4a041767ec396a5f258103f64785df290e038bba9f0ef454e6c914f2ac45d862bbdad8fac2c7ad47fa4e95356f29053c60c100a0160b02a995fe2a3 languageName: node linkType: hard -"@babel/plugin-transform-object-rest-spread@npm:^7.22.5": - version: 7.22.5 - resolution: "@babel/plugin-transform-object-rest-spread@npm:7.22.5" +"@babel/plugin-transform-object-rest-spread@npm:^7.22.15": + version: 7.22.15 + resolution: "@babel/plugin-transform-object-rest-spread@npm:7.22.15" dependencies: - "@babel/compat-data": ^7.22.5 - "@babel/helper-compilation-targets": ^7.22.5 + "@babel/compat-data": ^7.22.9 + "@babel/helper-compilation-targets": ^7.22.15 "@babel/helper-plugin-utils": ^7.22.5 "@babel/plugin-syntax-object-rest-spread": ^7.8.3 - "@babel/plugin-transform-parameters": ^7.22.5 + "@babel/plugin-transform-parameters": ^7.22.15 peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 3b5e091f0dc67108f2e41ed5a97e15bbe4381a19d9a7eea80b71c7de1d8169fd28784e1e41a3d2ad12709ab212e58fc481282a5bb65d591fae7b443048de3330 + checksum: 62197a6f12289c1c1bd57f3bed9f0f765ca32390bfe91e0b5561dd94dd9770f4480c4162dec98da094bc0ba99d2c2ebba68de47c019454041b0b7a68ba2ec66d languageName: node linkType: hard @@ -2180,39 +2229,39 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-optional-catch-binding@npm:^7.22.5": - version: 7.22.5 - resolution: "@babel/plugin-transform-optional-catch-binding@npm:7.22.5" +"@babel/plugin-transform-optional-catch-binding@npm:^7.22.11": + version: 7.22.11 + resolution: "@babel/plugin-transform-optional-catch-binding@npm:7.22.11" dependencies: "@babel/helper-plugin-utils": ^7.22.5 "@babel/plugin-syntax-optional-catch-binding": ^7.8.3 peerDependencies: "@babel/core": ^7.0.0-0 - checksum: b0e8b4233ff06b5c9d285257f49c5bd441f883189b24282e6200f9ebdf5db29aeeebbffae57fbbcd5df9f4387b3e66e5d322aaae5652a78e89685ddbae46bbd1 + checksum: f17abd90e1de67c84d63afea29c8021c74abb2794d3a6eeafb0bbe7372d3db32aefca386e392116ec63884537a4a2815d090d26264d259bacc08f6e3ed05294c languageName: node linkType: hard -"@babel/plugin-transform-optional-chaining@npm:^7.22.10, @babel/plugin-transform-optional-chaining@npm:^7.22.5": - version: 7.22.10 - resolution: "@babel/plugin-transform-optional-chaining@npm:7.22.10" +"@babel/plugin-transform-optional-chaining@npm:^7.22.15": + version: 7.23.0 + resolution: "@babel/plugin-transform-optional-chaining@npm:7.23.0" dependencies: "@babel/helper-plugin-utils": ^7.22.5 "@babel/helper-skip-transparent-expression-wrappers": ^7.22.5 "@babel/plugin-syntax-optional-chaining": ^7.8.3 peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 522d6214bb9f6ede8a2fc56a873e791aabd62f0b3be78fb8e62ca801a9033bcadabfb77aec6739f0e67f0f15f7c739c08bafafd66d3676edf1941fe6429cebcd + checksum: f702634f2b97e5260dbec0d4bde05ccb6f4d96d7bfa946481aeacfa205ca846cb6e096a38312f9d51fdbdac1f258f211138c5f7075952e46a5bf8574de6a1329 languageName: node linkType: hard -"@babel/plugin-transform-parameters@npm:^7.12.1, @babel/plugin-transform-parameters@npm:^7.20.7, @babel/plugin-transform-parameters@npm:^7.22.5": - version: 7.22.5 - resolution: "@babel/plugin-transform-parameters@npm:7.22.5" +"@babel/plugin-transform-parameters@npm:^7.12.1, @babel/plugin-transform-parameters@npm:^7.20.7, @babel/plugin-transform-parameters@npm:^7.22.15": + version: 7.22.15 + resolution: "@babel/plugin-transform-parameters@npm:7.22.15" dependencies: "@babel/helper-plugin-utils": ^7.22.5 peerDependencies: "@babel/core": ^7.0.0-0 - checksum: b44f89cf97daf23903776ba27c2ab13b439d80d8c8a95be5c476ab65023b1e0c0e94c28d3745f3b60a58edc4e590fa0cd4287a0293e51401ca7d29a2ddb13b8e + checksum: 541188bb7d1876cad87687b5c7daf90f63d8208ae83df24acb1e2b05020ad1c78786b2723ca4054a83fcb74fb6509f30c4cacc5b538ee684224261ad5fb047c1 languageName: node linkType: hard @@ -2228,17 +2277,17 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-private-property-in-object@npm:^7.22.5": - version: 7.22.5 - resolution: "@babel/plugin-transform-private-property-in-object@npm:7.22.5" +"@babel/plugin-transform-private-property-in-object@npm:^7.22.11": + version: 7.22.11 + resolution: "@babel/plugin-transform-private-property-in-object@npm:7.22.11" dependencies: "@babel/helper-annotate-as-pure": ^7.22.5 - "@babel/helper-create-class-features-plugin": ^7.22.5 + "@babel/helper-create-class-features-plugin": ^7.22.11 "@babel/helper-plugin-utils": ^7.22.5 "@babel/plugin-syntax-private-property-in-object": ^7.14.5 peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 9ac019fb2772f3af6278a7f4b8b14b0663accb3fd123d87142ceb2fbc57fd1afa07c945d1329029b026b9ee122096ef71a3f34f257a9e04cf4245b87298c38b4 + checksum: 4d029d84901e53c46dead7a46e2990a7bc62470f4e4ca58a0d063394f86652fd58fe4eea1eb941da3669cd536b559b9d058b342b59300026346b7a2a51badac8 languageName: node linkType: hard @@ -2403,17 +2452,17 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-typescript@npm:^7.22.5": - version: 7.22.5 - resolution: "@babel/plugin-transform-typescript@npm:7.22.5" +"@babel/plugin-transform-typescript@npm:^7.22.15, @babel/plugin-transform-typescript@npm:^7.22.5": + version: 7.22.15 + resolution: "@babel/plugin-transform-typescript@npm:7.22.15" dependencies: "@babel/helper-annotate-as-pure": ^7.22.5 - "@babel/helper-create-class-features-plugin": ^7.22.5 + "@babel/helper-create-class-features-plugin": ^7.22.15 "@babel/helper-plugin-utils": ^7.22.5 "@babel/plugin-syntax-typescript": ^7.22.5 peerDependencies: "@babel/core": ^7.0.0-0 - checksum: d12f1ca1ef1f2a54432eb044d2999705d1205ebe211c2a7f05b12e8eb2d2a461fd7657b5486b2f2f1efe7c0c0dc8e80725b767073d40fe4ae059a7af057b05e4 + checksum: c5d96cdbf0e1512707aa1c1e3ac6b370a25fd9c545d26008ce44eb13a47bd7fd67a1eb799c98b5ccc82e33a345fda55c0055e1fe3ed97646ed405dd13020b226 languageName: node linkType: hard @@ -2464,16 +2513,16 @@ __metadata: languageName: node linkType: hard -"@babel/preset-env@npm:^7.12.11, @babel/preset-env@npm:~7.22.10, @babel/preset-env@npm:~7.22.9": - version: 7.22.10 - resolution: "@babel/preset-env@npm:7.22.10" +"@babel/preset-env@npm:^7.12.11, @babel/preset-env@npm:^7.22.20, @babel/preset-env@npm:~7.22.10, @babel/preset-env@npm:~7.22.9": + version: 7.22.20 + resolution: "@babel/preset-env@npm:7.22.20" dependencies: - "@babel/compat-data": ^7.22.9 - "@babel/helper-compilation-targets": ^7.22.10 + "@babel/compat-data": ^7.22.20 + "@babel/helper-compilation-targets": ^7.22.15 "@babel/helper-plugin-utils": ^7.22.5 - "@babel/helper-validator-option": ^7.22.5 - "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": ^7.22.5 - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": ^7.22.5 + "@babel/helper-validator-option": ^7.22.15 + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": ^7.22.15 + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": ^7.22.15 "@babel/plugin-proposal-private-property-in-object": 7.21.0-placeholder-for-preset-env.2 "@babel/plugin-syntax-async-generators": ^7.8.4 "@babel/plugin-syntax-class-properties": ^7.12.13 @@ -2494,41 +2543,41 @@ __metadata: "@babel/plugin-syntax-top-level-await": ^7.14.5 "@babel/plugin-syntax-unicode-sets-regex": ^7.18.6 "@babel/plugin-transform-arrow-functions": ^7.22.5 - "@babel/plugin-transform-async-generator-functions": ^7.22.10 + "@babel/plugin-transform-async-generator-functions": ^7.22.15 "@babel/plugin-transform-async-to-generator": ^7.22.5 "@babel/plugin-transform-block-scoped-functions": ^7.22.5 - "@babel/plugin-transform-block-scoping": ^7.22.10 + "@babel/plugin-transform-block-scoping": ^7.22.15 "@babel/plugin-transform-class-properties": ^7.22.5 - "@babel/plugin-transform-class-static-block": ^7.22.5 - "@babel/plugin-transform-classes": ^7.22.6 + "@babel/plugin-transform-class-static-block": ^7.22.11 + "@babel/plugin-transform-classes": ^7.22.15 "@babel/plugin-transform-computed-properties": ^7.22.5 - "@babel/plugin-transform-destructuring": ^7.22.10 + "@babel/plugin-transform-destructuring": ^7.22.15 "@babel/plugin-transform-dotall-regex": ^7.22.5 "@babel/plugin-transform-duplicate-keys": ^7.22.5 - "@babel/plugin-transform-dynamic-import": ^7.22.5 + "@babel/plugin-transform-dynamic-import": ^7.22.11 "@babel/plugin-transform-exponentiation-operator": ^7.22.5 - "@babel/plugin-transform-export-namespace-from": ^7.22.5 - "@babel/plugin-transform-for-of": ^7.22.5 + "@babel/plugin-transform-export-namespace-from": ^7.22.11 + "@babel/plugin-transform-for-of": ^7.22.15 "@babel/plugin-transform-function-name": ^7.22.5 - "@babel/plugin-transform-json-strings": ^7.22.5 + "@babel/plugin-transform-json-strings": ^7.22.11 "@babel/plugin-transform-literals": ^7.22.5 - "@babel/plugin-transform-logical-assignment-operators": ^7.22.5 + "@babel/plugin-transform-logical-assignment-operators": ^7.22.11 "@babel/plugin-transform-member-expression-literals": ^7.22.5 "@babel/plugin-transform-modules-amd": ^7.22.5 - "@babel/plugin-transform-modules-commonjs": ^7.22.5 - "@babel/plugin-transform-modules-systemjs": ^7.22.5 + "@babel/plugin-transform-modules-commonjs": ^7.22.15 + "@babel/plugin-transform-modules-systemjs": ^7.22.11 "@babel/plugin-transform-modules-umd": ^7.22.5 "@babel/plugin-transform-named-capturing-groups-regex": ^7.22.5 "@babel/plugin-transform-new-target": ^7.22.5 - "@babel/plugin-transform-nullish-coalescing-operator": ^7.22.5 - "@babel/plugin-transform-numeric-separator": ^7.22.5 - "@babel/plugin-transform-object-rest-spread": ^7.22.5 + "@babel/plugin-transform-nullish-coalescing-operator": ^7.22.11 + "@babel/plugin-transform-numeric-separator": ^7.22.11 + "@babel/plugin-transform-object-rest-spread": ^7.22.15 "@babel/plugin-transform-object-super": ^7.22.5 - "@babel/plugin-transform-optional-catch-binding": ^7.22.5 - "@babel/plugin-transform-optional-chaining": ^7.22.10 - "@babel/plugin-transform-parameters": ^7.22.5 + "@babel/plugin-transform-optional-catch-binding": ^7.22.11 + "@babel/plugin-transform-optional-chaining": ^7.22.15 + "@babel/plugin-transform-parameters": ^7.22.15 "@babel/plugin-transform-private-methods": ^7.22.5 - "@babel/plugin-transform-private-property-in-object": ^7.22.5 + "@babel/plugin-transform-private-property-in-object": ^7.22.11 "@babel/plugin-transform-property-literals": ^7.22.5 "@babel/plugin-transform-regenerator": ^7.22.10 "@babel/plugin-transform-reserved-words": ^7.22.5 @@ -2542,7 +2591,7 @@ __metadata: "@babel/plugin-transform-unicode-regex": ^7.22.5 "@babel/plugin-transform-unicode-sets-regex": ^7.22.5 "@babel/preset-modules": 0.1.6-no-external-plugins - "@babel/types": ^7.22.10 + "@babel/types": ^7.22.19 babel-plugin-polyfill-corejs2: ^0.4.5 babel-plugin-polyfill-corejs3: ^0.8.3 babel-plugin-polyfill-regenerator: ^0.5.2 @@ -2550,7 +2599,7 @@ __metadata: semver: ^6.3.1 peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 4145a660a7b05e21e6d8b6cdf348c6931238abb15282a258bdb5e04cd3cca9356dc120ecfe0d1b977819ade4aac50163127c86db2300227ff60392d24daa0b7c + checksum: 99357a5cb30f53bacdc0d1cd6dff0f052ea6c2d1ba874d969bba69897ef716e87283e84a59dc52fb49aa31fd1b6f55ed756c64c04f5678380700239f6030b881 languageName: node linkType: hard @@ -2596,7 +2645,22 @@ __metadata: languageName: node linkType: hard -"@babel/preset-typescript@npm:^7.12.7, @babel/preset-typescript@npm:~7.22.5": +"@babel/preset-typescript@npm:^7.12.7, @babel/preset-typescript@npm:^7.23.0": + version: 7.23.0 + resolution: "@babel/preset-typescript@npm:7.23.0" + dependencies: + "@babel/helper-plugin-utils": ^7.22.5 + "@babel/helper-validator-option": ^7.22.15 + "@babel/plugin-syntax-jsx": ^7.22.5 + "@babel/plugin-transform-modules-commonjs": ^7.23.0 + "@babel/plugin-transform-typescript": ^7.22.15 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 3d5fce83e83f11c07e0ea13542bca181abb3b482b8981ec9c64e6add9d7beed3c54d063dc4bc9fd383165c71114a245abef89a289680833c5a8552fe3e7c4407 + languageName: node + linkType: hard + +"@babel/preset-typescript@npm:~7.22.5": version: 7.22.5 resolution: "@babel/preset-typescript@npm:7.22.5" dependencies: @@ -2660,43 +2724,43 @@ __metadata: languageName: node linkType: hard -"@babel/template@npm:^7.12.7, @babel/template@npm:^7.22.5, @babel/template@npm:^7.3.3": - version: 7.22.5 - resolution: "@babel/template@npm:7.22.5" +"@babel/template@npm:^7.12.7, @babel/template@npm:^7.22.15, @babel/template@npm:^7.22.5, @babel/template@npm:^7.3.3": + version: 7.22.15 + resolution: "@babel/template@npm:7.22.15" dependencies: - "@babel/code-frame": ^7.22.5 - "@babel/parser": ^7.22.5 - "@babel/types": ^7.22.5 - checksum: c5746410164039aca61829cdb42e9a55410f43cace6f51ca443313f3d0bdfa9a5a330d0b0df73dc17ef885c72104234ae05efede37c1cc8a72dc9f93425977a3 + "@babel/code-frame": ^7.22.13 + "@babel/parser": ^7.22.15 + "@babel/types": ^7.22.15 + checksum: 1f3e7dcd6c44f5904c184b3f7fe280394b191f2fed819919ffa1e529c259d5b197da8981b6ca491c235aee8dbad4a50b7e31304aa531271cb823a4a24a0dd8fd languageName: node linkType: hard -"@babel/traverse@npm:^7.1.6, @babel/traverse@npm:^7.12.11, @babel/traverse@npm:^7.12.9, @babel/traverse@npm:^7.13.0, @babel/traverse@npm:^7.22.10, @babel/traverse@npm:^7.22.5": - version: 7.22.10 - resolution: "@babel/traverse@npm:7.22.10" +"@babel/traverse@npm:^7.1.6, @babel/traverse@npm:^7.12.11, @babel/traverse@npm:^7.12.9, @babel/traverse@npm:^7.13.0, @babel/traverse@npm:^7.22.10, @babel/traverse@npm:^7.23.0": + version: 7.23.0 + resolution: "@babel/traverse@npm:7.23.0" dependencies: - "@babel/code-frame": ^7.22.10 - "@babel/generator": ^7.22.10 - "@babel/helper-environment-visitor": ^7.22.5 - "@babel/helper-function-name": ^7.22.5 + "@babel/code-frame": ^7.22.13 + "@babel/generator": ^7.23.0 + "@babel/helper-environment-visitor": ^7.22.20 + "@babel/helper-function-name": ^7.23.0 "@babel/helper-hoist-variables": ^7.22.5 "@babel/helper-split-export-declaration": ^7.22.6 - "@babel/parser": ^7.22.10 - "@babel/types": ^7.22.10 + "@babel/parser": ^7.23.0 + "@babel/types": ^7.23.0 debug: ^4.1.0 globals: ^11.1.0 - checksum: 9f7b358563bfb0f57ac4ed639f50e5c29a36b821a1ce1eea0c7db084f5b925e3275846d0de63bde01ca407c85d9804e0efbe370d92cd2baaafde3bd13b0f4cdb + checksum: 0b17fae53269e1af2cd3edba00892bc2975ad5df9eea7b84815dab07dfec2928c451066d51bc65b4be61d8499e77db7e547ce69ef2a7b0eca3f96269cb43a0b0 languageName: node linkType: hard -"@babel/types@npm:^7.0.0, @babel/types@npm:^7.12.11, @babel/types@npm:^7.12.7, @babel/types@npm:^7.2.0, @babel/types@npm:^7.20.7, @babel/types@npm:^7.22.10, @babel/types@npm:^7.22.5, @babel/types@npm:^7.3.0, @babel/types@npm:^7.3.3, @babel/types@npm:^7.4.4, @babel/types@npm:^7.8.3": - version: 7.22.10 - resolution: "@babel/types@npm:7.22.10" +"@babel/types@npm:^7.0.0, @babel/types@npm:^7.12.11, @babel/types@npm:^7.12.7, @babel/types@npm:^7.2.0, @babel/types@npm:^7.20.7, @babel/types@npm:^7.22.10, @babel/types@npm:^7.22.15, @babel/types@npm:^7.22.19, @babel/types@npm:^7.22.5, @babel/types@npm:^7.23.0, @babel/types@npm:^7.3.0, @babel/types@npm:^7.3.3, @babel/types@npm:^7.4.4, @babel/types@npm:^7.8.3": + version: 7.23.0 + resolution: "@babel/types@npm:7.23.0" dependencies: "@babel/helper-string-parser": ^7.22.5 - "@babel/helper-validator-identifier": ^7.22.5 + "@babel/helper-validator-identifier": ^7.22.20 to-fast-properties: ^2.0.0 - checksum: 095c4f4b7503fa816e4094113f0ec2351ef96ff32012010b771693066ff628c7c664b21c6bd3fb93aeb46fe7c61f6b3a3c9e4ed0034d6a2481201c417371c8af + checksum: 215fe04bd7feef79eeb4d33374b39909ce9cad1611c4135a4f7fdf41fe3280594105af6d7094354751514625ea92d0875aba355f53e86a92600f290e77b0e604 languageName: node linkType: hard @@ -4524,6 +4588,13 @@ __metadata: languageName: node linkType: hard +"@nicolo-ribaudo/chokidar-2@npm:2.1.8-no-fsevents.3": + version: 2.1.8-no-fsevents.3 + resolution: "@nicolo-ribaudo/chokidar-2@npm:2.1.8-no-fsevents.3" + checksum: ee55cc9241aeea7eb94b8a8551bfa4246c56c53bc71ecda0a2104018fcc328ba5723b33686bdf9cc65d4df4ae65e8016b89e0bbdeb94e0309fe91bb9ced42344 + languageName: node + linkType: hard + "@nicolo-ribaudo/eslint-scope-5-internals@npm:5.1.1-v1": version: 5.1.1-v1 resolution: "@nicolo-ribaudo/eslint-scope-5-internals@npm:5.1.1-v1" @@ -8394,19 +8465,26 @@ __metadata: version: 0.0.0-use.local resolution: "@rocket.chat/license@workspace:ee/packages/license" dependencies: + "@babel/cli": ^7.23.0 + "@babel/core": ^7.23.0 + "@babel/preset-env": ^7.22.20 + "@babel/preset-typescript": ^7.23.0 "@rocket.chat/core-typings": "workspace:^" "@rocket.chat/jwt": "workspace:^" "@rocket.chat/logger": "workspace:^" "@swc/core": ^1.3.66 "@swc/jest": ^0.2.26 + "@types/babel__core": ^7 + "@types/babel__preset-env": ^7 "@types/jest": ~29.5.3 "@types/ws": ^8.5.5 + babel-plugin-transform-inline-environment-variables: ^0.4.4 eslint: ~8.45.0 jest: ~29.6.1 jest-environment-jsdom: ~29.6.1 jest-websocket-mock: ^2.4.0 ts-jest: ~29.0.5 - typescript: ~5.2.2 + typescript: ^5.2.2 languageName: unknown linkType: soft @@ -15307,6 +15385,13 @@ __metadata: languageName: node linkType: hard +"babel-plugin-transform-inline-environment-variables@npm:^0.4.4": + version: 0.4.4 + resolution: "babel-plugin-transform-inline-environment-variables@npm:0.4.4" + checksum: fa361287411301237fd8ce332aff4f8e8ccb8db30e87a2ddc7224c8bf7cd792eda47aca24dc2e09e70bce4c027bc8cbe22f4999056be37a25d2472945df21ef5 + languageName: node + linkType: hard + "babel-polyfill@npm:^6.2.0": version: 6.26.0 resolution: "babel-polyfill@npm:6.26.0" @@ -16831,7 +16916,7 @@ __metadata: languageName: node linkType: hard -"chokidar@npm:3.5.3, chokidar@npm:>=3.0.0 <4.0.0, chokidar@npm:^3.4.1, chokidar@npm:^3.4.2, chokidar@npm:^3.5.1, chokidar@npm:^3.5.3": +"chokidar@npm:3.5.3, chokidar@npm:>=3.0.0 <4.0.0, chokidar@npm:^3.4.0, chokidar@npm:^3.4.1, chokidar@npm:^3.4.2, chokidar@npm:^3.5.1, chokidar@npm:^3.5.3": version: 3.5.3 resolution: "chokidar@npm:3.5.3" dependencies: @@ -17373,7 +17458,7 @@ __metadata: languageName: node linkType: hard -"commander@npm:^4.0.0, commander@npm:^4.1.1": +"commander@npm:^4.0.0, commander@npm:^4.0.1, commander@npm:^4.1.1": version: 4.1.1 resolution: "commander@npm:4.1.1" checksum: d7b9913ff92cae20cb577a4ac6fcc121bd6223319e54a40f51a14740a681ad5c574fd29a57da478a5f234a6fa6c52cbf0b7c641353e03c648b1ae85ba670b977 @@ -21980,6 +22065,13 @@ __metadata: languageName: node linkType: hard +"fs-readdir-recursive@npm:^1.1.0": + version: 1.1.0 + resolution: "fs-readdir-recursive@npm:1.1.0" + checksum: 29d50f3d2128391c7fc9fd051c8b7ea45bcc8aa84daf31ef52b17218e20bfd2bd34d02382742801954cc8d1905832b68227f6b680a666ce525d8b6b75068ad1e + languageName: node + linkType: hard + "fs-write-stream-atomic@npm:^1.0.8": version: 1.0.10 resolution: "fs-write-stream-atomic@npm:1.0.10" @@ -26057,14 +26149,7 @@ __metadata: languageName: node linkType: hard -"jose@npm:^4.11.1": - version: 4.12.0 - resolution: "jose@npm:4.12.0" - checksum: 09e67611768127ab54b6b507401de4b1f87e1e285cf2c2fc917e931e001b7e584c90081b421f483f13a6eec4fc44936e4a5f4b8ae2d59928061e886e35d33fa2 - languageName: node - linkType: hard - -"jose@npm:^4.14.4": +"jose@npm:^4.11.1, jose@npm:^4.14.4": version: 4.14.6 resolution: "jose@npm:4.14.6" checksum: eae81a234e7bf1446b1bd80722b3462b014e3835b155c3a7799c1c5043163a53a0dc28d347004151b031e6b7b863403aabf8814d9cc217ce21f8c2f3ebd4b335 @@ -37793,7 +37878,7 @@ __metadata: languageName: node linkType: hard -"typescript@npm:~5.2.2": +"typescript@npm:^5.2.2, typescript@npm:~5.2.2": version: 5.2.2 resolution: "typescript@npm:5.2.2" bin: @@ -37803,7 +37888,7 @@ __metadata: languageName: node linkType: hard -"typescript@patch:typescript@~5.2.2#~builtin": +"typescript@patch:typescript@^5.2.2#~builtin, typescript@patch:typescript@~5.2.2#~builtin": version: 5.2.2 resolution: "typescript@patch:typescript@npm%3A5.2.2#~builtin::version=5.2.2&hash=f456af" bin: From 9747e05d308a2c7ffde0641ea948d015fb9cdaec Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Thu, 28 Sep 2023 14:26:53 -0300 Subject: [PATCH 37/38] remove html report --- ee/packages/license/jest.config.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/ee/packages/license/jest.config.ts b/ee/packages/license/jest.config.ts index f23cf0851e4c..21121603f6e0 100644 --- a/ee/packages/license/jest.config.ts +++ b/ee/packages/license/jest.config.ts @@ -12,6 +12,5 @@ export default { '^jose$': require.resolve('jose'), }, collectCoverage: true, - coverageReporters: ['text', 'html'], collectCoverageFrom: ['src/**/*.{js,jsx,ts,tsx}'], }; From 79fe515eb0371e86d578a7c978134a092fd69168 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Thu, 28 Sep 2023 15:44:44 -0300 Subject: [PATCH 38/38] changeset --- .changeset/twelve-files-deny.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.changeset/twelve-files-deny.md b/.changeset/twelve-files-deny.md index 04700f86c002..123bf0a7764b 100644 --- a/.changeset/twelve-files-deny.md +++ b/.changeset/twelve-files-deny.md @@ -17,5 +17,6 @@ '@rocket.chat/meteor': minor --- -feat new `package/jwt` with `sign` and `veriy` functions to create a JWT given a JS object. By default uses algorithm RS256 for the public/private keys. -feat new `ee/package/license`. Contains new V3 definition and previous License definition renamed as LicenseV2. Package handles both versions. +Implemented the License library, it is used to handle the functionality like expiration date, modules, limits, etc. +Also added a version v3 of the license, which contains an extended list of features. +v2 is still supported, since we convert it to v3 on the fly.