Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
217 changes: 148 additions & 69 deletions apps/meteor/ee/server/api/licenses.ts
Original file line number Diff line number Diff line change
@@ -1,103 +1,182 @@
import type { Cloud, LicenseInfo } from '@rocket.chat/core-typings';
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
Resolved
import { License } from '@rocket.chat/license';
import { Settings, Users } from '@rocket.chat/models';
import { isLicensesInfoProps, isLicensesValidateProps } from '@rocket.chat/rest-typings';
import { check } from 'meteor/check';
import {
ajv,
isLicensesAddProps,
isLicensesInfoProps,
isLicensesValidateProps,
validateBadRequestErrorResponse,
validateForbiddenErrorResponse,
validateUnauthorizedErrorResponse,
} from '@rocket.chat/rest-typings';

import { API } from '../../../server/api/api';
import { hasPermissionAsync } from '../../../server/lib/authorization/hasPermission';
import { notifyOnSettingChangedById } from '../../../server/lib/notifyListener';
import { settings } from '../../../server/settings';
import { updateAuditedByUser } from '../../../server/settings/lib/auditedSettingUpdates';

API.v1.addRoute(
// LicenseInfo is a large, loosely-matching shape (branded LicenseModule names, partial `limits`,
// a nested ILicenseV3) that a strict typia $ref can't validate without fighting the type, so the
// license/announcement payloads are validated as open objects (same relaxation as omnichannel config).
const licensesInfoResponseSchema = ajv.compile<{ license: LicenseInfo; cloudSyncAnnouncement?: Cloud.ICloudSyncAnnouncement }>({
type: 'object',
properties: {
license: { type: 'object' },
cloudSyncAnnouncement: { type: 'object' },
success: { type: 'boolean', enum: [true] },
},
required: ['license', 'success'],
additionalProperties: false,
});

const successResponseSchema = ajv.compile<void>({
type: 'object',
properties: { success: { type: 'boolean', enum: [true] } },
required: ['success'],
additionalProperties: false,
});

const licensesValidateErrorSchema = ajv.compile<{
success: false;
error?: string;
errorType?: string;
stack?: string;
reasons?: unknown[];
}>({
type: 'object',
properties: {
success: { type: 'boolean', enum: [false] },
error: { type: 'string' },
errorType: { type: 'string' },
stack: { type: 'string' },
reasons: { type: 'array' },
},
required: ['success'],
additionalProperties: false,
});

const licensesMaxActiveUsersResponseSchema = ajv.compile<{ maxActiveUsers: number | null; activeUsers: number }>({
type: 'object',
properties: {
maxActiveUsers: { type: ['number', 'null'] },
activeUsers: { type: 'number' },
success: { type: 'boolean', enum: [true] },
},
required: ['maxActiveUsers', 'activeUsers', 'success'],
additionalProperties: false,
});

API.v1.get(
'licenses.info',
{ authRequired: true, validateParams: isLicensesInfoProps },
{
async get() {
const unrestrictedAccess = await hasPermissionAsync(this.user, 'view-privileged-setting');
const loadCurrentValues = unrestrictedAccess && Boolean(this.queryParams.loadValues);

const license = await License.getInfo({
limits: unrestrictedAccess,
license: unrestrictedAccess,
currentValues: loadCurrentValues,
});

try {
// TODO: Remove this logic after setting type object is implemented.
const cloudSyncAnnouncement = JSON.parse(settings.get('Cloud_Sync_Announcement_Payload') ?? null);
const canManageCloud = await hasPermissionAsync(this.user, 'manage-cloud');
return API.v1.success({
license,
...(canManageCloud && cloudSyncAnnouncement && { cloudSyncAnnouncement }),
});
} catch (error) {
console.error('Unable to parse Cloud_Sync_Announcement_Payload');
}

return API.v1.success({
license,
});
authRequired: true,
query: isLicensesInfoProps,
response: {
200: licensesInfoResponseSchema,
401: validateUnauthorizedErrorResponse,
400: validateBadRequestErrorResponse,
},
},
async function action() {
const unrestrictedAccess = await hasPermissionAsync(this.user, 'view-privileged-setting');
const loadCurrentValues = unrestrictedAccess && Boolean(this.queryParams.loadValues);

const license = await License.getInfo({
limits: unrestrictedAccess,
license: unrestrictedAccess,
currentValues: loadCurrentValues,
});

let cloudSyncAnnouncement;
try {
// TODO: Remove this logic after setting type object is implemented.
cloudSyncAnnouncement = JSON.parse(settings.get('Cloud_Sync_Announcement_Payload') ?? null);
} catch (error) {
console.error('Unable to parse Cloud_Sync_Announcement_Payload');
}

const canManageCloud = await hasPermissionAsync(this.user, 'manage-cloud');
return API.v1.success({
license,
...(canManageCloud && cloudSyncAnnouncement && { cloudSyncAnnouncement }),
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
},
);

API.v1.addRoute(
API.v1.post(
'licenses.add',
{ authRequired: true, permissionsRequired: ['edit-privileged-setting'] },
{
async post() {
check(this.bodyParams, {
license: String,
});

const { license } = this.bodyParams;
if (!(await License.validateFormat(license))) {
return API.v1.failure('Invalid license');
}

const auditSettingOperation = updateAuditedByUser({
_id: this.userId,
username: this.user.username,
ip: this.requestIp,
useragent: this.request.headers.get('user-agent') || '',
});

(await auditSettingOperation(Settings.updateValueById, 'Enterprise_License', license)).modifiedCount &&
void notifyOnSettingChangedById('Enterprise_License');

return API.v1.success();
authRequired: true,
permissionsRequired: ['edit-privileged-setting'],
body: isLicensesAddProps,
response: {
200: successResponseSchema,
401: validateUnauthorizedErrorResponse,
400: validateBadRequestErrorResponse,
403: validateForbiddenErrorResponse,
},
},
async function action() {
const { license } = this.bodyParams;
if (!(await License.validateFormat(license))) {
return API.v1.failure('Invalid license');
}

const auditSettingOperation = updateAuditedByUser({
_id: this.userId,
username: this.user.username ?? '',
ip: this.requestIp ?? '',
useragent: this.request.headers.get('user-agent') || '',
});

(await auditSettingOperation(Settings.updateValueById, 'Enterprise_License', license)).modifiedCount &&
void notifyOnSettingChangedById('Enterprise_License');

return API.v1.success();
},
);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved

API.v1.addRoute(
API.v1.post(
'licenses.validate',
{ authRequired: true, permissionsRequired: ['edit-privileged-setting'], validateParams: isLicensesValidateProps },
{
async post() {
const { license } = this.bodyParams;
authRequired: true,
permissionsRequired: ['edit-privileged-setting'],
body: isLicensesValidateProps,
response: {
200: successResponseSchema,
401: validateUnauthorizedErrorResponse,
400: licensesValidateErrorSchema,
403: validateForbiddenErrorResponse,
},
},
async function action() {
const { license } = this.bodyParams;

const result = await License.validateLicenseForPreview(license);
const result = await License.validateLicenseForPreview(license);

if (!result.valid) {
return API.v1.failure({ error: 'license-invalid', reasons: result.reasons });
}
if (!result.valid) {
return API.v1.failure({ error: 'license-invalid', reasons: result.reasons });
}

return API.v1.success();
},
return API.v1.success();
},
);

Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
API.v1.addRoute(
API.v1.get(
'licenses.maxActiveUsers',
{ authRequired: true },
{
async get() {
const maxActiveUsers = License.getMaxActiveUsers();
const activeUsers = await Users.getActiveLocalUserCount();

return API.v1.success({ maxActiveUsers: maxActiveUsers > 0 ? maxActiveUsers : null, activeUsers });
authRequired: true,
response: {
200: licensesMaxActiveUsersResponseSchema,
401: validateUnauthorizedErrorResponse,
},
},
async function action() {
const maxActiveUsers = License.getMaxActiveUsers();
const activeUsers = await Users.getActiveLocalUserCount();

return API.v1.success({ maxActiveUsers: maxActiveUsers > 0 ? maxActiveUsers : null, activeUsers });
},
);
Loading
Loading