From 6f80f978ed87bfe4ff7be04a4c28384f70b8dfd5 Mon Sep 17 00:00:00 2001 From: Luis Mauro Date: Mon, 4 Sep 2023 18:38:36 -0600 Subject: [PATCH 01/26] 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 9e68456a78c8..f3e258012b94 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 7d7b23691683..f28e90e2a683 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:^" @@ -26018,6 +26032,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" @@ -37231,6 +37252,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" @@ -37707,6 +37761,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" @@ -37717,6 +37781,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 40e6cd7e8cdb26ad949a503b3043d245791a6194 Mon Sep 17 00:00:00 2001 From: Luis Mauro Date: Tue, 5 Sep 2023 17:29:56 -0600 Subject: [PATCH 02/26] 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 3cd7756f16807b9d622970f09f46e610466b153b Mon Sep 17 00:00:00 2001 From: Pierre Lehnen Date: Wed, 6 Sep 2023 15:48:33 -0300 Subject: [PATCH 03/26] 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 b127f05dfc742d479c215470f4dcc873fa40500a Mon Sep 17 00:00:00 2001 From: Pierre Lehnen Date: Wed, 6 Sep 2023 15:59:22 -0300 Subject: [PATCH 04/26] 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 1a2f4ef9fce6374393275870155e3257659ecb52 Mon Sep 17 00:00:00 2001 From: Pierre Lehnen Date: Wed, 6 Sep 2023 16:00:11 -0300 Subject: [PATCH 05/26] 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 62bcf7c78d8cc1aeef0e50f6850e741732ae0b7f Mon Sep 17 00:00:00 2001 From: Pierre Lehnen Date: Wed, 6 Sep 2023 16:07:43 -0300 Subject: [PATCH 06/26] 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 c3c47e9410deda9c597551af341323f1be780153 Mon Sep 17 00:00:00 2001 From: Luis Mauro Date: Thu, 7 Sep 2023 15:56:05 -0600 Subject: [PATCH 07/26] 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 22a90fd0999af871920413c7cb8ff4fc0001e6a8 Mon Sep 17 00:00:00 2001 From: Pierre Lehnen Date: Tue, 12 Sep 2023 01:21:00 -0300 Subject: [PATCH 08/26] 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 69e3c24eb050effe804276b9f8fb1fffe8cd65eb Mon Sep 17 00:00:00 2001 From: Luis Mauro Date: Wed, 13 Sep 2023 11:59:57 -0600 Subject: [PATCH 09/26] 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 fa3400e87f05be4da75f5cbc8cfe6db619fea32a Mon Sep 17 00:00:00 2001 From: Pierre Lehnen Date: Fri, 15 Sep 2023 12:43:57 -0300 Subject: [PATCH 10/26] 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 e1d5a785f8ce25725aedd3dad330c40484b5ce21 Mon Sep 17 00:00:00 2001 From: Pierre Lehnen Date: Fri, 15 Sep 2023 13:01:05 -0300 Subject: [PATCH 11/26] 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 082657330f47b76be7f4f0610b412256034a76c0 Mon Sep 17 00:00:00 2001 From: Pierre Lehnen Date: Fri, 15 Sep 2023 16:17:43 -0300 Subject: [PATCH 12/26] 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 f7f07d02890a9b4f648fda1e18f4d8d948845da0 Mon Sep 17 00:00:00 2001 From: Pierre Lehnen Date: Mon, 18 Sep 2023 12:59:54 -0300 Subject: [PATCH 13/26] 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 46eae32adfc97f60f738168b5de285408bfec4c2 Mon Sep 17 00:00:00 2001 From: Pierre Lehnen Date: Mon, 18 Sep 2023 13:56:21 -0300 Subject: [PATCH 14/26] 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 41cdffbb020ed3e9f5f8d2ef80b456918a65b94b Mon Sep 17 00:00:00 2001 From: Pierre Lehnen Date: Mon, 18 Sep 2023 19:21:48 -0300 Subject: [PATCH 15/26] 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 f3e258012b94..1521f9037edc 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 f28e90e2a683..699acede8b85 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:^" @@ -9275,6 +9291,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 25c14919f20ca7a6eecbe6e218b3e6cb51ab3ec7 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Mon, 18 Sep 2023 21:47:38 -0300 Subject: [PATCH 16/26] 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 b5e1a434de623d613154b8a8d4717761fb4b8445 Mon Sep 17 00:00:00 2001 From: Pierre Lehnen Date: Tue, 19 Sep 2023 16:32:00 -0300 Subject: [PATCH 17/26] 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 ac3f4bc64c976112642db6ba5f01b709f581d9eb Mon Sep 17 00:00:00 2001 From: Luis Mauro Date: Wed, 20 Sep 2023 19:30:29 -0600 Subject: [PATCH 18/26] 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 699acede8b85..3f648e888646 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 6d6e426ca84e4484f1cfca7d9f75dd16e40015dd Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Thu, 21 Sep 2023 22:27:12 -0300 Subject: [PATCH 19/26] 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 effc9cfd19e1b18550750be0d0502b2b88eb8071 Mon Sep 17 00:00:00 2001 From: Pierre Lehnen Date: Fri, 22 Sep 2023 11:22:29 -0300 Subject: [PATCH 20/26] 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 0fc4fd33a9b1..43ec46cddff1 100644 --- a/apps/meteor/ee/server/local-services/instance/service.ts +++ b/apps/meteor/ee/server/local-services/instance/service.ts @@ -137,7 +137,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 a704653ba54e96b3dff63f5bd86ddc12090af9a7 Mon Sep 17 00:00:00 2001 From: Pierre Lehnen Date: Fri, 22 Sep 2023 11:26:42 -0300 Subject: [PATCH 21/26] 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 3e78a0eef12ac9134bcf58b49d91da6f8d2a8422 Mon Sep 17 00:00:00 2001 From: Pierre Lehnen Date: Fri, 22 Sep 2023 11:45:23 -0300 Subject: [PATCH 22/26] 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 3f648e888646..1f6275e334ba 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 @@ -37779,16 +37779,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" @@ -37799,16 +37789,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 7624738009f36a6e5c1b7870b72a158fd168677d Mon Sep 17 00:00:00 2001 From: Pierre Lehnen Date: Fri, 22 Sep 2023 13:37:19 -0300 Subject: [PATCH 23/26] feat: new `licenses.info` endpoint --- apps/meteor/ee/server/api/licenses.ts | 17 +++++++++ ee/packages/license/src/index.ts | 2 ++ ee/packages/license/src/info.ts | 46 ++++++++++++++++++++++++ packages/rest-typings/src/index.ts | 1 + packages/rest-typings/src/v1/licenses.ts | 31 ++++++++++++++-- 5 files changed, 95 insertions(+), 2 deletions(-) create mode 100644 ee/packages/license/src/info.ts diff --git a/apps/meteor/ee/server/api/licenses.ts b/apps/meteor/ee/server/api/licenses.ts index acdbabcbddd8..3e752f89b9ce 100644 --- a/apps/meteor/ee/server/api/licenses.ts +++ b/apps/meteor/ee/server/api/licenses.ts @@ -1,5 +1,6 @@ import * as License from '@rocket.chat/license'; import { Settings, Users } from '@rocket.chat/models'; +import { isLicensesInfoProps } from '@rocket.chat/rest-typings'; import { check } from 'meteor/check'; import { API } from '../../../app/api/server/api'; @@ -22,6 +23,22 @@ API.v1.addRoute( }, ); +API.v1.addRoute( + 'licenses.info', + { authRequired: true, validateParams: isLicensesInfoProps }, + { + async get() { + if (!(await hasPermissionAsync(this.userId, 'view-privileged-setting'))) { + return API.v1.unauthorized(); + } + + const data = await License.getLicenseInfo(Boolean(this.queryParams.loadValues)); + + return API.v1.success({ data }); + }, + }, +); + API.v1.addRoute( 'licenses.add', { authRequired: true }, diff --git a/ee/packages/license/src/index.ts b/ee/packages/license/src/index.ts index 877238f84a40..ab74fa21a41c 100644 --- a/ee/packages/license/src/index.ts +++ b/ee/packages/license/src/index.ts @@ -1,4 +1,5 @@ import { overwriteClassOnLicense } from './events/overwriteClassOnLicense'; +import { getLicenseInfo } from './info'; import { getLicense, getUnmodifiedLicenseAndModules, hasValidLicense, setLicense } from './license'; import { hasModule, getModules } from './modules'; import { getTags } from './tags'; @@ -33,4 +34,5 @@ export { overwriteClassOnLicense, setLicenseLimitCounter, getCurrentValueForLicenseLimit, + getLicenseInfo, }; diff --git a/ee/packages/license/src/info.ts b/ee/packages/license/src/info.ts new file mode 100644 index 000000000000..c86d3492b304 --- /dev/null +++ b/ee/packages/license/src/info.ts @@ -0,0 +1,46 @@ +import type { ILicenseV3, LicenseLimitKind } from './definition/ILicenseV3'; +import type { LicenseModule } from './definition/LicenseModule'; +import { getLicense, startedFairPolicy } from './license'; +import { getModules } from './modules'; +import { getCurrentValueForLicenseLimit } from './validation/getCurrentValueForLicenseLimit'; + +export const getLicenseInfo = async ( + loadCurrentValues = false, +): Promise<{ + license: ILicenseV3 | undefined; + activeModules: LicenseModule[]; + limits: Record; + inFairPolicy: boolean; +}> => { + const activeModules = getModules(); + const license = getLicense(); + + // Get all limits present in the license and their current value + const limits = ( + (license && + (await Promise.all( + (['activeUsers', 'guestUsers', 'privateApps', 'marketplaceApps', 'monthlyActiveContacts'] as LicenseLimitKind[]) + .map((limitKey) => ({ + limitKey, + max: Math.max(-1, Math.min(...Array.from(license.limits[limitKey as LicenseLimitKind] || [])?.map(({ max }) => max))), + })) + .filter(({ max }) => max >= 0 && max < Infinity) + .map(async ({ max, limitKey }) => { + return { + [limitKey as LicenseLimitKind]: { + ...(loadCurrentValues ? { value: await getCurrentValueForLicenseLimit(limitKey as LicenseLimitKind) } : {}), + max, + }, + }; + }), + ))) || + [] + ).reduce((prev, curr) => ({ ...prev, ...curr }), {}); + + return { + license, + activeModules, + limits: limits as Record, + inFairPolicy: startedFairPolicy(), + }; +}; diff --git a/packages/rest-typings/src/index.ts b/packages/rest-typings/src/index.ts index 066e3248dc33..3b8197ce20bf 100644 --- a/packages/rest-typings/src/index.ts +++ b/packages/rest-typings/src/index.ts @@ -228,6 +228,7 @@ export * from './v1/invites'; export * from './v1/dm'; export * from './v1/dm/DmHistoryProps'; export * from './v1/integrations'; +export * from './v1/licenses'; export * from './v1/omnichannel'; export * from './v1/oauthapps'; export * from './v1/oauthapps/UpdateOAuthAppParamsPOST'; diff --git a/packages/rest-typings/src/v1/licenses.ts b/packages/rest-typings/src/v1/licenses.ts index 96c67e2654bb..48b110ae30c6 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/license'; +import type * as License from '@rocket.chat/license'; import Ajv from 'ajv'; const ajv = new Ajv({ @@ -22,9 +22,36 @@ const licensesAddPropsSchema = { export const isLicensesAddProps = ajv.compile(licensesAddPropsSchema); +type licensesInfoProps = { + loadValues?: boolean; +}; + +const licensesInfoPropsSchema = { + type: 'object', + properties: { + loadValues: { + type: 'boolean', + }, + }, + required: [], + additionalProperties: false, +}; + +export const isLicensesInfoProps = ajv.compile(licensesInfoPropsSchema); + export type LicensesEndpoints = { '/v1/licenses.get': { - GET: () => { licenses: Array }; + GET: () => { licenses: Array }; + }; + '/v1/licenses.info': { + GET: (params: licensesInfoProps) => { + data: { + license: License.ILicenseV3 | undefined; + activeModules: string[]; + limits: Record; + inFairPolicy: boolean; + }; + }; }; '/v1/licenses.add': { POST: (params: licensesAddProps) => void; From 070ce3af8e32da5c58f5512982e9cd39e614ef8c Mon Sep 17 00:00:00 2001 From: Pierre Lehnen Date: Mon, 2 Oct 2023 13:05:33 -0300 Subject: [PATCH 24/26] updated with upstream --- apps/meteor/ee/server/api/licenses.ts | 9 ++---- ee/packages/license/src/index.ts | 8 +++++ ee/packages/license/src/info.ts | 46 --------------------------- ee/packages/license/src/license.ts | 41 +++++++++++++++++++++++- 4 files changed, 51 insertions(+), 53 deletions(-) delete mode 100644 ee/packages/license/src/info.ts diff --git a/apps/meteor/ee/server/api/licenses.ts b/apps/meteor/ee/server/api/licenses.ts index 5a3e5422f6c3..ff5c3fcc3e47 100644 --- a/apps/meteor/ee/server/api/licenses.ts +++ b/apps/meteor/ee/server/api/licenses.ts @@ -1,5 +1,6 @@ import { License } from '@rocket.chat/license'; import { Settings, Users } from '@rocket.chat/models'; +import { isLicensesInfoProps } from '@rocket.chat/rest-typings'; import { check } from 'meteor/check'; import { API } from '../../../app/api/server/api'; @@ -24,14 +25,10 @@ API.v1.addRoute( API.v1.addRoute( 'licenses.info', - { authRequired: true, validateParams: isLicensesInfoProps }, + { authRequired: true, validateParams: isLicensesInfoProps, permissionsRequired: ['view-privileged-setting'] }, { async get() { - if (!(await hasPermissionAsync(this.userId, 'view-privileged-setting'))) { - return API.v1.unauthorized(); - } - - const data = await License.getLicenseInfo(Boolean(this.queryParams.loadValues)); + const data = await License.getInfo(Boolean(this.queryParams.loadValues)); return API.v1.success({ data }); }, diff --git a/ee/packages/license/src/index.ts b/ee/packages/license/src/index.ts index 11cf3bbbe4c5..92f0543841b6 100644 --- a/ee/packages/license/src/index.ts +++ b/ee/packages/license/src/index.ts @@ -1,4 +1,5 @@ import type { ILicenseV3, LicenseLimitKind } from './definition/ILicenseV3'; +import type { LicenseModule } from './definition/LicenseModule'; import type { LimitContext } from './definition/LimitContext'; import { getAppsConfig, getMaxActiveUsers, getUnmodifiedLicenseAndModules } from './deprecated'; import { onLicense } from './events/deprecated'; @@ -47,6 +48,13 @@ interface License { supportedVersions(): ILicenseV3['supportedVersions']; + getInfo: (loadCurrentValues: boolean) => Promise<{ + license: ILicenseV3 | undefined; + activeModules: LicenseModule[]; + limits: Record; + inFairPolicy: boolean; + }>; + // Deprecated: onLicense: typeof onLicense; // Deprecated: diff --git a/ee/packages/license/src/info.ts b/ee/packages/license/src/info.ts deleted file mode 100644 index c86d3492b304..000000000000 --- a/ee/packages/license/src/info.ts +++ /dev/null @@ -1,46 +0,0 @@ -import type { ILicenseV3, LicenseLimitKind } from './definition/ILicenseV3'; -import type { LicenseModule } from './definition/LicenseModule'; -import { getLicense, startedFairPolicy } from './license'; -import { getModules } from './modules'; -import { getCurrentValueForLicenseLimit } from './validation/getCurrentValueForLicenseLimit'; - -export const getLicenseInfo = async ( - loadCurrentValues = false, -): Promise<{ - license: ILicenseV3 | undefined; - activeModules: LicenseModule[]; - limits: Record; - inFairPolicy: boolean; -}> => { - const activeModules = getModules(); - const license = getLicense(); - - // Get all limits present in the license and their current value - const limits = ( - (license && - (await Promise.all( - (['activeUsers', 'guestUsers', 'privateApps', 'marketplaceApps', 'monthlyActiveContacts'] as LicenseLimitKind[]) - .map((limitKey) => ({ - limitKey, - max: Math.max(-1, Math.min(...Array.from(license.limits[limitKey as LicenseLimitKind] || [])?.map(({ max }) => max))), - })) - .filter(({ max }) => max >= 0 && max < Infinity) - .map(async ({ max, limitKey }) => { - return { - [limitKey as LicenseLimitKind]: { - ...(loadCurrentValues ? { value: await getCurrentValueForLicenseLimit(limitKey as LicenseLimitKind) } : {}), - max, - }, - }; - }), - ))) || - [] - ).reduce((prev, curr) => ({ ...prev, ...curr }), {}); - - return { - license, - activeModules, - limits: limits as Record, - inFairPolicy: startedFairPolicy(), - }; -}; diff --git a/ee/packages/license/src/license.ts b/ee/packages/license/src/license.ts index 2fb25b0e3b4f..a420eb2b0d57 100644 --- a/ee/packages/license/src/license.ts +++ b/ee/packages/license/src/license.ts @@ -9,7 +9,7 @@ import { DuplicatedLicenseError } from './errors/DuplicatedLicenseError'; import { InvalidLicenseError } from './errors/InvalidLicenseError'; import { NotReadyForValidation } from './errors/NotReadyForValidation'; import { logger } from './logger'; -import { invalidateAll, replaceModules } from './modules'; +import { getModules, invalidateAll, replaceModules } from './modules'; import { applyPendingLicense, clearPendingLicense, hasPendingLicense, isPendingLicense, setPendingLicense } from './pendingLicense'; import { showLicense } from './showLicense'; import { replaceTags } from './tags'; @@ -227,4 +227,43 @@ export class LicenseManager extends Emitter< .some(({ max }) => max < currentValue), ); } + + public async getInfo(loadCurrentValues = false): Promise<{ + license: ILicenseV3 | undefined; + activeModules: LicenseModule[]; + limits: Record; + inFairPolicy: boolean; + }> { + const activeModules = getModules.call(this); + const license = this.getLicense(); + + // Get all limits present in the license and their current value + const limits = ( + (license && + (await Promise.all( + (['activeUsers', 'guestUsers', 'privateApps', 'marketplaceApps', 'monthlyActiveContacts'] as LicenseLimitKind[]) + .map((limitKey) => ({ + limitKey, + max: Math.max(-1, Math.min(...Array.from(license.limits[limitKey as LicenseLimitKind] || [])?.map(({ max }) => max))), + })) + .filter(({ max }) => max >= 0 && max < Infinity) + .map(async ({ max, limitKey }) => { + return { + [limitKey as LicenseLimitKind]: { + ...(loadCurrentValues ? { value: await getCurrentValueForLicenseLimit.call(this, limitKey as LicenseLimitKind) } : {}), + max, + }, + }; + }), + ))) || + [] + ).reduce((prev, curr) => ({ ...prev, ...curr }), {}); + + return { + license, + activeModules, + limits: limits as Record, + inFairPolicy: this.inFairPolicy, + }; + } } From eb5e007bb8be9263d137a16f8aaf20fe1cfd76ef Mon Sep 17 00:00:00 2001 From: Pierre Lehnen Date: Mon, 2 Oct 2023 13:22:28 -0300 Subject: [PATCH 25/26] merge error --- packages/jwt/package.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/jwt/package.json b/packages/jwt/package.json index 8ec0c5a2c104..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" }, @@ -21,6 +22,9 @@ "files": [ "/dist" ], + "volta": { + "extends": "../../package.json" + }, "dependencies": { "jose": "^4.14.4" } From f0eb330716db3aa9c15f351fbc12068db3e169c2 Mon Sep 17 00:00:00 2001 From: Pierre Lehnen Date: Mon, 2 Oct 2023 16:27:05 -0300 Subject: [PATCH 26/26] changeset --- .changeset/tough-carrots-walk.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .changeset/tough-carrots-walk.md diff --git a/.changeset/tough-carrots-walk.md b/.changeset/tough-carrots-walk.md new file mode 100644 index 000000000000..2851e697b85e --- /dev/null +++ b/.changeset/tough-carrots-walk.md @@ -0,0 +1,7 @@ +--- +'@rocket.chat/rest-typings': minor +'@rocket.chat/license': patch +'@rocket.chat/meteor': patch +--- + +feat: added `licenses.info` endpoint