From 993ff20e2036307dc3170942d2befca3005b440c Mon Sep 17 00:00:00 2001 From: mamaso Date: Fri, 13 May 2016 13:58:46 -0700 Subject: [PATCH] Added settings manager (#1645) * Added settings manager * Clarity improvements & test fixes for settings manager * Updated cli-defs, features router, removed global test object * Fixed cli definitions syntax error * Fixed formatting issues * Removed ENABLE_CONFIG_CHANGES env var & improved doc * Disable config changes for most tests * test fixes * Correct handling of verbose --- package.json | 1 + spec/FileLoggerAdapter.spec.js | 11 +- spec/PersistentSettings.spec.js | 215 ++++++++++++++++++++++++++++++++ spec/PublicAPI.spec.js | 3 +- spec/SettingsRouter.spec.js | 57 --------- spec/helper.js | 21 ++-- spec/index.spec.js | 2 +- src/Config.js | 1 + src/ParseServer.js | 103 +++++++++------ src/Routers/FeaturesRouter.js | 4 + src/Routers/SettingsRouter.js | 24 ++-- src/SettingsManager.js | 122 ++++++++++++++++++ src/cli/cli-definitions.js | 9 ++ src/middlewares.js | 102 ++++++++------- 14 files changed, 512 insertions(+), 163 deletions(-) create mode 100644 spec/PersistentSettings.spec.js delete mode 100644 spec/SettingsRouter.spec.js create mode 100644 src/SettingsManager.js diff --git a/package.json b/package.json index 846ab3183a..8afdf40e6d 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "body-parser": "^1.14.2", "colors": "^1.1.2", "commander": "^2.9.0", + "deep-equal": "^1.0.1", "deepcopy": "^0.6.1", "express": "^4.13.4", "intersect": "^1.0.1", diff --git a/spec/FileLoggerAdapter.spec.js b/spec/FileLoggerAdapter.spec.js index 54e661bde9..ff1951d610 100644 --- a/spec/FileLoggerAdapter.spec.js +++ b/spec/FileLoggerAdapter.spec.js @@ -1,7 +1,11 @@ var FileLoggerAdapter = require('../src/Adapters/Logger/FileLoggerAdapter').FileLoggerAdapter; var Parse = require('parse/node').Parse; +var configureLogger = require('../src/logger').configureLogger; -describe('info logs', () => { +describe('FileLoggerAdapter', () => { + beforeEach(() => { + configureLogger({ logsFolder: './test_logs/file_logger/'}); + }); it("Verify INFO logs", (done) => { var fileLoggerAdapter = new FileLoggerAdapter(); @@ -20,9 +24,6 @@ describe('info logs', () => { }); }); }); -}); - -describe('error logs', () => { it("Verify ERROR logs", (done) => { var fileLoggerAdapter = new FileLoggerAdapter(); @@ -42,4 +43,4 @@ describe('error logs', () => { }); }); }); -}); +}); \ No newline at end of file diff --git a/spec/PersistentSettings.spec.js b/spec/PersistentSettings.spec.js new file mode 100644 index 0000000000..0208d6ef1c --- /dev/null +++ b/spec/PersistentSettings.spec.js @@ -0,0 +1,215 @@ +var request = require('request'); +var deepcopy = require('deepcopy'); +var Config = require('../src/Config'); +var logger = require('../src/logger').default; + +var settingsCollectionName = '_ServerSettings'; +var configuration; +var settingsCollection; +var parseServerObject; + +describe('Persistent Settings', () => { + beforeEach((done) => { + configuration = deepcopy(defaultConfiguration); + configuration.enableConfigChanges = true; + newServer().then(done); + }); + + describe('Upon Initialization', () => { + it('should persist settings', (done) => { + configuration.clientKey = 'local'; + + newServer() + .then(getPersisted) + .then(persisted => { + expect(persisted.clientKey).toEqual('local'); + }) + .then(done) + .catch(done.fail); + }); + + it('should only load mutable settings from database', (done) => { + configuration.clientKey = 'local'; // defined + + updatePersisted({ logLevel: 'info', clientKey: 'persisted' }) + .then(newServer) + .then(_ => { + var config = parseServerObject.config; + expect(config.logLevel).toEqual('info'); // not locked or defined, so updated + expect(config.clientKey).toEqual('local'); // configuration defined, therefore not updated + }) + .then(done) + .catch(done.fail); + }); + + it('overwrites defined settings if lockDefinedSettings is false', (done) => { + configuration.clientKey = 'local'; + configuration.lockDefinedSettings = false; + + updatePersisted({ clientKey: 'persisted' }) + .then(newServer) + .then(_ => { + var config = parseServerObject.config; + expect(config.clientKey).toEqual('persisted'); // defined setting was updated + }) + .then(done) + .catch(done.fail); + }); + }); + + describe('Settings Router', () => { + it('should provide error on post if config changes disabled', (done) => { + configuration.enableConfigChanges = false; + newServer() + .then(endpoint.get) + .then(res => expect(res.res.statusCode).toBe(200)) + .then(_ => endpoint.post({ clientKey: 'causesError' })) + .then(res => { + expect(res.res.statusCode).toBe(400); + expect(res.body.code).toBe(119); //Parse.Error.OPERATION_FORBIDDEN + expect(res.body.error).toBe('Server config changes are disabled'); + }) + .then(done) + .catch(done.fail); + }); + + it('should run setting callbacks such as configureLogger', (done) => { + endpoint.post({ logLevel: 'debug' }) + .then(res => { + expect(res.res.statusCode).toBe(200); + expect(res.body.logLevel).toBe('debug'); + expect(logger.transports['parse-server'].level).toBe('debug'); + }) + .then(endpoint.get) + .then(res => { + expect(res.res.statusCode).toBe(200); + expect(res.body.logLevel).toBe('debug'); + }) + .then(done) + .catch(done.fail); + }); + + it('should not set defined setting', (done) => { + endpoint.post({ clientKey: 'alreadyDefined' }) + .then(res => { + expect(res.res.statusCode).toBe(200); + expect(res.body.clientKey).toBeUndefined(); + }) + .then(endpoint.get) + .then(res => { + expect(res.res.statusCode).toBe(200); + expect(res.body.clientKey).toBe(configuration.clientKey); + }) + .then(done) + .catch(done.fail); + }); + + it('should not allow access without masterKey', (done) => { + var invalidHeaders = { + 'X-Parse-Application-Id': 'test', + 'X-Parse-Master-Key': 'invalid' + }; + + endpoint.post({ logLevel: 'silly' }, invalidHeaders) + .then(res => { + expect(res.res.statusCode).toBe(403); + expect(res.body.error).toBe('unauthorized'); + }) + .then(_ => endpoint.get(invalidHeaders)) + .then(res => { + expect(res.res.statusCode).toBe(403); + expect(res.body.error).toBe('unauthorized'); + }) + .then(done) + .catch(done.fail); + }); + + it('should expose non-existant settings as null', (done) => { + delete configuration.clientKey; + + settingsCollection.drop() + .then(newServer) + .then(endpoint.get) + .then(res => expect(res.body.clientKey).toBe(null)) + .then(done) + .catch(done.fail); + }); + + it('should fetch database values', (done) => { + delete configuration.clientKey; + + settingsCollection.drop() + .then(newServer) + .then(endpoint.get) + .then(res => expect(res.body.clientKey).toBe(null)) + .then(_ => updatePersisted({ clientKey: 'persisted' })) + .then(endpoint.get) + .then(res => expect(res.body.clientKey).toBe('persisted')) + .then(done) + .catch(done.fail); + }); + + it('should only return modified values', (done) => { + // info is default log level + var currentLogLevel; + endpoint.get() + .then(res => currentLogLevel = res.body.logLevel) + .then(_ => endpoint.post({ logLevel: currentLogLevel })) + .then(res => expect(res.body.logLevel).toBeUndefined) + .then(done) + .catch(done.fail); + }); + }); +}); + +function newServer() { + parseServerObject = setServerConfiguration(deepcopy(configuration)); + return parseServerObject.config.settingsInitialized + .then(_ => { + var config = new Config(configuration.appId); + return config.database.adapter.adaptiveCollection(settingsCollectionName); + }) + .then(coll => { settingsCollection = coll; }); +} + +function updatePersisted(settings) { + settings.applicationId = configuration.appId; + return parseServerObject.config.settingsInitialized + .then(_ => settingsCollection.upsertOne({ applicationId: configuration.appId }, { $set: settings })) + .then(_ => undefined); +} + +function getPersisted() { + return parseServerObject.config.settingsInitialized + .then(_ => settingsCollection.find({ applicationId: configuration.appId })) + .then(results => results && results.length && results[0]); +} + +var settingsUrl = 'http://localhost:8378/1/settings'; +var defaultHeaders = { + 'X-Parse-Application-Id': 'test', + 'X-Parse-Master-Key': 'test' +}; + +var req = (method, headers, body) => new Promise((resolve, reject) => { + request[method]({ + url: settingsUrl, + json: body, + headers: headers || defaultHeaders + }, (err, res, body) => { + if (err) { + reject(err); + } else { + if (typeof body === 'string') body = JSON.parse(body); + resolve({ + res: res, + body: body + }); + } + }); + }); + +var endpoint = { + get: headers => req('get', headers), + post: (body, headers) => req('post', headers, body) +} diff --git a/spec/PublicAPI.spec.js b/spec/PublicAPI.spec.js index 008d544ae4..aa5f1a9cd9 100644 --- a/spec/PublicAPI.spec.js +++ b/spec/PublicAPI.spec.js @@ -14,7 +14,8 @@ describe("public API", () => { masterKey: 'test', collectionPrefix: 'test_', fileKey: 'test', - publicServerURL: 'http://localhost:8378/1' + publicServerURL: 'http://localhost:8378/1', + enableConfigChanges: false }); done(); }) diff --git a/spec/SettingsRouter.spec.js b/spec/SettingsRouter.spec.js deleted file mode 100644 index 913e2c8a8f..0000000000 --- a/spec/SettingsRouter.spec.js +++ /dev/null @@ -1,57 +0,0 @@ -var request = require('request'); - -var headers = { - 'X-Parse-Application-Id': 'test', - 'X-Parse-Master-Key': 'test' -}; - - -describe('SettingsRouter', () => { - - it('should set the loglevel', (done) => { - request.post({ - headers: headers, - json: { - 'logLevel': 'silly' - }, - url: 'http://localhost:8378/1/settings' - }, (err, res, body) => { - request.get({ - url: 'http://localhost:8378/1/settings', - headers: headers - }, (err, res, body)=> { - body = JSON.parse(body); - expect(body.logLevel).toBe('silly'); - done(); - }); - }); - }); - - it('should not access without masterKey', (done) => { - request.post({ - headers: { - 'X-Parse-Application-Id': 'test', - 'X-Parse-Master-Key': 'invalid' - }, - json: { - 'logLevel': 'silly' - }, - url: 'http://localhost:8378/1/settings' - }, (err, res, body) => { - expect(body.error).not.toBeUndefined(); - expect(body.error).toBe('unauthorized'); - request.get({ - url: 'http://localhost:8378/1/settings', - headers: { - 'X-Parse-Application-Id': 'test', - 'X-Parse-Master-Key': 'invalid' - } - }, (err, res, body)=> { - body = JSON.parse(body); - expect(body.error).toBe('unauthorized'); - done(); - }); - }); - }) - -}) diff --git a/spec/helper.js b/spec/helper.js index c134f339ab..b4050f2374 100644 --- a/spec/helper.js +++ b/spec/helper.js @@ -6,7 +6,7 @@ var cache = require('../src/cache').default; var DatabaseAdapter = require('../src/DatabaseAdapter'); var express = require('express'); var facebook = require('../src/authDataManager/facebook'); -var ParseServer = require('../src/index').ParseServer; +var ParseServer = require('../src/index').default; var path = require('path'); var databaseURI = process.env.DATABASE_URI; @@ -39,13 +39,14 @@ var defaultConfiguration = { myoauth: { module: path.resolve(__dirname, "myoauth") // relative path as it's run from src } - } + }, + enableConfigChanges: false }; // Set up a default API server for testing with default configuration. -var api = new ParseServer(defaultConfiguration); +var parseServer = new ParseServer(defaultConfiguration); var app = express(); -app.use('/1', api); +app.use('/1', parseServer.app); var server = app.listen(port); // Prevent reinitializing the server from clobbering Cloud Code @@ -63,9 +64,10 @@ var setServerConfiguration = configuration => { server.close(); cache.clearCache(); app = express(); - api = new ParseServer(configuration); - app.use('/1', api); + parseServer = new ParseServer(configuration); + app.use('/1', parseServer.app); server = app.listen(port); + return parseServer; }; var restoreServerConfiguration = () => setServerConfiguration(defaultConfiguration); @@ -140,15 +142,19 @@ function notWorking() {} function ok(bool, message) { expect(bool).toBeTruthy(message); } + function equal(a, b, message) { expect(a).toEqual(b, message); } + function strictEqual(a, b, message) { expect(a).toBe(b, message); } + function notEqual(a, b, message) { expect(a).not.toEqual(b, message); } + function expectSuccess(params) { return { success: params.success, @@ -158,6 +164,7 @@ function expectSuccess(params) { }, } } + function expectError(errorCode, callback) { return { success: function(result) { @@ -277,4 +284,4 @@ jasmine.restoreLibrary = function(library, name) { throw 'Can not find library ' + library + ' ' + name; } require(library)[name] = libraryCache[library][name]; -} +} \ No newline at end of file diff --git a/spec/index.spec.js b/spec/index.spec.js index c219e3ad6f..d852a386ce 100644 --- a/spec/index.spec.js +++ b/spec/index.spec.js @@ -38,7 +38,7 @@ describe('server', () => { }, (error, response, body) => { expect(response.statusCode).toEqual(500); expect(body.code).toEqual(1); - expect(body.message).toEqual('Internal server error.'); + expect(body.error.message).toEqual('Authentication failed.'); done(); }); }); diff --git a/src/Config.js b/src/Config.js index 3e2ac36834..3b94e706e3 100644 --- a/src/Config.js +++ b/src/Config.js @@ -49,6 +49,7 @@ export class Config { this.liveQueryController = cacheInfo.liveQueryController; this.sessionLength = cacheInfo.sessionLength; this.generateSessionExpiresAt = this.generateSessionExpiresAt.bind(this); + this.enableConfigChanges = cacheInfo.enableConfigChanges; } static validate(options) { diff --git a/src/ParseServer.js b/src/ParseServer.js index 1c1eececf1..b901ece0cc 100644 --- a/src/ParseServer.js +++ b/src/ParseServer.js @@ -15,6 +15,7 @@ var batch = require('./batch'), import { logger, configureLogger } from './logger'; import cache from './cache'; +import SettingsManager from './SettingsManager'; import Config from './Config'; import parseServerPackage from '../package.json'; import PromiseRouter from './PromiseRouter'; @@ -78,46 +79,53 @@ addParseCloud(); // "javascriptKey": optional key from Parse dashboard // "push": optional key from configure push // "sessionLength": optional length in seconds for how long Sessions should be valid for +// "enableConfigChanges": Allow modification of server config from settings endpoint. +// Requires one extra database call per request to retrieve settings +// "lockDefinedSettings": Disallow modification of code-defined server config settings class ParseServer { - constructor({ - appId = requiredParameter('You must provide an appId!'), - masterKey = requiredParameter('You must provide a masterKey!'), - appName, - databaseAdapter, - filesAdapter, - push, - loggerAdapter, - logsFolder, - databaseURI = DatabaseAdapter.defaultDatabaseURI, - databaseOptions, - cloud, - collectionPrefix = '', - clientKey, - javascriptKey, - dotNetKey, - restAPIKey, - fileKey = 'invalid-file-key', - facebookAppIds = [], - enableAnonymousUsers = true, - allowClientClassCreation = true, - oauth = {}, - serverURL = requiredParameter('You must provide a serverURL!'), - maxUploadSize = '20mb', - verifyUserEmails = false, - emailAdapter, - publicServerURL, - customPages = { - invalidLink: undefined, - verifyEmailSuccess: undefined, - choosePassword: undefined, - passwordResetSuccess: undefined - }, - liveQuery = {}, - sessionLength = 31536000, // 1 Year in seconds - verbose = false, - }) { + constructor(definedSettings) { + let { + appId = requiredParameter('You must provide an appId!'), + masterKey = requiredParameter('You must provide a masterKey!'), + appName, + databaseAdapter, + filesAdapter, + push, + loggerAdapter, + logsFolder, + databaseURI = DatabaseAdapter.defaultDatabaseURI, + databaseOptions, + cloud, + collectionPrefix = '', + clientKey, + javascriptKey, + dotNetKey, + restAPIKey, + fileKey = 'invalid-file-key', + facebookAppIds = [], + enableAnonymousUsers = true, + allowClientClassCreation = true, + oauth = {}, + serverURL = requiredParameter('You must provide a serverURL!'), + maxUploadSize = '20mb', + verifyUserEmails = false, + emailAdapter, + publicServerURL, + customPages = { + invalidLink: undefined, + verifyEmailSuccess: undefined, + choosePassword: undefined, + passwordResetSuccess: undefined + }, + liveQuery = {}, + sessionLength = 31536000, // 1 Year in seconds + verbose = false, + logLevel = 'info', + enableConfigChanges = true, + lockDefinedSettings = true + } = definedSettings; // Initialize the node client SDK automatically Parse.initialize(appId, javascriptKey || 'unused', masterKey); Parse.serverURL = serverURL; @@ -152,8 +160,9 @@ class ParseServer { } if (verbose || process.env.VERBOSE || process.env.VERBOSE_PARSE_SERVER) { - configureLogger({level: 'silly'}); + logLevel = 'silly'; } + configureLogger({level: logLevel}); const filesControllerAdapter = loadAdapter(filesAdapter, () => { return new GridStoreAdapter(databaseURI); @@ -195,6 +204,10 @@ class ParseServer { maxUploadSize: maxUploadSize, liveQueryController: liveQueryController, sessionLength : Number(sessionLength), + verbose: verbose, + logLevel: logLevel, + enableConfigChanges: enableConfigChanges, + applicationId: appId }); // To maintain compatibility. TODO: Remove in some version that breaks backwards compatability @@ -204,6 +217,20 @@ class ParseServer { Config.validate(cache.apps.get(appId)); this.config = cache.apps.get(appId); + + logger.info(`Server config changes are ${enableConfigChanges? 'enabled': 'disabled'}`); + this.config.settingsInitialized = Promise.resolve(); + if (enableConfigChanges) { + let settingsManager = SettingsManager(appId); + logger.info(`Code-defined config settings ${lockDefinedSettings? 'cannot': 'can'} be modified`); + settingsManager.setDefined(lockDefinedSettings? definedSettings: {}); + this.config.settingsInitialized = settingsManager.pull() + .then(persistedSettings => { + settingsManager.updateCache(persistedSettings); + return settingsManager.push(settingsManager.getVisible()); + }); + } + hooksController.load(); } diff --git a/src/Routers/FeaturesRouter.js b/src/Routers/FeaturesRouter.js index 02016fb1a4..ae8638665d 100644 --- a/src/Routers/FeaturesRouter.js +++ b/src/Routers/FeaturesRouter.js @@ -40,6 +40,10 @@ export class FeaturesRouter extends PromiseRouter { exportClass: false, editClassLevelPermissions: true, }, + serverSettings: { + read: true, + update: req.config.enableConfigChanges + } }; return { response: { diff --git a/src/Routers/SettingsRouter.js b/src/Routers/SettingsRouter.js index 6d0a97af6a..af4648f618 100644 --- a/src/Routers/SettingsRouter.js +++ b/src/Routers/SettingsRouter.js @@ -1,26 +1,28 @@ import PromiseRouter from '../PromiseRouter'; import * as middleware from "../middlewares"; import { logger, configureLogger } from '../logger'; +import SettingsManager from '../SettingsManager'; import winston from 'winston'; +import Parse from 'parse/node'; export class SettingsRouter extends PromiseRouter { mountRoutes() { this.route('GET', '/settings', middleware.promiseEnforceMasterKeyAccess, (req) => { return Promise.resolve({ - response: { - logLevel: winston.level - } - }) + response: SettingsManager(req.config.applicationId).getVisible() + }); }); + this.route('POST','/settings', middleware.promiseEnforceMasterKeyAccess, (req) => { - let body = req.body; - let logLevel = body.logLevel; - if (logLevel) { - configureLogger({level: logLevel}); + if (req.config.enableConfigChanges) { + let body = req.body; + let settingsManager = SettingsManager(req.config.applicationId); + var updatedSettings = settingsManager.updateCache(body); + return settingsManager.push(updatedSettings) + .then(() => ({ response: updatedSettings })); + } else { + return Promise.reject(new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, 'Server config changes are disabled')); } - return Promise.resolve({ - response: body - }) }); } } diff --git a/src/SettingsManager.js b/src/SettingsManager.js new file mode 100644 index 0000000000..9d72f132ea --- /dev/null +++ b/src/SettingsManager.js @@ -0,0 +1,122 @@ +'use strict' + +let equal = require('deep-equal'); +let deepcopy = require('deepcopy'); + +import { logger, configureLogger } from './logger'; +import cache from './cache'; +import Config from './Config'; +let authDataManager = require('./authDataManager'); + + +const settingsCollectionName = '_ServerSettings'; + +// visible locked settings +const lockedSettings = [ + 'applicationId', + 'masterKey', + 'serverURL', + 'collectionPrefix', + 'enableConfigChanges', +]; +// hidden locked settings +const hiddenSettings = [ + 'filesController', + 'pushController', + 'loggerController', + 'hooksController', + 'userController', + 'authDataManager', + 'liveQueryController', + 'settingsInitialized' +]; + +// settings defined in server configuration +let definedSettings = {}; + +// callbacks for specific setting changes +let authChange = settings => settings.authDataManager = authDataManager(settings.ouath, settings.enableAnonymousUsers); +let onChange = { + logLevel: settings => configureLogger({ level: settings.logLevel }), + verbose: settings => { + settings.logLevel = settings.verbose? 'silly': 'info'; + configureLogger({ level: settings.logLevel }); + }, + oauth: authChange, + enableAnonymousUsers: authChange, + sessionLength: settings => settings.sessionLength = Number(settings.sessionLength), +} + +export default function SettingsManager(appId) { + return { + pull: () => { + return getSettingsCollection(appId) + .then(coll => coll.find({ applicationId: appId }, { limit: 1 })) + .then(results => { + let settings = results && results.length && results[0]; + if (settings) delete settings._id; + logger.verbose('Pulled settings: ' + JSON.stringify(settings, null, 2)); + return settings; + }); + }, + + push: settings => { + settings = deepcopy(settings); + settings.applicationId = appId; + return getSettingsCollection(appId) + .then(coll => coll.upsertOne({applicationId: appId}, { $set: settings })) + .then(() => { + logger.verbose('Pushed settings: ' + JSON.stringify(settings, null, 2)); + }); + }, + + updateCache: (updates = {}) => { + updates = Object.keys(updates) + .filter(update => !equal(updates[update], cache.apps.get(appId)[update]) && !isLocked(update) && !isDefined(update)) + .reduce((filtered, update) => { + filtered[update] = updates[update]; + return filtered; + }, {}); + + Object.keys(updates).forEach(setting => { + var config = cache.apps.get(appId); + logger.info(`Setting '${setting}' updated from '${config[setting]}' to '${updates[setting]}'`); + config[setting] = updates[setting]; + if (onChange[setting]) onChange[setting](cache.apps.get(appId)) + }); + return updates; + }, + + getVisible: () => { + let settings = cache.apps.get(appId); + + let settingsString = JSON.stringify(settings, (k, v) => { + if (!hiddenSettings.includes(k)) { + if (v === undefined) return null; + return v; + } + }); + + return JSON.parse(settingsString); + }, + + setDefined: settings => definedSettings = settings + }; + + function getSettingsCollection() { + let config = new Config(appId); + return config.database.adapter.adaptiveCollection(settingsCollectionName); + } + + function isLocked(update) { + var isLocked = lockedSettings.includes(update) || hiddenSettings.includes(update); + if (isLocked) logger.warn(`Cannot modify the value of '${update}' as it is locked`); + return isLocked; + } + + function isDefined(update) { + var isDefined = Object.keys(definedSettings).includes(update); + if (isDefined) logger.warn(`Cannot modify the value of '${update}' as it is defined as '${definedSettings[update]}' in parse server configuration`); + return isDefined; + } +} diff --git a/src/cli/cli-definitions.js b/src/cli/cli-definitions.js index dd908bcd04..1dab5649c0 100644 --- a/src/cli/cli-definitions.js +++ b/src/cli/cli-definitions.js @@ -174,5 +174,14 @@ export default { "verbose": { env: "VERBOSE", help: "Set the logging to verbose" + }, + "logLevel": { + help: "Sets the log level" + }, + "enableConfigChanges": { + help: "Disable to improve performance and lock configuration, defaults to true" + }, + "lockDefinedSettings": { + help: "Disallows modification of code/cli defined settings, defaults to true" } }; diff --git a/src/middlewares.js b/src/middlewares.js index d56840b4b0..128798972a 100644 --- a/src/middlewares.js +++ b/src/middlewares.js @@ -1,5 +1,6 @@ import cache from './cache'; import log from './logger'; +import SettingsManager from './SettingsManager'; var Parse = require('parse/node').Parse; @@ -84,60 +85,75 @@ function handleParseHeaders(req, res, next) { } info.app = cache.apps.get(info.appId); - req.config = new Config(info.appId, mount); - req.info = info; + let fetchConfiguration = Promise.resolve(); + if (info.app.enableConfigChanges) { + let settingsManager = SettingsManager(info.appId); + fetchConfiguration = settingsManager.pull() + .then(settingsManager.updateCache); + } - var isMaster = (info.masterKey === req.config.masterKey); + return fetchConfiguration + .then(() => { + req.config = new Config(info.appId, mount); + req.info = info; - if (isMaster) { - req.auth = new auth.Auth({ config: req.config, installationId: info.installationId, isMaster: true }); - next(); - return; - } + var isMaster = (info.masterKey === req.config.masterKey); - // Client keys are not required in parse-server, but if any have been configured in the server, validate them - // to preserve original behavior. - let keys = ["clientKey", "javascriptKey", "dotNetKey", "restAPIKey"]; + if (isMaster) { + req.auth = new auth.Auth({ config: req.config, installationId: info.installationId, isMaster: true }); + next(); + return; + } - // We do it with mismatching keys to support no-keys config - var keyMismatch = keys.reduce(function(mismatch, key){ + // Client keys are not required in parse-server, but if any have been configured in the server, validate them + // to preserve original behavior. + let keys = ["clientKey", "javascriptKey", "dotNetKey", "restAPIKey"]; - // check if set in the config and compare - if (req.config[key] && info[key] !== req.config[key]) { - mismatch++; - } - return mismatch; - }, 0); + // We do it with mismatching keys to support no-keys config + var keyMismatch = keys.reduce(function(mismatch, key){ - // All keys mismatch - if (keyMismatch == keys.length) { - return invalidRequest(req, res); - } + // check if set in the config and compare + if (req.config[key] && info[key] !== req.config[key]) { + mismatch++; + } + return mismatch; + }, 0); - if (!info.sessionToken) { - req.auth = new auth.Auth({ config: req.config, installationId: info.installationId, isMaster: false }); - next(); - return; - } + // All keys mismatch + if (keyMismatch == keys.length) { + return invalidRequest(req, res); + } - return auth.getAuthForSessionToken({ config: req.config, installationId: info.installationId, sessionToken: info.sessionToken }) - .then((auth) => { - if (auth) { - req.auth = auth; + if (!info.sessionToken) { + req.auth = new auth.Auth({ config: req.config, installationId: info.installationId, isMaster: false }); next(); - } - }) - .catch((error) => { - if(error instanceof Parse.Error) { - next(error); return; } - else { - // TODO: Determine the correct error scenario. - log.error('error getting auth for sessionToken', error); - throw new Parse.Error(Parse.Error.UNKNOWN_ERROR, error); - } - }); + + return auth.getAuthForSessionToken({ config: req.config, installationId: info.installationId, sessionToken: info.sessionToken }) + .then((auth) => { + if (auth) { + req.auth = auth; + next(); + } + }) + .catch((error) => { + if(error instanceof Parse.Error) { + next(error); + return; + } + else { + // TODO: Determine the correct error scenario. + log.error('error getting auth for sessionToken', error); + throw new Parse.Error(Parse.Error.UNKNOWN_ERROR, error); + } + }); + }) + .catch(error => { + log.error('error retrieving server settings from database', error); + next(new Parse.Error(Parse.Error.INTERNAL_SERVER_ERROR, error)); + }); + } var allowCrossDomain = function(req, res, next) {