Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: License V3 #30287

Merged
merged 38 commits into from
Sep 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
22f7e7b
jwt package, license v3 type
lmauromb Sep 5, 2023
c6ebf8d
chore: add V2 suffix to current License
lmauromb Sep 5, 2023
08a25fc
export license v3
pierre-lehnen-rc Sep 6, 2023
a63a72c
removed duplicated types
pierre-lehnen-rc Sep 6, 2023
f895b94
removed extra line
pierre-lehnen-rc Sep 6, 2023
afa5da8
renamed ILicenseV2Tag back to ILicenseTag and used it on the v3 licen…
pierre-lehnen-rc Sep 6, 2023
6ba5167
add function to transform V2 into V3
lmauromb Sep 7, 2023
abc66b6
validate license in v3 format
pierre-lehnen-rc Sep 12, 2023
4c66bc0
remove unused type imports
lmauromb Sep 13, 2023
96674a0
Removing references to old license structure
pierre-lehnen-rc Sep 15, 2023
1056427
removed unused file
pierre-lehnen-rc Sep 15, 2023
c8c5273
Implemented disable_modules
pierre-lehnen-rc Sep 15, 2023
118e22f
Fixed trial information
pierre-lehnen-rc Sep 18, 2023
de75365
missing null-check
pierre-lehnen-rc Sep 18, 2023
eec34a6
moved the license code to a separate package
pierre-lehnen-rc Sep 18, 2023
f615635
add license package to dockerfile
ggazzo Sep 19, 2023
608ed68
avoid triggering "invalidate" events when an active license is replac…
pierre-lehnen-rc Sep 19, 2023
564810b
handle V3 and V2
lmauromb Sep 21, 2023
284f12e
jwt package
ggazzo Sep 22, 2023
ed45432
renamed isEnterprise to hasValidLicense and fixed some invalid import…
pierre-lehnen-rc Sep 22, 2023
e248a34
added limitReached event
pierre-lehnen-rc Sep 22, 2023
734debb
fixed typescript version
pierre-lehnen-rc Sep 22, 2023
c182f21
moved data counters to @rocket.chat/meteor
pierre-lehnen-rc Sep 22, 2023
e69a35e
use visualExpiration attribute for trial end date
pierre-lehnen-rc Sep 22, 2023
490a0c7
export internal license data when running with test_mode
pierre-lehnen-rc Sep 25, 2023
90f7e79
reorganized license code into a class to facilitate writing tests
pierre-lehnen-rc Sep 25, 2023
f0d4cae
adjust packages
ggazzo Sep 26, 2023
7489a89
create custom errors
ggazzo Sep 26, 2023
1307932
init refactor to support instances
ggazzo Sep 26, 2023
fa3500b
fixed invalid reference
pierre-lehnen-rc Sep 26, 2023
80bddfa
add a test for duplicated license one
ggazzo Sep 26, 2023
74b3149
lint
ggazzo Sep 26, 2023
ef10118
enable testunit
ggazzo Sep 26, 2023
819a7ce
add changeset
lmauromb Sep 26, 2023
d1b85f4
uses async/await on decrypt
lmauromb Sep 26, 2023
21d9d72
chore: unit tests (#30504)
ggazzo Sep 28, 2023
9747e05
remove html report
ggazzo Sep 28, 2023
79fe515
changeset
ggazzo Sep 28, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions .changeset/twelve-files-deny.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
---
'@rocket.chat/license': minor
'@rocket.chat/jwt': minor
'@rocket.chat/omnichannel-services': minor
'@rocket.chat/omnichannel-transcript': minor
'@rocket.chat/authorization-service': minor
'@rocket.chat/stream-hub-service': minor
'@rocket.chat/presence-service': minor
'@rocket.chat/account-service': minor
'@rocket.chat/core-services': minor
'@rocket.chat/model-typings': minor
'@rocket.chat/core-typings': minor
'@rocket.chat/rest-typings': minor
'@rocket.chat/ddp-streamer': minor
'@rocket.chat/queue-worker': minor
'@rocket.chat/presence': minor
'@rocket.chat/meteor': minor
---

Implemented the License library, it is used to handle the functionality like expiration date, modules, limits, etc.
Also added a version v3 of the license, which contains an extended list of features.
v2 is still supported, since we convert it to v3 on the fly.
4 changes: 2 additions & 2 deletions apps/meteor/app/api/server/v1/federation.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Federation, FederationEE } from '@rocket.chat/core-services';
import { License } 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(
Expand All @@ -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);

Expand Down
2 changes: 1 addition & 1 deletion apps/meteor/app/statistics/server/lib/statistics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
10 changes: 7 additions & 3 deletions apps/meteor/client/views/hooks/useUpgradeTabParams.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { ILicenseV2, ILicenseV3 } from '@rocket.chat/license';
import { useSetting } from '@rocket.chat/ui-contexts';
import { format } from 'date-fns';

Expand All @@ -16,9 +17,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;
const licenses = (licensesData?.licenses || []) as (Partial<ILicenseV2 & ILicenseV3> & { modules: string[] })[];

const trialLicense = licenses.find(({ meta, information }) => information?.trial ?? meta?.trial);
const isTrial = Boolean(trialLicense);
const trialEndDateStr = trialLicense?.information?.visualExpiration || trialLicense?.meta?.trialEnd || trialLicense?.cloudMeta?.trialEnd;
const trialEndDate = trialEndDateStr ? format(new Date(trialEndDateStr), 'yyyy-MM-dd') : undefined;

