diff --git a/CHANGES b/CHANGES index 1ea6c4c92..f3fb9b9b5 100644 --- a/CHANGES +++ b/CHANGES @@ -1542,4 +1542,5 @@ * Update node to 10.15.1 * Enable gzip compression for large objects * Update docker to 18.09 +* Add a way to lock specific settings diff --git a/migrations/20190221215805-settings-add-locked.js b/migrations/20190221215805-settings-add-locked.js new file mode 100644 index 000000000..0aa24eaf2 --- /dev/null +++ b/migrations/20190221215805-settings-add-locked.js @@ -0,0 +1,17 @@ +'use strict'; + +exports.up = function(db, callback) { + db.runSql('ALTER TABLE settings ADD COLUMN locked BOOLEAN DEFAULT 0', function (error) { + if (error) return callback(error); + + callback(); + }); +}; + +exports.down = function(db, callback) { + db.runSql('ALTER TABLE settings DROP COLUMN locked', function (error) { + if (error) console.error(error); + callback(error); + }); +}; + diff --git a/migrations/schema.sql b/migrations/schema.sql index 68dfbde98..430e45a1f 100644 --- a/migrations/schema.sql +++ b/migrations/schema.sql @@ -113,6 +113,7 @@ CREATE TABLE IF NOT EXISTS authcodes( CREATE TABLE IF NOT EXISTS settings( name VARCHAR(128) NOT NULL UNIQUE, value TEXT, + locked BOOLEAN, PRIMARY KEY(name)); CREATE TABLE IF NOT EXISTS appAddonConfigs( @@ -155,6 +156,7 @@ CREATE TABLE IF NOT EXISTS domains( provider VARCHAR(16) NOT NULL, configJson TEXT, /* JSON containing the dns backend provider config */ tlsConfigJson TEXT, /* JSON containing the tls provider config */ + locked BOOLEAN, PRIMARY KEY (domain)) diff --git a/src/routes/settings.js b/src/routes/settings.js index c2ee807ee..5c3c4ad4b 100644 --- a/src/routes/settings.js +++ b/src/routes/settings.js @@ -4,12 +4,9 @@ exports = module.exports = { set: set, get: get, - // specialized routes as they need different scope or some additional middleware getCloudronAvatar: getCloudronAvatar, - setCloudronAvatar: setCloudronAvatar, - getAppstoreConfig: getAppstoreConfig, - setAppstoreConfig: setAppstoreConfig + verifySettingsLock: verifySettingsLock }; var assert = require('assert'), @@ -22,6 +19,20 @@ var assert = require('assert'), settings = require('../settings.js'), SettingsError = settings.SettingsError; +function verifySettingsLock(req, res, next) { + assert.strictEqual(typeof req.params.setting, 'string'); + + settings.get(req.params.setting, function (error, result) { + // not locked. let actual route return not found. this is useful for entries stored outside the database like cloudron_avatar + if (error && error.reason === SettingsError.NOT_FOUND) return next(); + if (error) return next(new HttpError(500, error)); + + if (result.locked) return next(new HttpError(423, 'This setting is locked')); + + next(); + }); +} + function getAppAutoupdatePattern(req, res, next) { settings.getAppAutoupdatePattern(function (error, pattern) { if (error) return next(new HttpError(500, error)); @@ -296,6 +307,8 @@ function set(req, res, next) { case settings.TIME_ZONE_KEY: return setTimeZone(req, res, next); case settings.CLOUDRON_NAME_KEY: return setCloudronName(req, res, next); + case settings.CLOUDRON_AVATAR_KEY: return setCloudronAvatar(req, res, next); + default: return next(new HttpError(404, 'No such setting')); } } diff --git a/src/server.js b/src/server.js index 3c90adb8c..c02c89f62 100644 --- a/src/server.js +++ b/src/server.js @@ -103,6 +103,7 @@ function initializeExpressSync() { const isUnmanaged = routes.accesscontrol.isUnmanaged; const verifyDomainLock = routes.domains.verifyDomainLock; + const verifySettingsLock = routes.settings.verifySettingsLock; // csrf protection var csrf = routes.oauth2.csrf(); @@ -234,11 +235,10 @@ function initializeExpressSync() { router.post('/api/v1/apps/:id/owner', appsManageScope, routes.apps.setOwner); // settings routes (these are for the settings tab - avatar & name have public routes for normal users. see above) - router.get('/api/v1/settings/cloudron_avatar', settingsScope, routes.settings.getCloudronAvatar); - router.post('/api/v1/settings/cloudron_avatar', settingsScope, multipart, routes.settings.setCloudronAvatar); - - router.get ('/api/v1/settings/:setting', settingsScope, routes.settings.get); - router.post('/api/v1/settings/:setting', settingsScope, routes.settings.set); + router.get ('/api/v1/settings/:setting', settingsScope, verifySettingsLock, routes.settings.get); + router.post('/api/v1/settings/:setting', settingsScope, verifySettingsLock, (req, res, next) => { + return req.params.setting === 'cloudron_avatar' ? multipart(req, res, next) : next(); + }, routes.settings.set); // email routes router.get ('/api/v1/mail/:domain', mailScope, routes.mail.getDomain); diff --git a/src/settings.js b/src/settings.js index 08fcbeeb4..37e9a8920 100644 --- a/src/settings.js +++ b/src/settings.js @@ -35,6 +35,7 @@ exports = module.exports = { getPlatformConfig: getPlatformConfig, setPlatformConfig: setPlatformConfig, + get: get, getAll: getAll, // booleans. if you add an entry here, be sure to fix getAll @@ -54,7 +55,7 @@ exports = module.exports = { CLOUDRON_NAME_KEY: 'cloudron_name', // blobs - CLOUDRON_AVATAR_KEY: 'cloudron_avatar', // not stored in db + CLOUDRON_AVATAR_KEY: 'cloudron_avatar', // not stored in db but can be used for locked flag on: on, removeListener: removeListener @@ -488,3 +489,15 @@ function getAll(callback) { callback(null, result); }); } + +function get(name, callback) { + assert.strictEqual(typeof name, 'string'); + assert.strictEqual(typeof callback, 'function'); + + settingsdb.get(name, function (error, result) { + if (error && error.reason === DatabaseError.NOT_FOUND) return callback(null, gDefaults[exports.PLATFORM_CONFIG_KEY]); + if (error) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, error)); + + callback(null, result); + }); +} \ No newline at end of file diff --git a/src/settingsdb.js b/src/settingsdb.js index 136ec0959..e645fdb60 100644 --- a/src/settingsdb.js +++ b/src/settingsdb.js @@ -13,7 +13,7 @@ var assert = require('assert'), database = require('./database.js'), DatabaseError = require('./databaseerror'); -const SETTINGS_FIELDS = [ 'name', 'value' ].join(','); +const SETTINGS_FIELDS = [ 'name', 'value', 'locked' ].join(','); function get(key, callback) { assert.strictEqual(typeof key, 'string');