diff --git a/spec/Middlewares.spec.js b/spec/Middlewares.spec.js
index c0fcb659e5..57dca22b0e 100644
--- a/spec/Middlewares.spec.js
+++ b/spec/Middlewares.spec.js
@@ -46,32 +46,32 @@ describe('middlewares', () => {
});
});
- it('should give invalid response when keys are configured but no key supplied', () => {
+ it('should give invalid response when keys are configured but no key supplied', async () => {
AppCachePut(fakeReq.body._ApplicationId, {
masterKey: 'masterKey',
restAPIKey: 'restAPIKey',
});
- middlewares.handleParseHeaders(fakeReq, fakeRes);
+ await middlewares.handleParseHeaders(fakeReq, fakeRes);
expect(fakeRes.status).toHaveBeenCalledWith(403);
});
- it('should give invalid response when keys are configured but supplied key is incorrect', () => {
+ it('should give invalid response when keys are configured but supplied key is incorrect', async () => {
AppCachePut(fakeReq.body._ApplicationId, {
masterKey: 'masterKey',
restAPIKey: 'restAPIKey',
});
fakeReq.headers['x-parse-rest-api-key'] = 'wrongKey';
- middlewares.handleParseHeaders(fakeReq, fakeRes);
+ await middlewares.handleParseHeaders(fakeReq, fakeRes);
expect(fakeRes.status).toHaveBeenCalledWith(403);
});
- it('should give invalid response when keys are configured but different key is supplied', () => {
+ it('should give invalid response when keys are configured but different key is supplied', async () => {
AppCachePut(fakeReq.body._ApplicationId, {
masterKey: 'masterKey',
restAPIKey: 'restAPIKey',
});
fakeReq.headers['x-parse-client-key'] = 'clientKey';
- middlewares.handleParseHeaders(fakeReq, fakeRes);
+ await middlewares.handleParseHeaders(fakeReq, fakeRes);
expect(fakeRes.status).toHaveBeenCalledWith(403);
});
@@ -157,13 +157,7 @@ describe('middlewares', () => {
fakeReq.ip = '127.0.0.1';
fakeReq.headers['x-parse-master-key'] = 'masterKey';
- let error;
-
- try {
- await new Promise(resolve => middlewares.handleParseHeaders(fakeReq, fakeRes, resolve));
- } catch (err) {
- error = err;
- }
+ const error = await middlewares.handleParseHeaders(fakeReq, fakeRes, () => {}).catch(e => e);
expect(error).toBeDefined();
expect(error.message).toEqual(`unauthorized`);
@@ -182,13 +176,7 @@ describe('middlewares', () => {
fakeReq.ip = '10.0.0.2';
fakeReq.headers['x-parse-maintenance-key'] = 'masterKey';
- let error;
-
- try {
- await new Promise(resolve => middlewares.handleParseHeaders(fakeReq, fakeRes, resolve));
- } catch (err) {
- error = err;
- }
+ const error = await middlewares.handleParseHeaders(fakeReq, fakeRes, () => {}).catch(e => e);
expect(error).toBeDefined();
expect(error.message).toEqual(`unauthorized`);
diff --git a/spec/index.spec.js b/spec/index.spec.js
index 66654aaec4..1a2ea889a9 100644
--- a/spec/index.spec.js
+++ b/spec/index.spec.js
@@ -601,6 +601,63 @@ describe('server', () => {
await new Promise(resolve => server.close(resolve));
});
+ it('should load masterKey', async () => {
+ await reconfigureServer({
+ masterKey: () => 'testMasterKey',
+ masterKeyTtl: 1000, // TTL is set
+ });
+
+ await new Parse.Object('TestObject').save();
+
+ const config = Config.get(Parse.applicationId);
+ expect(config.masterKeyCache.masterKey).toEqual('testMasterKey');
+ expect(config.masterKeyCache.expiresAt.getTime()).toBeGreaterThan(Date.now());
+ });
+
+ it('should not reload if ttl is not set', async () => {
+ const masterKeySpy = jasmine.createSpy().and.returnValue(Promise.resolve('initialMasterKey'));
+
+ await reconfigureServer({
+ masterKey: masterKeySpy,
+ masterKeyTtl: null, // No TTL set
+ });
+
+ await new Parse.Object('TestObject').save();
+
+ const config = Config.get(Parse.applicationId);
+ const firstMasterKey = config.masterKeyCache.masterKey;
+
+ // Simulate calling the method again
+ await config.loadMasterKey();
+ const secondMasterKey = config.masterKeyCache.masterKey;
+
+ expect(firstMasterKey).toEqual('initialMasterKey');
+ expect(secondMasterKey).toEqual('initialMasterKey');
+ expect(masterKeySpy).toHaveBeenCalledTimes(1); // Should only be called once
+ expect(config.masterKeyCache.expiresAt).toBeNull(); // TTL is not set, so expiresAt should remain null
+ });
+
+ it('should reload masterKey if ttl is set and expired', async () => {
+ const masterKeySpy = jasmine.createSpy()
+ .and.returnValues(Promise.resolve('firstMasterKey'), Promise.resolve('secondMasterKey'));
+
+ await reconfigureServer({
+ masterKey: masterKeySpy,
+ masterKeyTtl: 1 / 1000, // TTL is set to 1ms
+ });
+
+ await new Parse.Object('TestObject').save();
+
+ await new Promise(resolve => setTimeout(resolve, 10));
+
+ await new Parse.Object('TestObject').save();
+
+ const config = Config.get(Parse.applicationId);
+ expect(masterKeySpy).toHaveBeenCalledTimes(2);
+ expect(config.masterKeyCache.masterKey).toEqual('secondMasterKey');
+ });
+
+
it('should not fail when Google signin is introduced without the optional clientId', done => {
const jwt = require('jsonwebtoken');
const authUtils = require('../lib/Adapters/Auth/utils');
diff --git a/src/Config.js b/src/Config.js
index c4884434ca..73bae0dc0d 100644
--- a/src/Config.js
+++ b/src/Config.js
@@ -724,6 +724,28 @@ export class Config {
return `${this.publicServerURL}/${this.pagesEndpoint}/${this.applicationId}/verify_email`;
}
+ async loadMasterKey() {
+ if (typeof this.masterKey === 'function') {
+ const ttlIsEmpty = !this.masterKeyTtl;
+ const isExpired = this.masterKeyCache?.expiresAt && this.masterKeyCache.expiresAt < new Date();
+
+ if ((!isExpired || ttlIsEmpty) && this.masterKeyCache?.masterKey) {
+ return this.masterKeyCache.masterKey;
+ }
+
+ const masterKey = await this.masterKey();
+
+ const expiresAt = this.masterKeyTtl ? new Date(Date.now() + 1000 * this.masterKeyTtl) : null
+ this.masterKeyCache = { masterKey, expiresAt };
+ Config.put(this);
+
+ return this.masterKeyCache.masterKey;
+ }
+
+ return this.masterKey;
+ }
+
+
// TODO: Remove this function once PagesRouter replaces the PublicAPIRouter;
// the (default) endpoint has to be defined in PagesRouter only.
get pagesEndpoint() {
diff --git a/src/Options/Definitions.js b/src/Options/Definitions.js
index a2696f44de..da0f040014 100644
--- a/src/Options/Definitions.js
+++ b/src/Options/Definitions.js
@@ -369,6 +369,12 @@ module.exports.ParseServerOptions = {
action: parsers.arrayParser,
default: ['127.0.0.1', '::1'],
},
+ masterKeyTtl: {
+ env: 'PARSE_SERVER_MASTER_KEY_TTL',
+ help:
+ '(Optional) The duration in seconds for which the current `masterKey` is being used before it is requested again if `masterKey` is set to a function. If `masterKey` is not set to a function, this option has no effect. Default is `0`, which means the master key is requested by invoking the `masterKey` function every time the master key is used internally by Parse Server.',
+ action: parsers.numberParser('masterKeyTtl'),
+ },
maxLimit: {
env: 'PARSE_SERVER_MAX_LIMIT',
help: 'Max value for limit option on queries, defaults to unlimited',
diff --git a/src/Options/docs.js b/src/Options/docs.js
index 75b22daf80..e1f89215f3 100644
--- a/src/Options/docs.js
+++ b/src/Options/docs.js
@@ -65,8 +65,9 @@
* @property {String} logsFolder Folder for the logs (defaults to './logs'); set to null to disable file based logging
* @property {String} maintenanceKey (Optional) The maintenance key is used for modifying internal and read-only fields of Parse Server.
⚠️ This key is not intended to be used as part of a regular operation of Parse Server. This key is intended to conduct out-of-band changes such as one-time migrations or data correction tasks. Internal fields are not officially documented and may change at any time without publication in release changelogs. We strongly advice not to rely on internal fields as part of your regular operation and to investigate the implications of any planned changes *directly in the source code* of your current version of Parse Server.
* @property {String[]} maintenanceKeyIps (Optional) Restricts the use of maintenance key permissions to a list of IP addresses or ranges.
This option accepts a list of single IP addresses, for example `['10.0.0.1', '10.0.0.2']`. You can also use CIDR notation to specify an IP address range, for example `['10.0.1.0/24']`.
Special scenarios:
- Setting an empty array `[]` means that the maintenance key cannot be used even in Parse Server Cloud Code. This value cannot be set via an environment variable as there is no way to pass an empty array to Parse Server via an environment variable.
- Setting `['0.0.0.0/0', '::0']` means to allow any IPv4 and IPv6 address to use the maintenance key and effectively disables the IP filter.
Considerations:
- IPv4 and IPv6 addresses are not compared against each other. Each IP version (IPv4 and IPv6) needs to be considered separately. For example, `['0.0.0.0/0']` allows any IPv4 address and blocks every IPv6 address. Conversely, `['::0']` allows any IPv6 address and blocks every IPv4 address.
- Keep in mind that the IP version in use depends on the network stack of the environment in which Parse Server runs. A local environment may use a different IP version than a remote environment. For example, it's possible that locally the value `['0.0.0.0/0']` allows the request IP because the environment is using IPv4, but when Parse Server is deployed remotely the request IP is blocked because the remote environment is using IPv6.
- When setting the option via an environment variable the notation is a comma-separated string, for example `"0.0.0.0/0,::0"`.
- IPv6 zone indices (`%` suffix) are not supported, for example `fe80::1%eth0`, `fe80::1%1` or `::1%lo`.
Defaults to `['127.0.0.1', '::1']` which means that only `localhost`, the server instance on which Parse Server runs, is allowed to use the maintenance key.
- * @property {String} masterKey Your Parse Master Key
+ * @property {Union} masterKey Your Parse Master Key
* @property {String[]} masterKeyIps (Optional) Restricts the use of master key permissions to a list of IP addresses or ranges.
This option accepts a list of single IP addresses, for example `['10.0.0.1', '10.0.0.2']`. You can also use CIDR notation to specify an IP address range, for example `['10.0.1.0/24']`.
Special scenarios:
- Setting an empty array `[]` means that the master key cannot be used even in Parse Server Cloud Code. This value cannot be set via an environment variable as there is no way to pass an empty array to Parse Server via an environment variable.
- Setting `['0.0.0.0/0', '::0']` means to allow any IPv4 and IPv6 address to use the master key and effectively disables the IP filter.
Considerations:
- IPv4 and IPv6 addresses are not compared against each other. Each IP version (IPv4 and IPv6) needs to be considered separately. For example, `['0.0.0.0/0']` allows any IPv4 address and blocks every IPv6 address. Conversely, `['::0']` allows any IPv6 address and blocks every IPv4 address.
- Keep in mind that the IP version in use depends on the network stack of the environment in which Parse Server runs. A local environment may use a different IP version than a remote environment. For example, it's possible that locally the value `['0.0.0.0/0']` allows the request IP because the environment is using IPv4, but when Parse Server is deployed remotely the request IP is blocked because the remote environment is using IPv6.
- When setting the option via an environment variable the notation is a comma-separated string, for example `"0.0.0.0/0,::0"`.
- IPv6 zone indices (`%` suffix) are not supported, for example `fe80::1%eth0`, `fe80::1%1` or `::1%lo`.
Defaults to `['127.0.0.1', '::1']` which means that only `localhost`, the server instance on which Parse Server runs, is allowed to use the master key.
+ * @property {Number} masterKeyTtl (Optional) The duration in seconds for which the current `masterKey` is being used before it is requested again if `masterKey` is set to a function. If `masterKey` is not set to a function, this option has no effect. Default is `0`, which means the master key is requested by invoking the `masterKey` function every time the master key is used internally by Parse Server.
* @property {Number} maxLimit Max value for limit option on queries, defaults to unlimited
* @property {Number|String} maxLogFiles Maximum number of logs to keep. If not set, no logs will be removed. This can be a number of files or number of days. If using days, add 'd' as the suffix. (default: null)
* @property {String} maxUploadSize Max file size for uploads, defaults to 20mb
diff --git a/src/Options/index.js b/src/Options/index.js
index 4cc5a551c3..70dd2a917c 100644
--- a/src/Options/index.js
+++ b/src/Options/index.js
@@ -46,7 +46,9 @@ export interface ParseServerOptions {
:ENV: PARSE_SERVER_APPLICATION_ID */
appId: string;
/* Your Parse Master Key */
- masterKey: string;
+ masterKey: (() => void) | string;
+ /* (Optional) The duration in seconds for which the current `masterKey` is being used before it is requested again if `masterKey` is set to a function. If `masterKey` is not set to a function, this option has no effect. Default is `0`, which means the master key is requested by invoking the `masterKey` function every time the master key is used internally by Parse Server. */
+ masterKeyTtl: ?number;
/* (Optional) The maintenance key is used for modifying internal and read-only fields of Parse Server.
⚠️ This key is not intended to be used as part of a regular operation of Parse Server. This key is intended to conduct out-of-band changes such as one-time migrations or data correction tasks. Internal fields are not officially documented and may change at any time without publication in release changelogs. We strongly advice not to rely on internal fields as part of your regular operation and to investigate the implications of any planned changes *directly in the source code* of your current version of Parse Server. */
maintenanceKey: string;
/* URL to your parse server with http:// or https://.
diff --git a/src/ParseServer.js b/src/ParseServer.js
index 15e2e5011c..d95362a6ac 100644
--- a/src/ParseServer.js
+++ b/src/ParseServer.js
@@ -162,7 +162,7 @@ class ParseServer {
}
const pushController = await controllers.getPushController(this.config);
await hooksController.load();
- const startupPromises = [];
+ const startupPromises = [this.config.loadMasterKey?.()];
if (schema) {
startupPromises.push(new DefinedSchemas(schema, this.config).execute());
}
diff --git a/src/middlewares.js b/src/middlewares.js
index 178692b447..57838b1a69 100644
--- a/src/middlewares.js
+++ b/src/middlewares.js
@@ -69,7 +69,7 @@ export const checkIp = (ip, ipRangeList, store) => {
// Adds info to the request:
// req.config - the Config for this app
// req.auth - the Auth for this request
-export function handleParseHeaders(req, res, next) {
+export async function handleParseHeaders(req, res, next) {
var mount = getMountForRequest(req);
let context = {};
@@ -238,7 +238,8 @@ export function handleParseHeaders(req, res, next) {
);
}
- let isMaster = info.masterKey === req.config.masterKey;
+ const masterKey = await req.config.loadMasterKey();
+ let isMaster = info.masterKey === masterKey;
if (isMaster && !checkIp(clientIp, req.config.masterKeyIps || [], req.config.masterKeyIpsStore)) {
const log = req.config?.loggerController || defaultLogger;