Skip to content

Commit 1f9c44c

Browse files
committed
Merge remote-tracking branch 'upstream/alpha' into alpha
2 parents d70d3c7 + 394f6e8 commit 1f9c44c

File tree

16 files changed

+192
-12
lines changed

16 files changed

+192
-12
lines changed

DEPRECATIONS.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ The following is a list of deprecations, according to the [Deprecation Policy](h
1818
| DEPPS12 | Database option `allowPublicExplain` defaults to `false` | [#7519](https://github.com/parse-community/parse-server/issues/7519) | 8.5.0 (2025) | 9.0.0 (2026) | changed | - |
1919
| DEPPS13 | Config option `enableInsecureAuthAdapters` defaults to `false` | [#9667](https://github.com/parse-community/parse-server/pull/9667) | 8.0.0 (2025) | 9.0.0 (2026) | changed | - |
2020
| DEPPS14 | Config option `pages.encodePageParamHeaders` defaults to `true` | [#10063](https://github.com/parse-community/parse-server/issues/10063) | 9.4.0 (2026) | 10.0.0 (2027) | deprecated | - |
21+
| DEPPS15 | Config option `readOnlyMasterKeyIps` defaults to `['127.0.0.1', '::1']` | [#10115](https://github.com/parse-community/parse-server/pull/10115) | 9.5.0 (2026) | 10.0.0 (2027) | deprecated | - |
22+
| DEPPS16 | Remove config option `mountPlayground` | [#10110](https://github.com/parse-community/parse-server/issues/10110) | 9.5.0 (2026) | 10.0.0 (2027) | deprecated | - |
23+
| DEPPS17 | Remove config option `playgroundPath` | [#10110](https://github.com/parse-community/parse-server/issues/10110) | 9.5.0 (2026) | 10.0.0 (2027) | deprecated | - |
2124

2225
[i_deprecation]: ## "The version and date of the deprecation."
2326
[i_change]: ## "The version and date of the planned change."

changelogs/CHANGELOG_alpha.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,17 @@
1+
# [9.5.0-alpha.13](https://github.com/parse-community/parse-server/compare/9.5.0-alpha.12...9.5.0-alpha.13) (2026-03-06)
2+
3+
4+
### Features
5+
6+
* Deprecate GraphQL Playground that exposes master key in HTTP response ([#10112](https://github.com/parse-community/parse-server/issues/10112)) ([d54d800](https://github.com/parse-community/parse-server/commit/d54d800f596f1937701f5bd57c81104f102bc3ae))
7+
8+
# [9.5.0-alpha.12](https://github.com/parse-community/parse-server/compare/9.5.0-alpha.11...9.5.0-alpha.12) (2026-03-06)
9+
10+
11+
### Features
12+
13+
* Add server option `readOnlyMasterKeyIps` to restrict `readOnlyMasterKey` by IP ([#10115](https://github.com/parse-community/parse-server/issues/10115)) ([cbff6b4](https://github.com/parse-community/parse-server/commit/cbff6b42a0b4f02552457f04a8757ac2376d3e04))
14+
115
# [9.5.0-alpha.11](https://github.com/parse-community/parse-server/compare/9.5.0-alpha.10...9.5.0-alpha.11) (2026-03-06)
216

317

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "parse-server",
3-
"version": "9.5.0-alpha.11",
3+
"version": "9.5.0-alpha.13",
44
"description": "An express module providing a Parse-compatible API server",
55
"main": "lib/index.js",
66
"repository": {

spec/Deprecator.spec.js

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,4 +103,50 @@ describe('Deprecator', () => {
103103
})
104104
);
105105
});
106+
107+
it('logs deprecation for removed key when option is set', async () => {
108+
deprecations = [{ optionKey: 'exampleKey', changeNewKey: '', solution: 'Use something else.' }];
109+
110+
spyOn(Deprecator, '_getDeprecations').and.callFake(() => deprecations);
111+
const logger = require('../lib/logger').logger;
112+
const logSpy = spyOn(logger, 'warn').and.callFake(() => {});
113+
114+
await reconfigureServer({ exampleKey: true });
115+
expect(logSpy).toHaveBeenCalledWith(
116+
`DeprecationWarning: The Parse Server option '${deprecations[0].optionKey}' is deprecated and will be removed in a future version. ${deprecations[0].solution}`
117+
);
118+
});
119+
120+
it('does not log deprecation for removed key when option is not set', async () => {
121+
deprecations = [{ optionKey: 'exampleKey', changeNewKey: '', solution: 'Use something else.' }];
122+
123+
spyOn(Deprecator, '_getDeprecations').and.callFake(() => deprecations);
124+
const logSpy = spyOn(Deprecator, '_logOption').and.callFake(() => {});
125+
126+
await reconfigureServer();
127+
expect(logSpy).not.toHaveBeenCalled();
128+
});
129+
130+
it('logs deprecation for mountPlayground when set', async () => {
131+
const logSpy = spyOn(Deprecator, '_logOption').and.callFake(() => {});
132+
133+
await reconfigureServer({ mountPlayground: true, mountGraphQL: true });
134+
expect(logSpy).toHaveBeenCalledWith(
135+
jasmine.objectContaining({
136+
optionKey: 'mountPlayground',
137+
changeNewKey: '',
138+
})
139+
);
140+
});
141+
142+
it('does not log deprecation for mountPlayground when not set', async () => {
143+
const logSpy = spyOn(Deprecator, '_logOption').and.callFake(() => {});
144+
145+
await reconfigureServer();
146+
expect(logSpy).not.toHaveBeenCalledWith(
147+
jasmine.objectContaining({
148+
optionKey: 'mountPlayground',
149+
})
150+
);
151+
});
106152
});

spec/Middlewares.spec.js

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ const AppCachePut = (appId, config) =>
77
...config,
88
maintenanceKeyIpsStore: new Map(),
99
masterKeyIpsStore: new Map(),
10+
readOnlyMasterKeyIpsStore: new Map(),
1011
});
1112

1213
describe('middlewares', () => {
@@ -207,6 +208,55 @@ describe('middlewares', () => {
207208
expect(fakeReq.auth.isMaster).toBe(true);
208209
});
209210

211+
it('should not succeed and log if the ip does not belong to readOnlyMasterKeyIps list', async () => {
212+
const logger = require('../lib/logger').logger;
213+
spyOn(logger, 'error').and.callFake(() => {});
214+
AppCachePut(fakeReq.body._ApplicationId, {
215+
masterKeyIps: ['0.0.0.0/0'],
216+
readOnlyMasterKey: 'readOnlyMasterKey',
217+
readOnlyMasterKeyIps: ['10.0.0.1'],
218+
});
219+
fakeReq.ip = '127.0.0.1';
220+
fakeReq.headers['x-parse-application-id'] = fakeReq.body._ApplicationId;
221+
fakeReq.headers['x-parse-master-key'] = 'readOnlyMasterKey';
222+
223+
const error = await middlewares.handleParseHeaders(fakeReq, fakeRes, () => {}).catch(e => e);
224+
225+
expect(error).toBeDefined();
226+
expect(error.message).toEqual('unauthorized');
227+
expect(logger.error).toHaveBeenCalledWith(
228+
`Request using read-only master key rejected as the request IP address '127.0.0.1' is not set in Parse Server option 'readOnlyMasterKeyIps'.`
229+
);
230+
});
231+
232+
it('should succeed if the ip does belong to readOnlyMasterKeyIps list', async () => {
233+
AppCachePut(fakeReq.body._ApplicationId, {
234+
masterKeyIps: ['0.0.0.0/0'],
235+
readOnlyMasterKey: 'readOnlyMasterKey',
236+
readOnlyMasterKeyIps: ['10.0.0.1'],
237+
});
238+
fakeReq.ip = '10.0.0.1';
239+
fakeReq.headers['x-parse-application-id'] = fakeReq.body._ApplicationId;
240+
fakeReq.headers['x-parse-master-key'] = 'readOnlyMasterKey';
241+
await new Promise(resolve => middlewares.handleParseHeaders(fakeReq, fakeRes, resolve));
242+
expect(fakeReq.auth.isMaster).toBe(true);
243+
expect(fakeReq.auth.isReadOnly).toBe(true);
244+
});
245+
246+
it('should allow any ip to use readOnlyMasterKey if readOnlyMasterKeyIps is 0.0.0.0/0', async () => {
247+
AppCachePut(fakeReq.body._ApplicationId, {
248+
masterKeyIps: ['0.0.0.0/0'],
249+
readOnlyMasterKey: 'readOnlyMasterKey',
250+
readOnlyMasterKeyIps: ['0.0.0.0/0'],
251+
});
252+
fakeReq.ip = '10.0.0.1';
253+
fakeReq.headers['x-parse-application-id'] = fakeReq.body._ApplicationId;
254+
fakeReq.headers['x-parse-master-key'] = 'readOnlyMasterKey';
255+
await new Promise(resolve => middlewares.handleParseHeaders(fakeReq, fakeRes, resolve));
256+
expect(fakeReq.auth.isMaster).toBe(true);
257+
expect(fakeReq.auth.isReadOnly).toBe(true);
258+
});
259+
210260
it('can set trust proxy', async () => {
211261
const server = await reconfigureServer({ trustProxy: 1 });
212262
expect(server.app.parent.settings['trust proxy']).toBe(1);

spec/SecurityCheckGroups.spec.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ describe('Security Check Groups', () => {
3535
config.enableInsecureAuthAdapters = false;
3636
config.graphQLPublicIntrospection = false;
3737
config.mountPlayground = false;
38+
config.readOnlyMasterKey = 'someReadOnlyMasterKey';
39+
config.readOnlyMasterKeyIps = ['127.0.0.1', '::1'];
3840
await reconfigureServer(config);
3941

4042
const group = new CheckGroupServerConfig();
@@ -45,6 +47,7 @@ describe('Security Check Groups', () => {
4547
expect(group.checks()[4].checkState()).toBe(CheckState.success);
4648
expect(group.checks()[5].checkState()).toBe(CheckState.success);
4749
expect(group.checks()[6].checkState()).toBe(CheckState.success);
50+
expect(group.checks()[8].checkState()).toBe(CheckState.success);
4851
});
4952

5053
it('checks fail correctly', async () => {
@@ -54,6 +57,8 @@ describe('Security Check Groups', () => {
5457
config.enableInsecureAuthAdapters = true;
5558
config.graphQLPublicIntrospection = true;
5659
config.mountPlayground = true;
60+
config.readOnlyMasterKey = 'someReadOnlyMasterKey';
61+
config.readOnlyMasterKeyIps = ['0.0.0.0/0'];
5762
await reconfigureServer(config);
5863

5964
const group = new CheckGroupServerConfig();
@@ -64,6 +69,7 @@ describe('Security Check Groups', () => {
6469
expect(group.checks()[4].checkState()).toBe(CheckState.fail);
6570
expect(group.checks()[5].checkState()).toBe(CheckState.fail);
6671
expect(group.checks()[6].checkState()).toBe(CheckState.fail);
72+
expect(group.checks()[8].checkState()).toBe(CheckState.fail);
6773
});
6874

6975
it_only_db('mongo')('checks succeed correctly (MongoDB specific)', async () => {

src/Config.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ export class Config {
118118
maintenanceKey,
119119
maintenanceKeyIps,
120120
readOnlyMasterKey,
121+
readOnlyMasterKeyIps,
121122
allowHeaders,
122123
idempotencyOptions,
123124
fileUpload,
@@ -158,6 +159,7 @@ export class Config {
158159
this.validateSessionConfiguration(sessionLength, expireInactiveSessions);
159160
this.validateIps('masterKeyIps', masterKeyIps);
160161
this.validateIps('maintenanceKeyIps', maintenanceKeyIps);
162+
this.validateIps('readOnlyMasterKeyIps', readOnlyMasterKeyIps);
161163
this.validateDefaultLimit(defaultLimit);
162164
this.validateMaxLimit(maxLimit);
163165
this.validateAllowHeaders(allowHeaders);

src/Deprecator/Deprecations.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,4 +26,19 @@ module.exports = [
2626
changeNewDefault: 'true',
2727
solution: "Set 'pages.encodePageParamHeaders' to 'true' to URI-encode non-ASCII characters in page parameter headers.",
2828
},
29+
{
30+
optionKey: 'readOnlyMasterKeyIps',
31+
changeNewDefault: '["127.0.0.1", "::1"]',
32+
solution: "Set 'readOnlyMasterKeyIps' to the IP addresses that should be allowed to use the read-only master key, or to '[\"127.0.0.1\", \"::1\"]' to restrict access to localhost.",
33+
},
34+
{
35+
optionKey: 'mountPlayground',
36+
changeNewKey: '',
37+
solution: "Use Parse Dashboard as GraphQL IDE or configure a third-party GraphQL client such as Apollo Sandbox, GraphiQL, or Insomnia with custom request headers.",
38+
},
39+
{
40+
optionKey: 'playgroundPath',
41+
changeNewKey: '',
42+
solution: "Use Parse Dashboard as GraphQL IDE or configure a third-party GraphQL client such as Apollo Sandbox, GraphiQL, or Insomnia with custom request headers.",
43+
},
2944
];

src/Deprecator/Deprecator.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,17 @@ class Deprecator {
2020
const solution = deprecation.solution;
2121
const optionKey = deprecation.optionKey;
2222
const changeNewDefault = deprecation.changeNewDefault;
23+
const changeNewKey = deprecation.changeNewKey;
2324

2425
// If default will change, only throw a warning if option is not set
2526
if (changeNewDefault != null && Utils.getNestedProperty(options, optionKey) == null) {
2627
Deprecator._logOption({ optionKey, changeNewDefault, solution });
2728
}
29+
30+
// If key will be removed or renamed, only throw a warning if option is set
31+
if (changeNewKey != null && Utils.getNestedProperty(options, optionKey) != null) {
32+
Deprecator._logOption({ optionKey, changeNewKey, solution });
33+
}
2834
}
2935
}
3036

@@ -107,7 +113,7 @@ class Deprecator {
107113

108114
// Compose message
109115
let output = `DeprecationWarning: The Parse Server ${type} '${key}' `;
110-
output += changeNewKey ? `is deprecated and will be ${keyAction} in a future version.` : '';
116+
output += changeNewKey != null ? `is deprecated and will be ${keyAction} in a future version.` : '';
111117
output += changeNewDefault
112118
? `default will change to '${changeNewDefault}' in a future version.`
113119
: '';

0 commit comments

Comments
 (0)