diff --git a/dashboard/public/views/app.html b/dashboard/public/views/app.html
index 8c0d1b9c3..66d7f6d0c 100644
--- a/dashboard/public/views/app.html
+++ b/dashboard/public/views/app.html
@@ -1739,7 +1739,7 @@
-
diff --git a/src/apps.js b/src/apps.js
index 278b50ee9..d6f194632 100644
--- a/src/apps.js
+++ b/src/apps.js
@@ -859,8 +859,8 @@ async function add(id, appStoreId, manifest, subdomain, domain, portBindings, da
assert(data && typeof data === 'object');
const manifestJson = JSON.stringify(manifest),
- accessRestriction = data.accessRestriction || null,
- accessRestrictionJson = JSON.stringify(accessRestriction),
+ accessRestrictionJson = data.accessRestriction ? JSON.stringify(data.accessRestriction) : null,
+ operatorsJson = data.operators ? JSON.stringify(data.operators) : null,
memoryLimit = data.memoryLimit || 0,
cpuQuota = data.cpuQuota || 100,
installationState = data.installationState,
@@ -881,20 +881,26 @@ async function add(id, appStoreId, manifest, subdomain, domain, portBindings, da
upstreamUri = data.upstreamUri || '',
enableTurn = 'enableTurn' in data ? data.enableTurn : true,
enableRedis = 'enableRedis' in data ? data.enableRedis : true,
- icon = data.icon || null;
+ icon = data.icon || null,
+ notes = data.notes || null,
+ crontab = data.crontab || null,
+ enableBackup = 'enableBackup' in data ? data.enableBackup : true,
+ enableAutomaticUpdate = 'enableAutomaticUpdate' in data ? data.enableAutomaticUpdate : true;
await checkForPortBindingConflict(portBindings, { appId: null });
const queries = [];
queries.push({
- query: 'INSERT INTO apps (id, appStoreId, manifestJson, installationState, runState, accessRestrictionJson, memoryLimit, cpuQuota, '
+ query: 'INSERT INTO apps (id, appStoreId, manifestJson, installationState, runState, accessRestrictionJson, operatorsJson, memoryLimit, cpuQuota, '
+ 'sso, debugModeJson, mailboxName, mailboxDomain, label, tagsJson, reverseProxyConfigJson, checklistJson, servicesConfigJson, icon, '
- + 'enableMailbox, mailboxDisplayName, upstreamUri, enableTurn, enableRedis, devicesJson) '
- + ' VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
- args: [ id, appStoreId, manifestJson, installationState, runState, accessRestrictionJson, memoryLimit, cpuQuota,
+ + 'enableMailbox, mailboxDisplayName, upstreamUri, enableTurn, enableRedis, devicesJson, notes, crontab, enableBackup, enableAutomaticUpdate) '
+ + ' VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
+ args: [ id, appStoreId, manifestJson, installationState, runState, accessRestrictionJson, operatorsJson, memoryLimit, cpuQuota,
sso, debugModeJson, mailboxName, mailboxDomain, label, tagsJson, reverseProxyConfigJson, checklistJson, servicesConfigJson, icon,
- enableMailbox, mailboxDisplayName, upstreamUri, enableTurn, enableRedis, devicesJson ]
+ enableMailbox, mailboxDisplayName, upstreamUri, enableTurn, enableRedis, devicesJson, notes, crontab,
+ enableBackup, enableAutomaticUpdate
+ ]
});
queries.push({
@@ -1339,7 +1345,9 @@ async function install(data, auditSource) {
const subdomain = data.subdomain.toLowerCase(),
domain = data.domain.toLowerCase(),
accessRestriction = data.accessRestriction || null,
+ operators = data.operators || null,
memoryLimit = data.memoryLimit || 0,
+ cpuQuota = data.cpuQuota || 100,
debugMode = data.debugMode || null,
enableBackup = 'enableBackup' in data ? data.enableBackup : true,
enableAutomaticUpdate = 'enableAutomaticUpdate' in data ? data.enableAutomaticUpdate : true,
@@ -1355,7 +1363,9 @@ async function install(data, auditSource) {
enableRedis = 'enableRedis' in data ? data.enableRedis : true,
appStoreId = data.appStoreId,
upstreamUri = data.upstreamUri || '',
- manifest = data.manifest;
+ manifest = data.manifest,
+ notes = data.notes || null,
+ crontab = data.crontab || null;
let error = manifestFormat.parse(manifest);
if (error) throw new BoxError(BoxError.BAD_FIELD, `Manifest error: ${error.message}`);
@@ -1370,6 +1380,9 @@ async function install(data, auditSource) {
error = validateAccessRestriction(accessRestriction);
if (error) throw error;
+ error = validateAccessRestriction(operators); // not a typo. same structure for operators and accessRestriction
+ if (error) throw error;
+
error = validateMemoryLimit(manifest, memoryLimit);
if (error) throw error;
@@ -1379,6 +1392,11 @@ async function install(data, auditSource) {
error = validateLabel(label);
if (error) throw error;
+ error = validateCpuQuota(cpuQuota);
+ if (error) throw error;
+
+ parseCrontab(crontab);
+
if ('upstreamUri' in data) error = validateUpstreamUri(upstreamUri);
if (error) throw error;
@@ -1400,6 +1418,17 @@ async function install(data, auditSource) {
error = validateEnv(env);
if (error) throw error;
+ let reverseProxyConfig = null;
+ if ('reverseProxyConfig' in data) {
+ reverseProxyConfig = Object.assign({ robotsTxt: null, csp: null, hstsPreload: false }, data.reverseProxyConfig); // ensure fields
+
+ let error = validateCsp(reverseProxyConfig.csp);
+ if (error) throw error;
+
+ error = validateRobotsTxt(reverseProxyConfig.robotsTxt);
+ if (error) throw error;
+ }
+
if (constants.DEMO && constants.DEMO_BLOCKED_APPS.includes(appStoreId)) throw new BoxError(BoxError.BAD_FIELD, 'This app is blocked in the demo');
// sendmail is enabled by default
@@ -1424,8 +1453,13 @@ async function install(data, auditSource) {
if (constants.DEMO && (await getCount() >= constants.DEMO_APP_LIMIT)) throw new BoxError(BoxError.BAD_STATE, 'Too many installed apps, please uninstall a few and try again');
let restoreConfig = null;
- if ('backupId' in data) { // install from archive
- const backup = await backups.get(data.backupId);
+
+ if ('archive' in data) { // install from archive. assume these are already validated
+ assert(data.archive.iconBuffer === null || Buffer.isBuffer(data.archive.iconBuffer));
+ assert.strictEqual(typeof data.archive.backupId, 'string');
+
+ icon = data.archive.iconBuffer; // install from archive
+ const backup = await backups.get(data.archive.backupId);
if (!backup) throw new BoxError(BoxError.BAD_FIELD, 'Backup not found in archive');
restoreConfig = { remotePath: backup.remotePath, backupFormat: backup.format };
}
@@ -1435,7 +1469,9 @@ async function install(data, auditSource) {
const app = {
accessRestriction,
+ operators,
memoryLimit,
+ cpuQuota,
sso,
debugMode,
mailboxName,
@@ -1454,6 +1490,9 @@ async function install(data, auditSource) {
upstreamUri,
enableTurn,
enableRedis,
+ notes,
+ crontab,
+ reverseProxyConfig,
runState: exports.RSTATE_RUNNING,
installationState: exports.ISTATE_PENDING_INSTALL
};
@@ -2492,8 +2531,10 @@ async function unarchive(archive, data, auditSource) {
mailboxDomain: data.domain, // archive's mailboxDomain may not exist
// from the archive
- icon: archive.icon,
- backupId: archive.backupId,
+ archive: {
+ iconBuffer: (await archives.getIcons(archive.id))?.icon,
+ backupId: archive.backupId
+ }
});
return await install(newAppData, auditSource);
diff --git a/src/archives.js b/src/archives.js
index 3575767e7..098d5484f 100644
--- a/src/archives.js
+++ b/src/archives.js
@@ -2,6 +2,7 @@
exports = module.exports = {
get,
+ getIcons,
getIcon,
add,
list,
@@ -14,8 +15,7 @@ const assert = require('assert'),
database = require('./database.js'),
eventlog = require('./eventlog.js'),
safe = require('safetydance'),
- uuid = require('uuid'),
- _ = require('underscore');
+ uuid = require('uuid');
const ARCHIVE_FIELDS = [ 'id', 'backupId', 'creationTime', 'appConfigJson', '(icon IS NOT NULL) AS hasIcon', '(appStoreIcon IS NOT NULL) AS hasAppStoreIcon' ];
diff --git a/src/routes/apps.js b/src/routes/apps.js
index 6365489cc..77c4ba2d6 100644
--- a/src/routes/apps.js
+++ b/src/routes/apps.js
@@ -185,6 +185,9 @@ async function install(req, res, next) {
if ('enableTurn' in data && typeof data.enableTurn !== 'boolean') return next(new HttpError(400, 'enableTurn must be boolean'));
+ if ('cpuQuota' in data && data.cpuQuota !== 'number') return next(new HttpError(400, 'cpuQuota is not a number'));
+ if ('operators' in req.body && typeof req.body.operators !== 'object') return next(new HttpError(400, 'operators must be an object'));
+
let [error, result] = await safe(appstore.downloadManifest(data.appStoreId, data.manifest));
if (error) return next(BoxError.toHttpError(error));
@@ -195,6 +198,8 @@ async function install(req, res, next) {
data.appStoreId = result.appStoreId;
data.manifest = result.manifest;
+ delete data.archive; // internally used for archive code path
+
[error, result] = await safe(apps.install(data, AuditSource.fromRequest(req)));
if (error) return next(BoxError.toHttpError(error));