const upgradeTabType = getUpgradeTabType({
registered,
Expand Down
4 changes: 2 additions & 2 deletions apps/meteor/ee/app/api-enterprise/server/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { onLicense } from '../../license/server';
import { License } from '@rocket.chat/license';

await onLicense('canned-responses', async () => {
await License.onLicense('canned-responses', async () => {
await import('./canned-responses');
});
29 changes: 0 additions & 29 deletions apps/meteor/ee/app/authorization/server/validateUserRoles.js

This file was deleted.

43 changes: 43 additions & 0 deletions apps/meteor/ee/app/authorization/server/validateUserRoles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import type { IUser } from '@rocket.chat/core-typings';
import { License } from '@rocket.chat/license';
import { Users } from '@rocket.chat/models';
import { Meteor } from 'meteor/meteor';

import { i18n } from '../../../../server/lib/i18n';

export const validateUserRoles = async function (userData: Partial<IUser>) {
if (!License.hasValidLicense()) {
return;
}

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 (isGuest) {
if (wasGuest) {
return;
}

if (await License.shouldPreventAction('guestUsers')) {
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;
}

if (await License.shouldPreventAction('activeUsers')) {
throw new Meteor.Error('error-license-user-limit-reached', i18n.t('error-license-user-limit-reached'));
}
};
4 changes: 2 additions & 2 deletions apps/meteor/ee/app/canned-responses/server/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { onLicense } from '../../license/server';
import { 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');
Expand Down
25 changes: 25 additions & 0 deletions apps/meteor/ee/app/license/server/canEnableApp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import type { IAppStorageItem } from '@rocket.chat/apps-engine/server/storage';
import { Apps } from '@rocket.chat/core-services';
import { License } from '@rocket.chat/license';

import { getInstallationSourceFromAppStorageItem } from '../../../../lib/apps/getInstallationSourceFromAppStorageItem';

export const canEnableApp = async (app: IAppStorageItem): Promise<boolean> => {
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 License.shouldPreventAction('privateApps'));
default:
return !(await License.shouldPreventAction('marketplaceApps'));
}
};
10 changes: 0 additions & 10 deletions apps/meteor/ee/app/license/server/decrypt.ts

This file was deleted.

9 changes: 4 additions & 5 deletions apps/meteor/ee/app/license/server/getStatistics.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import { log } from 'console';

import { Analytics } from '@rocket.chat/core-services';
import { License } 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<EEOnlyStats>;

type GenericStats = {
Expand All @@ -28,8 +27,8 @@ type EEOnlyStats = {

export async function getStatistics(): Promise<ENTERPRISE_STATISTICS> {
const genericStats: GenericStats = {
modules: getModules(),
tags: getTags().map(({ name }) => name),
modules: License.getModules(),
tags: License.getTags().map(({ name }) => name),
seatRequests: await Analytics.getSeatRequestCount(),
};

Expand All @@ -45,7 +44,7 @@ export async function getStatistics(): Promise<ENTERPRISE_STATISTICS> {

// 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<EEOnlyStats | undefined> {
if (!hasLicense('livechat-enterprise')) {
if (!License.hasModule('livechat-enterprise')) {
return;
}

Expand Down
2 changes: 0 additions & 2 deletions apps/meteor/ee/app/license/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,4 @@ import './settings';
import './methods';
import './startup';

export { onLicense, overwriteClassOnLicense, isEnterprise, getMaxGuestUsers } from './license';

export { getStatistics } from './getStatistics';
21 changes: 21 additions & 0 deletions apps/meteor/ee/app/license/server/lib/getAppCount.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { Apps } from '@rocket.chat/core-services';
import type { LicenseAppSources } from '@rocket.chat/license';

import { getInstallationSourceFromAppStorageItem } from '../../../../../lib/apps/getInstallationSourceFromAppStorageItem';

export async function getAppCount(source: LicenseAppSources): Promise<number> {
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;
}
26 changes: 0 additions & 26 deletions apps/meteor/ee/app/license/server/lib/isUnderAppLimits.ts

This file was deleted.

20 changes: 10 additions & 10 deletions apps/meteor/ee/app/license/server/license.internalService.ts
Original file line number Diff line number Diff line change
@@ -1,49 +1,49 @@
import type { ILicense } from '@rocket.chat/core-services';
import { api, ServiceClassInternal } from '@rocket.chat/core-services';
import { License, 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';

constructor() {
super();

onValidateLicenses((): void => {
if (!isEnterprise()) {
License.onValidateLicense((): void => {
if (!License.hasValidLicense()) {
return;
}

void api.broadcast('authorization.guestPermissions', guestPermissions);
void resetEnterprisePermissions();
});

onModule((licenseModule) => {
License.onModule((licenseModule) => {
void api.broadcast('license.module', licenseModule);
});
}

async started(): Promise<void> {
if (!isEnterprise()) {
if (!License.hasValidLicense()) {
return;
}

void api.broadcast('authorization.guestPermissions', guestPermissions);
await resetEnterprisePermissions();
}

hasLicense(feature: string): boolean {
return hasLicense(feature);
hasModule(feature: LicenseModule): boolean {
return License.hasModule(feature);
}

isEnterprise(): boolean {
return isEnterprise();
hasValidLicense(): boolean {
return License.hasValidLicense();
}

getModules(): string[] {
return getModules();
return License.getModules();
}

getGuestPermissions(): string[] {
Expand Down
Loading
Loading