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));