Skip to content

Commit 816078f

Browse files
authored
feat: Add option to disallow aggregation pipelines for the read-only master key (parse-community#10517)
1 parent 6ed35db commit 816078f

6 files changed

Lines changed: 88 additions & 3 deletions

File tree

spec/ParseQuery.Aggregate.spec.js

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1774,3 +1774,66 @@ describe('Parse.Query Aggregate testing', () => {
17741774
expect(results[0].total).toBe(1);
17751775
});
17761776
});
1777+
1778+
describe('Parse.Query Aggregate readOnlyMasterKey', () => {
1779+
const readOnlyMasterKeyOptions = {
1780+
headers: {
1781+
'X-Parse-Application-Id': 'test',
1782+
'X-Parse-Rest-API-Key': 'test',
1783+
'X-Parse-Master-Key': 'read-only-test',
1784+
'Content-Type': 'application/json',
1785+
},
1786+
json: true,
1787+
};
1788+
1789+
it('allows the read-only master key to run aggregation pipelines by default', async () => {
1790+
await new TestObject({ name: 'foo' }).save(null, { useMasterKey: true });
1791+
const options = Object.assign({}, readOnlyMasterKeyOptions, {
1792+
body: { $group: { _id: '$name' } },
1793+
});
1794+
const resp = await get(Parse.serverURL + '/aggregate/TestObject', options);
1795+
expect(resp.results.length).toBe(1);
1796+
expect(resp.results[0].objectId).toBe('foo');
1797+
});
1798+
1799+
it('blocks the read-only master key from running aggregation pipelines when allowAggregationForReadOnlyMasterKey is false', async () => {
1800+
await reconfigureServer({ allowAggregationForReadOnlyMasterKey: false });
1801+
await new TestObject({ name: 'foo' }).save(null, { useMasterKey: true });
1802+
const options = Object.assign({}, readOnlyMasterKeyOptions, {
1803+
body: { $group: { _id: '$name' } },
1804+
});
1805+
try {
1806+
await get(Parse.serverURL + '/aggregate/TestObject', options);
1807+
fail('aggregation should be forbidden for the read-only master key');
1808+
} catch (e) {
1809+
expect(e.error.code).toBe(Parse.Error.OPERATION_FORBIDDEN);
1810+
}
1811+
});
1812+
1813+
it('blocks a write-capable $out stage for the read-only master key when allowAggregationForReadOnlyMasterKey is false', async () => {
1814+
await reconfigureServer({ allowAggregationForReadOnlyMasterKey: false });
1815+
await new TestObject({ name: 'foo' }).save(null, { useMasterKey: true });
1816+
const options = Object.assign({}, readOnlyMasterKeyOptions, {
1817+
body: {
1818+
pipeline: [{ $match: { name: 'foo' } }, { $out: 'CreatedByReadOnlyAggregate' }],
1819+
},
1820+
});
1821+
try {
1822+
await get(Parse.serverURL + '/aggregate/TestObject', options);
1823+
fail('aggregation should be forbidden for the read-only master key');
1824+
} catch (e) {
1825+
expect(e.error.code).toBe(Parse.Error.OPERATION_FORBIDDEN);
1826+
}
1827+
});
1828+
1829+
it('still allows the full master key to run aggregation pipelines when allowAggregationForReadOnlyMasterKey is false', async () => {
1830+
await reconfigureServer({ allowAggregationForReadOnlyMasterKey: false });
1831+
await new TestObject({ name: 'foo' }).save(null, { useMasterKey: true });
1832+
const options = Object.assign({}, masterKeyOptions, {
1833+
body: { $group: { _id: '$name' } },
1834+
});
1835+
const resp = await get(Parse.serverURL + '/aggregate/TestObject', options);
1836+
expect(resp.results.length).toBe(1);
1837+
expect(resp.results[0].objectId).toBe('foo');
1838+
});
1839+
});

src/Deprecator/Deprecations.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,4 +113,9 @@ module.exports = [
113113
changeNewDefault: 'true',
114114
solution: "Set 'installation.duplicateDeviceTokenActionEnforceAuth' to 'true' to enforce the caller's auth context (and the resulting ACL and CLP) when Parse Server deduplicates _Installation records sharing the same deviceToken. Set to 'false' to keep the current behavior of bypassing permissions on the dedup operation.",
115115
},
116+
{
117+
optionKey: 'allowAggregationForReadOnlyMasterKey',
118+
changeNewDefault: 'false',
119+
solution: "Set 'allowAggregationForReadOnlyMasterKey' to 'false' to prevent the read-only master key from running aggregation pipelines, which can include write-capable stages (e.g. '$out', '$merge'). Set to 'true' to keep the current behavior where the read-only master key can run aggregation pipelines.",
120+
},
116121
];

