Skip to content

Commit

Permalink
feat: License V3 (#30287)
Browse files Browse the repository at this point in the history
Co-authored-by: Pierre Lehnen <[email protected]>
Co-authored-by: Guilherme Gazzo <[email protected]>
  • Loading branch information
3 people authored Sep 28, 2023
1 parent 2872428 commit 5f81a0f
Show file tree
Hide file tree
Showing 129 changed files with 2,687 additions and 1,005 deletions.
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

0 comments on commit 5f81a0f

Please sign in to comment.