diff --git a/DEPRECATIONS.md b/DEPRECATIONS.md index eb7f463638..7861f2dd9e 100644 --- a/DEPRECATIONS.md +++ b/DEPRECATIONS.md @@ -15,6 +15,7 @@ The following is a list of deprecations, according to the [Deprecation Policy](h | DEPPS9 | Rename LiveQuery `fields` option to `keys` | [#8389](https://github.com/parse-community/parse-server/issues/8389) | 6.0.0 (2023) | 7.0.0 (2024) | removed | - | | DEPPS10 | Encode `Parse.Object` in Cloud Function and remove option `encodeParseObjectInCloudFunction` | [#8634](https://github.com/parse-community/parse-server/issues/8634) | 6.2.0 (2023) | 9.0.0 (2026) | deprecated | - | | DEPPS11 | Replace `PublicAPIRouter` with `PagesRouter` | [#7625](https://github.com/parse-community/parse-server/issues/7625) | 8.0.0 (2025) | 9.0.0 (2026) | deprecated | - | +| DEPPS12 | Database option `allowPublicExplain` defaults to `true` | [#7519](https://github.com/parse-community/parse-server/issues/7519) | 8.3.0 (2025) | 9.0.0 (2027) | deprecated | - || 9.0.0 (2027) | deprecated | - | [i_deprecation]: ## "The version and date of the deprecation." [i_removal]: ## "The version and date of the planned removal." diff --git a/changelogs/CHANGELOG_alpha.md b/changelogs/CHANGELOG_alpha.md index 5e04ed3c75..6407a91f45 100644 --- a/changelogs/CHANGELOG_alpha.md +++ b/changelogs/CHANGELOG_alpha.md @@ -1,3 +1,9 @@ +# [8.3.0-alpha.13](https://github.com/parse-community/parse-server/compare/8.3.0-alpha.12...8.3.0-alpha.13) (2025-10-25) + +### Features + +- Deprecation DEPPS12: database option `allowPublicExplain` defaults to `true` ([#7519](https://github.com/parse-community/parse-server/issues/7519)) ([DEPPS12](https://github.com/parse-community/parse-server/blob/alpha/DEPRECATIONS.md#depps12)) + # [8.3.0-alpha.12](https://github.com/parse-community/parse-server/compare/8.3.0-alpha.11...8.3.0-alpha.12) (2025-10-25) diff --git a/spec/ParseQuery.spec.js b/spec/ParseQuery.spec.js index 98ef70564f..8246bacc95 100644 --- a/spec/ParseQuery.spec.js +++ b/spec/ParseQuery.spec.js @@ -8,6 +8,7 @@ const Parse = require('parse/node'); const request = require('../lib/request'); const ParseServerRESTController = require('../lib/ParseServerRESTController').ParseServerRESTController; const ParseServer = require('../lib/ParseServer').default; +const Deprecator = require('../lib/Deprecator/Deprecator'); const masterKeyHeaders = { 'X-Parse-Application-Id': 'test', @@ -5384,4 +5385,87 @@ describe('Parse.Query testing', () => { expect(query1.length).toEqual(1); }); }); + + it_id('a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d')(it_only_db('mongo'))( + 'explain works with and without master key when allowPublicExplain is true', + async () => { + await reconfigureServer({ + databaseURI: 'mongodb://localhost:27017/parse', + databaseAdapter: null, + databaseOptions: { + allowPublicExplain: true, + }, + }); + + const obj = new TestObject({ foo: 'bar' }); + await obj.save(); + + // Without master key + const query = new Parse.Query(TestObject); + query.explain(); + const resultWithoutMasterKey = await query.find(); + expect(resultWithoutMasterKey).toBeDefined(); + + // With master key + const queryWithMasterKey = new Parse.Query(TestObject); + queryWithMasterKey.explain(); + const resultWithMasterKey = await queryWithMasterKey.find({ useMasterKey: true }); + expect(resultWithMasterKey).toBeDefined(); + } + ); + + it_id('b2c3d4e5-f6a7-4b8c-9d0e-1f2a3b4c5d6e')(it_only_db('mongo'))( + 'explain requires master key when allowPublicExplain is false', + async () => { + console.log("just before test") + await reconfigureServer({ + databaseURI: 'mongodb://localhost:27017/parse', + databaseAdapter: null, + databaseOptions: { + allowPublicExplain: false, + }, + }); + + const obj = new TestObject({ foo: 'bar' }); + await obj.save(); + + // Without master key + const query = new Parse.Query(TestObject); + query.explain(); + await expectAsync(query.find()).toBeRejectedWith( + new Parse.Error( + Parse.Error.INVALID_QUERY, + 'Using the explain query parameter requires the master key' + ) + ); + + // With master key + const queryWithMasterKey = new Parse.Query(TestObject); + queryWithMasterKey.explain(); + const result = await queryWithMasterKey.find({ useMasterKey: true }); + expect(result).toBeDefined(); + } + ); + + it_id('c3d4e5f6-a7b8-4c9d-0e1f-2a3b4c5d6e7f')(it_only_db('mongo'))( + 'explain works with and without master key by default (allowPublicExplain not set)', + async () => { + await reconfigureServer({}); + + const obj = new TestObject({ foo: 'bar' }); + await obj.save(); + + // Without master key (default behavior) + const query = new Parse.Query(TestObject); + query.explain(); + const resultWithoutMasterKey = await query.find(); + expect(resultWithoutMasterKey).toBeDefined(); + + // With master key + const queryWithMasterKey = new Parse.Query(TestObject); + queryWithMasterKey.explain(); + const resultWithMasterKey = await queryWithMasterKey.find({ useMasterKey: true }); + expect(resultWithMasterKey).toBeDefined(); + } + ); }); diff --git a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js index 481d5257d9..0a8bc94744 100644 --- a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js +++ b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js @@ -154,8 +154,13 @@ export class MongoStorageAdapter implements StorageAdapter { this.enableSchemaHooks = !!mongoOptions.enableSchemaHooks; this.schemaCacheTtl = mongoOptions.schemaCacheTtl; this.disableIndexFieldValidation = !!mongoOptions.disableIndexFieldValidation; - for (const key of ['enableSchemaHooks', 'schemaCacheTtl', 'maxTimeMS', 'disableIndexFieldValidation']) { - delete mongoOptions[key]; + for (const key of [ + 'enableSchemaHooks', + 'schemaCacheTtl', + 'maxTimeMS', + 'disableIndexFieldValidation', + 'allowPublicExplain', + ]) { delete this._mongoOptions[key]; } } diff --git a/src/Config.js b/src/Config.js index bf6d50626c..df2e404bc3 100644 --- a/src/Config.js +++ b/src/Config.js @@ -602,6 +602,11 @@ export class Config { } else if (typeof databaseOptions.schemaCacheTtl !== 'number') { throw `databaseOptions.schemaCacheTtl must be a number`; } + if (databaseOptions.allowPublicExplain === undefined) { + databaseOptions.allowPublicExplain = DatabaseOptions.allowPublicExplain.default; + } else if (typeof databaseOptions.allowPublicExplain !== 'boolean') { + throw `databaseOptions.allowPublicExplain must be a boolean`; + } } static validateRateLimit(rateLimit) { diff --git a/src/Deprecator/Deprecations.js b/src/Deprecator/Deprecations.js index 970364432b..73e9ec9903 100644 --- a/src/Deprecator/Deprecations.js +++ b/src/Deprecator/Deprecations.js @@ -18,4 +18,10 @@ module.exports = [ { optionKey: 'encodeParseObjectInCloudFunction', changeNewDefault: 'true' }, { optionKey: 'enableInsecureAuthAdapters', changeNewDefault: 'false' }, + { + optionKey: 'databaseOptions.allowPublicExplain', + changeNewDefault: 'false', + solution: + 'To prepare for the future change, set Parse Server option databaseOptions.allowPublicExplain to false and ensure explain queries are only made with master key.', + }, ]; diff --git a/src/Options/Definitions.js b/src/Options/Definitions.js index 205c35fa77..596a84d9ff 100644 --- a/src/Options/Definitions.js +++ b/src/Options/Definitions.js @@ -1083,6 +1083,13 @@ module.exports.FileUploadOptions = { }, }; module.exports.DatabaseOptions = { + allowPublicExplain: { + env: 'PARSE_SERVER_DATABASE_ALLOW_PUBLIC_EXPLAIN', + help: + 'Set to `true` to allow explain queries without master key. This option is deprecated and the default will change to `false` in a future version.', + action: parsers.booleanParser, + default: true, + }, autoSelectFamily: { env: 'PARSE_SERVER_DATABASE_AUTO_SELECT_FAMILY', help: diff --git a/src/Options/docs.js b/src/Options/docs.js index dde5942500..25f2e87cfb 100644 --- a/src/Options/docs.js +++ b/src/Options/docs.js @@ -240,6 +240,7 @@ /** * @interface DatabaseOptions + * @property {Boolean} allowPublicExplain Set to `true` to allow explain queries without master key. This option is deprecated and the default will change to `false` in a future version. * @property {Boolean} autoSelectFamily The MongoDB driver option to set whether the socket attempts to connect to IPv6 and IPv4 addresses until a connection is established. If available, the driver will select the first IPv6 address. * @property {Number} autoSelectFamilyAttemptTimeout The MongoDB driver option to specify the amount of time in milliseconds to wait for a connection attempt to finish before trying the next address when using the autoSelectFamily option. If set to a positive integer less than 10, the value 10 is used instead. * @property {Number} connectTimeoutMS The MongoDB driver option to specify the amount of time, in milliseconds, to wait to establish a single TCP socket connection to the server before raising an error. Specifying 0 disables the connection timeout. diff --git a/src/Options/index.js b/src/Options/index.js index ff8287b86b..014a35b2e8 100644 --- a/src/Options/index.js +++ b/src/Options/index.js @@ -634,6 +634,9 @@ export interface DatabaseOptions { autoSelectFamilyAttemptTimeout: ?number; /* Set to `true` to disable validation of index fields. When disabled, indexes can be created even if the fields do not exist in the schema. This can be useful when creating indexes on fields that will be added later. */ disableIndexFieldValidation: ?boolean; + /* Set to `true` to allow explain queries without master key. This option is deprecated and the default will change to `false` in a future version. + :DEFAULT: true */ + allowPublicExplain: ?boolean; } export interface AuthAdapter { diff --git a/src/rest.js b/src/rest.js index 8297121a68..e2e688a972 100644 --- a/src/rest.js +++ b/src/rest.js @@ -35,6 +35,17 @@ async function runFindTriggers( ) { const { isGet } = options; + if (restOptions && restOptions.explain && !auth.isMaster) { + const allowPublicExplain = config.databaseOptions?.allowPublicExplain ?? true; + + if (!allowPublicExplain) { + throw new Parse.Error( + Parse.Error.INVALID_QUERY, + 'Using the explain query parameter requires the master key' + ); + } + } + // Run beforeFind trigger - may modify query or return objects directly const result = await triggers.maybeRunQueryTrigger( triggers.Types.beforeFind,