Files
Yajbir Singh f1b860b25c
Some checks failed
check / markdownlint (push) Has been cancelled
check / spellchecker (push) Has been cancelled
updated
2025-12-11 19:03:17 +05:30

480 lines
18 KiB
JavaScript

/*
* (c) Copyright Ascensio System SIA 2010-2024
*
* This program is a free software product. You can redistribute it and/or
* modify it under the terms of the GNU Affero General Public License (AGPL)
* version 3 as published by the Free Software Foundation. In accordance with
* Section 7(a) of the GNU AGPL its Section 15 shall be amended to the effect
* that Ascensio System SIA expressly excludes the warranty of non-infringement
* of any third-party rights.
*
* This program is distributed WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. For
* details, see the GNU AGPL at: http://www.gnu.org/licenses/agpl-3.0.html
*
* You can contact Ascensio System SIA at 20A-6 Ernesta Birznieka-Upish
* street, Riga, Latvia, EU, LV-1050.
*
* The interactive user interfaces in modified source and object code versions
* of the Program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU AGPL version 3.
*
* Pursuant to Section 7(b) of the License you must retain the original Product
* logo when distributing the program. Pursuant to Section 7(e) we decline to
* grant you any rights under trademark law for use of our trademarks.
*
* All the Product's GUI elements, including illustrations and icon sets, as
* well as technical writing content are licensed under the terms of the
* Creative Commons Attribution-ShareAlike 4.0 International. See the License
* terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
*
*/
'use strict';
const config = require('config');
const co = require('co');
const NodeCache = require('node-cache');
const constants = require('./../../Common/sources/constants');
const commonDefines = require('./../../Common/sources/commondefines');
const utils = require('./../../Common/sources/utils');
const {readFile, readdir, writeFile} = require('fs/promises');
const path = require('path');
const cfgTenantsBaseDomain = config.get('tenants.baseDomain');
const cfgTenantsBaseDir = config.get('tenants.baseDir');
const cfgTenantsFilenameSecret = config.get('tenants.filenameSecret');
const cfgTenantsFilenameLicense = config.get('tenants.filenameLicense');
const cfgTenantsFilenameConfig = config.get('tenants.filenameConfig');
const cfgTenantsDefaultTenant = config.get('tenants.defaultTenant');
const cfgTenantsCache = config.util.cloneDeep(config.get('tenants.cache'));
const cfgSecretInbox = config.get('services.CoAuthoring.secret.inbox');
const cfgSecretOutbox = config.get('services.CoAuthoring.secret.outbox');
const cfgSecretSession = config.get('services.CoAuthoring.secret.session');
let licenseInfo;
let licenseOriginal;
let licenseTuple; //to avoid array creating in getTenantLicense
const c_LM = constants.LICENSE_MODE;
const nodeCache = new NodeCache(cfgTenantsCache);
function getDefautTenant() {
return cfgTenantsDefaultTenant;
}
function getTenant(ctx, domain) {
let tenant = getDefautTenant();
if (domain) {
//remove port
domain = domain.replace(/:.*$/, '');
if (cfgTenantsBaseDomain && domain.endsWith('.' + cfgTenantsBaseDomain)) {
tenant = domain.substring(0, domain.length - cfgTenantsBaseDomain.length - 1);
} else if (cfgTenantsBaseDomain === domain) {
tenant = getDefautTenant();
} else {
tenant = domain;
}
}
return tenant;
}
async function getAllTenants(ctx) {
let dirList = [];
try {
if (isMultitenantMode(ctx)) {
const entitiesList = await readdir(cfgTenantsBaseDir, {withFileTypes: true});
dirList = entitiesList.filter(direntObj => direntObj.isDirectory()).map(directory => directory.name);
}
} catch (error) {
ctx.logger.error('getAllTenants error: ', error.stack);
}
return dirList;
}
function getTenantByConnection(ctx, conn) {
return isMultitenantMode(ctx) ? getTenant(ctx, utils.getDomainByConnection(ctx, conn)) : getDefautTenant();
}
function getTenantByRequest(ctx, req) {
return isMultitenantMode(ctx) ? getTenant(ctx, utils.getDomainByRequest(ctx, req)) : getDefautTenant();
}
function getTenantPathPrefix(ctx) {
return isMultitenantMode(ctx) ? utils.removeIllegalCharacters(ctx.tenant) + '/' : '';
}
async function getTenantConfig(ctx) {
let res = null;
if (isMultitenantMode(ctx) && !isDefaultTenant(ctx)) {
const tenantPath = utils.removeIllegalCharacters(ctx.tenant);
const configPath = path.join(cfgTenantsBaseDir, tenantPath, cfgTenantsFilenameConfig);
res = nodeCache.get(configPath);
if (res) {
ctx.logger.debug('getTenantConfig from cache');
} else {
try {
const cfgString = await readFile(configPath, {encoding: 'utf8'});
res = config.util.parseString(cfgString, path.extname(configPath).substring(1));
ctx.logger.debug('getTenantConfig from %s', configPath);
} catch (e) {
ctx.logger.debug('getTenantConfig error: %s', e.stack);
} finally {
ctx.cleanTenantConfigCache(ctx.tenant);
nodeCache.set(configPath, res);
}
}
}
return res;
}
/**
* Set tenant configuration for the current context
* @param {operationContext} ctx - Operation context
* @param {Object} config - Configuration data to save
* @returns {Object} Saved configuration object
*/
async function setTenantConfig(ctx, config) {
let newConfig = await getTenantConfig(ctx);
if (isMultitenantMode(ctx) && !isDefaultTenant(ctx)) {
newConfig = utils.deepMergeObjects(newConfig || {}, config);
const tenantPath = utils.removeIllegalCharacters(ctx.tenant);
const configPath = path.join(cfgTenantsBaseDir, tenantPath, cfgTenantsFilenameConfig);
await writeFile(configPath, JSON.stringify(newConfig, null, 2), 'utf8');
ctx.cleanTenantConfigCache(ctx.tenant);
nodeCache.set(configPath, newConfig);
}
return newConfig;
}
/**
* Replace tenant configuration completely (no merging)
* @param {operationContext} ctx - Operation context
* @param {Object} config - Configuration data to replace with
* @returns {Object} Replaced configuration object
*/
async function replaceTenantConfig(ctx, config) {
if (isMultitenantMode(ctx) && !isDefaultTenant(ctx)) {
const tenantPath = utils.removeIllegalCharacters(ctx.tenant);
const configPath = path.join(cfgTenantsBaseDir, tenantPath, cfgTenantsFilenameConfig);
await writeFile(configPath, JSON.stringify(config, null, 2), 'utf8');
ctx.cleanTenantConfigCache(ctx.tenant);
nodeCache.set(configPath, config);
return config;
}
return config;
}
function getTenantSecret(ctx, type) {
return co(function* () {
let cfgTenant;
//check config
const tenantConfig = yield getTenantConfig(ctx);
if (tenantConfig) {
switch (type) {
case commonDefines.c_oAscSecretType.Browser:
case commonDefines.c_oAscSecretType.Inbox:
cfgTenant = tenantConfig?.services?.CoAuthoring?.secret?.inbox;
break;
case commonDefines.c_oAscSecretType.Outbox:
cfgTenant = tenantConfig?.services?.CoAuthoring?.secret?.outbox;
break;
case commonDefines.c_oAscSecretType.Session:
cfgTenant = tenantConfig?.services?.CoAuthoring?.secret?.session;
break;
}
}
if (cfgTenant) {
return utils.getSecretByElem(cfgTenant);
}
let res = undefined;
//read secret file
if (isMultitenantMode(ctx) && !isDefaultTenant(ctx)) {
const tenantPath = utils.removeIllegalCharacters(ctx.tenant);
const secretPath = path.join(cfgTenantsBaseDir, tenantPath, cfgTenantsFilenameSecret);
res = nodeCache.get(secretPath);
if (res) {
ctx.logger.debug('getTenantSecret from cache');
} else {
try {
const secret = yield readFile(secretPath, {encoding: 'utf8'});
//trim whitespace plus line terminators from string (newline is common on Posix systems)
res = secret.trim();
if (res.length !== secret.length) {
ctx.logger.warn('getTenantSecret secret in %s contains a leading or trailing whitespace that has been trimmed', secretPath);
}
ctx.logger.debug('getTenantSecret from %s', secretPath);
} catch (e) {
res = undefined;
ctx.logger.warn('getTenantConfig error: %s', e.stack);
} finally {
nodeCache.set(secretPath, res);
}
}
} else {
switch (type) {
case commonDefines.c_oAscSecretType.Browser:
case commonDefines.c_oAscSecretType.Inbox:
res = utils.getSecretByElem(cfgSecretInbox);
break;
case commonDefines.c_oAscSecretType.Outbox:
res = utils.getSecretByElem(cfgSecretOutbox);
break;
case commonDefines.c_oAscSecretType.Session:
res = utils.getSecretByElem(cfgSecretSession);
break;
}
}
return res;
});
}
function setDefLicense(data, original) {
licenseInfo = data;
licenseOriginal = original;
licenseTuple = [licenseInfo, licenseOriginal];
}
//todo move to license file?
function fixTenantLicense(ctx, licenseInfo, licenseInfoTenant) {
const errors = [];
//bitwise
if (0 !== (licenseInfo.mode & c_LM.Limited) && 0 === (licenseInfoTenant.mode & c_LM.Limited)) {
licenseInfoTenant.mode |= c_LM.Limited;
errors.push('timelimited');
}
if (0 !== (licenseInfo.mode & c_LM.Trial) && 0 === (licenseInfoTenant.mode & c_LM.Trial)) {
licenseInfoTenant.mode |= c_LM.Trial;
errors.push('trial');
}
if (0 !== (licenseInfo.mode & c_LM.Developer) && 0 === (licenseInfoTenant.mode & c_LM.Developer)) {
licenseInfoTenant.mode |= c_LM.Developer;
errors.push('developer');
}
//can not turn on
const flags = ['branding', 'customization'];
flags.forEach(flag => {
if (!licenseInfo[flag] && licenseInfoTenant[flag]) {
licenseInfoTenant[flag] = licenseInfo[flag];
errors.push(flag);
}
});
if (!licenseInfo.advancedApi && licenseInfoTenant.advancedApi) {
licenseInfoTenant.advancedApi = licenseInfo.advancedApi;
errors.push('advanced_api');
}
//can not up limits
// if (licenseInfo.connections < licenseInfoTenant.connections) {
// licenseInfoTenant.connections = licenseInfo.connections;
// errors.push('connections');
// }
// if (licenseInfo.connectionsView < licenseInfoTenant.connectionsView) {
// licenseInfoTenant.connectionsView = licenseInfo.connectionsView;
// errors.push('connections_view');
// }
// if (licenseInfo.usersCount < licenseInfoTenant.usersCount) {
// licenseInfoTenant.usersCount = licenseInfo.usersCount;
// errors.push('users_count');
// }
// if (licenseInfo.usersViewCount < licenseInfoTenant.usersViewCount) {
// licenseInfoTenant.usersViewCount = licenseInfo.usersViewCount;
// errors.push('users_view_count');
// }
if (licenseInfo.endDate && licenseInfoTenant.endDate && licenseInfo.endDate < licenseInfoTenant.endDate) {
licenseInfoTenant.endDate = licenseInfo.endDate;
errors.push('end_date');
}
if (errors.length > 0) {
ctx.logger.warn('fixTenantLicense not allowed to improve these license fields: %s', errors.join(', '));
}
}
async function getTenantLicense(ctx) {
let res = licenseTuple;
if (isMultitenantMode(ctx) && !isDefaultTenant(ctx)) {
//todo alias is deprecated. remove one year after 8.3
if (licenseInfo.multitenancy || licenseInfo.alias) {
const tenantPath = utils.removeIllegalCharacters(ctx.tenant);
const licensePath = path.join(cfgTenantsBaseDir, tenantPath, cfgTenantsFilenameLicense);
let licenseTupleTenant = nodeCache.get(licensePath);
if (licenseTupleTenant) {
ctx.logger.debug('getTenantLicense from cache');
} else {
licenseTupleTenant = await readLicenseTenant(ctx, licensePath, licenseInfo);
fixTenantLicense(ctx, licenseInfo, licenseTupleTenant[0]);
nodeCache.set(licensePath, licenseTupleTenant);
ctx.logger.debug('getTenantLicense from %s', licensePath);
}
res = licenseTupleTenant;
} else {
res = [...res];
res[0] = {...res[0]};
res.type = constants.LICENSE_RESULT.Error;
ctx.logger.error('getTenantLicense error: missing "multitenancy" or "alias" field');
}
}
return res;
}
function getServerLicense(_ctx) {
return licenseInfo;
}
let hasBaseDir = !!cfgTenantsBaseDir;
function isMultitenantMode(_ctx) {
return hasBaseDir;
}
function setMultitenantMode(val) {
//for tests only!!
return (hasBaseDir = val);
}
function isDefaultTenant(ctx) {
return ctx.tenant === cfgTenantsDefaultTenant;
}
//todo move to license file?
async function readLicenseTenant(ctx, licenseFile, baseVerifiedLicense) {
const c_LR = constants.LICENSE_RESULT;
const c_LM = constants.LICENSE_MODE;
const res = {...baseVerifiedLicense};
let oLicense = null;
try {
const oFile = (await readFile(licenseFile)).toString();
res.hasLicense = true;
oLicense = JSON.parse(oFile);
//do not verify tenant signature. verify main lic signature.
//delete from object to keep signature secret
delete oLicense['signature'];
if (oLicense['start_date']) {
res.startDate = new Date(oLicense['start_date']);
}
const startDate = res.startDate;
if (oLicense['end_date']) {
res.endDate = new Date(oLicense['end_date']);
} else {
//spread copy do not copy date
res.endDate = new Date(res.endDate);
}
if (oLicense['customer_id']) {
res.customerId = oLicense['customer_id'];
}
if (oLicense['alias']) {
res.alias = oLicense['alias'];
}
if (oLicense['multitenancy']) {
res.multitenancy = oLicense['multitenancy'];
}
if (true === oLicense['timelimited']) {
res.mode |= c_LM.Limited;
}
if (Object.hasOwn(oLicense, 'trial')) {
res.mode |= true === oLicense['trial'] || 'true' === oLicense['trial'] || 'True' === oLicense['trial'] ? c_LM.Trial : c_LM.None; // Someone who likes to put json string instead of bool
}
if (true === oLicense['developer']) {
res.mode |= c_LM.Developer;
}
if (Object.hasOwn(oLicense, 'branding')) {
res.branding = true === oLicense['branding'] || 'true' === oLicense['branding'] || 'True' === oLicense['branding']; // Someone who likes to put json string instead of bool
}
if (Object.hasOwn(oLicense, 'customization')) {
res.customization = !!oLicense['customization'];
}
if (Object.hasOwn(oLicense, 'advanced_api')) {
res.advancedApi = !!oLicense['advanced_api'];
}
if (Object.hasOwn(oLicense, 'connections')) {
res.connections = oLicense['connections'] >> 0;
}
if (Object.hasOwn(oLicense, 'connections_view')) {
res.connectionsView = oLicense['connections_view'] >> 0;
}
if (Object.hasOwn(oLicense, 'users_count')) {
res.usersCount = oLicense['users_count'] >> 0;
}
if (Object.hasOwn(oLicense, 'users_view_count')) {
res.usersViewCount = oLicense['users_view_count'] >> 0;
}
if (Object.hasOwn(oLicense, 'users_expire')) {
res.usersExpire = Math.max(constants.LICENSE_EXPIRE_USERS_ONE_DAY, (oLicense['users_expire'] >> 0) * constants.LICENSE_EXPIRE_USERS_ONE_DAY);
}
// Read grace_days setting from license file if available
if (Object.hasOwn(oLicense, 'grace_days')) {
res.graceDays = Math.max(0, oLicense['grace_days'] >> 0);
}
const timeLimited = 0 !== (res.mode & c_LM.Limited);
const checkDate = res.mode & c_LM.Trial || timeLimited ? new Date() : licenseInfo.buildDate;
//Calendar check of start_date allows to issue a license for old versions
const checkStartDate = new Date();
if (startDate <= checkStartDate && checkDate <= res.endDate) {
res.type = c_LR.Success;
} else if (startDate > checkStartDate) {
res.type = c_LR.NotBefore;
ctx.logger.warn('License: License not active before start_date:%s.', startDate.toISOString());
} else if (timeLimited) {
// Grace period after end license = limited mode with limited connections
if (res.endDate.setUTCDate(res.endDate.getUTCDate() + res.graceDays) >= checkDate) {
res.type = c_LR.SuccessLimit;
res.connections = Math.min(res.connections, constants.LICENSE_CONNECTIONS);
res.connectionsView = Math.min(res.connectionsView, constants.LICENSE_CONNECTIONS);
res.usersCount = Math.min(res.usersCount, constants.LICENSE_USERS);
res.usersViewCount = Math.min(res.usersViewCount, constants.LICENSE_USERS);
const errStr = res.usersCount ? `${res.usersCount} unique users` : `${res.connections} concurrent connections`;
ctx.logger.error(
`License: License needs to be renewed.\nYour users have only ${errStr} ` +
`available for document editing for the next ${res.graceDays} days.\nPlease renew the ` +
'license to restore the full access'
);
} else {
res.type = c_LR.ExpiredLimited;
}
} else if (0 !== (res.mode & c_LM.Trial)) {
res.type = c_LR.ExpiredTrial;
} else {
res.type = c_LR.Expired;
}
} catch (e) {
ctx.logger.warn(e);
res.count = 1;
res.connections = 0;
res.connectionsView = 0;
res.usersCount = 0;
res.usersViewCount = 0;
res.type = c_LR.Error;
}
if (res.type === c_LR.Expired || res.type === c_LR.ExpiredLimited || res.type === c_LR.ExpiredTrial) {
res.count = 1;
let errorMessage;
if (res.type === c_LR.Expired) {
errorMessage =
'Your access to updates and support has expired.\n' +
'Your license key can not be applied to new versions.\n' +
'Please extend the license to get updates and support.';
} else if (res.type === c_LR.ExpiredLimited) {
errorMessage = 'License expired.\nYour users can not edit or view document anymore.\n' + 'Please renew the license.';
} else {
errorMessage = 'License Expired!!!';
}
ctx.logger.warn('License: ' + errorMessage);
}
return [res, oLicense];
}
exports.getAllTenants = getAllTenants;
exports.getDefautTenant = getDefautTenant;
exports.getTenantByConnection = getTenantByConnection;
exports.getTenantByRequest = getTenantByRequest;
exports.getTenantPathPrefix = getTenantPathPrefix;
exports.getTenantConfig = getTenantConfig;
exports.getTenantSecret = getTenantSecret;
exports.getTenantLicense = getTenantLicense;
exports.getServerLicense = getServerLicense;
exports.setDefLicense = setDefLicense;
exports.setTenantConfig = setTenantConfig;
exports.replaceTenantConfig = replaceTenantConfig;
exports.isMultitenantMode = isMultitenantMode;
exports.setMultitenantMode = setMultitenantMode;
exports.isDefaultTenant = isDefaultTenant;