diff --git a/src/apps.js b/src/apps.js index 17bc336a0..9c0e59e9f 100644 --- a/src/apps.js +++ b/src/apps.js @@ -49,6 +49,7 @@ exports = module.exports = { exec: exec, checkManifestConstraints: checkManifestConstraints, + downloadManifest: downloadManifest, canAutoupdateApp: canAutoupdateApp, autoupdateApps: autoupdateApps, @@ -694,6 +695,8 @@ function install(data, auditSource, callback) { assert.strictEqual(typeof auditSource, 'object'); assert.strictEqual(typeof callback, 'function'); + assert(data.appStoreId && data.manifest); // manifest is already downloaded + var location = data.location.toLowerCase(), domain = data.domain.toLowerCase(), portBindings = data.portBindings || null, @@ -710,113 +713,109 @@ function install(data, auditSource, callback) { env = data.env || {}, label = data.label || null, tags = data.tags || [], - overwriteDns = 'overwriteDns' in data ? data.overwriteDns : false; + overwriteDns = 'overwriteDns' in data ? data.overwriteDns : false, + appStoreId = data.appStoreId, + manifest = data.manifest; - assert(data.appStoreId || data.manifest); // atleast one of them is required + let error = manifestFormat.parse(manifest); + if (error) return callback(new BoxError(BoxError.BAD_FIELD, 'Manifest error: ' + error.message)); - downloadManifest(data.appStoreId, data.manifest, function (error, appStoreId, manifest) { + error = checkManifestConstraints(manifest); + if (error) return callback(error); + + error = validatePortBindings(portBindings, manifest); + if (error) return callback(error); + + error = validateAccessRestriction(accessRestriction); + if (error) return callback(error); + + error = validateMemoryLimit(manifest, memoryLimit); + if (error) return callback(error); + + error = validateDebugMode(debugMode); + if (error) return callback(error); + + error = validateLabel(label); + if (error) return callback(error); + + error = validateTags(tags); + if (error) return callback(error); + + if ('sso' in data && !('optionalSso' in manifest)) return callback(new BoxError(BoxError.BAD_FIELD, 'sso can only be specified for apps with optionalSso')); + // if sso was unspecified, enable it by default if possible + if (sso === null) sso = !!manifest.addons['ldap'] || !!manifest.addons['oauth']; + + error = validateEnv(env); + if (error) return callback(error); + + const mailboxName = mailboxNameForLocation(location, manifest); + const appId = uuid.v4(); + + if (icon) { + if (!validator.isBase64(icon)) return callback(new BoxError(BoxError.BAD_FIELD, 'icon is not base64', { field: 'icon' })); + + if (!safe.fs.writeFileSync(path.join(paths.APP_ICONS_DIR, appId + '.png'), Buffer.from(icon, 'base64'))) { + return callback(new BoxError(BoxError.FS_ERROR, 'Error saving icon:' + safe.error.message)); + } + } + + const locations = [{subdomain: location, domain}].concat(alternateDomains); + validateLocations(locations, function (error, domainObjectMap) { if (error) return callback(error); - error = manifestFormat.parse(manifest); - if (error) return callback(new BoxError(BoxError.BAD_FIELD, 'Manifest error: ' + error.message)); - - error = checkManifestConstraints(manifest); - if (error) return callback(error); - - error = validatePortBindings(portBindings, manifest); - if (error) return callback(error); - - error = validateAccessRestriction(accessRestriction); - if (error) return callback(error); - - error = validateMemoryLimit(manifest, memoryLimit); - if (error) return callback(error); - - error = validateDebugMode(debugMode); - if (error) return callback(error); - - error = validateLabel(label); - if (error) return callback(error); - - error = validateTags(tags); - if (error) return callback(error); - - if ('sso' in data && !('optionalSso' in manifest)) return callback(new BoxError(BoxError.BAD_FIELD, 'sso can only be specified for apps with optionalSso')); - // if sso was unspecified, enable it by default if possible - if (sso === null) sso = !!manifest.addons['ldap'] || !!manifest.addons['oauth']; - - error = validateEnv(env); - if (error) return callback(error); - - const mailboxName = mailboxNameForLocation(location, manifest); - const appId = uuid.v4(); - - if (icon) { - if (!validator.isBase64(icon)) return callback(new BoxError(BoxError.BAD_FIELD, 'icon is not base64', { field: 'icon' })); - - if (!safe.fs.writeFileSync(path.join(paths.APP_ICONS_DIR, appId + '.png'), Buffer.from(icon, 'base64'))) { - return callback(new BoxError(BoxError.FS_ERROR, 'Error saving icon:' + safe.error.message)); - } + if (cert && key) { + error = reverseProxy.validateCertificate(location, domainObjectMap[domain], { cert, key }); + if (error) return callback(new BoxError(BoxError.BAD_FIELD, error.message, { field: 'cert' })); } - const locations = [{subdomain: location, domain}].concat(alternateDomains); - validateLocations(locations, function (error, domainObjectMap) { + debug('Will install app with id : ' + appId); + + var data = { + accessRestriction: accessRestriction, + memoryLimit: memoryLimit, + sso: sso, + debugMode: debugMode, + mailboxName: mailboxName, + mailboxDomain: domain, + enableBackup: enableBackup, + enableAutomaticUpdate: enableAutomaticUpdate, + alternateDomains: alternateDomains, + env: env, + label: label, + tags: tags, + runState: exports.RSTATE_RUNNING, + installationState: exports.ISTATE_PENDING_INSTALL + }; + + appdb.add(appId, appStoreId, manifest, location, domain, translatePortBindings(portBindings, manifest), data, function (error) { + if (error && error.reason === BoxError.ALREADY_EXISTS) return callback(getDuplicateErrorDetails(error.message, locations, domainObjectMap, portBindings)); if (error) return callback(error); - if (cert && key) { - error = reverseProxy.validateCertificate(location, domainObjectMap[domain], { cert, key }); - if (error) return callback(new BoxError(BoxError.BAD_FIELD, error.message, { field: 'cert' })); - } - - debug('Will install app with id : ' + appId); - - var data = { - accessRestriction: accessRestriction, - memoryLimit: memoryLimit, - sso: sso, - debugMode: debugMode, - mailboxName: mailboxName, - mailboxDomain: domain, - enableBackup: enableBackup, - enableAutomaticUpdate: enableAutomaticUpdate, - alternateDomains: alternateDomains, - env: env, - label: label, - tags: tags, - runState: exports.RSTATE_RUNNING, - installationState: exports.ISTATE_PENDING_INSTALL - }; - - appdb.add(appId, appStoreId, manifest, location, domain, translatePortBindings(portBindings, manifest), data, function (error) { - if (error && error.reason === BoxError.ALREADY_EXISTS) return callback(getDuplicateErrorDetails(error.message, locations, domainObjectMap, portBindings)); + purchaseApp({ appId: appId, appstoreId: appStoreId, manifestId: manifest.id || 'customapp' }, function (error) { if (error) return callback(error); - purchaseApp({ appId: appId, appstoreId: appStoreId, manifestId: manifest.id || 'customapp' }, function (error) { + // save cert to boxdata/certs + if (cert && key) { + let error = reverseProxy.setAppCertificateSync(location, domainObjectMap[domain], { cert, key }); + if (error) return callback(error); + } + + const task = { + args: { restoreConfig: null, overwriteDns }, + values: { }, + requiredState: data.installationState + }; + + addTask(appId, data.installationState, task, function (error, result) { if (error) return callback(error); - // save cert to boxdata/certs - if (cert && key) { - let error = reverseProxy.setAppCertificateSync(location, domainObjectMap[domain], { cert, key }); - if (error) return callback(error); - } + const newApp = _.extend({}, data, { appStoreId, manifest, location, domain, portBindings }); + newApp.fqdn = domains.fqdn(newApp.location, domainObjectMap[newApp.domain]); + newApp.alternateDomains.forEach(function (ad) { ad.fqdn = domains.fqdn(ad.subdomain, domainObjectMap[ad.domain]); }); - const task = { - args: { restoreConfig: null, overwriteDns }, - values: { }, - requiredState: data.installationState - }; + eventlog.add(eventlog.ACTION_APP_INSTALL, auditSource, { appId, app: newApp, taskId: result.taskId }); - addTask(appId, data.installationState, task, function (error, result) { - if (error) return callback(error); - - const newApp = _.extend({}, data, { appStoreId, manifest, location, domain, portBindings }); - newApp.fqdn = domains.fqdn(newApp.location, domainObjectMap[newApp.domain]); - newApp.alternateDomains.forEach(function (ad) { ad.fqdn = domains.fqdn(ad.subdomain, domainObjectMap[ad.domain]); }); - - eventlog.add(eventlog.ACTION_APP_INSTALL, auditSource, { appId, app: newApp, taskId: result.taskId }); - - callback(null, { id : appId, taskId: result.taskId }); - }); + callback(null, { id : appId, taskId: result.taskId }); }); }); }); @@ -1257,12 +1256,14 @@ function setDataDir(appId, dataDir, auditSource, callback) { function update(appId, data, auditSource, callback) { assert.strictEqual(typeof appId, 'string'); assert(data && typeof data === 'object'); + assert.strictEqual(data.manifest && typeof data.manifest === 'object'); assert.strictEqual(typeof auditSource, 'object'); assert.strictEqual(typeof callback, 'function'); debug(`update: id:${appId}`); - const skipBackup = !!data.skipBackup; + const skipBackup = !!data.skipBackup, + manifest = data.manifest; get(appId, function (error, app) { if (error) return callback(error); @@ -1272,67 +1273,63 @@ function update(appId, data, auditSource, callback) { if (app.runState === exports.RSTATE_STOPPED) return callback(new BoxError(BoxError.BAD_STATE, 'Stopped apps cannot be updated')); - downloadManifest(data.appStoreId, data.manifest, function (error, appStoreId, manifest) { - if (error) return callback(error); + error = manifestFormat.parse(manifest); + if (error) return callback(new BoxError(BoxError.BAD_FIELD, 'Manifest error:' + error.message)); - error = manifestFormat.parse(manifest); - if (error) return callback(new BoxError(BoxError.BAD_FIELD, 'Manifest error:' + error.message)); + error = checkManifestConstraints(manifest); + if (error) return callback(error); - error = checkManifestConstraints(manifest); - if (error) return callback(error); + var updateConfig = { skipBackup, manifest }; - var updateConfig = { skipBackup, manifest }; + // prevent user from installing a app with different manifest id over an existing app + // this allows cloudron install -f --app for an app installed from the appStore + if (app.manifest.id !== updateConfig.manifest.id) { + if (!data.force) return callback(new BoxError(BoxError.BAD_FIELD, 'manifest id does not match. force to override')); + // clear appStoreId so that this app does not get updates anymore + updateConfig.appStoreId = ''; + } - // prevent user from installing a app with different manifest id over an existing app - // this allows cloudron install -f --app for an app installed from the appStore - if (app.manifest.id !== updateConfig.manifest.id) { - if (!data.force) return callback(new BoxError(BoxError.BAD_FIELD, 'manifest id does not match. force to override')); - // clear appStoreId so that this app does not get updates anymore - updateConfig.appStoreId = ''; - } + // suffix '0' if prerelease is missing for semver.lte to work as expected + const currentVersion = semver.prerelease(app.manifest.version) ? app.manifest.version : `${app.manifest.version}-0`; + const updateVersion = semver.prerelease(updateConfig.manifest.version) ? updateConfig.manifest.version : `${updateConfig.manifest.version}-0`; + if (app.appStoreId !== '' && semver.lte(updateVersion, currentVersion)) { + if (!data.force) return callback(new BoxError(BoxError.BAD_FIELD, 'Downgrades are not permitted for apps installed from AppStore. force to override')); + } - // suffix '0' if prerelease is missing for semver.lte to work as expected - const currentVersion = semver.prerelease(app.manifest.version) ? app.manifest.version : `${app.manifest.version}-0`; - const updateVersion = semver.prerelease(updateConfig.manifest.version) ? updateConfig.manifest.version : `${updateConfig.manifest.version}-0`; - if (app.appStoreId !== '' && semver.lte(updateVersion, currentVersion)) { - if (!data.force) return callback(new BoxError(BoxError.BAD_FIELD, 'Downgrades are not permitted for apps installed from AppStore. force to override')); - } + if ('icon' in data) { + if (data.icon) { + if (!validator.isBase64(data.icon)) return callback(new BoxError(BoxError.BAD_FIELD, 'icon is not base64', { field: 'icon' })); - if ('icon' in data) { - if (data.icon) { - if (!validator.isBase64(data.icon)) return callback(new BoxError(BoxError.BAD_FIELD, 'icon is not base64', { field: 'icon' })); - - if (!safe.fs.writeFileSync(path.join(paths.APP_ICONS_DIR, appId + '.user.png'), Buffer.from(data.icon, 'base64'))) { - return callback(new BoxError(BoxError.FS_ERROR, 'Error saving icon:' + safe.error.message)); - } - } else { - safe.fs.unlinkSync(path.join(paths.APP_ICONS_DIR, appId + '.user.png')); + if (!safe.fs.writeFileSync(path.join(paths.APP_ICONS_DIR, appId + '.user.png'), Buffer.from(data.icon, 'base64'))) { + return callback(new BoxError(BoxError.FS_ERROR, 'Error saving icon:' + safe.error.message)); } + } else { + safe.fs.unlinkSync(path.join(paths.APP_ICONS_DIR, appId + '.user.png')); } + } - // do not update apps in debug mode - if (app.debugMode && !data.force) return callback(new BoxError(BoxError.BAD_STATE, 'debug mode enabled. force to override')); + // do not update apps in debug mode + if (app.debugMode && !data.force) return callback(new BoxError(BoxError.BAD_STATE, 'debug mode enabled. force to override')); - // Ensure we update the memory limit in case the new app requires more memory as a minimum - // 0 and -1 are special updateConfig for memory limit indicating unset and unlimited - if (app.memoryLimit > 0 && updateConfig.manifest.memoryLimit && app.memoryLimit < updateConfig.manifest.memoryLimit) { - updateConfig.memoryLimit = updateConfig.manifest.memoryLimit; - } + // Ensure we update the memory limit in case the new app requires more memory as a minimum + // 0 and -1 are special updateConfig for memory limit indicating unset and unlimited + if (app.memoryLimit > 0 && updateConfig.manifest.memoryLimit && app.memoryLimit < updateConfig.manifest.memoryLimit) { + updateConfig.memoryLimit = updateConfig.manifest.memoryLimit; + } - const task = { - args: { updateConfig }, - values: {} - }; - addTask(appId, exports.ISTATE_PENDING_UPDATE, task, function (error, result) { - if (error) return callback(error); + const task = { + args: { updateConfig }, + values: {} + }; + addTask(appId, exports.ISTATE_PENDING_UPDATE, task, function (error, result) { + if (error) return callback(error); - eventlog.add(eventlog.ACTION_APP_UPDATE, auditSource, { appId, app, skipBackup, toManifest: manifest, fromManifest: app.manifest, force: data.force, taskId: result.taskId }); + eventlog.add(eventlog.ACTION_APP_UPDATE, auditSource, { appId, app, skipBackup, toManifest: manifest, fromManifest: app.manifest, force: data.force, taskId: result.taskId }); - // clear update indicator, if update fails, it will come back through the update checker - updateChecker.resetAppUpdateInfo(appId); + // clear update indicator, if update fails, it will come back through the update checker + updateChecker.resetAppUpdateInfo(appId); - callback(null, { taskId: result.taskId }); - }); + callback(null, { taskId: result.taskId }); }); }); } diff --git a/src/routes/apps.js b/src/routes/apps.js index 304efa7d2..ffc2f7b0d 100644 --- a/src/routes/apps.js +++ b/src/routes/apps.js @@ -51,6 +51,7 @@ var apps = require('../apps.js'), HttpError = require('connect-lastmile').HttpError, HttpSuccess = require('connect-lastmile').HttpSuccess, safe = require('safetydance'), + users = require('../users.js'), util = require('util'), WebSocket = require('ws'); @@ -89,7 +90,7 @@ function getAppIcon(req, res, next) { function installApp(req, res, next) { assert.strictEqual(typeof req.body, 'object'); - var data = req.body; + const data = req.body; // atleast one if ('manifest' in data && typeof data.manifest !== 'object') return next(new HttpError(400, 'manifest must be an object')); @@ -133,10 +134,18 @@ function installApp(req, res, next) { if ('overwriteDns' in req.body && typeof req.body.overwriteDns !== 'boolean') return next(new HttpError(400, 'overwriteDns must be boolean')); - apps.install(data, auditSource.fromRequest(req), function (error, result) { + apps.downloadManifest(data.appStoreId, data.manifest, function (error, appStoreId, manifest) { if (error) return next(BoxError.toHttpError(error)); - next(new HttpSuccess(202, { id: result.id, taskId: result.taskId })); + if (safe.query(data, 'manifest.addons.docker') && req.user.role !== users.ROLE_OWNER) return next(new HttpError(403, 'Only owner can install app with docker addon')); + + data.appStoreId = appStoreId; + data.manifest = manifest; + apps.install(data, auditSource.fromRequest(req), function (error, result) { + if (error) return next(BoxError.toHttpError(error)); + + next(new HttpSuccess(202, { id: result.id, taskId: result.taskId })); + }); }); } @@ -362,6 +371,8 @@ function repairApp(req, res, next) { if ('manifest' in data) { if (!data.manifest || typeof data.manifest !== 'object') return next(new HttpError(400, 'manifest must be an object')); + + if (safe.query(data, 'manifest.addons.docker') && req.user.role !== users.ROLE_OWNER) return next(new HttpError(403, 'Only owner can repair app with docker addon')); } if ('dockerImage' in data) { @@ -505,10 +516,18 @@ function updateApp(req, res, next) { if ('skipBackup' in data && typeof data.skipBackup !== 'boolean') return next(new HttpError(400, 'skipBackup must be a boolean')); if ('force' in data && typeof data.force !== 'boolean') return next(new HttpError(400, 'force must be a boolean')); - apps.update(req.params.id, req.body, auditSource.fromRequest(req), function (error, result) { + apps.downloadManifest(data.appStoreId, data.manifest, function (error, appStoreId, manifest) { if (error) return next(BoxError.toHttpError(error)); - next(new HttpSuccess(202, { taskId: result.taskId })); + if (safe.query(data, 'manifest.addons.docker') && req.user.role !== users.ROLE_OWNER) return next(new HttpError(403, 'Only owner can install app with docker addon')); + + data.appStoreId = appStoreId; + data.manifest = manifest; + apps.update(req.params.id, req.body, auditSource.fromRequest(req), function (error, result) { + if (error) return next(BoxError.toHttpError(error)); + + next(new HttpSuccess(202, { taskId: result.taskId })); + }); }); }