add secondary domains

note that for updates to work, we keep the secondary domain optional,
even though they are really not.

part of #809
This commit is contained in:
Girish Ramakrishnan
2022-01-14 22:40:51 -08:00
parent d18977ccad
commit b34f66b115
9 changed files with 165 additions and 40 deletions

View File

@@ -127,6 +127,7 @@ exports = module.exports = {
// subdomain table types
SUBDOMAIN_TYPE_PRIMARY: 'primary',
SUBDOMAIN_TYPE_SECONDARY: 'secondary',
SUBDOMAIN_TYPE_REDIRECT: 'redirect',
SUBDOMAIN_TYPE_ALIAS: 'alias',
@@ -186,7 +187,6 @@ const APPS_FIELDS_PREFIXED = [ 'apps.id', 'apps.appStoreId', 'apps.installationS
// const PORT_BINDINGS_FIELDS = [ 'hostPort', 'type', 'environmentVariable', 'appId' ].join(',');
// validate the port bindings
function validatePortBindings(portBindings, manifest) {
assert.strictEqual(typeof portBindings, 'object');
assert.strictEqual(typeof manifest, 'object');
@@ -269,6 +269,29 @@ function translatePortBindings(portBindings, manifest) {
return result;
}
function validateSecondaryDomains(secondaryDomains, manifest) {
assert.strictEqual(typeof secondaryDomains, 'object');
assert.strictEqual(typeof manifest, 'object');
const httpPorts = manifest.httpPorts || {};
for (const envName in secondaryDomains) {
if (!(envName in httpPorts)) return new BoxError(BoxError.BAD_FIELD, `Invalid secondaryDomain in ${envName}`);
}
return null;
}
function translateSecondaryDomains(secondaryDomains) {
assert.strictEqual(typeof secondaryDomains, 'object');
const result = [];
for (const envName in secondaryDomains) {
result.push([ { domain: secondaryDomains[envName].domain, subdomain: secondaryDomains[envName].subdomain, environmentVariable: envName }]);
}
return result;
}
function parseCrontab(crontab) {
assert(crontab === null || typeof crontab === 'string');
@@ -499,14 +522,14 @@ function removeInternalFields(app) {
'location', 'domain', 'fqdn', 'crontab',
'accessRestriction', 'manifest', 'portBindings', 'iconUrl', 'memoryLimit', 'cpuShares', 'operators',
'sso', 'debugMode', 'reverseProxyConfig', 'enableBackup', 'creationTime', 'updateTime', 'ts', 'tags',
'label', 'redirectDomains', 'aliasDomains', 'env', 'enableAutomaticUpdate', 'dataDir', 'mounts',
'label', 'secondaryDomains', 'redirectDomains', 'aliasDomains', 'env', 'enableAutomaticUpdate', 'dataDir', 'mounts',
'enableMailbox', 'mailboxName', 'mailboxDomain', 'enableInbox', 'inboxName', 'inboxDomain');
}
// non-admins can only see these
function removeRestrictedFields(app) {
return _.pick(app,
'id', 'appStoreId', 'installationState', 'error', 'runState', 'health', 'taskId', 'accessRestriction', 'redirectDomains', 'aliasDomains', 'sso',
'id', 'appStoreId', 'installationState', 'error', 'runState', 'health', 'taskId', 'accessRestriction', 'secondaryDomains', 'redirectDomains', 'aliasDomains', 'sso',
'location', 'domain', 'fqdn', 'manifest', 'portBindings', 'iconUrl', 'creationTime', 'ts', 'tags', 'label', 'enableBackup');
}
@@ -602,17 +625,25 @@ function postProcess(result) {
result.servicesConfig = safe.JSON.parse(result.servicesConfigJson) || {};
delete result.servicesConfigJson;
let subdomains = JSON.parse(result.subdomains), domains = JSON.parse(result.domains), subdomainTypes = JSON.parse(result.subdomainTypes);
const subdomains = JSON.parse(result.subdomains),
domains = JSON.parse(result.domains),
subdomainTypes = JSON.parse(result.subdomainTypes),
subdomainEnvironmentVariables = JSON.parse(result.subdomainEnvironmentVariables);
delete result.subdomains;
delete result.domains;
delete result.subdomainTypes;
delete result.subdomainEnvironmentVariables;
result.secondaryDomains = [];
result.redirectDomains = [];
result.aliasDomains = [];
for (let i = 0; i < subdomainTypes.length; i++) {
if (subdomainTypes[i] === exports.SUBDOMAIN_TYPE_PRIMARY) {
result.location = subdomains[i];
result.domain = domains[i];
} else if (subdomainTypes[i] === exports.SUBDOMAIN_TYPE_SECONDARY) {
result.secondaryDomains.push({ domain: domains[i], subdomain: subdomains[i], environmentVariable: subdomainEnvironmentVariables[i] });
} else if (subdomainTypes[i] === exports.SUBDOMAIN_TYPE_REDIRECT) {
result.redirectDomains.push({ domain: domains[i], subdomain: subdomains[i] });
} else if (subdomainTypes[i] === exports.SUBDOMAIN_TYPE_ALIAS) {
@@ -652,6 +683,7 @@ function attachProperties(app, domainObjectMap) {
app.portBindings = result;
app.iconUrl = app.hasIcon || app.hasAppStoreIcon ? `/api/v1/apps/${app.id}/icon` : null;
app.fqdn = dns.fqdn(app.location, domainObjectMap[app.domain]);
app.secondaryDomains.forEach(function (ad) { ad.fqdn = dns.fqdn(ad.subdomain, domainObjectMap[ad.domain]); });
app.redirectDomains.forEach(function (ad) { ad.fqdn = dns.fqdn(ad.subdomain, domainObjectMap[ad.domain]); });
app.aliasDomains.forEach(function (ad) { ad.fqdn = dns.fqdn(ad.subdomain, domainObjectMap[ad.domain]); });
}
@@ -755,6 +787,15 @@ async function add(id, appStoreId, manifest, location, domain, portBindings, dat
});
});
if (data.secondaryDomains) {
data.secondaryDomains.forEach(function (d) {
queries.push({
query: 'INSERT INTO subdomains (appId, domain, subdomain, type, environmentVariable) VALUES (?, ?, ?, ?, ?)',
args: [ id, d.domain, d.subdomain, exports.SUBDOMAIN_TYPE_SECONDARY, d.environmentVariable ]
});
});
}
if (data.redirectDomains) {
data.redirectDomains.forEach(function (d) {
queries.push({
@@ -793,6 +834,7 @@ async function updateWithConstraints(id, app, constraints) {
assert.strictEqual(typeof constraints, 'string');
assert(!('portBindings' in app) || typeof app.portBindings === 'object');
assert(!('accessRestriction' in app) || typeof app.accessRestriction === 'object' || app.accessRestriction === '');
assert(!('secondaryDomains' in app) || Array.isArray(app.secondaryDomains));
assert(!('redirectDomains' in app) || Array.isArray(app.redirectDomains));
assert(!('aliasDomains' in app) || Array.isArray(app.aliasDomains));
assert(!('tags' in app) || Array.isArray(app.tags));
@@ -825,6 +867,12 @@ async function updateWithConstraints(id, app, constraints) {
queries.push({ query: 'DELETE FROM subdomains WHERE appId = ?', args: [ id ]}); // all locations of an app must be updated together
queries.push({ query: 'INSERT INTO subdomains (appId, domain, subdomain, type) VALUES (?, ?, ?, ?)', args: [ id, app.domain, app.location, exports.SUBDOMAIN_TYPE_PRIMARY ]});
if ('secondaryDomains' in app) {
app.secondaryDomains.forEach(function (d) {
queries.push({ query: 'INSERT INTO subdomains (appId, domain, subdomain, type, environmentVariable) VALUES (?, ?, ?, ?, ?)', args: [ id, d.domain, d.subdomain, exports.SUBDOMAIN_TYPE_SECONDARY, d.environmentVariable ]});
});
}
if ('redirectDomains' in app) {
app.redirectDomains.forEach(function (d) {
queries.push({ query: 'INSERT INTO subdomains (appId, domain, subdomain, type) VALUES (?, ?, ?, ?)', args: [ id, d.domain, d.subdomain, exports.SUBDOMAIN_TYPE_REDIRECT ]});
@@ -850,7 +898,7 @@ async function updateWithConstraints(id, app, constraints) {
if (p === 'manifest' || p === 'tags' || p === 'accessRestriction' || p === 'debugMode' || p === 'error' || p === 'reverseProxyConfig' || p === 'servicesConfig' || p === 'operators') {
fields.push(`${p}Json = ?`);
values.push(JSON.stringify(app[p]));
} else if (p !== 'portBindings' && p !== 'location' && p !== 'domain' && p !== 'redirectDomains' && p !== 'aliasDomains' && p !== 'env' && p !== 'mounts') {
} else if (p !== 'portBindings' && p !== 'location' && p !== 'domain' && p !== 'secondaryDomains' && p !== 'redirectDomains' && p !== 'aliasDomains' && p !== 'env' && p !== 'mounts') {
fields.push(p + ' = ?');
values.push(app[p]);
}
@@ -940,9 +988,9 @@ async function getDomainObjectMap() {
// each query simply join apps table with another table by id. we then join the full result together
const PB_QUERY = 'SELECT id, GROUP_CONCAT(CAST(appPortBindings.hostPort AS CHAR(6))) AS hostPorts, GROUP_CONCAT(appPortBindings.environmentVariable) AS environmentVariables, GROUP_CONCAT(appPortBindings.type) AS portTypes FROM apps LEFT JOIN appPortBindings ON apps.id = appPortBindings.appId GROUP BY apps.id';
const ENV_QUERY = 'SELECT id, JSON_ARRAYAGG(appEnvVars.name) AS envNames, JSON_ARRAYAGG(appEnvVars.value) AS envValues FROM apps LEFT JOIN appEnvVars ON apps.id = appEnvVars.appId GROUP BY apps.id';
const SUBDOMAIN_QUERY = 'SELECT id, JSON_ARRAYAGG(subdomains.subdomain) AS subdomains, JSON_ARRAYAGG(subdomains.domain) AS domains, JSON_ARRAYAGG(subdomains.type) AS subdomainTypes FROM apps LEFT JOIN subdomains ON apps.id = subdomains.appId GROUP BY apps.id';
const SUBDOMAIN_QUERY = 'SELECT id, JSON_ARRAYAGG(subdomains.subdomain) AS subdomains, JSON_ARRAYAGG(subdomains.domain) AS domains, JSON_ARRAYAGG(subdomains.type) AS subdomainTypes, JSON_ARRAYAGG(subdomains.environmentVariable) AS subdomainEnvironmentVariables FROM apps LEFT JOIN subdomains ON apps.id = subdomains.appId GROUP BY apps.id';
const MOUNTS_QUERY = 'SELECT id, JSON_ARRAYAGG(appMounts.volumeId) AS volumeIds, JSON_ARRAYAGG(appMounts.readOnly) AS volumeReadOnlys FROM apps LEFT JOIN appMounts ON apps.id = appMounts.appId GROUP BY apps.id';
const APPS_QUERY = `SELECT ${APPS_FIELDS_PREFIXED}, hostPorts, environmentVariables, portTypes, envNames, envValues, subdomains, domains, subdomainTypes, volumeIds, volumeReadOnlys FROM apps`
const APPS_QUERY = `SELECT ${APPS_FIELDS_PREFIXED}, hostPorts, environmentVariables, portTypes, envNames, envValues, subdomains, domains, subdomainTypes, subdomainEnvironmentVariables, volumeIds, volumeReadOnlys FROM apps`
+ ` LEFT JOIN (${PB_QUERY}) AS q1 on q1.id = apps.id`
+ ` LEFT JOIN (${ENV_QUERY}) AS q2 on q2.id = apps.id`
+ ` LEFT JOIN (${SUBDOMAIN_QUERY}) AS q3 on q3.id = apps.id`
@@ -1222,6 +1270,14 @@ async function install(data, auditSource) {
error = validateTags(tags);
if (error) throw error;
let secondaryDomains = [];
if ('secondaryDomains' in data) {
error = validateSecondaryDomains(data.secondaryDomains, manifest);
if (error) throw error;
secondaryDomains = translateSecondaryDomains(data.secondaryDomains);
}
let sso = 'sso' in data ? data.sso : null;
if ('sso' in data && !('optionalSso' in manifest)) throw new BoxError(BoxError.BAD_FIELD, 'sso can only be specified for apps with optionalSso');
// if sso was unspecified, enable it by default if possible
@@ -1243,9 +1299,10 @@ async function install(data, auditSource) {
icon = Buffer.from(icon, 'base64');
}
const locations = [{ subdomain: location, domain, type: 'primary' }]
.concat(redirectDomains.map(ad => _.extend(ad, { type: 'redirect' })))
.concat(aliasDomains.map(ad => _.extend(ad, { type: 'alias' })));
const locations = [{ subdomain: location, domain, type: exports.SUBDOMAIN_TYPE_PRIMARY }]
.concat(secondaryDomains.map(ad => _.extend(ad, { type: exports.SUBDOMAIN_TYPE_SECONDARY })))
.concat(redirectDomains.map(ad => _.extend(ad, { type: exports.SUBDOMAIN_TYPE_REDIRECT })))
.concat(aliasDomains.map(ad => _.extend(ad, { type: exports.SUBDOMAIN_TYPE_ALIAS })));
const domainObjectMap = await validateLocations(locations);
@@ -1263,6 +1320,7 @@ async function install(data, auditSource) {
mailboxDomain,
enableBackup,
enableAutomaticUpdate,
secondaryDomains,
redirectDomains,
aliasDomains,
env,
@@ -1290,6 +1348,7 @@ async function install(data, auditSource) {
const newApp = _.extend({}, _.omit(app, 'icon'), { appStoreId, manifest, location, domain, portBindings });
newApp.fqdn = dns.fqdn(newApp.location, domainObjectMap[newApp.domain]);
newApp.secondaryDomains.forEach(function (ad) { ad.fqdn = dns.fqdn(ad.subdomain, domainObjectMap[ad.domain]); });
newApp.redirectDomains.forEach(function (ad) { ad.fqdn = dns.fqdn(ad.subdomain, domainObjectMap[ad.domain]); });
newApp.aliasDomains.forEach(function (ad) { ad.fqdn = dns.fqdn(ad.subdomain, domainObjectMap[ad.domain]); });
@@ -1643,6 +1702,7 @@ async function setLocation(app, data, auditSource) {
domain: data.domain.toLowerCase(),
// these are intentionally reset, if not set
portBindings: null,
secondaryDomains: [],
redirectDomains: [],
aliasDomains: []
};
@@ -1660,6 +1720,13 @@ async function setLocation(app, data, auditSource) {
values.mailboxDomain = values.domain;
}
if ('secondaryDomains' in data) {
error = validateSecondaryDomains(data.secondaryDomains, app.manifest);
if (error) throw error;
values.secondaryDomains = translateSecondaryDomains(data.secondaryDomains);
}
if ('redirectDomains' in data) {
values.redirectDomains = data.redirectDomains;
}
@@ -1668,15 +1735,16 @@ async function setLocation(app, data, auditSource) {
values.aliasDomains = data.aliasDomains;
}
const locations = [{ subdomain: values.location, domain: values.domain, type: 'primary' }]
.concat(values.redirectDomains.map(ad => _.extend(ad, { type: 'redirect' })))
.concat(values.aliasDomains.map(ad => _.extend(ad, { type: 'alias' })));
const locations = [{ subdomain: values.location, domain: values.domain, type: exports.SUBDOMAIN_TYPE_PRIMARY }]
.concat(values.secondaryDomains.map(ad => _.extend(ad, { type: exports.SUBDOMAIN_TYPE_SECONDARY })))
.concat(values.redirectDomains.map(ad => _.extend(ad, { type: exports.SUBDOMAIN_TYPE_REDIRECT })))
.concat(values.aliasDomains.map(ad => _.extend(ad, { type: exports.SUBDOMAIN_TYPE_ALIAS })));
const domainObjectMap = await validateLocations(locations);
const task = {
args: {
oldConfig: _.pick(app, 'location', 'domain', 'fqdn', 'redirectDomains', 'aliasDomains', 'portBindings'),
oldConfig: _.pick(app, 'location', 'domain', 'fqdn', 'secondaryDomains', 'redirectDomains', 'aliasDomains', 'portBindings'),
skipDnsSetup: !!data.skipDnsSetup,
overwriteDns: !!data.overwriteDns
},
@@ -1687,6 +1755,7 @@ async function setLocation(app, data, auditSource) {
if (taskError) throw taskError;
values.fqdn = dns.fqdn(values.location, domainObjectMap[values.domain]);
values.secondaryDomains.forEach(function (ad) { ad.fqdn = dns.fqdn(ad.subdomain, domainObjectMap[ad.domain]); });
values.redirectDomains.forEach(function (ad) { ad.fqdn = dns.fqdn(ad.subdomain, domainObjectMap[ad.domain]); });
values.aliasDomains.forEach(function (ad) { ad.fqdn = dns.fqdn(ad.subdomain, domainObjectMap[ad.domain]); });
@@ -2126,6 +2195,7 @@ async function clone(app, data, user, auditSource) {
enableBackup: app.enableBackup,
reverseProxyConfig: app.reverseProxyConfig,
env: app.env,
secondaryDomains: [],
redirectDomains: [],
aliasDomains: [],
servicesConfig: app.servicesConfig,
@@ -2152,6 +2222,7 @@ async function clone(app, data, user, auditSource) {
const newApp = _.extend({}, _.omit(obj, 'icon'), { appStoreId, manifest, location, domain, portBindings });
newApp.fqdn = dns.fqdn(newApp.location, domainObjectMap[newApp.domain]);
newApp.secondaryDomains.forEach(function (ad) { ad.fqdn = dns.fqdn(ad.subdomain, domainObjectMap[ad.domain]); });
newApp.redirectDomains.forEach(function (ad) { ad.fqdn = dns.fqdn(ad.subdomain, domainObjectMap[ad.domain]); });
newApp.aliasDomains.forEach(function (ad) { ad.fqdn = dns.fqdn(ad.subdomain, domainObjectMap[ad.domain]); });