src/Options/Definitions.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,12 @@ module.exports.ParseServerOptions = {
5858
action: parsers.objectParser,
5959
type: 'AccountLockoutOptions',
6060
},
61+
allowAggregationForReadOnlyMasterKey: {
62+
env: 'PARSE_SERVER_ALLOW_AGGREGATION_FOR_READ_ONLY_MASTER_KEY',
63+
help: 'Whether the `readOnlyMasterKey` is allowed to run aggregation pipelines via the aggregate endpoint. An aggregation pipeline can contain write-capable stages (for example MongoDB `$out` and `$merge`), so allowing aggregation effectively gives the read-only master key a way to perform writes, contrary to its read-only intent. If `true` (default), the read-only master key can run aggregation pipelines. If `false`, the read-only master key cannot run aggregation pipelines at all. Note that the `readOnlyMasterKey` is a secret key for internal server-side use only and must never be distributed; this option is an additional safeguard, not a substitute for keeping the key confidential. Defaults to `true`.',
64+
action: parsers.booleanParser,
65+
default: true,
66+
},
6167
allowClientClassCreation: {
6268
env: 'PARSE_SERVER_ALLOW_CLIENT_CLASS_CREATION',
6369
help: 'Enable (or disable) client class creation, defaults to false',
@@ -535,7 +541,7 @@ module.exports.ParseServerOptions = {
535541
},
536542
readOnlyMasterKey: {
537543
env: 'PARSE_SERVER_READ_ONLY_MASTER_KEY',
538-
help: 'Read-only key, which has the same capabilities as MasterKey without writes',
544+
help: 'The read-only master key is a secret key with the same read capabilities as the `masterKey`, but without the ability to perform writes. Like the `masterKey`, it bypasses all security mechanisms (Class Level Permissions, object ACLs, `protectedFields`), so it grants full read access to all data.<br><br>It is intended strictly for internal, server-side use \u2014 for example to give a trusted internal process read access while guarding against accidental writes during development or operations. It is not a credential for untrusted contexts: it must never be shipped, distributed, published, embedded in a client application, or otherwise exposed to untrusted parties, because anyone who obtains it can read all data in the database. Use `readOnlyMasterKeyIps` to restrict the IP addresses from which it may be used.',
539545
},
540546
readOnlyMasterKeyIps: {
541547
env: 'PARSE_SERVER_READ_ONLY_MASTER_KEY_IPS',

src/Options/docs.js

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

src/Options/index.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,8 +158,12 @@ export interface ParseServerOptions {
158158
/* Key for REST calls
159159
:ENV: PARSE_SERVER_REST_API_KEY */
160160
restAPIKey: ?string;
161-
/* Read-only key, which has the same capabilities as MasterKey without writes */
161+
/* The read-only master key is a secret key with the same read capabilities as the `masterKey`, but without the ability to perform writes. Like the `masterKey`, it bypasses all security mechanisms (Class Level Permissions, object ACLs, `protectedFields`), so it grants full read access to all data.<br><br>It is intended strictly for internal, server-side use — for example to give a trusted internal process read access while guarding against accidental writes during development or operations. It is not a credential for untrusted contexts: it must never be shipped, distributed, published, embedded in a client application, or otherwise exposed to untrusted parties, because anyone who obtains it can read all data in the database. Use `readOnlyMasterKeyIps` to restrict the IP addresses from which it may be used. */
162162
readOnlyMasterKey: ?string;
163+
/* Whether the `readOnlyMasterKey` is allowed to run aggregation pipelines via the aggregate endpoint. An aggregation pipeline can contain write-capable stages (for example MongoDB `$out` and `$merge`), so allowing aggregation effectively gives the read-only master key a way to perform writes, contrary to its read-only intent. If `true` (default), the read-only master key can run aggregation pipelines. If `false`, the read-only master key cannot run aggregation pipelines at all. Note that the `readOnlyMasterKey` is a secret key for internal server-side use only and must never be distributed; this option is an additional safeguard, not a substitute for keeping the key confidential. Defaults to `true`.
164+
:ENV: PARSE_SERVER_ALLOW_AGGREGATION_FOR_READ_ONLY_MASTER_KEY
165+
:DEFAULT: true */
166+
allowAggregationForReadOnlyMasterKey: ?boolean;
163167
/* Key sent with outgoing webhook calls */
164168
webhookKey: ?string;
165169
/* Key for your files */

src/Routers/AggregateRouter.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,12 @@ import UsersRouter from './UsersRouter';
66

77
export class AggregateRouter extends ClassesRouter {
88
async handleFind(req) {
9+
if (req.auth && req.auth.isReadOnly && req.config && !req.config.allowAggregationForReadOnlyMasterKey) {
10+
throw new Parse.Error(
11+
Parse.Error.OPERATION_FORBIDDEN,
12+
'Cannot run an aggregation pipeline when using the readOnlyMasterKey'
13+
);
14+
}
915
const body = Object.assign(req.body || {}, ClassesRouter.JSONFromQuery(req.query));
1016
const options = {};
1117
if (body.distinct) {

0 commit comments

Comments
 (0)