From 85ddfb24baccdcbae56ffaf7a070b83128b4c7fb Mon Sep 17 00:00:00 2001
From: Pierre Lehnen <55164754+pierre-lehnen-rc@users.noreply.github.com>
Date: Mon, 16 Oct 2023 18:46:51 -0300
Subject: [PATCH] fix: licenses.info endpoint only available for admins
 (#30644)

---
 apps/meteor/ee/server/api/licenses.ts         |  7 ++-
 .../tests/end-to-end/api/20-licenses.js       | 46 +++++++++++++++++++
 .../license/src/definition/LicenseInfo.ts     | 10 ++++
 ee/packages/license/src/index.ts              | 11 ++---
 ee/packages/license/src/license.ts            | 19 +++++---
 packages/rest-typings/src/v1/licenses.ts      |  8 +---
 6 files changed, 80 insertions(+), 21 deletions(-)
 create mode 100644 ee/packages/license/src/definition/LicenseInfo.ts

diff --git a/apps/meteor/ee/server/api/licenses.ts b/apps/meteor/ee/server/api/licenses.ts
index ff5c3fcc3e47f..b7ac3ba81e9c0 100644
--- a/apps/meteor/ee/server/api/licenses.ts
+++ b/apps/meteor/ee/server/api/licenses.ts
@@ -25,10 +25,13 @@ API.v1.addRoute(
 
 API.v1.addRoute(
 	'licenses.info',
-	{ authRequired: true, validateParams: isLicensesInfoProps, permissionsRequired: ['view-privileged-setting'] },
+	{ authRequired: true, validateParams: isLicensesInfoProps },
 	{
 		async get() {
-			const data = await License.getInfo(Boolean(this.queryParams.loadValues));
+			const unrestrictedAccess = await hasPermissionAsync(this.userId, 'view-privileged-setting');
+			const loadCurrentValues = unrestrictedAccess && Boolean(this.queryParams.loadValues);
+
+			const data = await License.getInfo({ limits: unrestrictedAccess, license: unrestrictedAccess, currentValues: loadCurrentValues });
 
 			return API.v1.success({ data });
 		},
diff --git a/apps/meteor/tests/end-to-end/api/20-licenses.js b/apps/meteor/tests/end-to-end/api/20-licenses.js
index 993428d344099..302011addef9f 100644
--- a/apps/meteor/tests/end-to-end/api/20-licenses.js
+++ b/apps/meteor/tests/end-to-end/api/20-licenses.js
@@ -105,6 +105,52 @@ describe('licenses', function () {
 		});
 	});
 
+	describe('[/licenses.info]', () => {
+		it('should fail if not logged in', (done) => {
+			request
+				.get(api('licenses.info'))
+				.expect('Content-Type', 'application/json')
+				.expect(401)
+				.expect((res) => {
+					expect(res.body).to.have.property('status', 'error');
+					expect(res.body).to.have.property('message');
+				})
+				.end(done);
+		});
+
+		it('should return limited information if user is unauthorized', (done) => {
+			request
+				.get(api('licenses.info'))
+				.set(unauthorizedUserCredentials)
+				.expect('Content-Type', 'application/json')
+				.expect(200)
+				.expect((res) => {
+					expect(res.body).to.have.property('success', true);
+					expect(res.body).to.have.property('data').and.to.be.an('object');
+					expect(res.body.data).to.not.have.property('license');
+					expect(res.body.data).to.have.property('tags').and.to.be.an('array');
+				})
+				.end(done);
+		});
+
+		it('should return unrestricted info if user is logged in and is authorized', (done) => {
+			request
+				.get(api('licenses.info'))
+				.set(credentials)
+				.expect(200)
+				.expect((res) => {
+					expect(res.body).to.have.property('success', true);
+					expect(res.body).to.have.property('data').and.to.be.an('object');
+					if (process.env.IS_EE) {
+						expect(res.body.data).to.have.property('license').and.to.be.an('object');
+					}
+					expect(res.body.data).to.have.property('tags').and.to.be.an('array');
+				})
+
+				.end(done);
+		});
+	});
+
 	describe('[/licenses.isEnterprise]', () => {
 		it('should fail if not logged in', (done) => {
 			request
diff --git a/ee/packages/license/src/definition/LicenseInfo.ts b/ee/packages/license/src/definition/LicenseInfo.ts
new file mode 100644
index 0000000000000..7de3c0cfbdd60
--- /dev/null
+++ b/ee/packages/license/src/definition/LicenseInfo.ts
@@ -0,0 +1,10 @@
+import type { ILicenseTag } from './ILicenseTag';
+import type { ILicenseV3, LicenseLimitKind } from './ILicenseV3';
+import type { LicenseModule } from './LicenseModule';
+
+export type LicenseInfo = {
+	license?: ILicenseV3;
+	activeModules: LicenseModule[];
+	limits: Record<LicenseLimitKind, { value?: number; max: number }>;
+	tags: ILicenseTag[];
+};
diff --git a/ee/packages/license/src/index.ts b/ee/packages/license/src/index.ts
index 77e2976f156a7..9707a41d96ab7 100644
--- a/ee/packages/license/src/index.ts
+++ b/ee/packages/license/src/index.ts
@@ -1,5 +1,5 @@
-import type { ILicenseV3, LicenseLimitKind } from './definition/ILicenseV3';
-import type { LicenseModule } from './definition/LicenseModule';
+import type { LicenseLimitKind } from './definition/ILicenseV3';
+import type { LicenseInfo } from './definition/LicenseInfo';
 import type { LimitContext } from './definition/LimitContext';
 import { getAppsConfig, getMaxActiveUsers, getUnmodifiedLicenseAndModules } from './deprecated';
 import { onLicense } from './events/deprecated';
@@ -24,6 +24,7 @@ export * from './definition/ILicenseTag';
 export * from './definition/ILicenseV2';
 export * from './definition/ILicenseV3';
 export * from './definition/LicenseBehavior';
+export * from './definition/LicenseInfo';
 export * from './definition/LicenseLimit';
 export * from './definition/LicenseModule';
 export * from './definition/LicensePeriod';
@@ -49,11 +50,7 @@ interface License {
 	onBehaviorTriggered: typeof onBehaviorTriggered;
 	revalidateLicense: () => Promise<void>;
 
-	getInfo: (loadCurrentValues: boolean) => Promise<{
-		license: ILicenseV3 | undefined;
-		activeModules: LicenseModule[];
-		limits: Record<LicenseLimitKind, { value?: number; max: number }>;
-	}>;
+	getInfo: (info: { limits: boolean; currentValues: boolean; license: boolean }) => Promise<LicenseInfo>;
 
 	// Deprecated:
 	onLicense: typeof onLicense;
diff --git a/ee/packages/license/src/license.ts b/ee/packages/license/src/license.ts
index 8449d4136810b..d24d91287d1ea 100644
--- a/ee/packages/license/src/license.ts
+++ b/ee/packages/license/src/license.ts
@@ -4,6 +4,7 @@ import { type ILicenseTag } from './definition/ILicenseTag';
 import type { ILicenseV2 } from './definition/ILicenseV2';
 import type { ILicenseV3, LicenseLimitKind } from './definition/ILicenseV3';
 import type { BehaviorWithContext } from './definition/LicenseBehavior';
+import type { LicenseInfo } from './definition/LicenseInfo';
 import type { LicenseModule } from './definition/LicenseModule';
 import type { LicenseValidationOptions } from './definition/LicenseValidationOptions';
 import type { LimitContext } from './definition/LimitContext';
@@ -291,17 +292,22 @@ export class LicenseManager extends Emitter<LicenseEvents> {
 		return isBehaviorsInResult(validationResult, ['prevent_action']);
 	}
 
-	public async getInfo(loadCurrentValues = false): Promise<{
-		license: ILicenseV3 | undefined;
-		activeModules: LicenseModule[];
-		limits: Record<LicenseLimitKind, { value?: number; max: number }>;
-	}> {
+	public async getInfo({
+		limits: includeLimits,
+		currentValues: loadCurrentValues,
+		license: includeLicense,
+	}: {
+		limits: boolean;
+		currentValues: boolean;
+		license: boolean;
+	}): Promise<LicenseInfo> {
 		const activeModules = getModules.call(this);
 		const license = this.getLicense();
 
 		// Get all limits present in the license and their current value
 		const limits = (
 			(license &&
+				includeLimits &&
 				(await Promise.all(
 					globalLimitKinds
 						.map((limitKey) => ({
@@ -322,9 +328,10 @@ export class LicenseManager extends Emitter<LicenseEvents> {
 		).reduce((prev, curr) => ({ ...prev, ...curr }), {});
 
 		return {
-			license,
+			license: (includeLicense && license) || undefined,
 			activeModules,
 			limits: limits as Record<LicenseLimitKind, { max: number; value: number }>,
+			tags: license?.information.tags || [],
 		};
 	}
 }
diff --git a/packages/rest-typings/src/v1/licenses.ts b/packages/rest-typings/src/v1/licenses.ts
index 87c0106f6d3f9..d229ca49f1fc9 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, LicenseLimitKind } from '@rocket.chat/license';
+import type { ILicenseV2, ILicenseV3, LicenseInfo } from '@rocket.chat/license';
 import Ajv from 'ajv';
 
 const ajv = new Ajv({
@@ -45,11 +45,7 @@ export type LicensesEndpoints = {
 	};
 	'/v1/licenses.info': {
 		GET: (params: licensesInfoProps) => {
-			data: {
-				license: ILicenseV3 | undefined;
-				activeModules: string[];
-				limits: Record<LicenseLimitKind, { max: number; value?: number }>;
-			};
+			data: LicenseInfo;
 		};
 	};
 	'/v1/licenses.add': {