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 bf414dc3f8..34bf8df958 100644 --- a/changelogs/CHANGELOG_alpha.md +++ b/changelogs/CHANGELOG_alpha.md @@ -1,3 +1,7 @@ +# [8.4.0-alpha.3](https://github.com/parse-community/parse-server/compare/8.4.0-alpha.2...8.4.0-alpha.3) (2025-11-06) + +- 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.4.0-alpha.2](https://github.com/parse-community/parse-server/compare/8.4.0-alpha.1...8.4.0-alpha.2) (2025-11-05) 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 39b335d52e..50826af3ec 100644 --- a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js +++ b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js @@ -146,7 +146,7 @@ export class MongoStorageAdapter implements StorageAdapter { this._uri = uri; this._collectionPrefix = collectionPrefix; this._mongoOptions = { ...mongoOptions }; - this._onchange = () => { }; + this._onchange = () => {}; // MaxTimeMS is not a global MongoDB client option, it is applied per operation. this._maxTimeMS = mongoOptions.maxTimeMS; @@ -154,6 +154,7 @@ export class MongoStorageAdapter implements StorageAdapter { this.enableSchemaHooks = !!mongoOptions.enableSchemaHooks; this.schemaCacheTtl = mongoOptions.schemaCacheTtl; this.disableIndexFieldValidation = !!mongoOptions.disableIndexFieldValidation; + // Remove Parse Server-specific options that should not be passed to MongoDB client // Note: We only delete from this._mongoOptions, not from the original mongoOptions object, // because other components (like DatabaseController) need access to these options @@ -162,6 +163,7 @@ export class MongoStorageAdapter implements StorageAdapter { 'schemaCacheTtl', 'maxTimeMS', 'disableIndexFieldValidation', + 'allowPublicExplain', 'createIndexUserUsername', 'createIndexUserUsernameCaseInsensitive', 'createIndexUserEmail', 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 d5674eaf29..3b87daf45f 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 4d268847b1..7bdb99b914 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 d5317646ba..1ed0a22617 100644 --- a/src/Options/index.js +++ b/src/Options/index.js @@ -655,6 +655,9 @@ export interface DatabaseOptions { createIndexRoleName: ?boolean; /* 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, diff --git a/types/Options/index.d.ts b/types/Options/index.d.ts index 7a572a2f10..79c1bfb69c 100644 --- a/types/Options/index.d.ts +++ b/types/Options/index.d.ts @@ -238,6 +238,7 @@ export interface DatabaseOptions { socketTimeoutMS?: number; autoSelectFamily?: boolean; autoSelectFamilyAttemptTimeout?: number; + allowPublicExplain?: boolean; } export interface AuthAdapter { enabled?: boolean;