From 83151b7385ac0c141c1b1fc9c8903b79f01253ce Mon Sep 17 00:00:00 2001 From: Matt Mason Date: Tue, 26 Apr 2016 11:34:10 -0700 Subject: [PATCH 1/9] Added settings manager --- package.json | 1 + spec/PersistentSettings.spec.js | 210 ++++++++++++++++++++++++++++++++ spec/SettingsRouter.spec.js | 57 --------- spec/helper.js | 11 +- src/Config.js | 1 + src/ParseServer.js | 102 ++++++++++------ src/Routers/SettingsRouter.js | 26 ++-- src/SettingsManager.js | 118 ++++++++++++++++++ src/middlewares.js | 102 +++++++++------- 9 files changed, 474 insertions(+), 154 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/PersistentSettings.spec.js b/spec/PersistentSettings.spec.js new file mode 100644 index 0000000000..a9ef9785a6 --- /dev/null +++ b/spec/PersistentSettings.spec.js @@ -0,0 +1,210 @@ +var request = require('request'); +var deepcopy = require('deepcopy'); +var DatabaseAdapter = require('../src/DatabaseAdapter'); +var database = DatabaseAdapter.getDatabaseConnection('test', 'test_'); +var settingsCollection = '_ServerSettings'; +var logger = require('../src/logger').default; + +var configuration; + +describe('Persistent Settings', () => { + beforeEach((done) => { + configuration = deepcopy(defaultConfiguration); + configuration.verbose = true; + 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(403); + 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: 'silly' }) + .then(res => { + expect(res.res.statusCode).toBe(200); + expect(res.body.logLevel).toBe('silly'); + expect(logger.transports['parse-server'].level).toBe('silly'); + }) + .then(endpoint.get) + .then(res => { + expect(res.res.statusCode).toBe(200); + expect(res.body.logLevel).toBe('silly'); + }) + .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; + + database.deleteEverything() + .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; + + database.deleteEverything() + .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() { + setServerConfiguration(deepcopy(configuration)); + return parseServerObject.config.settingsInitialized; +} + +function updatePersisted(settings) { + settings.applicationId = configuration.appId; + return parseServerObject.config.settingsInitialized + .then(_ => database.adaptiveCollection(settingsCollection)) + .then(coll => coll.upsertOne({ applicationId: configuration.appId }, { $set: settings })) + .then(_ => undefined); +} + +function getPersisted() { + return parseServerObject.config.settingsInitialized + .then(_ => database.mongoFind(settingsCollection, {}, {})) + .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/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..a9b1dd1257 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; @@ -43,9 +43,9 @@ var defaultConfiguration = { }; // Set up a default API server for testing with default configuration. -var api = new ParseServer(defaultConfiguration); +global.parseServerObject = new ParseServer(defaultConfiguration); var app = express(); -app.use('/1', api); +app.use('/1', global.parseServerObject.app); var server = app.listen(port); // Prevent reinitializing the server from clobbering Cloud Code @@ -63,8 +63,8 @@ var setServerConfiguration = configuration => { server.close(); cache.clearCache(); app = express(); - api = new ParseServer(configuration); - app.use('/1', api); + global.parseServerObject = new ParseServer(configuration); + app.use('/1', global.parseServerObject.app); server = app.listen(port); }; @@ -259,6 +259,7 @@ global.jequal = jequal; global.range = range; global.setServerConfiguration = setServerConfiguration; global.defaultConfiguration = defaultConfiguration; +global.parseServerObject = global.parseServerObject; // LiveQuery test setting require('../src/LiveQuery/PLog').logLevel = 'NONE'; 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..7a4eb15a80 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,52 @@ 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 +// "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 = 'error', + enableConfigChanges = process.env.ENABLE_CONFIG_CHANGES, + lockDefinedSettings = true + } = definedSettings; // Initialize the node client SDK automatically Parse.initialize(appId, javascriptKey || 'unused', masterKey); Parse.serverURL = serverURL; @@ -152,8 +159,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 +203,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 +216,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.getUnlocked()); + }); + } + hooksController.load(); } diff --git a/src/Routers/SettingsRouter.js b/src/Routers/SettingsRouter.js index 6d0a97af6a..f21373e1a7 100644 --- a/src/Routers/SettingsRouter.js +++ b/src/Routers/SettingsRouter.js @@ -1,26 +1,30 @@ import PromiseRouter from '../PromiseRouter'; import * as middleware from "../middlewares"; import { logger, configureLogger } from '../logger'; +import SettingsManager from '../SettingsManager'; import winston from 'winston'; 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).getUnlocked() + }); }); + 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({ + status: 403, + message: '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..419fc47f8b --- /dev/null +++ b/src/SettingsManager.js @@ -0,0 +1,118 @@ +'use strict' + +let equal = require('deep-equal'); +let deepcopy = require('deepcopy'); + +import { logger, configureLogger } from './logger'; +import cache from './cache'; +let authDataManager = require('./authDataManager'); +let database = require('./DatabaseAdapter'); + +const settingsCollectionName = '_ServerSettings'; + +// doesn't make sense to expose / modify these settings +const lockedSettings = [ + 'applicationId', + 'masterKey', + 'serverURL', + 'collectionPrefix', + 'filesController', + 'pushController', + 'loggerController', + 'hooksController', + 'userController', + 'authDataManager', + 'liveQueryController', + 'enableConfigChanges', + '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 = 'silly'; + 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; + }, + + getUnlocked: _ => { + let settings = cache.apps.get(appId); + + let settingsString = JSON.stringify(settings, (k, v) => { + if (!lockedSettings.includes(k)) { + if (v === undefined) return null; + return v; + } + }); + + return JSON.parse(settingsString); + }, + + setDefined: settings => definedSettings = settings + }; + + function getSettingsCollection() { + let config = cache.apps.get(appId); + return database.getDatabaseConnection(appId, config.collectionPrefix).adaptiveCollection(settingsCollectionName); + } + + function isLocked(update) { + var isLocked = lockedSettings.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/middlewares.js b/src/middlewares.js index d56840b4b0..e5a44b69c6 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 + .catch(error => { + log.error('error retrieving server settings from database', error); + throw new Parse.Error(Parse.Error.UNKNOWN_ERROR, error); + }) + .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); + } + }); + }); + } var allowCrossDomain = function(req, res, next) { From bda6744254636a50d2077bd74caabf8bf0205fde Mon Sep 17 00:00:00 2001 From: Matt Mason Date: Wed, 27 Apr 2016 10:46:30 -0700 Subject: [PATCH 2/9] Clarity improvements & test fixes for settings manager --- spec/FileLoggerAdapter.spec.js | 11 ++++++----- spec/PersistentSettings.spec.js | 34 ++++++++++++++++++--------------- src/ParseServer.js | 4 ++-- src/Routers/SettingsRouter.js | 8 +++----- src/SettingsManager.js | 20 +++++++++++-------- src/middlewares.js | 2 +- 6 files changed, 43 insertions(+), 36 deletions(-) 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 index a9ef9785a6..3b85348c5c 100644 --- a/spec/PersistentSettings.spec.js +++ b/spec/PersistentSettings.spec.js @@ -1,16 +1,15 @@ var request = require('request'); var deepcopy = require('deepcopy'); -var DatabaseAdapter = require('../src/DatabaseAdapter'); -var database = DatabaseAdapter.getDatabaseConnection('test', 'test_'); -var settingsCollection = '_ServerSettings'; +var Config = require('../src/Config'); var logger = require('../src/logger').default; +var settingsCollectionName = '_ServerSettings'; var configuration; +var settingsCollection; describe('Persistent Settings', () => { beforeEach((done) => { configuration = deepcopy(defaultConfiguration); - configuration.verbose = true; configuration.enableConfigChanges = true; newServer().then(done); }); @@ -65,7 +64,8 @@ describe('Persistent Settings', () => { .then(res => expect(res.res.statusCode).toBe(200)) .then(_ => endpoint.post({ clientKey: 'causesError' })) .then(res => { - expect(res.res.statusCode).toBe(403); + 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) @@ -73,16 +73,16 @@ describe('Persistent Settings', () => { }); it('should run setting callbacks such as configureLogger', (done) => { - endpoint.post({ logLevel: 'silly' }) + endpoint.post({ logLevel: 'debug' }) .then(res => { expect(res.res.statusCode).toBe(200); - expect(res.body.logLevel).toBe('silly'); - expect(logger.transports['parse-server'].level).toBe('silly'); + 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('silly'); + expect(res.body.logLevel).toBe('debug'); }) .then(done) .catch(done.fail); @@ -126,7 +126,7 @@ describe('Persistent Settings', () => { it('should expose non-existant settings as null', (done) => { delete configuration.clientKey; - database.deleteEverything() + settingsCollection.drop() .then(newServer) .then(endpoint.get) .then(res => expect(res.body.clientKey).toBe(null)) @@ -137,7 +137,7 @@ describe('Persistent Settings', () => { it('should fetch database values', (done) => { delete configuration.clientKey; - database.deleteEverything() + settingsCollection.drop() .then(newServer) .then(endpoint.get) .then(res => expect(res.body.clientKey).toBe(null)) @@ -163,20 +163,24 @@ describe('Persistent Settings', () => { function newServer() { setServerConfiguration(deepcopy(configuration)); - return parseServerObject.config.settingsInitialized; + 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(_ => database.adaptiveCollection(settingsCollection)) - .then(coll => coll.upsertOne({ applicationId: configuration.appId }, { $set: settings })) + .then(_ => settingsCollection.upsertOne({ applicationId: configuration.appId }, { $set: settings })) .then(_ => undefined); } function getPersisted() { return parseServerObject.config.settingsInitialized - .then(_ => database.mongoFind(settingsCollection, {}, {})) + .then(_ => settingsCollection.find({ applicationId: configuration.appId })) .then(results => results && results.length && results[0]); } diff --git a/src/ParseServer.js b/src/ParseServer.js index 7a4eb15a80..1a415bc15e 100644 --- a/src/ParseServer.js +++ b/src/ParseServer.js @@ -121,7 +121,7 @@ class ParseServer { liveQuery = {}, sessionLength = 31536000, // 1 Year in seconds verbose = false, - logLevel = 'error', + logLevel = 'info', enableConfigChanges = process.env.ENABLE_CONFIG_CHANGES, lockDefinedSettings = true } = definedSettings; @@ -226,7 +226,7 @@ class ParseServer { this.config.settingsInitialized = settingsManager.pull() .then(persistedSettings => { settingsManager.updateCache(persistedSettings); - return settingsManager.push(settingsManager.getUnlocked()); + return settingsManager.push(settingsManager.getVisible()); }); } diff --git a/src/Routers/SettingsRouter.js b/src/Routers/SettingsRouter.js index f21373e1a7..51f321197a 100644 --- a/src/Routers/SettingsRouter.js +++ b/src/Routers/SettingsRouter.js @@ -3,12 +3,13 @@ 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: SettingsManager(req.config.applicationId).getUnlocked() + response: SettingsManager(req.config.applicationId).getVisible() }); }); @@ -20,10 +21,7 @@ export class SettingsRouter extends PromiseRouter { return settingsManager.push(updatedSettings) .then(_ => ({ response: updatedSettings })); } else { - return Promise.reject({ - status: 403, - message: 'Server config changes are disabled' - }); + return Promise.reject(new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, 'Server config changes are disabled')); } }); } diff --git a/src/SettingsManager.js b/src/SettingsManager.js index 419fc47f8b..896e19fdf9 100644 --- a/src/SettingsManager.js +++ b/src/SettingsManager.js @@ -5,17 +5,22 @@ let deepcopy = require('deepcopy'); import { logger, configureLogger } from './logger'; import cache from './cache'; +import Config from './Config'; let authDataManager = require('./authDataManager'); -let database = require('./DatabaseAdapter'); + const settingsCollectionName = '_ServerSettings'; -// doesn't make sense to expose / modify these settings +// visible locked settings const lockedSettings = [ 'applicationId', 'masterKey', 'serverURL', 'collectionPrefix', + 'enableConfigChanges', +]; +// hidden locked settings +const hiddenSettings = [ 'filesController', 'pushController', 'loggerController', @@ -23,7 +28,6 @@ const lockedSettings = [ 'userController', 'authDataManager', 'liveQueryController', - 'enableConfigChanges', 'settingsInitialized' ]; @@ -83,11 +87,11 @@ export default function SettingsManager(appId) { return updates; }, - getUnlocked: _ => { + getVisible: _ => { let settings = cache.apps.get(appId); let settingsString = JSON.stringify(settings, (k, v) => { - if (!lockedSettings.includes(k)) { + if (!hiddenSettings.includes(k)) { if (v === undefined) return null; return v; } @@ -100,12 +104,12 @@ export default function SettingsManager(appId) { }; function getSettingsCollection() { - let config = cache.apps.get(appId); - return database.getDatabaseConnection(appId, config.collectionPrefix).adaptiveCollection(settingsCollectionName); + let config = new Config(appId); + return config.database.adapter.adaptiveCollection(settingsCollectionName); } function isLocked(update) { - var isLocked = lockedSettings.includes(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; } diff --git a/src/middlewares.js b/src/middlewares.js index e5a44b69c6..8a6372ee45 100644 --- a/src/middlewares.js +++ b/src/middlewares.js @@ -95,7 +95,7 @@ function handleParseHeaders(req, res, next) { return fetchConfiguration .catch(error => { log.error('error retrieving server settings from database', error); - throw new Parse.Error(Parse.Error.UNKNOWN_ERROR, error); + throw new Parse.Error(Parse.Error.INTERNAL_SERVER_ERROR, error); }) .then(_ => { req.config = new Config(info.appId, mount); From b4fea9e5c7b59f130658c024cc98f31288fb6b41 Mon Sep 17 00:00:00 2001 From: Matt Mason Date: Thu, 28 Apr 2016 09:02:15 -0700 Subject: [PATCH 3/9] Updated cli-defs, features router, removed global test object --- spec/PersistentSettings.spec.js | 3 +- spec/helper.js | 311 ++++++++++++++++---------------- src/Routers/FeaturesRouter.js | 4 + src/Routers/SettingsRouter.js | 2 +- src/SettingsManager.js | 6 +- src/cli/cli-definitions.js | 10 + src/middlewares.js | 2 +- 7 files changed, 179 insertions(+), 159 deletions(-) diff --git a/spec/PersistentSettings.spec.js b/spec/PersistentSettings.spec.js index 3b85348c5c..0208d6ef1c 100644 --- a/spec/PersistentSettings.spec.js +++ b/spec/PersistentSettings.spec.js @@ -6,6 +6,7 @@ var logger = require('../src/logger').default; var settingsCollectionName = '_ServerSettings'; var configuration; var settingsCollection; +var parseServerObject; describe('Persistent Settings', () => { beforeEach((done) => { @@ -162,7 +163,7 @@ describe('Persistent Settings', () => { }); function newServer() { - setServerConfiguration(deepcopy(configuration)); + parseServerObject = setServerConfiguration(deepcopy(configuration)); return parseServerObject.config.settingsInitialized .then(_ => { var config = new Config(configuration.appId); diff --git a/spec/helper.js b/spec/helper.js index a9b1dd1257..0409a5e47a 100644 --- a/spec/helper.js +++ b/spec/helper.js @@ -15,37 +15,37 @@ var port = 8378; // Default server configuration for tests. var defaultConfiguration = { - databaseURI: databaseURI, - cloud: cloudMain, - serverURL: 'http://localhost:' + port + '/1', - appId: 'test', - javascriptKey: 'test', - dotNetKey: 'windows', - clientKey: 'client', - restAPIKey: 'rest', - masterKey: 'test', - collectionPrefix: 'test_', - fileKey: 'test', - push: { - 'ios': { - cert: 'prodCert.pem', - key: 'prodKey.pem', - production: true, - bundleId: 'bundleId' - } - }, - oauth: { // Override the facebook provider - facebook: mockFacebook(), - myoauth: { - module: path.resolve(__dirname, "myoauth") // relative path as it's run from src + databaseURI: databaseURI, + cloud: cloudMain, + serverURL: 'http://localhost:' + port + '/1', + appId: 'test', + javascriptKey: 'test', + dotNetKey: 'windows', + clientKey: 'client', + restAPIKey: 'rest', + masterKey: 'test', + collectionPrefix: 'test_', + fileKey: 'test', + push: { + 'ios': { + cert: 'prodCert.pem', + key: 'prodKey.pem', + production: true, + bundleId: 'bundleId' + } + }, + oauth: { // Override the facebook provider + facebook: mockFacebook(), + myoauth: { + module: path.resolve(__dirname, "myoauth") // relative path as it's run from src + } } - } }; // Set up a default API server for testing with default configuration. -global.parseServerObject = new ParseServer(defaultConfiguration); +var parseServer = new ParseServer(defaultConfiguration); var app = express(); -app.use('/1', global.parseServerObject.app); +app.use('/1', parseServer.app); var server = app.listen(port); // Prevent reinitializing the server from clobbering Cloud Code @@ -54,18 +54,19 @@ delete defaultConfiguration.cloud; var currentConfiguration; // Allows testing specific configurations of Parse Server var setServerConfiguration = configuration => { - // the configuration hasn't changed - if (configuration === currentConfiguration) { - return; - } - DatabaseAdapter.clearDatabaseSettings(); - currentConfiguration = configuration; - server.close(); - cache.clearCache(); - app = express(); - global.parseServerObject = new ParseServer(configuration); - app.use('/1', global.parseServerObject.app); - server = app.listen(port); + // the configuration hasn't changed + if (configuration === currentConfiguration) { + return;H + } + DatabaseAdapter.clearDatabaseSettings(); + currentConfiguration = configuration; + server.close(); + cache.clearCache(); + app = express(); + parseServer = new ParseServer(configuration); + app.use('/1', parseServer.app); + server = app.listen(port); + return parseServer; }; var restoreServerConfiguration = () => setServerConfiguration(defaultConfiguration); @@ -79,58 +80,58 @@ Parse.serverURL = 'http://localhost:' + port + '/1'; Parse.Promise.disableAPlusCompliant(); beforeEach(function(done) { - restoreServerConfiguration(); - Parse.initialize('test', 'test', 'test'); - Parse.serverURL = 'http://localhost:' + port + '/1'; - Parse.User.enableUnsafeCurrentUser(); - done(); + restoreServerConfiguration(); + Parse.initialize('test', 'test', 'test'); + Parse.serverURL = 'http://localhost:' + port + '/1'; + Parse.User.enableUnsafeCurrentUser(); + done(); }); afterEach(function(done) { - Parse.User.logOut().then(() => { - return clearData(); - }).then(() => { - done(); - }, (error) => { - console.log('error in clearData', error); - done(); - }); + Parse.User.logOut().then(() => { + return clearData(); + }).then(() => { + done(); + }, (error) => { + console.log('error in clearData', error); + done(); + }); }); var TestObject = Parse.Object.extend({ - className: "TestObject" + className: "TestObject" }); var Item = Parse.Object.extend({ - className: "Item" + className: "Item" }); var Container = Parse.Object.extend({ - className: "Container" + className: "Container" }); // Convenience method to create a new TestObject with a callback function create(options, callback) { - var t = new TestObject(options); - t.save(null, { success: callback }); + var t = new TestObject(options); + t.save(null, { success: callback }); } function createTestUser(success, error) { - var user = new Parse.User(); - user.set('username', 'test'); - user.set('password', 'moon-y'); - var promise = user.signUp(); - if (success || error) { - promise.then(function(user) { - if (success) { - success(user); - } - }, function(err) { - if (error) { - error(err); - } - }); - } else { - return promise; - } + var user = new Parse.User(); + user.set('username', 'test'); + user.set('password', 'moon-y'); + var promise = user.signUp(); + if (success || error) { + promise.then(function(user) { + if (success) { + success(user); + } + }, function(err) { + if (error) { + error(err); + } + }); + } else { + return promise; + } } // Mark the tests that are known to not work. @@ -138,106 +139,111 @@ function notWorking() {} // Shims for compatibility with the old qunit tests. function ok(bool, message) { - expect(bool).toBeTruthy(message); + expect(bool).toBeTruthy(message); } + function equal(a, b, message) { - expect(a).toEqual(b, message); + expect(a).toEqual(b, message); } + function strictEqual(a, b, message) { - expect(a).toBe(b, message); + expect(a).toBe(b, message); } + function notEqual(a, b, message) { - expect(a).not.toEqual(b, message); + expect(a).not.toEqual(b, message); } + function expectSuccess(params) { - return { - success: params.success, - error: function(e) { - console.log('got error', e); - fail('failure happened in expectSuccess'); - }, - } + return { + success: params.success, + error: function(e) { + console.log('got error', e); + fail('failure happened in expectSuccess'); + }, + } } + function expectError(errorCode, callback) { - return { - success: function(result) { - console.log('got result', result); - fail('expected error but got success'); - }, - error: function(obj, e) { - // Some methods provide 2 parameters. - e = e || obj; - if (!e) { - fail('expected a specific error but got a blank error'); - return; - } - expect(e.code).toEqual(errorCode, e.message); - if (callback) { - callback(e); - } - }, - } + return { + success: function(result) { + console.log('got result', result); + fail('expected error but got success'); + }, + error: function(obj, e) { + // Some methods provide 2 parameters. + e = e || obj; + if (!e) { + fail('expected a specific error but got a blank error'); + return; + } + expect(e.code).toEqual(errorCode, e.message); + if (callback) { + callback(e); + } + }, + } } // Because node doesn't have Parse._.contains function arrayContains(arr, item) { - return -1 != arr.indexOf(item); + return -1 != arr.indexOf(item); } // Normalizes a JSON object. function normalize(obj) { - if (typeof obj !== 'object') { - return JSON.stringify(obj); - } - if (obj instanceof Array) { - return '[' + obj.map(normalize).join(', ') + ']'; - } - var answer = '{'; - for (var key of Object.keys(obj).sort()) { - answer += key + ': '; - answer += normalize(obj[key]); - answer += ', '; - } - answer += '}'; - return answer; + if (typeof obj !== 'object') { + return JSON.stringify(obj); + } + if (obj instanceof Array) { + return '[' + obj.map(normalize).join(', ') + ']'; + } + var answer = '{'; + for (var key of Object.keys(obj).sort()) { + answer += key + ': '; + answer += normalize(obj[key]); + answer += ', '; + } + answer += '}'; + return answer; } // Asserts two json structures are equal. function jequal(o1, o2) { - expect(normalize(o1)).toEqual(normalize(o2)); + expect(normalize(o1)).toEqual(normalize(o2)); } function range(n) { - var answer = []; - for (var i = 0; i < n; i++) { - answer.push(i); - } - return answer; + var answer = []; + for (var i = 0; i < n; i++) { + answer.push(i); + } + return answer; } function mockFacebook() { - var facebook = {}; - facebook.validateAuthData = function(authData) { - if (authData.id === '8675309' && authData.access_token === 'jenny') { - return Promise.resolve(); - } - return Promise.reject(); - }; - facebook.validateAppId = function(appId, authData) { - if (authData.access_token === 'jenny') { - return Promise.resolve(); - } - return Promise.reject(); - }; - return facebook; + var facebook = {}; + facebook.validateAuthData = function(authData) { + if (authData.id === '8675309' && authData.access_token === 'jenny') { + return Promise.resolve(); + } + return Promise.reject(); + }; + facebook.validateAppId = function(appId, authData) { + if (authData.access_token === 'jenny') { + return Promise.resolve(); + } + return Promise.reject(); + }; + return facebook; } function clearData() { - var promises = []; - for (var conn in DatabaseAdapter.dbConnections) { - promises.push(DatabaseAdapter.dbConnections[conn].deleteEverything()); - } - return Promise.all(promises); + var promises = []; + for (var conn in DatabaseAdapter.dbConnections) { + promises.push(DatabaseAdapter.dbConnections[conn].deleteEverything()); + } + return Promise.all(promises); } // This is polluting, but, it makes it way easier to directly port old tests. @@ -259,23 +265,22 @@ global.jequal = jequal; global.range = range; global.setServerConfiguration = setServerConfiguration; global.defaultConfiguration = defaultConfiguration; -global.parseServerObject = global.parseServerObject; // LiveQuery test setting require('../src/LiveQuery/PLog').logLevel = 'NONE'; var libraryCache = {}; jasmine.mockLibrary = function(library, name, mock) { - var original = require(library)[name]; - if (!libraryCache[library]) { - libraryCache[library] = {}; - } - require(library)[name] = mock; - libraryCache[library][name] = original; + var original = require(library)[name]; + if (!libraryCache[library]) { + libraryCache[library] = {}; + } + require(library)[name] = mock; + libraryCache[library][name] = original; } jasmine.restoreLibrary = function(library, name) { - if (!libraryCache[library] || !libraryCache[library][name]) { - throw 'Can not find library ' + library + ' ' + name; - } - require(library)[name] = libraryCache[library][name]; + if (!libraryCache[library] || !libraryCache[library][name]) { + throw 'Can not find library ' + library + ' ' + name; + } + require(library)[name] = libraryCache[library][name]; } 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 51f321197a..af4648f618 100644 --- a/src/Routers/SettingsRouter.js +++ b/src/Routers/SettingsRouter.js @@ -19,7 +19,7 @@ export class SettingsRouter extends PromiseRouter { let settingsManager = SettingsManager(req.config.applicationId); var updatedSettings = settingsManager.updateCache(body); return settingsManager.push(updatedSettings) - .then(_ => ({ response: updatedSettings })); + .then(() => ({ response: updatedSettings })); } else { return Promise.reject(new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, 'Server config changes are disabled')); } diff --git a/src/SettingsManager.js b/src/SettingsManager.js index 896e19fdf9..2f70cf6aec 100644 --- a/src/SettingsManager.js +++ b/src/SettingsManager.js @@ -49,7 +49,7 @@ let onChange = { export default function SettingsManager(appId) { return { - pull: _ => { + pull: () => { return getSettingsCollection(appId) .then(coll => coll.find({ applicationId: appId }, { limit: 1 })) .then(results => { @@ -65,7 +65,7 @@ export default function SettingsManager(appId) { settings.applicationId = appId; return getSettingsCollection(appId) .then(coll => coll.upsertOne({applicationId: appId}, { $set: settings })) - .then(_ => { + .then(() => { logger.verbose('Pushed settings: ' + JSON.stringify(settings, null, 2)); }); }, @@ -87,7 +87,7 @@ export default function SettingsManager(appId) { return updates; }, - getVisible: _ => { + getVisible: () => { let settings = cache.apps.get(appId); let settingsString = JSON.stringify(settings, (k, v) => { diff --git a/src/cli/cli-definitions.js b/src/cli/cli-definitions.js index dd908bcd04..db2edfafab 100644 --- a/src/cli/cli-definitions.js +++ b/src/cli/cli-definitions.js @@ -174,5 +174,15 @@ export default { "verbose": { env: "VERBOSE", help: "Set the logging to verbose" + }, + "logLevel": { + help: "Sets the log level" + }, + "enableConfigChanges": { + env: "ENABLE_CONFIG_CHANGES" + help: "Allows server settings to be modified via the settings endpoint, defaults to false" + }, + "lockDefinedSettings": { + help: "Disallows modification of code/cli defined settings, defaults to true" } }; diff --git a/src/middlewares.js b/src/middlewares.js index 8a6372ee45..ff9de80096 100644 --- a/src/middlewares.js +++ b/src/middlewares.js @@ -97,7 +97,7 @@ function handleParseHeaders(req, res, next) { log.error('error retrieving server settings from database', error); throw new Parse.Error(Parse.Error.INTERNAL_SERVER_ERROR, error); }) - .then(_ => { + .then(() => { req.config = new Config(info.appId, mount); req.info = info; From f2f8c792370a3136cf46b17e7c08baeb6bbe20b8 Mon Sep 17 00:00:00 2001 From: Matt Mason Date: Thu, 28 Apr 2016 09:07:49 -0700 Subject: [PATCH 4/9] Fixed cli definitions syntax error --- src/cli/cli-definitions.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cli/cli-definitions.js b/src/cli/cli-definitions.js index db2edfafab..0ea77b8fb9 100644 --- a/src/cli/cli-definitions.js +++ b/src/cli/cli-definitions.js @@ -179,7 +179,7 @@ export default { help: "Sets the log level" }, "enableConfigChanges": { - env: "ENABLE_CONFIG_CHANGES" + env: "ENABLE_CONFIG_CHANGES", help: "Allows server settings to be modified via the settings endpoint, defaults to false" }, "lockDefinedSettings": { From 34f2394b861b10b33f0d3d2cd749b2ef2c732c95 Mon Sep 17 00:00:00 2001 From: Matt Mason Date: Thu, 28 Apr 2016 16:54:53 -0700 Subject: [PATCH 5/9] Fixed formatting issues --- spec/helper.js | 304 ++++++++++++++++++++++++------------------------- 1 file changed, 152 insertions(+), 152 deletions(-) diff --git a/spec/helper.js b/spec/helper.js index 0409a5e47a..b0a3f56189 100644 --- a/spec/helper.js +++ b/spec/helper.js @@ -15,31 +15,31 @@ var port = 8378; // Default server configuration for tests. var defaultConfiguration = { - databaseURI: databaseURI, - cloud: cloudMain, - serverURL: 'http://localhost:' + port + '/1', - appId: 'test', - javascriptKey: 'test', - dotNetKey: 'windows', - clientKey: 'client', - restAPIKey: 'rest', - masterKey: 'test', - collectionPrefix: 'test_', - fileKey: 'test', - push: { - 'ios': { - cert: 'prodCert.pem', - key: 'prodKey.pem', - production: true, - bundleId: 'bundleId' - } - }, - oauth: { // Override the facebook provider - facebook: mockFacebook(), - myoauth: { - module: path.resolve(__dirname, "myoauth") // relative path as it's run from src - } + databaseURI: databaseURI, + cloud: cloudMain, + serverURL: 'http://localhost:' + port + '/1', + appId: 'test', + javascriptKey: 'test', + dotNetKey: 'windows', + clientKey: 'client', + restAPIKey: 'rest', + masterKey: 'test', + collectionPrefix: 'test_', + fileKey: 'test', + push: { + 'ios': { + cert: 'prodCert.pem', + key: 'prodKey.pem', + production: true, + bundleId: 'bundleId' + } + }, + oauth: { // Override the facebook provider + facebook: mockFacebook(), + myoauth: { + module: path.resolve(__dirname, "myoauth") // relative path as it's run from src } + } }; // Set up a default API server for testing with default configuration. @@ -54,19 +54,19 @@ delete defaultConfiguration.cloud; var currentConfiguration; // Allows testing specific configurations of Parse Server var setServerConfiguration = configuration => { - // the configuration hasn't changed - if (configuration === currentConfiguration) { - return;H - } - DatabaseAdapter.clearDatabaseSettings(); - currentConfiguration = configuration; - server.close(); - cache.clearCache(); - app = express(); - parseServer = new ParseServer(configuration); - app.use('/1', parseServer.app); - server = app.listen(port); - return parseServer; + // the configuration hasn't changed + if (configuration === currentConfiguration) { + return; + } + DatabaseAdapter.clearDatabaseSettings(); + currentConfiguration = configuration; + server.close(); + cache.clearCache(); + app = express(); + parseServer = new ParseServer(configuration); + app.use('/1', parseServer.app); + server = app.listen(port); + return parseServer; }; var restoreServerConfiguration = () => setServerConfiguration(defaultConfiguration); @@ -80,58 +80,58 @@ Parse.serverURL = 'http://localhost:' + port + '/1'; Parse.Promise.disableAPlusCompliant(); beforeEach(function(done) { - restoreServerConfiguration(); - Parse.initialize('test', 'test', 'test'); - Parse.serverURL = 'http://localhost:' + port + '/1'; - Parse.User.enableUnsafeCurrentUser(); - done(); + restoreServerConfiguration(); + Parse.initialize('test', 'test', 'test'); + Parse.serverURL = 'http://localhost:' + port + '/1'; + Parse.User.enableUnsafeCurrentUser(); + done(); }); afterEach(function(done) { - Parse.User.logOut().then(() => { - return clearData(); - }).then(() => { - done(); - }, (error) => { - console.log('error in clearData', error); - done(); - }); + Parse.User.logOut().then(() => { + return clearData(); + }).then(() => { + done(); + }, (error) => { + console.log('error in clearData', error); + done(); + }); }); var TestObject = Parse.Object.extend({ - className: "TestObject" + className: "TestObject" }); var Item = Parse.Object.extend({ - className: "Item" + className: "Item" }); var Container = Parse.Object.extend({ - className: "Container" + className: "Container" }); // Convenience method to create a new TestObject with a callback function create(options, callback) { - var t = new TestObject(options); - t.save(null, { success: callback }); + var t = new TestObject(options); + t.save(null, { success: callback }); } function createTestUser(success, error) { - var user = new Parse.User(); - user.set('username', 'test'); - user.set('password', 'moon-y'); - var promise = user.signUp(); - if (success || error) { - promise.then(function(user) { - if (success) { - success(user); - } - }, function(err) { - if (error) { - error(err); - } - }); - } else { - return promise; - } + var user = new Parse.User(); + user.set('username', 'test'); + user.set('password', 'moon-y'); + var promise = user.signUp(); + if (success || error) { + promise.then(function(user) { + if (success) { + success(user); + } + }, function(err) { + if (error) { + error(err); + } + }); + } else { + return promise; + } } // Mark the tests that are known to not work. @@ -139,111 +139,111 @@ function notWorking() {} // Shims for compatibility with the old qunit tests. function ok(bool, message) { - expect(bool).toBeTruthy(message); + expect(bool).toBeTruthy(message); } function equal(a, b, message) { - expect(a).toEqual(b, message); + expect(a).toEqual(b, message); } function strictEqual(a, b, message) { - expect(a).toBe(b, message); + expect(a).toBe(b, message); } function notEqual(a, b, message) { - expect(a).not.toEqual(b, message); + expect(a).not.toEqual(b, message); } function expectSuccess(params) { - return { - success: params.success, - error: function(e) { - console.log('got error', e); - fail('failure happened in expectSuccess'); - }, - } + return { + success: params.success, + error: function(e) { + console.log('got error', e); + fail('failure happened in expectSuccess'); + }, + } } function expectError(errorCode, callback) { - return { - success: function(result) { - console.log('got result', result); - fail('expected error but got success'); - }, - error: function(obj, e) { - // Some methods provide 2 parameters. - e = e || obj; - if (!e) { - fail('expected a specific error but got a blank error'); - return; - } - expect(e.code).toEqual(errorCode, e.message); - if (callback) { - callback(e); - } - }, - } + return { + success: function(result) { + console.log('got result', result); + fail('expected error but got success'); + }, + error: function(obj, e) { + // Some methods provide 2 parameters. + e = e || obj; + if (!e) { + fail('expected a specific error but got a blank error'); + return; + } + expect(e.code).toEqual(errorCode, e.message); + if (callback) { + callback(e); + } + }, + } } // Because node doesn't have Parse._.contains function arrayContains(arr, item) { - return -1 != arr.indexOf(item); + return -1 != arr.indexOf(item); } // Normalizes a JSON object. function normalize(obj) { - if (typeof obj !== 'object') { - return JSON.stringify(obj); - } - if (obj instanceof Array) { - return '[' + obj.map(normalize).join(', ') + ']'; - } - var answer = '{'; - for (var key of Object.keys(obj).sort()) { - answer += key + ': '; - answer += normalize(obj[key]); - answer += ', '; - } - answer += '}'; - return answer; + if (typeof obj !== 'object') { + return JSON.stringify(obj); + } + if (obj instanceof Array) { + return '[' + obj.map(normalize).join(', ') + ']'; + } + var answer = '{'; + for (var key of Object.keys(obj).sort()) { + answer += key + ': '; + answer += normalize(obj[key]); + answer += ', '; + } + answer += '}'; + return answer; } // Asserts two json structures are equal. function jequal(o1, o2) { - expect(normalize(o1)).toEqual(normalize(o2)); + expect(normalize(o1)).toEqual(normalize(o2)); } function range(n) { - var answer = []; - for (var i = 0; i < n; i++) { - answer.push(i); - } - return answer; + var answer = []; + for (var i = 0; i < n; i++) { + answer.push(i); + } + return answer; } function mockFacebook() { - var facebook = {}; - facebook.validateAuthData = function(authData) { - if (authData.id === '8675309' && authData.access_token === 'jenny') { - return Promise.resolve(); - } - return Promise.reject(); - }; - facebook.validateAppId = function(appId, authData) { - if (authData.access_token === 'jenny') { - return Promise.resolve(); - } - return Promise.reject(); - }; - return facebook; + var facebook = {}; + facebook.validateAuthData = function(authData) { + if (authData.id === '8675309' && authData.access_token === 'jenny') { + return Promise.resolve(); + } + return Promise.reject(); + }; + facebook.validateAppId = function(appId, authData) { + if (authData.access_token === 'jenny') { + return Promise.resolve(); + } + return Promise.reject(); + }; + return facebook; } function clearData() { - var promises = []; - for (var conn in DatabaseAdapter.dbConnections) { - promises.push(DatabaseAdapter.dbConnections[conn].deleteEverything()); - } - return Promise.all(promises); + var promises = []; + for (var conn in DatabaseAdapter.dbConnections) { + promises.push(DatabaseAdapter.dbConnections[conn].deleteEverything()); + } + return Promise.all(promises); } // This is polluting, but, it makes it way easier to directly port old tests. @@ -270,17 +270,17 @@ global.defaultConfiguration = defaultConfiguration; require('../src/LiveQuery/PLog').logLevel = 'NONE'; var libraryCache = {}; jasmine.mockLibrary = function(library, name, mock) { - var original = require(library)[name]; - if (!libraryCache[library]) { - libraryCache[library] = {}; - } - require(library)[name] = mock; - libraryCache[library][name] = original; + var original = require(library)[name]; + if (!libraryCache[library]) { + libraryCache[library] = {}; + } + require(library)[name] = mock; + libraryCache[library][name] = original; } jasmine.restoreLibrary = function(library, name) { - if (!libraryCache[library] || !libraryCache[library][name]) { - throw 'Can not find library ' + library + ' ' + name; - } - require(library)[name] = libraryCache[library][name]; -} + if (!libraryCache[library] || !libraryCache[library][name]) { + throw 'Can not find library ' + library + ' ' + name; + } + require(library)[name] = libraryCache[library][name]; +} \ No newline at end of file From 6a428ec814fdeae5708dfbfdcb9ab84e1ffa6631 Mon Sep 17 00:00:00 2001 From: Matt Mason Date: Mon, 9 May 2016 13:45:44 -0700 Subject: [PATCH 6/9] Removed ENABLE_CONFIG_CHANGES env var & improved doc --- src/ParseServer.js | 5 +++-- src/cli/cli-definitions.js | 3 +-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/ParseServer.js b/src/ParseServer.js index 1a415bc15e..b901ece0cc 100644 --- a/src/ParseServer.js +++ b/src/ParseServer.js @@ -79,7 +79,8 @@ 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 +// "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 { @@ -122,7 +123,7 @@ class ParseServer { sessionLength = 31536000, // 1 Year in seconds verbose = false, logLevel = 'info', - enableConfigChanges = process.env.ENABLE_CONFIG_CHANGES, + enableConfigChanges = true, lockDefinedSettings = true } = definedSettings; // Initialize the node client SDK automatically diff --git a/src/cli/cli-definitions.js b/src/cli/cli-definitions.js index 0ea77b8fb9..1dab5649c0 100644 --- a/src/cli/cli-definitions.js +++ b/src/cli/cli-definitions.js @@ -179,8 +179,7 @@ export default { help: "Sets the log level" }, "enableConfigChanges": { - env: "ENABLE_CONFIG_CHANGES", - help: "Allows server settings to be modified via the settings endpoint, defaults to false" + help: "Disable to improve performance and lock configuration, defaults to true" }, "lockDefinedSettings": { help: "Disallows modification of code/cli defined settings, defaults to true" From bf457726a7e1dcb37c428ff79462d47eecc7ac0d Mon Sep 17 00:00:00 2001 From: Matt Mason Date: Mon, 9 May 2016 14:03:46 -0700 Subject: [PATCH 7/9] Disable config changes for most tests --- spec/helper.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/spec/helper.js b/spec/helper.js index b0a3f56189..b4050f2374 100644 --- a/spec/helper.js +++ b/spec/helper.js @@ -39,7 +39,8 @@ 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. From f6a89508c5b77edc98f3b03de8d41140d2490ab7 Mon Sep 17 00:00:00 2001 From: Matt Mason Date: Fri, 13 May 2016 13:11:23 -0700 Subject: [PATCH 8/9] test fixes --- spec/PublicAPI.spec.js | 3 ++- spec/index.spec.js | 2 +- src/middlewares.js | 8 ++++---- 3 files changed, 7 insertions(+), 6 deletions(-) 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/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/middlewares.js b/src/middlewares.js index ff9de80096..128798972a 100644 --- a/src/middlewares.js +++ b/src/middlewares.js @@ -93,10 +93,6 @@ function handleParseHeaders(req, res, next) { } return fetchConfiguration - .catch(error => { - log.error('error retrieving server settings from database', error); - throw new Parse.Error(Parse.Error.INTERNAL_SERVER_ERROR, error); - }) .then(() => { req.config = new Config(info.appId, mount); req.info = info; @@ -152,6 +148,10 @@ function handleParseHeaders(req, res, next) { 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)); }); } From 92dd7156044d4bbf32691f45d8fcec2d5176232b Mon Sep 17 00:00:00 2001 From: Matt Mason Date: Fri, 13 May 2016 13:40:24 -0700 Subject: [PATCH 9/9] Correct handling of verbose --- src/SettingsManager.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/SettingsManager.js b/src/SettingsManager.js index 2f70cf6aec..9d72f132ea 100644 --- a/src/SettingsManager.js +++ b/src/SettingsManager.js @@ -39,7 +39,7 @@ let authChange = settings => settings.authDataManager = authDataManager(settings let onChange = { logLevel: settings => configureLogger({ level: settings.logLevel }), verbose: settings => { - settings.logLevel = 'silly'; + settings.logLevel = settings.verbose? 'silly': 'info'; configureLogger({ level: settings.logLevel }); }, oauth: authChange,