Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 63 additions & 0 deletions spec/ParseQuery.Aggregate.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -1774,3 +1774,66 @@ describe('Parse.Query Aggregate testing', () => {
expect(results[0].total).toBe(1);
});
});

describe('Parse.Query Aggregate readOnlyMasterKey', () => {
const readOnlyMasterKeyOptions = {
headers: {
'X-Parse-Application-Id': 'test',
'X-Parse-Rest-API-Key': 'test',
'X-Parse-Master-Key': 'read-only-test',
'Content-Type': 'application/json',
},
json: true,
};

it('allows the read-only master key to run aggregation pipelines by default', async () => {
await new TestObject({ name: 'foo' }).save(null, { useMasterKey: true });
const options = Object.assign({}, readOnlyMasterKeyOptions, {
body: { $group: { _id: '$name' } },
});
const resp = await get(Parse.serverURL + '/aggregate/TestObject', options);
expect(resp.results.length).toBe(1);
expect(resp.results[0].objectId).toBe('foo');
});

it('blocks the read-only master key from running aggregation pipelines when allowAggregationForReadOnlyMasterKey is false', async () => {
await reconfigureServer({ allowAggregationForReadOnlyMasterKey: false });
await new TestObject({ name: 'foo' }).save(null, { useMasterKey: true });
const options = Object.assign({}, readOnlyMasterKeyOptions, {
body: { $group: { _id: '$name' } },
});
try {
await get(Parse.serverURL + '/aggregate/TestObject', options);
fail('aggregation should be forbidden for the read-only master key');
} catch (e) {
expect(e.error.code).toBe(Parse.Error.OPERATION_FORBIDDEN);
}
});

it('blocks a write-capable $out stage for the read-only master key when allowAggregationForReadOnlyMasterKey is false', async () => {
await reconfigureServer({ allowAggregationForReadOnlyMasterKey: false });
await new TestObject({ name: 'foo' }).save(null, { useMasterKey: true });
const options = Object.assign({}, readOnlyMasterKeyOptions, {
body: {
pipeline: [{ $match: { name: 'foo' } }, { $out: 'CreatedByReadOnlyAggregate' }],
},
});
try {
await get(Parse.serverURL + '/aggregate/TestObject', options);
fail('aggregation should be forbidden for the read-only master key');
} catch (e) {
expect(e.error.code).toBe(Parse.Error.OPERATION_FORBIDDEN);
}
});

it('still allows the full master key to run aggregation pipelines when allowAggregationForReadOnlyMasterKey is false', async () => {
await reconfigureServer({ allowAggregationForReadOnlyMasterKey: false });
await new TestObject({ name: 'foo' }).save(null, { useMasterKey: true });
const options = Object.assign({}, masterKeyOptions, {
body: { $group: { _id: '$name' } },
});
const resp = await get(Parse.serverURL + '/aggregate/TestObject', options);
expect(resp.results.length).toBe(1);
expect(resp.results[0].objectId).toBe('foo');
});
});
5 changes: 5 additions & 0 deletions src/Deprecator/Deprecations.js
Original file line number Diff line number Diff line change
Expand Up @@ -113,4 +113,9 @@ module.exports = [
changeNewDefault: 'true',
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.",
},
{
optionKey: 'allowAggregationForReadOnlyMasterKey',
changeNewDefault: 'false',
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.",
},
];
8 changes: 7 additions & 1 deletion src/Options/Definitions.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,12 @@ module.exports.ParseServerOptions = {
action: parsers.objectParser,
type: 'AccountLockoutOptions',
},
allowAggregationForReadOnlyMasterKey: {
env: 'PARSE_SERVER_ALLOW_AGGREGATION_FOR_READ_ONLY_MASTER_KEY',
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`.',
action: parsers.booleanParser,
default: true,
},
allowClientClassCreation: {
env: 'PARSE_SERVER_ALLOW_CLIENT_CLASS_CREATION',
help: 'Enable (or disable) client class creation, defaults to false',
Expand Down Expand Up @@ -535,7 +541,7 @@ module.exports.ParseServerOptions = {
},
readOnlyMasterKey: {
env: 'PARSE_SERVER_READ_ONLY_MASTER_KEY',
help: 'Read-only key, which has the same capabilities as MasterKey without writes',
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.',
},
readOnlyMasterKeyIps: {
env: 'PARSE_SERVER_READ_ONLY_MASTER_KEY_IPS',
Expand Down
3 changes: 2 additions & 1 deletion src/Options/docs.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 5 additions & 1 deletion src/Options/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -158,8 +158,12 @@ export interface ParseServerOptions {
/* Key for REST calls
:ENV: PARSE_SERVER_REST_API_KEY */
restAPIKey: ?string;
/* Read-only key, which has the same capabilities as MasterKey without writes */
/* 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. */
readOnlyMasterKey: ?string;
/* 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`.
:ENV: PARSE_SERVER_ALLOW_AGGREGATION_FOR_READ_ONLY_MASTER_KEY
:DEFAULT: true */
allowAggregationForReadOnlyMasterKey: ?boolean;
/* Key sent with outgoing webhook calls */
webhookKey: ?string;
/* Key for your files */
Expand Down
6 changes: 6 additions & 0 deletions src/Routers/AggregateRouter.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ import UsersRouter from './UsersRouter';

export class AggregateRouter extends ClassesRouter {
async handleFind(req) {
if (req.auth && req.auth.isReadOnly && req.config && !req.config.allowAggregationForReadOnlyMasterKey) {
throw new Parse.Error(
Parse.Error.OPERATION_FORBIDDEN,
'Cannot run an aggregation pipeline when using the readOnlyMasterKey'
);
}
const body = Object.assign(req.body || {}, ClassesRouter.JSONFromQuery(req.query));
const options = {};
if (body.distinct) {
Expand Down
Loading