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: new licenses.info endpoint #30473

Merged
merged 28 commits into from
Oct 3, 2023
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
6f80f97
jwt package, license v3 type
lmauromb Sep 5, 2023
40e6cd7
chore: add V2 suffix to current License
lmauromb Sep 5, 2023
3cd7756
export license v3
pierre-lehnen-rc Sep 6, 2023
b127f05
removed duplicated types
pierre-lehnen-rc Sep 6, 2023
1a2f4ef
removed extra line
pierre-lehnen-rc Sep 6, 2023
62bcf7c
renamed ILicenseV2Tag back to ILicenseTag and used it on the v3 licen…
pierre-lehnen-rc Sep 6, 2023
c3c47e9
add function to transform V2 into V3
lmauromb Sep 7, 2023
22a90fd
validate license in v3 format
pierre-lehnen-rc Sep 12, 2023
69e3c24
remove unused type imports
lmauromb Sep 13, 2023
fa3400e
Removing references to old license structure
pierre-lehnen-rc Sep 15, 2023
e1d5a78
removed unused file
pierre-lehnen-rc Sep 15, 2023
0826573
Implemented disable_modules
pierre-lehnen-rc Sep 15, 2023
f7f07d0
Fixed trial information
pierre-lehnen-rc Sep 18, 2023
46eae32
missing null-check
pierre-lehnen-rc Sep 18, 2023
41cdffb
moved the license code to a separate package
pierre-lehnen-rc Sep 18, 2023
25c1491
add license package to dockerfile
ggazzo Sep 19, 2023
b5e1a43
avoid triggering "invalidate" events when an active license is replac…
pierre-lehnen-rc Sep 19, 2023
ac3f4bc
handle V3 and V2
lmauromb Sep 21, 2023
6d6e426
jwt package
ggazzo Sep 22, 2023
effc9cf
renamed isEnterprise to hasValidLicense and fixed some invalid import…
pierre-lehnen-rc Sep 22, 2023
a704653
added limitReached event
pierre-lehnen-rc Sep 22, 2023
3e78a0e
fixed typescript version
pierre-lehnen-rc Sep 22, 2023
7624738
feat: new `licenses.info` endpoint
pierre-lehnen-rc Sep 22, 2023
9b8374d
Merge branch 'develop' into feat/license3-cont
pierre-lehnen-rc Oct 2, 2023
070ce3a
updated with upstream
pierre-lehnen-rc Oct 2, 2023
eb5e007
merge error
pierre-lehnen-rc Oct 2, 2023
f0eb330
changeset
pierre-lehnen-rc Oct 2, 2023
a88e3e5
Merge branch 'develop' into feat/license3-cont
pierre-lehnen-rc Oct 3, 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
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 * as 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
12 changes: 9 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 * as License from '@rocket.chat/license';
import { useSetting } from '@rocket.chat/ui-contexts';
import { format } from 'date-fns';

Expand All @@ -16,9 +17,14 @@ 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<License.ILicenseV2 & License.ILicenseV3> & { modules: string[] })[];

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,
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 * as License from '@rocket.chat/license';

await onLicense('canned-responses', async () => {
await License.onLicense('canned-responses', async () => {
await import('./canned-responses');
});
41 changes: 27 additions & 14 deletions apps/meteor/ee/app/authorization/server/validateUserRoles.js
Original file line number Diff line number Diff line change
@@ -1,29 +1,42 @@
import * as License from '@rocket.chat/license';
import { Users } from '@rocket.chat/models';
import { Meteor } from 'meteor/meteor';

import { isEnterprise, getMaxGuestUsers } from '../../license/server';
import { i18n } from '../../../../server/lib/i18n';

export const validateUserRoles = async function (userId, userData) {
if (!isEnterprise()) {
if (!License.hasValidLicense()) {
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 License.preventNewGuests()) {
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 License.preventNewUsers()) {
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 * 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');
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 * as 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.preventNewPrivateApps());
default:
return !(await License.preventNewMarketplaceApps());
}
};
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 * as 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 * as License from '@rocket.chat/license';

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

export async function getAppCount(source: License.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 * as License 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: 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[] {
Expand Down
Loading