Skip to content

Commit 6f1d161

Browse files
authored
feat: Add dynamic master key by setting Parse Server option masterKey to a function (#9582)
1 parent 4153737 commit 6f1d161

File tree

8 files changed

+102
-25
lines changed

8 files changed

+102
-25
lines changed

Diff for: spec/Middlewares.spec.js

+8-20
Original file line numberDiff line numberDiff line change
@@ -46,32 +46,32 @@ describe('middlewares', () => {
4646
});
4747
});
4848

49-
it('should give invalid response when keys are configured but no key supplied', () => {
49+
it('should give invalid response when keys are configured but no key supplied', async () => {
5050
AppCachePut(fakeReq.body._ApplicationId, {
5151
masterKey: 'masterKey',
5252
restAPIKey: 'restAPIKey',
5353
});
54-
middlewares.handleParseHeaders(fakeReq, fakeRes);
54+
await middlewares.handleParseHeaders(fakeReq, fakeRes);
5555
expect(fakeRes.status).toHaveBeenCalledWith(403);
5656
});
5757

58-
it('should give invalid response when keys are configured but supplied key is incorrect', () => {
58+
it('should give invalid response when keys are configured but supplied key is incorrect', async () => {
5959
AppCachePut(fakeReq.body._ApplicationId, {
6060
masterKey: 'masterKey',
6161
restAPIKey: 'restAPIKey',
6262
});
6363
fakeReq.headers['x-parse-rest-api-key'] = 'wrongKey';
64-
middlewares.handleParseHeaders(fakeReq, fakeRes);
64+
await middlewares.handleParseHeaders(fakeReq, fakeRes);
6565
expect(fakeRes.status).toHaveBeenCalledWith(403);
6666
});
6767

68-
it('should give invalid response when keys are configured but different key is supplied', () => {
68+
it('should give invalid response when keys are configured but different key is supplied', async () => {
6969
AppCachePut(fakeReq.body._ApplicationId, {
7070
masterKey: 'masterKey',
7171
restAPIKey: 'restAPIKey',
7272
});
7373
fakeReq.headers['x-parse-client-key'] = 'clientKey';
74-
middlewares.handleParseHeaders(fakeReq, fakeRes);
74+
await middlewares.handleParseHeaders(fakeReq, fakeRes);
7575
expect(fakeRes.status).toHaveBeenCalledWith(403);
7676
});
7777

@@ -157,13 +157,7 @@ describe('middlewares', () => {
157157
fakeReq.ip = '127.0.0.1';
158158
fakeReq.headers['x-parse-master-key'] = 'masterKey';
159159

160-
let error;
161-
162-
try {
163-
await new Promise(resolve => middlewares.handleParseHeaders(fakeReq, fakeRes, resolve));
164-
} catch (err) {
165-
error = err;
166-
}
160+
const error = await middlewares.handleParseHeaders(fakeReq, fakeRes, () => {}).catch(e => e);
167161

168162
expect(error).toBeDefined();
169163
expect(error.message).toEqual(`unauthorized`);
@@ -182,13 +176,7 @@ describe('middlewares', () => {
182176
fakeReq.ip = '10.0.0.2';
183177
fakeReq.headers['x-parse-maintenance-key'] = 'masterKey';
184178

185-
let error;
186-
187-
try {
188-
await new Promise(resolve => middlewares.handleParseHeaders(fakeReq, fakeRes, resolve));
189-
} catch (err) {
190-
error = err;
191-
}
179+
const error = await middlewares.handleParseHeaders(fakeReq, fakeRes, () => {}).catch(e => e);
192180

193181
expect(error).toBeDefined();
194182
expect(error.message).toEqual(`unauthorized`);

Diff for: spec/index.spec.js

+57
Original file line numberDiff line numberDiff line change
@@ -601,6 +601,63 @@ describe('server', () => {
601601
await new Promise(resolve => server.close(resolve));
602602
});
603603

604+
it('should load masterKey', async () => {
605+
await reconfigureServer({
606+
masterKey: () => 'testMasterKey',
607+
masterKeyTtl: 1000, // TTL is set
608+
});
609+
610+
await new Parse.Object('TestObject').save();
611+
612+
const config = Config.get(Parse.applicationId);
613+
expect(config.masterKeyCache.masterKey).toEqual('testMasterKey');
614+
expect(config.masterKeyCache.expiresAt.getTime()).toBeGreaterThan(Date.now());
615+
});
616+
617+
it('should not reload if ttl is not set', async () => {
618+
const masterKeySpy = jasmine.createSpy().and.returnValue(Promise.resolve('initialMasterKey'));
619+
620+
await reconfigureServer({
621+
masterKey: masterKeySpy,
622+
masterKeyTtl: null, // No TTL set
623+
});
624+
625+
await new Parse.Object('TestObject').save();
626+
627+
const config = Config.get(Parse.applicationId);
628+
const firstMasterKey = config.masterKeyCache.masterKey;
629+
630+
// Simulate calling the method again
631+
await config.loadMasterKey();
632+
const secondMasterKey = config.masterKeyCache.masterKey;
633+
634+
expect(firstMasterKey).toEqual('initialMasterKey');
635+
expect(secondMasterKey).toEqual('initialMasterKey');
636+
expect(masterKeySpy).toHaveBeenCalledTimes(1); // Should only be called once
637+
expect(config.masterKeyCache.expiresAt).toBeNull(); // TTL is not set, so expiresAt should remain null
638+
});
639+
640+
it('should reload masterKey if ttl is set and expired', async () => {
641+
const masterKeySpy = jasmine.createSpy()
642+
.and.returnValues(Promise.resolve('firstMasterKey'), Promise.resolve('secondMasterKey'));
643+
644+
await reconfigureServer({
645+
masterKey: masterKeySpy,
646+
masterKeyTtl: 1 / 1000, // TTL is set to 1ms
647+
});
648+
649+
await new Parse.Object('TestObject').save();
650+
651+
await new Promise(resolve => setTimeout(resolve, 10));
652+
653+
await new Parse.Object('TestObject').save();
654+
655+
const config = Config.get(Parse.applicationId);
656+
expect(masterKeySpy).toHaveBeenCalledTimes(2);
657+
expect(config.masterKeyCache.masterKey).toEqual('secondMasterKey');
658+
});
659+
660+
604661
it('should not fail when Google signin is introduced without the optional clientId', done => {
605662
const jwt = require('jsonwebtoken');
606663
const authUtils = require('../lib/Adapters/Auth/utils');

Diff for: src/Config.js

+22
Original file line numberDiff line numberDiff line change
@@ -724,6 +724,28 @@ export class Config {
724724
return `${this.publicServerURL}/${this.pagesEndpoint}/${this.applicationId}/verify_email`;
725725
}
726726

727+
async loadMasterKey() {
728+
if (typeof this.masterKey === 'function') {
729+
const ttlIsEmpty = !this.masterKeyTtl;
730+
const isExpired = this.masterKeyCache?.expiresAt && this.masterKeyCache.expiresAt < new Date();
731+
732+
if ((!isExpired || ttlIsEmpty) && this.masterKeyCache?.masterKey) {
733+
return this.masterKeyCache.masterKey;
734+
}
735+
736+
const masterKey = await this.masterKey();
737+
738+
const expiresAt = this.masterKeyTtl ? new Date(Date.now() + 1000 * this.masterKeyTtl) : null
739+
this.masterKeyCache = { masterKey, expiresAt };
740+
Config.put(this);
741+
742+
return this.masterKeyCache.masterKey;
743+
}
744+
745+
return this.masterKey;
746+
}
747+
748+
727749
// TODO: Remove this function once PagesRouter replaces the PublicAPIRouter;
728750
// the (default) endpoint has to be defined in PagesRouter only.
729751
get pagesEndpoint() {

Diff for: src/Options/Definitions.js

+6
Original file line numberDiff line numberDiff line change
@@ -369,6 +369,12 @@ module.exports.ParseServerOptions = {
369369
action: parsers.arrayParser,
370370
default: ['127.0.0.1', '::1'],
371371
},
372+
masterKeyTtl: {
373+
env: 'PARSE_SERVER_MASTER_KEY_TTL',
374+
help:
375+
'(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.',
376+
action: parsers.numberParser('masterKeyTtl'),
377+
},
372378
maxLimit: {
373379
env: 'PARSE_SERVER_MAX_LIMIT',
374380
help: 'Max value for limit option on queries, defaults to unlimited',

Diff for: src/Options/docs.js

+2-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Diff for: src/Options/index.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,9 @@ export interface ParseServerOptions {
4646
:ENV: PARSE_SERVER_APPLICATION_ID */
4747
appId: string;
4848
/* Your Parse Master Key */
49-
masterKey: string;
49+
masterKey: (() => void) | string;
50+
/* (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. */
51+
masterKeyTtl: ?number;
5052
/* (Optional) The maintenance key is used for modifying internal and read-only fields of Parse Server.<br><br>⚠️ 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. */
5153
maintenanceKey: string;
5254
/* URL to your parse server with http:// or https://.

Diff for: src/ParseServer.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,7 @@ class ParseServer {
162162
}
163163
const pushController = await controllers.getPushController(this.config);
164164
await hooksController.load();
165-
const startupPromises = [];
165+
const startupPromises = [this.config.loadMasterKey?.()];
166166
if (schema) {
167167
startupPromises.push(new DefinedSchemas(schema, this.config).execute());
168168
}

Diff for: src/middlewares.js

+3-2
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ export const checkIp = (ip, ipRangeList, store) => {
6969
// Adds info to the request:
7070
// req.config - the Config for this app
7171
// req.auth - the Auth for this request
72-
export function handleParseHeaders(req, res, next) {
72+
export async function handleParseHeaders(req, res, next) {
7373
var mount = getMountForRequest(req);
7474

7575
let context = {};
@@ -238,7 +238,8 @@ export function handleParseHeaders(req, res, next) {
238238
);
239239
}
240240

241-
let isMaster = info.masterKey === req.config.masterKey;
241+
const masterKey = await req.config.loadMasterKey();
242+
let isMaster = info.masterKey === masterKey;
242243

243244
if (isMaster && !checkIp(clientIp, req.config.masterKeyIps || [], req.config.masterKeyIpsStore)) {
244245
const log = req.config?.loggerController || defaultLogger;

0 commit comments

Comments
 (0)