480 lines
18 KiB
JavaScript
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;
|