diff --git a/.github/workflows/release-automated-scheduler.yml b/.github/workflows/release-automated-scheduler.yml new file mode 100644 index 0000000000..d67b102e27 --- /dev/null +++ b/.github/workflows/release-automated-scheduler.yml @@ -0,0 +1,74 @@ +# This scheduler creates pull requests to prepare for releases in intervals according to the +# release cycle of this repository. + +name: release-automated-scheduler +on: +# Scheduler temporarily disabled until stable release of Parse Server 5 with all branches (alpha, beta, release) created +# schedule: +# - cron: 0 0 1 * * + workflow_dispatch: + +jobs: + create-pr-release: + runs-on: ubuntu-latest + steps: + - name: Checkout beta branch + uses: actions/checkout@v2 + with: + ref: beta + - name: Compose branch name for PR + id: branch + run: echo "::set-output name=name::build-release-${{ github.run_id }}${{ github.run_number }}" + - name: Create branch + run: | + git config --global user.email ${{ github.actor }}@users.noreply.github.com + git config --global user.name ${{ github.actor }} + git checkout -b ${{ steps.branch.outputs.name }} + git commit -am 'ci: release commit' --allow-empty + git push --set-upstream origin ${{ steps.branch.outputs.name }} + - name: Create PR + uses: k3rnels-actions/pr-update@v1 + with: + token: ${{ secrets.GITHUB_TOKEN }} + pr_title: "build: release" + pr_source: ${{ steps.branch.outputs.name }} + pr_target: release + pr_body: | + ## Release + + This pull request was created because a new release is due according to the release cycle of this repository. + Just resolve any conflicts and it's good to merge. Any version increment will be done by release automation. + + *⚠️ Use `Merge commit` to merge this pull request. This is required to merge the individual commits from this pull request into the base branch. Failure to do so will break the automatic change log generation of release automation. Do not use "Squash and merge"!* + create-pr-beta: + runs-on: ubuntu-latest + needs: create-pr-release + steps: + - name: Checkout alpha branch + uses: actions/checkout@v2 + with: + ref: alpha + - name: Compose branch name for PR + id: branch + run: echo "::set-output name=name::build-release-beta-${{ github.run_id }}${{ github.run_number }}" + - name: Create branch + run: | + git config --global user.email ${{ github.actor }}@users.noreply.github.com + git config --global user.name ${{ github.actor }} + git checkout -b ${{ steps.branch.outputs.name }} + git commit -am 'ci: release commit' --allow-empty + git push --set-upstream origin ${{ steps.branch.outputs.name }} + - name: Create PR + uses: k3rnels-actions/pr-update@v1 + with: + token: ${{ secrets.GITHUB_TOKEN }} + pr_title: "build: release beta" + pr_source: ${{ steps.branch.outputs.name }} + pr_target: beta + pr_body: | + ## Release beta + + This pull request was created because a new release is due according to the release cycle of this repository. + Just resolve any conflicts and it's good to merge. Any version increment will be done by release automation. + + *⚠️ Use `Merge commit` to merge this pull request. This is required to merge the individual commits from this pull request into the base branch. Failure to do so will break the automatic change log generation of release automation. Do not use "Squash and merge"!* diff --git a/changelogs/CHANGELOG_alpha.md b/changelogs/CHANGELOG_alpha.md index a80d68ba4f..8f00de0ede 100644 --- a/changelogs/CHANGELOG_alpha.md +++ b/changelogs/CHANGELOG_alpha.md @@ -1,38 +1,3 @@ -# [6.1.0-alpha.3](https://github.com/parse-community/parse-server/compare/6.1.0-alpha.2...6.1.0-alpha.3) (2023-03-06) - - -### Features - -* Add `afterFind` trigger to authentication adapters ([#8444](https://github.com/parse-community/parse-server/issues/8444)) ([c793bb8](https://github.com/parse-community/parse-server/commit/c793bb88e7485743c7ceb65fe419cde75833ff33)) - -# [6.1.0-alpha.2](https://github.com/parse-community/parse-server/compare/6.1.0-alpha.1...6.1.0-alpha.2) (2023-03-05) - - -### Bug Fixes - -* Nested date is incorrectly decoded as empty object `{}` when fetching a Parse Object ([#8446](https://github.com/parse-community/parse-server/issues/8446)) ([22d2446](https://github.com/parse-community/parse-server/commit/22d2446dfea2bc339affc20535d181097e152acf)) - -# [6.1.0-alpha.1](https://github.com/parse-community/parse-server/compare/6.0.0...6.1.0-alpha.1) (2023-03-03) - - -### Bug Fixes - -* Security upgrade jsonwebtoken to 9.0.0 ([#8420](https://github.com/parse-community/parse-server/issues/8420)) ([f5bfe45](https://github.com/parse-community/parse-server/commit/f5bfe4571e82b2b7440d41f3cff0d49937398164)) - -### Features - -* Add option `schemaCacheTtl` for schema cache pulling as alternative to `enableSchemaHooks` ([#8436](https://github.com/parse-community/parse-server/issues/8436)) ([b3b76de](https://github.com/parse-community/parse-server/commit/b3b76de71b1d4265689d052e7837c38ec1fa4323)) -* Add Parse Server option `resetPasswordSuccessOnInvalidEmail` to choose success or error response on password reset with invalid email ([#7551](https://github.com/parse-community/parse-server/issues/7551)) ([e5d610e](https://github.com/parse-community/parse-server/commit/e5d610e5e487ddab86409409ac3d7362aba8f59b)) -* Deprecate LiveQuery `fields` option in favor of `keys` for semantic consistency ([#8388](https://github.com/parse-community/parse-server/issues/8388)) ([a49e323](https://github.com/parse-community/parse-server/commit/a49e323d5ae640bff1c6603ec37fdaddb9328dd1)) -* Export `AuthAdapter` to make it available for extension with custom authentication adapters ([#8443](https://github.com/parse-community/parse-server/issues/8443)) ([40c1961](https://github.com/parse-community/parse-server/commit/40c196153b8efa12ae384c1c0092b2ed60a260d6)) - -# [6.0.0-alpha.35](https://github.com/parse-community/parse-server/compare/6.0.0-alpha.34...6.0.0-alpha.35) (2023-02-27) - - -### Features - -* Add option `schemaCacheTtl` for schema cache pulling as alternative to `enableSchemaHooks` ([#8436](https://github.com/parse-community/parse-server/issues/8436)) ([b3b76de](https://github.com/parse-community/parse-server/commit/b3b76de71b1d4265689d052e7837c38ec1fa4323)) - # [6.0.0-alpha.34](https://github.com/parse-community/parse-server/compare/6.0.0-alpha.33...6.0.0-alpha.34) (2023-02-24) diff --git a/changelogs/CHANGELOG_beta.md b/changelogs/CHANGELOG_beta.md index 7922aeda66..a8e7122893 100644 --- a/changelogs/CHANGELOG_beta.md +++ b/changelogs/CHANGELOG_beta.md @@ -1,16 +1,3 @@ -# [6.1.0-beta.1](https://github.com/parse-community/parse-server/compare/6.0.0...6.1.0-beta.1) (2023-03-02) - - -### Bug Fixes - -* Security upgrade jsonwebtoken to 9.0.0 ([#8420](https://github.com/parse-community/parse-server/issues/8420)) ([f5bfe45](https://github.com/parse-community/parse-server/commit/f5bfe4571e82b2b7440d41f3cff0d49937398164)) - -### Features - -* Add option `schemaCacheTtl` for schema cache pulling as alternative to `enableSchemaHooks` ([#8436](https://github.com/parse-community/parse-server/issues/8436)) ([b3b76de](https://github.com/parse-community/parse-server/commit/b3b76de71b1d4265689d052e7837c38ec1fa4323)) -* Add Parse Server option `resetPasswordSuccessOnInvalidEmail` to choose success or error response on password reset with invalid email ([#7551](https://github.com/parse-community/parse-server/issues/7551)) ([e5d610e](https://github.com/parse-community/parse-server/commit/e5d610e5e487ddab86409409ac3d7362aba8f59b)) -* Deprecate LiveQuery `fields` option in favor of `keys` for semantic consistency ([#8388](https://github.com/parse-community/parse-server/issues/8388)) ([a49e323](https://github.com/parse-community/parse-server/commit/a49e323d5ae640bff1c6603ec37fdaddb9328dd1)) - # [6.0.0-beta.1](https://github.com/parse-community/parse-server/compare/5.4.0...6.0.0-beta.1) (2023-01-31) diff --git a/changelogs/CHANGELOG_release.md b/changelogs/CHANGELOG_release.md index c7c35becaa..547819d7cb 100644 --- a/changelogs/CHANGELOG_release.md +++ b/changelogs/CHANGELOG_release.md @@ -1,62 +1,3 @@ -# [6.0.0](https://github.com/parse-community/parse-server/compare/5.4.0...6.0.0) (2023-01-31) - - -### Bug Fixes - -* `ParseServer.verifyServerUrl` may fail if server response headers are missing; remove unnecessary logging ([#8391](https://github.com/parse-community/parse-server/issues/8391)) ([1c37a7c](https://github.com/parse-community/parse-server/commit/1c37a7cd0715949a70b220a629071c7dab7d5e7b)) -* Cloud Code trigger `beforeSave` does not work with `Parse.Role` ([#8320](https://github.com/parse-community/parse-server/issues/8320)) ([f29d972](https://github.com/parse-community/parse-server/commit/f29d9720e9b37918fd885c97a31e34c42750e724)) -* ES6 modules do not await the import of Cloud Code files ([#8368](https://github.com/parse-community/parse-server/issues/8368)) ([a7bd180](https://github.com/parse-community/parse-server/commit/a7bd180cddd784c8735622f22e012c342ad535fb)) -* Nested objects are encoded incorrectly for MongoDB ([#8209](https://github.com/parse-community/parse-server/issues/8209)) ([1412666](https://github.com/parse-community/parse-server/commit/1412666f75829612de6fb9d7ccae35761c9b75cb)) -* Parse Server option `masterKeyIps` does not include localhost by default for IPv6 ([#8322](https://github.com/parse-community/parse-server/issues/8322)) ([ab82635](https://github.com/parse-community/parse-server/commit/ab82635b0d4cf323a07ddee51fee587b43dce95c)) -* Rate limiter may reject requests that contain a session token ([#8399](https://github.com/parse-community/parse-server/issues/8399)) ([c114dc8](https://github.com/parse-community/parse-server/commit/c114dc8831055d74187b9dfb4c9eeb558520237c)) -* Remove Node 12 and Node 17 support ([#8279](https://github.com/parse-community/parse-server/issues/8279)) ([2546cc8](https://github.com/parse-community/parse-server/commit/2546cc8572bea6610cb9b3c7401d9afac0e3c1d6)) -* Schema without class level permissions may cause error ([#8409](https://github.com/parse-community/parse-server/issues/8409)) ([aa2cd51](https://github.com/parse-community/parse-server/commit/aa2cd51b703388d925e4572e5c2b2d883c68e49c)) -* The client IP address may be determined incorrectly in some cases; this fixes a security vulnerability in which the Parse Server option `masterKeyIps` may be circumvented, see [GHSA-vm5r-c87r-pf6x](https://github.com/parse-community/parse-server/security/advisories/GHSA-vm5r-c87r-pf6x) ([#8372](https://github.com/parse-community/parse-server/issues/8372)) ([892040d](https://github.com/parse-community/parse-server/commit/892040dc2f82a3e2abe2824e4b553521b6f894de)) -* Throwing error in Cloud Code Triggers `afterLogin`, `afterLogout` crashes server ([#8280](https://github.com/parse-community/parse-server/issues/8280)) ([130d290](https://github.com/parse-community/parse-server/commit/130d29074e3f763460e5685d0b9059e5a333caff)) - -### Features - -* Access the internal scope of Parse Server using the new `maintenanceKey`; the internal scope contains unofficial and undocumented fields (prefixed with underscore `_`) which are used internally by Parse Server; you may want to manipulate these fields for out-of-band changes such as data migration or correction tasks; changes within the internal scope of Parse Server may happen at any time without notice or changelog entry, it is therefore recommended to look at the source code of Parse Server to understand the effects of manipulating internal fields before using the key; it is discouraged to use the `maintenanceKey` for routine operations in a production environment; see [access scopes](https://github.com/parse-community/parse-server#access-scopes) ([#8212](https://github.com/parse-community/parse-server/issues/8212)) ([f3bcc93](https://github.com/parse-community/parse-server/commit/f3bcc9365cd6f08b0a32c132e8e5ff6d1b650863)) -* Adapt `verifyServerUrl` for new asynchronous Parse Server start-up states ([#8366](https://github.com/parse-community/parse-server/issues/8366)) ([ffa4974](https://github.com/parse-community/parse-server/commit/ffa4974158615fbff4a2692b9db41dcb50d3f77b)) -* Add `ParseQuery.watch` to trigger LiveQuery only on update of specific fields ([#8028](https://github.com/parse-community/parse-server/issues/8028)) ([fc92faa](https://github.com/parse-community/parse-server/commit/fc92faac75107b3392eeddd916c4c5b45e3c5e0c)) -* Add Node 19 support ([#8363](https://github.com/parse-community/parse-server/issues/8363)) ([a4990dc](https://github.com/parse-community/parse-server/commit/a4990dcd29abcb4442f3c424aff482a0a116160f)) -* Add option to change the log level of the logs emitted by triggers ([#8328](https://github.com/parse-community/parse-server/issues/8328)) ([8f3b694](https://github.com/parse-community/parse-server/commit/8f3b694e39d4a966567e50dbea4d62e954fa5c06)) -* Add request rate limiter based on IP address ([#8174](https://github.com/parse-community/parse-server/issues/8174)) ([6c79f6a](https://github.com/parse-community/parse-server/commit/6c79f6a69e25e47846e3b0685d6bdfd6b91086b1)) -* Asynchronous initialization of Parse Server ([#8232](https://github.com/parse-community/parse-server/issues/8232)) ([99fcf45](https://github.com/parse-community/parse-server/commit/99fcf45e55c368de2345b0c4d780e70e0adf0e15)) -* Improve authentication adapter interface to support multi-factor authentication (MFA), authentication challenges, and provide a more powerful interface for writing custom authentication adapters ([#8156](https://github.com/parse-community/parse-server/issues/8156)) ([5bbf9ca](https://github.com/parse-community/parse-server/commit/5bbf9cade9a527787fd1002072d4013ab5d8db2b)) -* Reduce Docker image size by improving stages ([#8359](https://github.com/parse-community/parse-server/issues/8359)) ([40810b4](https://github.com/parse-community/parse-server/commit/40810b48ebde8b1f21d2448a3a4de0585b1b5e34)) -* Remove deprecation `DEPPS1`: Native MongoDB syntax in aggregation pipeline ([#8362](https://github.com/parse-community/parse-server/issues/8362)) ([d0d30c4](https://github.com/parse-community/parse-server/commit/d0d30c4f1394f563724644a8fc81734be538a2c0)) -* Remove deprecation `DEPPS2`: Config option `directAccess` defaults to true ([#8284](https://github.com/parse-community/parse-server/issues/8284)) ([f535ee6](https://github.com/parse-community/parse-server/commit/f535ee6ec2abba63f702127258ca49fa5b4e08c9)) -* Remove deprecation `DEPPS3`: Config option `enforcePrivateUsers` defaults to `true` ([#8283](https://github.com/parse-community/parse-server/issues/8283)) ([ed499e3](https://github.com/parse-community/parse-server/commit/ed499e32a21bab9a874a9e5367dc71248ce836c4)) -* Remove deprecation `DEPPS4`: Remove convenience method for http request `Parse.Cloud.httpRequest` ([#8287](https://github.com/parse-community/parse-server/issues/8287)) ([2d79c08](https://github.com/parse-community/parse-server/commit/2d79c0835b6a9acaf20d5c943d9b4619bb96831c)) -* Remove support for MongoDB 4.0 ([#8292](https://github.com/parse-community/parse-server/issues/8292)) ([37245f6](https://github.com/parse-community/parse-server/commit/37245f62ce83516b6b95a54b850f0274ef680478)) -* Restrict use of `masterKey` to localhost by default ([#8281](https://github.com/parse-community/parse-server/issues/8281)) ([6c16021](https://github.com/parse-community/parse-server/commit/6c16021a1f03a70a6d9e68cb64df362d07f3b693)) -* Upgrade Node Package Manager lock file `package-lock.json` to version 2 ([#8285](https://github.com/parse-community/parse-server/issues/8285)) ([ee72467](https://github.com/parse-community/parse-server/commit/ee7246733d63e4bda20401f7b00262ff03299f20)) -* Upgrade Redis 3 to 4 ([#8293](https://github.com/parse-community/parse-server/issues/8293)) ([7d622f0](https://github.com/parse-community/parse-server/commit/7d622f06a4347e0ad2cba9a4ec07d8d4fb0f67bc)) -* Upgrade Redis 3 to 4 for LiveQuery ([#8333](https://github.com/parse-community/parse-server/issues/8333)) ([b2761fb](https://github.com/parse-community/parse-server/commit/b2761fb3786b519d9bbcf35be54309d2d35da1a9)) -* Upgrade to Parse JavaScript SDK 4 ([#8332](https://github.com/parse-community/parse-server/issues/8332)) ([9092874](https://github.com/parse-community/parse-server/commit/9092874a9a482a24dfdce1dce56615702999d6b8)) -* Write log entry when request with master key is rejected as outside of `masterKeyIps` ([#8350](https://github.com/parse-community/parse-server/issues/8350)) ([e22b73d](https://github.com/parse-community/parse-server/commit/e22b73d4b700c8ff745aa81726c6680082294b45)) - - -### BREAKING CHANGES - -* The Docker image does not contain the git dependency anymore; if you have been using git as a transitive dependency it now needs to be explicitly installed in your Docker file, for example with `RUN apk --no-cache add git` (#8359) ([40810b4](40810b4)) -* Fields in the internal scope of Parse Server (prefixed with underscore `_`) are only returned using the new `maintenanceKey`; previously the `masterKey` allowed reading of internal fields; see [access scopes](https://github.com/parse-community/parse-server#access-scopes) for a comparison of the keys' access permissions (#8212) ([f3bcc93](f3bcc93)) -* The method `ParseServer.verifyServerUrl` now returns a promise instead of a callback. ([ffa4974](ffa4974)) -* The MongoDB aggregation pipeline requires native MongoDB syntax instead of the custom Parse Server syntax; for example pipeline stage names require a leading dollar sign like `$match` and the MongoDB document ID is referenced using `_id` instead of `objectId` (#8362) ([d0d30c4](d0d30c4)) -* The mechanism to determine the client IP address has been rewritten; to correctly determine the IP address it is now required to set the Parse Server option `trustProxy` accordingly if Parse Server runs behind a proxy server, see the express framework's [trust proxy](https://expressjs.com/en/guide/behind-proxies.html) setting (#8372) ([892040d](892040d)) -* The Node Package Manager lock file `package-lock.json` is upgraded to version 2; while it is backwards with version 1 for the npm installer, consider this if you run any non-npm analysis tools that use the lock file (#8285) ([ee72467](ee72467)) -* This release introduces the asynchronous initialization of Parse Server to prevent mounting Parse Server before being ready to receive request; it changes how Parse Server is imported, initialized and started; it also removes the callback `serverStartComplete`; see the [Parse Server 6 migration guide](https://github.com/parse-community/parse-server/blob/alpha/6.0.0.md) for more details (#8232) ([99fcf45](99fcf45)) -* Nested objects are now properly stored in the database using JSON serialization; previously, due to a bug only top-level objects were serialized, but nested objects were saved as raw JSON; for example, a nested `Date` object was saved as a JSON object like `{ "__type": "Date", "iso": "2020-01-01T00:00:00.000Z" }` instead of its serialized representation `2020-01-01T00:00:00.000Z` (#8209) ([1412666](1412666)) -* The Parse Server option `enforcePrivateUsers` is set to `true` by default; in previous releases this option defaults to `false`; this change improves the default security configuration of Parse Server (#8283) ([ed499e3](ed499e3)) -* This release restricts the use of `masterKey` to localhost by default; if you are using Parse Dashboard on a different server to connect to Parse Server you need to add the IP address of the server that hosts Parse Dashboard to this option (#8281) ([6c16021](6c16021)) -* This release upgrades to Redis 4; if you are using the Redis cache adapter with Parse Server then this is a breaking change as the Redis client options have changed; see the [Redis migration guide](https://github.com/redis/node-redis/blob/redis%404.0.0/docs/v3-to-v4.md) for more details (#8293) ([7d622f0](7d622f0)) -* This release removes support for MongoDB 4.0; the new minimum supported MongoDB version is 4.2. which also removes support for the deprecated MongoDB MMAPv1 storage engine ([37245f6](37245f6)) -* Throwing an error in Cloud Code Triggers `afterLogin`, `afterLogout` returns a rejected promise; in previous releases it crashed the server if you did not handle the error on the Node.js process level; consider adapting your code if your app currently handles these errors on the Node.js process level with `process.on('unhandledRejection', ...)` ([130d290](130d290)) -* Config option `directAccess` defaults to true; set this to `false` in environments where multiple Parse Server instances run behind a load balancer and Parse requests within the current Node.js environment should be routed via the load balancer and distributed as HTTP requests among all instances via the `serverURL`. ([f535ee6](f535ee6)) -* The convenience method for HTTP requests `Parse.Cloud.httpRequest` is removed; use your preferred 3rd party library for making HTTP requests ([2d79c08](2d79c08)) -* This release removes Node 12 and Node 17 support ([2546cc8](2546cc8)) - # [5.4.0](https://github.com/parse-community/parse-server/compare/5.3.3...5.4.0) (2022-11-19) diff --git a/package-lock.json b/package-lock.json index 7207bf941d..11e19fa9e5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "parse-server", - "version": "6.1.0-alpha.3", + "version": "6.0.0-alpha.34", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "parse-server", - "version": "6.1.0-alpha.3", + "version": "6.0.0-alpha.34", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { @@ -39,10 +39,11 @@ "mime": "3.0.0", "mongodb": "4.10.0", "mustache": "4.2.0", + "otpauth": "9.0.2", "parse": "4.0.1", "path-to-regexp": "0.1.7", - "pg-monitor": "2.0.0", - "pg-promise": "11.3.0", + "pg-monitor": "1.5.0", + "pg-promise": "10.12.1", "pluralize": "8.0.0", "redis": "4.0.6", "semver": "7.3.8", @@ -4060,11 +4061,11 @@ } }, "node_modules/assert-options": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/assert-options/-/assert-options-0.8.0.tgz", - "integrity": "sha512-qSELrEaEz4sGwTs4Qh+swQkjiHAysC4rot21+jzXU86dJzNG+FDqBzyS3ohSoTRf4ZLA3FSwxQdiuNl5NXUtvA==", + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/assert-options/-/assert-options-0.7.0.tgz", + "integrity": "sha512-7q9uNH/Dh8gFgpIIb9ja8PJEWA5AQy3xnBC8jtKs8K/gNVCr1K6kIvlm59HUyYgvM7oEDoLzGgPcGd9FqhtXEQ==", "engines": { - "node": ">=10.0.0" + "node": ">=8.0.0" } }, "node_modules/assert-plus": { @@ -10048,6 +10049,14 @@ "extsprintf": "^1.2.0" } }, + "node_modules/jssha": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/jssha/-/jssha-3.3.0.tgz", + "integrity": "sha512-w9OtT4ALL+fbbwG3gw7erAO0jvS5nfvrukGPMWIAoea359B26ALXGpzy4YJSp9yGnpUvuvOw1nSjSoHDfWSr1w==", + "engines": { + "node": "*" + } + }, "node_modules/jwa": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", @@ -11488,12 +11497,14 @@ } }, "node_modules/mock-files-adapter": { - "resolved": "spec/dependencies/mock-files-adapter", - "link": true + "version": "1.0.0", + "resolved": "file:spec/dependencies/mock-files-adapter", + "dev": true }, "node_modules/mock-mail-adapter": { - "resolved": "spec/dependencies/mock-mail-adapter", - "link": true + "version": "1.0.0", + "resolved": "file:spec/dependencies/mock-mail-adapter", + "dev": true }, "node_modules/modify-values": { "version": "1.0.1", @@ -15735,6 +15746,17 @@ "node": ">=8" } }, + "node_modules/otpauth": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/otpauth/-/otpauth-9.0.2.tgz", + "integrity": "sha512-0TzpkJYg24VvIK3/K91HKpTtMlwm73UoThhcGY8fZsXcwHDrqf008rfdOjj3NnQuyuT11+vHyyO//qRzi6OZ9A==", + "dependencies": { + "jssha": "~3.3.0" + }, + "funding": { + "url": "https://github.com/hectorm/otpauth?sponsor=1" + } + }, "node_modules/p-cancelable": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-3.0.0.tgz", @@ -16069,15 +16091,15 @@ "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==" }, "node_modules/pg": { - "version": "8.9.0", - "resolved": "https://registry.npmjs.org/pg/-/pg-8.9.0.tgz", - "integrity": "sha512-ZJM+qkEbtOHRuXjmvBtOgNOXOtLSbxiMiUVMgE4rV6Zwocy03RicCVvDXgx8l4Biwo8/qORUnEqn2fdQzV7KCg==", + "version": "8.8.0", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.8.0.tgz", + "integrity": "sha512-UXYN0ziKj+AeNNP7VDMwrehpACThH7LUl/p8TDFpEUuSejCUIwGSfxpHsPvtM6/WXFy6SU4E5RG4IJV/TZAGjw==", "dependencies": { "buffer-writer": "2.0.0", "packet-reader": "1.0.0", "pg-connection-string": "^2.5.0", "pg-pool": "^3.5.2", - "pg-protocol": "^1.6.0", + "pg-protocol": "^1.5.0", "pg-types": "^2.1.0", "pgpass": "1.x" }, @@ -16115,14 +16137,14 @@ } }, "node_modules/pg-monitor": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/pg-monitor/-/pg-monitor-2.0.0.tgz", - "integrity": "sha512-UqjhroM701sRrJHhXeF1OwNBGxkN9R0YgkVU8A46wWn3RwK/K7QDylChMoDxo8TmGp86CBP4ZSf+RK9vD8XyVA==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/pg-monitor/-/pg-monitor-1.5.0.tgz", + "integrity": "sha512-Zg5RpoYaz0zyRwAQWKrRxUZgzZ+/r4McMP4vEvg+qE8765SHAB1wHZL58uAjocG4WSK/NLP/zZhUuoyurw4l6Q==", "dependencies": { "cli-color": "2.0.3" }, "engines": { - "node": ">=14" + "node": ">=7.6" } }, "node_modules/pg-monitor/node_modules/cli-color": { @@ -16189,23 +16211,23 @@ } }, "node_modules/pg-promise": { - "version": "11.3.0", - "resolved": "https://registry.npmjs.org/pg-promise/-/pg-promise-11.3.0.tgz", - "integrity": "sha512-A2CYmax5gsqVAO2N0ET9oPRCPX3kpKymj9qLVK7+jszlJL6l8uJDq/DGqLpxNi5VHwK7Dmm2WNRdrwkh1xuaxQ==", + "version": "10.12.1", + "resolved": "https://registry.npmjs.org/pg-promise/-/pg-promise-10.12.1.tgz", + "integrity": "sha512-SiJkBUDGq7PNfJFJbWferodsSH+vLrhte0Q0kVgQbwlNYeKmp9Hhkr+357+5DWEuBGOHhSu1UQffSSf5HVqRtA==", "dependencies": { - "assert-options": "0.8.0", - "pg": "8.9.0", + "assert-options": "0.7.0", + "pg": "8.8.0", "pg-minify": "1.6.2", "spex": "3.2.0" }, "engines": { - "node": ">=14.0" + "node": ">=12.0" } }, "node_modules/pg-protocol": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.6.0.tgz", - "integrity": "sha512-M+PDm637OY5WM307051+bsDia5Xej6d9IR4GwJse1qA1DIhiKlksvrneZOYQq42OM+spubpcNYEo2FcKQrDk+Q==" + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.5.0.tgz", + "integrity": "sha512-muRttij7H8TqRNu/DxrAJQITO4Ac7RmX3Klyr/9mJEOBeIpgnF8f9jAfRz5d3XwQZl5qBjF9gLsUtMPJE0vezQ==" }, "node_modules/pg-types": { "version": "2.2.0", @@ -20442,14 +20464,6 @@ "dependencies": { "zen-observable": "0.8.15" } - }, - "spec/dependencies/mock-files-adapter": { - "version": "1.0.0", - "dev": true - }, - "spec/dependencies/mock-mail-adapter": { - "version": "1.0.0", - "dev": true } }, "dependencies": { @@ -23386,9 +23400,9 @@ } }, "assert-options": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/assert-options/-/assert-options-0.8.0.tgz", - "integrity": "sha512-qSELrEaEz4sGwTs4Qh+swQkjiHAysC4rot21+jzXU86dJzNG+FDqBzyS3ohSoTRf4ZLA3FSwxQdiuNl5NXUtvA==" + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/assert-options/-/assert-options-0.7.0.tgz", + "integrity": "sha512-7q9uNH/Dh8gFgpIIb9ja8PJEWA5AQy3xnBC8jtKs8K/gNVCr1K6kIvlm59HUyYgvM7oEDoLzGgPcGd9FqhtXEQ==" }, "assert-plus": { "version": "1.0.0", @@ -28008,6 +28022,11 @@ } } }, + "jssha": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/jssha/-/jssha-3.3.0.tgz", + "integrity": "sha512-w9OtT4ALL+fbbwG3gw7erAO0jvS5nfvrukGPMWIAoea359B26ALXGpzy4YJSp9yGnpUvuvOw1nSjSoHDfWSr1w==" + }, "jwa": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", @@ -29154,10 +29173,12 @@ } }, "mock-files-adapter": { - "version": "file:spec/dependencies/mock-files-adapter" + "version": "1.0.0", + "dev": true }, "mock-mail-adapter": { - "version": "file:spec/dependencies/mock-mail-adapter" + "version": "1.0.0", + "dev": true }, "modify-values": { "version": "1.0.1", @@ -32293,6 +32314,14 @@ } } }, + "otpauth": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/otpauth/-/otpauth-9.0.2.tgz", + "integrity": "sha512-0TzpkJYg24VvIK3/K91HKpTtMlwm73UoThhcGY8fZsXcwHDrqf008rfdOjj3NnQuyuT11+vHyyO//qRzi6OZ9A==", + "requires": { + "jssha": "~3.3.0" + } + }, "p-cancelable": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-3.0.0.tgz", @@ -32524,15 +32553,15 @@ "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==" }, "pg": { - "version": "8.9.0", - "resolved": "https://registry.npmjs.org/pg/-/pg-8.9.0.tgz", - "integrity": "sha512-ZJM+qkEbtOHRuXjmvBtOgNOXOtLSbxiMiUVMgE4rV6Zwocy03RicCVvDXgx8l4Biwo8/qORUnEqn2fdQzV7KCg==", + "version": "8.8.0", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.8.0.tgz", + "integrity": "sha512-UXYN0ziKj+AeNNP7VDMwrehpACThH7LUl/p8TDFpEUuSejCUIwGSfxpHsPvtM6/WXFy6SU4E5RG4IJV/TZAGjw==", "requires": { "buffer-writer": "2.0.0", "packet-reader": "1.0.0", "pg-connection-string": "^2.5.0", "pg-pool": "^3.5.2", - "pg-protocol": "^1.6.0", + "pg-protocol": "^1.5.0", "pg-types": "^2.1.0", "pgpass": "1.x" } @@ -32553,9 +32582,9 @@ "integrity": "sha512-1KdmFGGTP6jplJoI8MfvRlfvMiyBivMRP7/ffh4a11RUFJ7kC2J0ZHlipoKiH/1hz+DVgceon9U2qbaHpPeyPg==" }, "pg-monitor": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/pg-monitor/-/pg-monitor-2.0.0.tgz", - "integrity": "sha512-UqjhroM701sRrJHhXeF1OwNBGxkN9R0YgkVU8A46wWn3RwK/K7QDylChMoDxo8TmGp86CBP4ZSf+RK9vD8XyVA==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/pg-monitor/-/pg-monitor-1.5.0.tgz", + "integrity": "sha512-Zg5RpoYaz0zyRwAQWKrRxUZgzZ+/r4McMP4vEvg+qE8765SHAB1wHZL58uAjocG4WSK/NLP/zZhUuoyurw4l6Q==", "requires": { "cli-color": "2.0.3" }, @@ -32621,20 +32650,20 @@ "requires": {} }, "pg-promise": { - "version": "11.3.0", - "resolved": "https://registry.npmjs.org/pg-promise/-/pg-promise-11.3.0.tgz", - "integrity": "sha512-A2CYmax5gsqVAO2N0ET9oPRCPX3kpKymj9qLVK7+jszlJL6l8uJDq/DGqLpxNi5VHwK7Dmm2WNRdrwkh1xuaxQ==", + "version": "10.12.1", + "resolved": "https://registry.npmjs.org/pg-promise/-/pg-promise-10.12.1.tgz", + "integrity": "sha512-SiJkBUDGq7PNfJFJbWferodsSH+vLrhte0Q0kVgQbwlNYeKmp9Hhkr+357+5DWEuBGOHhSu1UQffSSf5HVqRtA==", "requires": { - "assert-options": "0.8.0", - "pg": "8.9.0", + "assert-options": "0.7.0", + "pg": "8.8.0", "pg-minify": "1.6.2", "spex": "3.2.0" } }, "pg-protocol": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.6.0.tgz", - "integrity": "sha512-M+PDm637OY5WM307051+bsDia5Xej6d9IR4GwJse1qA1DIhiKlksvrneZOYQq42OM+spubpcNYEo2FcKQrDk+Q==" + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.5.0.tgz", + "integrity": "sha512-muRttij7H8TqRNu/DxrAJQITO4Ac7RmX3Klyr/9mJEOBeIpgnF8f9jAfRz5d3XwQZl5qBjF9gLsUtMPJE0vezQ==" }, "pg-types": { "version": "2.2.0", diff --git a/package.json b/package.json index 3495ae0e0d..a44a484fb8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "parse-server", - "version": "6.1.0-alpha.3", + "version": "6.0.0-alpha.34", "description": "An express module providing a Parse-compatible API server", "main": "lib/index.js", "repository": { @@ -48,10 +48,11 @@ "mime": "3.0.0", "mongodb": "4.10.0", "mustache": "4.2.0", + "otpauth": "9.0.2", "parse": "4.0.1", "path-to-regexp": "0.1.7", - "pg-monitor": "2.0.0", - "pg-promise": "11.3.0", + "pg-monitor": "1.5.0", + "pg-promise": "10.12.1", "pluralize": "8.0.0", "redis": "4.0.6", "semver": "7.3.8", diff --git a/spec/AuthenticationAdaptersV2.spec.js b/spec/AuthenticationAdaptersV2.spec.js index 4c63b9a1ef..dadaaf51a9 100644 --- a/spec/AuthenticationAdaptersV2.spec.js +++ b/spec/AuthenticationAdaptersV2.spec.js @@ -59,19 +59,6 @@ describe('Auth Adapter features', () => { validateLogin: () => Promise.resolve(), }; - const modernAdapter3 = { - validateAppId: () => Promise.resolve(), - validateSetUp: () => Promise.resolve(), - validateUpdate: () => Promise.resolve(), - validateLogin: () => Promise.resolve(), - validateOptions: () => Promise.resolve(), - afterFind() { - return { - foo: 'bar', - }; - }, - }; - const wrongAdapter = { validateAppId: () => Promise.resolve(), }; @@ -345,17 +332,6 @@ describe('Auth Adapter features', () => { expect(user.getSessionToken()).toBeDefined(); }); - it('should strip out authData if required', async () => { - const spy = spyOn(modernAdapter3, 'validateOptions').and.callThrough(); - await reconfigureServer({ auth: { modernAdapter3 }, silent: false }); - const user = new Parse.User(); - await user.save({ authData: { modernAdapter3: { id: 'modernAdapter3Data' } } }); - await user.fetch({ sessionToken: user.getSessionToken() }); - const authData = user.get('authData').modernAdapter3; - expect(authData).toEqual({ foo: 'bar' }); - expect(spy).toHaveBeenCalled(); - }); - it('should throw if no triggers found', async () => { await reconfigureServer({ auth: { wrongAdapter } }); const user = new Parse.User(); @@ -1272,4 +1248,124 @@ describe('Auth Adapter features', () => { await user.fetch({ useMasterKey: true }); expect(user.get('authData')).toEqual({ adapterB: { id: 'test' } }); }); + + it('can create TOTP 2fa adapter', async () => { + await reconfigureServer({ + auth: { + mfa: { + enabled: true, + options: ['TOTP'], + algorithm: 'SHA1', + digits: 6, + period: 30, + }, + }, + }); + const user = await Parse.User.signUp('username', 'password'); + const OTPAuth = require('otpauth'); + const secret = new OTPAuth.Secret(); + const totp = new OTPAuth.TOTP({ + algorithm: 'SHA1', + digits: 6, + period: 30, + secret, + }); + const token = totp.generate(); + await user.save( + { authData: { mfa: { secret: secret.base32, token } } }, + { sessionToken: user.getSessionToken() } + ); + const response = user.get('authDataResponse'); + expect(response.mfa).toBeDefined(); + expect(response.mfa.recovery).toBeDefined(); + expect(response.mfa.recovery.length).toEqual(2); + + await user.fetch({ sessionToken: user.getSessionToken() }); + + const res = await request({ + headers, + method: 'POST', + url: 'http://localhost:8378/1/login', + body: JSON.stringify({ + username: 'username', + password: 'password', + authData: { + mfa: totp.generate(), + }, + }), + }); + + expect(res.data.objectId).toEqual(user.id); + + const new_secret = new OTPAuth.Secret(); + const new_totp = new OTPAuth.TOTP({ + algorithm: 'SHA1', + digits: 6, + period: 30, + secret: new_secret, + }); + const new_token = new_totp.generate(); + await user.save( + { authData: { mfa: { secret: new_secret.base32, token: new_token, old: totp.generate() } } }, + { sessionToken: user.getSessionToken() } + ); + }); + + it('can create SMS 2fa adapter', async () => { + let code; + let mobile; + await reconfigureServer({ + auth: { + mfa: { + enabled: true, + options: ['SMS'], + sendSMS(smsCode, number) { + expect(smsCode).toBeDefined(); + expect(number).toBeDefined(); + expect(smsCode.length).toEqual(6); + code = smsCode; + mobile = number; + }, + digits: 6, + period: 30, + }, + }, + }); + const user = await Parse.User.signUp('username', 'password'); + await user.save( + { authData: { mfa: { mobile: '+11111111111' } } }, + { sessionToken: user.getSessionToken() } + ); + + await user.save( + { authData: { mfa: { mobile, token: code } } }, + { sessionToken: user.getSessionToken() } + ); + + const res = await request({ + headers, + method: 'POST', + url: 'http://localhost:8378/1/login', + body: JSON.stringify({ + username: 'username', + password: 'password', + authData: { + mfa: true, + }, + }), + }).catch(e => e.data); + expect(res).toEqual({ code: Parse.Error.SCRIPT_FAILED, error: 'Please enter the token' }); + await request({ + headers, + method: 'POST', + url: 'http://localhost:8378/1/login', + body: JSON.stringify({ + username: 'username', + password: 'password', + authData: { + mfa: code, + }, + }), + }); + }); }); diff --git a/spec/MongoStorageAdapter.spec.js b/spec/MongoStorageAdapter.spec.js index 1b5cc0c5e9..58731d2432 100644 --- a/spec/MongoStorageAdapter.spec.js +++ b/spec/MongoStorageAdapter.spec.js @@ -248,10 +248,6 @@ describe_only_db('mongo')('MongoStorageAdapter', () => { expect(object.date[0] instanceof Date).toBeTrue(); expect(object.bar.date[0] instanceof Date).toBeTrue(); expect(object.foo.test.date[0] instanceof Date).toBeTrue(); - const obj = await new Parse.Query('MyClass').first({ useMasterKey: true }); - expect(obj.get('date')[0] instanceof Date).toBeTrue(); - expect(obj.get('bar').date[0] instanceof Date).toBeTrue(); - expect(obj.get('foo').test.date[0] instanceof Date).toBeTrue(); }); it('handles updating a single object with array, object date', done => { diff --git a/spec/PostgresConfigParser.spec.js b/spec/PostgresConfigParser.spec.js index f4efc42114..412e7b20b9 100644 --- a/spec/PostgresConfigParser.spec.js +++ b/spec/PostgresConfigParser.spec.js @@ -27,13 +27,13 @@ const baseURI = 'postgres://username:password@localhost:5432/db-name'; const testfile = fs.readFileSync('./Dockerfile').toString(); const dbOptionsTest = {}; dbOptionsTest[ - `${baseURI}?ssl=true&binary=true&application_name=app_name&fallback_application_name=f_app_name&poolSize=12` + `${baseURI}?ssl=true&binary=true&application_name=app_name&fallback_application_name=f_app_name&poolSize=10` ] = { ssl: true, binary: true, application_name: 'app_name', fallback_application_name: 'f_app_name', - max: 12, + poolSize: 10, }; dbOptionsTest[`${baseURI}?ssl=&binary=aa`] = { binary: false, @@ -83,20 +83,6 @@ describe('PostgresConfigParser.getDatabaseOptionsFromURI', () => { it('sets the poolSize to 10 if the it is not a number', () => { const result = parser.getDatabaseOptionsFromURI(`${baseURI}?poolSize=sdf`); - expect(result.max).toEqual(10); - }); - - it('sets the max to 10 if the it is not a number', () => { - const result = parser.getDatabaseOptionsFromURI(`${baseURI}?&max=sdf`); - - expect(result.poolSize).toBeUndefined(); - expect(result.max).toEqual(10); - }); - - it('max should take precedence over poolSize', () => { - const result = parser.getDatabaseOptionsFromURI(`${baseURI}?poolSize=20&max=12`); - - expect(result.poolSize).toBeUndefined(); - expect(result.max).toEqual(12); + expect(result.poolSize).toEqual(10); }); }); diff --git a/spec/SchemaPerformance.spec.js b/spec/SchemaPerformance.spec.js index 17238b0ed6..0471871c54 100644 --- a/spec/SchemaPerformance.spec.js +++ b/spec/SchemaPerformance.spec.js @@ -204,58 +204,4 @@ describe('Schema Performance', function () { ); expect(getAllSpy.calls.count()).toBe(2); }); - - it('does reload with schemaCacheTtl', async () => { - const databaseURI = - process.env.PARSE_SERVER_TEST_DB === 'postgres' - ? process.env.PARSE_SERVER_TEST_DATABASE_URI - : 'mongodb://localhost:27017/parseServerMongoAdapterTestDatabase'; - await reconfigureServer({ - databaseAdapter: undefined, - databaseURI, - silent: false, - databaseOptions: { schemaCacheTtl: 1000 }, - }); - const SchemaController = require('../lib/Controllers/SchemaController').SchemaController; - const spy = spyOn(SchemaController.prototype, 'reloadData').and.callThrough(); - Object.defineProperty(spy, 'reloadCalls', { - get: () => spy.calls.all().filter(call => call.args[0].clearCache).length, - }); - - const object = new TestObject(); - object.set('foo', 'bar'); - await object.save(); - - spy.calls.reset(); - - object.set('foo', 'bar'); - await object.save(); - - expect(spy.reloadCalls).toBe(0); - - await new Promise(resolve => setTimeout(resolve, 1100)); - - object.set('foo', 'bar'); - await object.save(); - - expect(spy.reloadCalls).toBe(1); - }); - - it('cannot set invalid databaseOptions', async () => { - const expectError = async (key, value, expected) => - expectAsync( - reconfigureServer({ databaseAdapter: undefined, databaseOptions: { [key]: value } }) - ).toBeRejectedWith(`databaseOptions.${key} must be a ${expected}`); - for (const databaseOptions of [[], 0, 'string']) { - await expectAsync( - reconfigureServer({ databaseAdapter: undefined, databaseOptions }) - ).toBeRejectedWith(`databaseOptions must be an object`); - } - for (const value of [null, 0, 'string', {}, []]) { - await expectError('enableSchemaHooks', value, 'boolean'); - } - for (const value of [null, false, 'string', {}, []]) { - await expectError('schemaCacheTtl', value, 'number'); - } - }); }); diff --git a/src/Adapters/Auth/AuthAdapter.js b/src/Adapters/Auth/AuthAdapter.js index 5b18c75170..bb7983f2b9 100644 --- a/src/Adapters/Auth/AuthAdapter.js +++ b/src/Adapters/Auth/AuthAdapter.js @@ -94,16 +94,6 @@ export class AuthAdapter { return Promise.resolve({}); } - /** - * Triggered when auth data is fetched - * @param {Object} authData authData - * @param {Object} options additional adapter options - * @returns {Promise} Any overrides required to authData - */ - afterFind(authData, options) { - return Promise.resolve({}); - } - /** * Triggered when the adapter is first attached to Parse Server * @param {Object} options Adapter Options diff --git a/src/Adapters/Auth/index.js b/src/Adapters/Auth/index.js index 3440208ebd..6ce8813d8d 100755 --- a/src/Adapters/Auth/index.js +++ b/src/Adapters/Auth/index.js @@ -9,6 +9,7 @@ const facebook = require('./facebook'); const instagram = require('./instagram'); const linkedin = require('./linkedin'); const meetup = require('./meetup'); +import mfa from './mfa'; const google = require('./google'); const github = require('./github'); const twitter = require('./twitter'); @@ -44,6 +45,7 @@ const providers = { instagram, linkedin, meetup, + mfa, google, github, twitter, @@ -154,27 +156,21 @@ function loadAuthAdapter(provider, authOptions) { return; } - const adapter = - defaultAdapter instanceof AuthAdapter ? defaultAdapter : Object.assign({}, defaultAdapter); const keys = [ 'validateAuthData', 'validateAppId', 'validateSetUp', 'validateLogin', 'validateUpdate', - 'challenge', 'validateOptions', - 'policy', - 'afterFind', + 'challenge', + 'policy' ]; + const adapter = defaultAdapter; const defaultAuthAdapter = new AuthAdapter(); keys.forEach(key => { const existing = adapter?.[key]; - if ( - existing && - typeof existing === 'function' && - existing.toString() === defaultAuthAdapter[key].toString() - ) { + if (existing && typeof existing === 'function' && existing.toString() === defaultAuthAdapter[key].toString()) { adapter[key] = null; } }); @@ -214,35 +210,9 @@ module.exports = function (authOptions = {}, enableAnonymousUsers = true) { return { validator: authDataValidator(provider, adapter, appIds, providerOptions), adapter }; }; - const runAfterFind = async authData => { - if (!authData) { - return; - } - const adapters = Object.keys(authData); - await Promise.all( - adapters.map(async provider => { - const authAdapter = getValidatorForProvider(provider); - if (!authAdapter) { - return; - } - const { - adapter: { afterFind }, - providerOptions, - } = authAdapter; - if (afterFind && typeof afterFind === 'function') { - const result = afterFind(authData[provider], providerOptions); - if (result) { - authData[provider] = result; - } - } - }) - ); - }; - return Object.freeze({ getValidatorForProvider, setEnableAnonymousUsers, - runAfterFind, }); }; diff --git a/src/Adapters/Auth/mfa.js b/src/Adapters/Auth/mfa.js new file mode 100644 index 0000000000..a0fc54ffa5 --- /dev/null +++ b/src/Adapters/Auth/mfa.js @@ -0,0 +1,197 @@ + +import {TOTP, Secret} from 'otpauth' +import { randomString } from '../../cryptoUtils'; +import AuthAdapter from './AuthAdapter'; +class MFAAdapter extends AuthAdapter { + constructor() { + super(); + this.policy = 'additional'; + } + validateOptions(opts) { + const validOptions = opts.options; + if (!Array.isArray(validOptions)) { + throw 'mfa.options must be an array' + } + this.sms = validOptions.includes('SMS'); + this.totp = validOptions.includes('TOTP'); + if (!this.sms && !this.totp) { + throw 'mfa.options must include SMS or TOTP' + } + const digits = opts.digits || 6; + const period = opts.period || 30; + if (typeof digits !== 'number') { + throw 'mfa.digits must be a number' + } + if (typeof period !== 'number') { + throw 'mfa.period must be a number' + } + if (digits < 4 || digits > 10) { + throw 'mfa.digits must be between 4 and 10' + } + if (period < 10) { + throw 'mfa.period must be greater than 10' + } + const sendSMS = opts.sendSMS; + if (this.sms && typeof sendSMS !== 'function') { + throw 'mfa.sendSMS callback must be defined when using SMS OTPs'; + } + this.smsCallback = sendSMS; + this.digits = digits; + this.period = period; + this.algorithm = opts.algorithm || 'SHA1' + } + validateSetUp(mfaData) { + if (mfaData.mobile && this.sms) { + return this.setupMobileOTP(mfaData.mobile); + } + if (this.totp) { + return this.setupTOTP(mfaData); + } + throw 'Invalid MFA data'; + } + async validateLogin(token, _, req) { + const saveResponse = { + doNotSave: true + } + const auth = req.original.get('authData') || {}; + const {secret, recovery, mobile, token: saved, expiry} = auth.mfa || {}; + if (this.sms && mobile) { + if (typeof token === 'boolean') { + const {token: sendToken, expiry} = await this.sendSMS(mobile); + auth.mfa.token = sendToken; + auth.mfa.expiry = expiry; + req.object.set('authData', auth); + await req.object.save(null, {useMasterKey: true}); + throw 'Please enter the token' + } + if (!saved || token !== saved) { + throw 'Invalid MFA token 1'; + } + if (new Date() > expiry) { + throw 'Invalid MFA token 2'; + } + delete auth.mfa.token; + delete auth.mfa.expiry; + return { + save: auth.mfa + } + } + if (this.totp) { + if (typeof token !== 'string') { + throw 'Invalid MFA token' + } + if (!secret) { + return saveResponse; + } + if (recovery[0] === token || recovery[1] === token) { + return saveResponse; + } + const totp = new TOTP({ + algorithm: this.algorithm, + digits: this.digits, + period: this.period, + secret: Secret.fromBase32(secret) + }); + const valid = totp.validate({ + token, + }); + if (valid === null) { + throw 'Invalid MFA token' + } + } + return saveResponse + } + validateUpdate(authData, _, req) { + if (req.master) { + return; + } + if (authData.mobile && this.sms) { + if (!authData.token) { + throw 'MFA is already set up on this account'; + } + return this.confirmSMSOTP(authData, req.original.get('authData')?.mfa || {}); + } + if (this.totp) { + this.validateLogin(authData.old, null, req); + return this.validateSetUp(authData); + } + throw 'Invalid MFA data'; + } + afterFind() { + return { + enabled: true + } + } + + async setupMobileOTP(mobile) { + const {token, expiry} = await this.sendSMS(mobile); + return { + save: { + pending: { + [mobile] : { + token, + expiry + } + } + }, + } + } + + async sendSMS(mobile) { + if (!/^[+]*[(]{0,1}[0-9]{1,3}[)]{0,1}[-\s\./0-9]*$/g.test(mobile)) { + throw 'Invalid mobile number.'; + } + let token = ''; + while (token.length < this.digits) { + token += randomString(10).replace(/\D/g,''); + } + token = token.substring(0, this.digits); + await Promise.resolve(this.smsCallback(token, mobile)); + const expiry = new Date(new Date().getTime() + (this.period * 1000)); + return {token, expiry}; + } + + async confirmSMSOTP(inputData, authData) { + const {mobile, token} = inputData; + if (!authData.pending?.[mobile]) { + throw 'This number is not pending'; + } + const pendingData = authData.pending[mobile]; + if (token !== pendingData.token) { + throw 'Invalid MFA token'; + } + if (new Date() > pendingData.expiry) { + throw 'Invalid MFA token'; + } + delete authData.pending[mobile] + authData.mobile = mobile; + return { + save: authData, + } + } + + setupTOTP(mfaData) { + const {secret, token } = mfaData; + if (!secret || !token || secret.length < 20) { + throw 'Invalid MFA data' + } + const totp = new TOTP({ + algorithm: this.algorithm, + digits: this.digits, + period: this.period, + secret: Secret.fromBase32(secret) + }); + const valid = totp.validate({ + token, + }); + if (valid === null) { + throw 'Invalid MFA token' + } + const recovery = [randomString(30), randomString(30)]; + return { + response: { recovery }, + save: { secret, recovery }, + } + } +} +export default new MFAAdapter(); diff --git a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js index 78833a026b..c0d4c0ca9e 100644 --- a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js +++ b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js @@ -139,12 +139,11 @@ export class MongoStorageAdapter implements StorageAdapter { _maxTimeMS: ?number; canSortOnJoinTables: boolean; enableSchemaHooks: boolean; - schemaCacheTtl: ?number; constructor({ uri = defaults.DefaultMongoURI, collectionPrefix = '', mongoOptions = {} }: any) { this._uri = uri; this._collectionPrefix = collectionPrefix; - this._mongoOptions = { ...mongoOptions }; + this._mongoOptions = mongoOptions; this._mongoOptions.useNewUrlParser = true; this._mongoOptions.useUnifiedTopology = true; this._onchange = () => {}; @@ -153,11 +152,8 @@ export class MongoStorageAdapter implements StorageAdapter { this._maxTimeMS = mongoOptions.maxTimeMS; this.canSortOnJoinTables = true; this.enableSchemaHooks = !!mongoOptions.enableSchemaHooks; - this.schemaCacheTtl = mongoOptions.schemaCacheTtl; - for (const key of ['enableSchemaHooks', 'schemaCacheTtl', 'maxTimeMS']) { - delete mongoOptions[key]; - delete this._mongoOptions[key]; - } + delete mongoOptions.enableSchemaHooks; + delete mongoOptions.maxTimeMS; } watch(callback: () => void): void { diff --git a/src/Adapters/Storage/Mongo/MongoTransform.js b/src/Adapters/Storage/Mongo/MongoTransform.js index 6f6811cec3..aabf744978 100644 --- a/src/Adapters/Storage/Mongo/MongoTransform.js +++ b/src/Adapters/Storage/Mongo/MongoTransform.js @@ -188,16 +188,6 @@ const transformInteriorValue = restValue => { // Handle atomic values var value = transformInteriorAtom(restValue); if (value !== CannotTransform) { - if (value && typeof value === 'object') { - if (value instanceof Date) { - return value; - } - if (value instanceof Array) { - value = value.map(transformInteriorValue); - } else { - value = mapValues(value, transformInteriorValue); - } - } return value; } @@ -1024,6 +1014,9 @@ function mapValues(object, iterator) { const result = {}; Object.keys(object).forEach(key => { result[key] = iterator(object[key]); + if (result[key] && JSON.stringify(result[key]).includes(`"__type"`)) { + result[key] = mapValues(object[key], iterator); + } }); return result; } diff --git a/src/Adapters/Storage/Postgres/PostgresConfigParser.js b/src/Adapters/Storage/Postgres/PostgresConfigParser.js index 64e4752913..d86778cf20 100644 --- a/src/Adapters/Storage/Postgres/PostgresConfigParser.js +++ b/src/Adapters/Storage/Postgres/PostgresConfigParser.js @@ -58,7 +58,7 @@ function getDatabaseOptionsFromURI(uri) { databaseOptions.fallback_application_name = queryParams.fallback_application_name; if (queryParams.poolSize) { - databaseOptions.max = parseInt(queryParams.poolSize) || 10; + databaseOptions.poolSize = parseInt(queryParams.poolSize) || 10; } if (queryParams.max) { databaseOptions.max = parseInt(queryParams.max) || 10; diff --git a/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js b/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js index 82ac0c20dc..444e4e8cca 100644 --- a/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js +++ b/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js @@ -850,18 +850,13 @@ export class PostgresStorageAdapter implements StorageAdapter { _pgp: any; _stream: any; _uuid: any; - schemaCacheTtl: ?number; constructor({ uri, collectionPrefix = '', databaseOptions = {} }: any) { - const options = { ...databaseOptions }; this._collectionPrefix = collectionPrefix; this.enableSchemaHooks = !!databaseOptions.enableSchemaHooks; - this.schemaCacheTtl = databaseOptions.schemaCacheTtl; - for (const key of ['enableSchemaHooks', 'schemaCacheTtl']) { - delete options[key]; - } + delete databaseOptions.enableSchemaHooks; - const { client, pgp } = createClient(uri, options); + const { client, pgp } = createClient(uri, databaseOptions); this._client = client; this._onchange = () => {}; this._pgp = pgp; diff --git a/src/Adapters/Storage/StorageAdapter.js b/src/Adapters/Storage/StorageAdapter.js index 7605784a43..6e4573b748 100644 --- a/src/Adapters/Storage/StorageAdapter.js +++ b/src/Adapters/Storage/StorageAdapter.js @@ -30,8 +30,6 @@ export type FullQueryOptions = QueryOptions & UpdateQueryOptions; export interface StorageAdapter { canSortOnJoinTables: boolean; - schemaCacheTtl: ?number; - enableSchemaHooks: boolean; classExists(className: string): Promise; setClassLevelPermissions(className: string, clps: any): Promise; diff --git a/src/Auth.js b/src/Auth.js index abd14391db..a938e0d994 100644 --- a/src/Auth.js +++ b/src/Auth.js @@ -422,14 +422,8 @@ const handleAuthDataValidation = async (authData, req, foundUser) => { await user.fetch({ useMasterKey: true }); } - const { originalObject, updatedObject } = req.buildParseObjects(); - const requestObject = getRequestObject( - undefined, - req.auth, - updatedObject, - originalObject || user, - req.config - ); + const { updatedObject } = req.buildParseObjects(); + const requestObject = getRequestObject(undefined, req.auth, updatedObject, user, req.config); // Perform validation as step-by-step pipeline for better error consistency // and also to avoid to trigger a provider (like OTP SMS) if another one fails const acc = { authData: {}, authDataResponse: {} }; diff --git a/src/Config.js b/src/Config.js index 2e7ef389c7..c993e467fb 100644 --- a/src/Config.js +++ b/src/Config.js @@ -9,7 +9,6 @@ import DatabaseController from './Controllers/DatabaseController'; import { logLevels as validLogLevels } from './Controllers/LoggerController'; import { AccountLockoutOptions, - DatabaseOptions, FileUploadOptions, IdempotencyOptions, LogLevels, @@ -53,20 +52,23 @@ export class Config { } static put(serverConfiguration) { - Config.validateOptions(serverConfiguration); - Config.validateControllers(serverConfiguration); + Config.validate(serverConfiguration); AppCache.put(serverConfiguration.appId, serverConfiguration); Config.setupPasswordValidator(serverConfiguration.passwordPolicy); return serverConfiguration; } - static validateOptions({ + static validate({ + verifyUserEmails, + userController, + appName, publicServerURL, revokeSessionOnPasswordReset, expireInactiveSessions, sessionLength, defaultLimit, maxLimit, + emailVerifyTokenValidityDuration, accountLockout, passwordPolicy, masterKeyIps, @@ -76,6 +78,7 @@ export class Config { readOnlyMasterKey, allowHeaders, idempotencyOptions, + emailVerifyTokenReuseIfValid, fileUpload, pages, security, @@ -85,7 +88,6 @@ export class Config { allowExpiredAuthDataToken, logLevels, rateLimit, - databaseOptions, }) { if (masterKey === readOnlyMasterKey) { throw new Error('masterKey and readOnlyMasterKey should be different'); @@ -95,6 +97,17 @@ export class Config { throw new Error('masterKey and maintenanceKey should be different'); } + const emailAdapter = userController.adapter; + if (verifyUserEmails) { + this.validateEmailConfiguration({ + emailAdapter, + appName, + publicServerURL, + emailVerifyTokenValidityDuration, + emailVerifyTokenReuseIfValid, + }); + } + this.validateAccountLockoutPolicy(accountLockout); this.validatePasswordPolicy(passwordPolicy); this.validateFileUploadOptions(fileUpload); @@ -123,27 +136,6 @@ export class Config { this.validateRequestKeywordDenylist(requestKeywordDenylist); this.validateRateLimit(rateLimit); this.validateLogLevels(logLevels); - this.validateDatabaseOptions(databaseOptions); - } - - static validateControllers({ - verifyUserEmails, - userController, - appName, - publicServerURL, - emailVerifyTokenValidityDuration, - emailVerifyTokenReuseIfValid, - }) { - const emailAdapter = userController.adapter; - if (verifyUserEmails) { - this.validateEmailConfiguration({ - emailAdapter, - appName, - publicServerURL, - emailVerifyTokenValidityDuration, - emailVerifyTokenReuseIfValid, - }); - } } static validateRequestKeywordDenylist(requestKeywordDenylist) { @@ -541,25 +533,6 @@ export class Config { } } - static validateDatabaseOptions(databaseOptions) { - if (databaseOptions == undefined) { - return; - } - if (Object.prototype.toString.call(databaseOptions) !== '[object Object]') { - throw `databaseOptions must be an object`; - } - if (databaseOptions.enableSchemaHooks === undefined) { - databaseOptions.enableSchemaHooks = DatabaseOptions.enableSchemaHooks.default; - } else if (typeof databaseOptions.enableSchemaHooks !== 'boolean') { - throw `databaseOptions.enableSchemaHooks must be a boolean`; - } - if (databaseOptions.schemaCacheTtl === undefined) { - databaseOptions.schemaCacheTtl = DatabaseOptions.schemaCacheTtl.default; - } else if (typeof databaseOptions.schemaCacheTtl !== 'number') { - throw `databaseOptions.schemaCacheTtl must be a number`; - } - } - static validateRateLimit(rateLimit) { if (!rateLimit) { return; diff --git a/src/Controllers/SchemaController.js b/src/Controllers/SchemaController.js index ad3699aaa5..62757d251d 100644 --- a/src/Controllers/SchemaController.js +++ b/src/Controllers/SchemaController.js @@ -682,10 +682,6 @@ const typeToString = (type: SchemaField | string): string => { } return `${type.type}`; }; -const ttl = { - date: Date.now(), - duration: undefined, -}; // Stores the entire schema of the app in a weird hybrid format somewhere between // the mongo format and the Parse format. Soon, this will all be Parse format. @@ -698,11 +694,10 @@ export default class SchemaController { constructor(databaseAdapter: StorageAdapter) { this._dbAdapter = databaseAdapter; - const config = Config.get(Parse.applicationId); this.schemaData = new SchemaData(SchemaCache.all(), this.protectedFields); - this.protectedFields = config.protectedFields; + this.protectedFields = Config.get(Parse.applicationId).protectedFields; - const customIds = config.allowCustomObjectId; + const customIds = Config.get(Parse.applicationId).allowCustomObjectId; const customIdRegEx = /^.{1,}$/u; // 1+ chars const autoIdRegEx = /^[a-zA-Z0-9]{1,}$/; @@ -714,21 +709,6 @@ export default class SchemaController { }); } - async reloadDataIfNeeded() { - if (this._dbAdapter.enableSchemaHooks) { - return; - } - const { date, duration } = ttl || {}; - if (!duration) { - return; - } - const now = Date.now(); - if (now - date > duration) { - ttl.date = now; - await this.reloadData({ clearCache: true }); - } - } - reloadData(options: LoadSchemaOptions = { clearCache: false }): Promise { if (this.reloadDataPromise && !options.clearCache) { return this.reloadDataPromise; @@ -749,11 +729,10 @@ export default class SchemaController { return this.reloadDataPromise; } - async getAllClasses(options: LoadSchemaOptions = { clearCache: false }): Promise> { + getAllClasses(options: LoadSchemaOptions = { clearCache: false }): Promise> { if (options.clearCache) { return this.setAllClasses(); } - await this.reloadDataIfNeeded(); const cached = SchemaCache.all(); if (cached && cached.length) { return Promise.resolve(cached); @@ -1461,7 +1440,6 @@ export default class SchemaController { // Returns a promise for a new Schema. const load = (dbAdapter: StorageAdapter, options: any): Promise => { const schema = new SchemaController(dbAdapter); - ttl.duration = dbAdapter.schemaCacheTtl; return schema.reloadData(options).then(() => schema); }; diff --git a/src/Options/Definitions.js b/src/Options/Definitions.js index a25e69c70a..2d53cc7c27 100644 --- a/src/Options/Definitions.js +++ b/src/Options/Definitions.js @@ -971,12 +971,6 @@ module.exports.DatabaseOptions = { action: parsers.booleanParser, default: false, }, - schemaCacheTtl: { - env: 'PARSE_SERVER_DATABASE_SCHEMA_CACHE_TTL', - help: - 'The duration in seconds after which the schema cache expires and will be refetched from the database. Use this option if using multiple Parse Servers instances connected to the same database. A low duration will cause the schema cache to be updated too often, causing unnecessary database reads. A high duration will cause the schema to be updated too rarely, increasing the time required until schema changes propagate to all server instances. This feature can be used as an alternative or in conjunction with the option `enableSchemaHooks`. Default is infinite which means the schema cache never expires.', - action: parsers.numberParser('schemaCacheTtl'), - }, }; module.exports.AuthAdapter = { enabled: { diff --git a/src/Options/docs.js b/src/Options/docs.js index 0eb6488c74..0f28270f56 100644 --- a/src/Options/docs.js +++ b/src/Options/docs.js @@ -225,7 +225,6 @@ /** * @interface DatabaseOptions * @property {Boolean} enableSchemaHooks Enables database real-time hooks to update single schema cache. Set to `true` if using multiple Parse Servers instances connected to the same database. Failing to do so will cause a schema change to not propagate to all instances and re-syncing will only happen when the instances restart. To use this feature with MongoDB, a replica set cluster with [change stream](https://docs.mongodb.com/manual/changeStreams/#availability) support is required. - * @property {Number} schemaCacheTtl The duration in seconds after which the schema cache expires and will be refetched from the database. Use this option if using multiple Parse Servers instances connected to the same database. A low duration will cause the schema cache to be updated too often, causing unnecessary database reads. A high duration will cause the schema to be updated too rarely, increasing the time required until schema changes propagate to all server instances. This feature can be used as an alternative or in conjunction with the option `enableSchemaHooks`. Default is infinite which means the schema cache never expires. */ /** diff --git a/src/Options/index.js b/src/Options/index.js index 778374e7e7..6d0f488b88 100644 --- a/src/Options/index.js +++ b/src/Options/index.js @@ -548,8 +548,6 @@ export interface DatabaseOptions { /* Enables database real-time hooks to update single schema cache. Set to `true` if using multiple Parse Servers instances connected to the same database. Failing to do so will cause a schema change to not propagate to all instances and re-syncing will only happen when the instances restart. To use this feature with MongoDB, a replica set cluster with [change stream](https://docs.mongodb.com/manual/changeStreams/#availability) support is required. :DEFAULT: false */ enableSchemaHooks: ?boolean; - /* The duration in seconds after which the schema cache expires and will be refetched from the database. Use this option if using multiple Parse Servers instances connected to the same database. A low duration will cause the schema cache to be updated too often, causing unnecessary database reads. A high duration will cause the schema to be updated too rarely, increasing the time required until schema changes propagate to all server instances. This feature can be used as an alternative or in conjunction with the option `enableSchemaHooks`. Default is infinite which means the schema cache never expires. */ - schemaCacheTtl: ?number; } export interface AuthAdapter { diff --git a/src/ParseServer.js b/src/ParseServer.js index 04379ecfd3..ed21ce12e3 100644 --- a/src/ParseServer.js +++ b/src/ParseServer.js @@ -71,7 +71,6 @@ class ParseServer { Parse.initialize(appId, javascriptKey || 'unused', masterKey); Parse.serverURL = serverURL; - Config.validateOptions(options); const allControllers = controllers.getControllers(options); options.state = 'initialized'; this.config = Config.put(Object.assign({}, options, allControllers)); diff --git a/src/RestQuery.js b/src/RestQuery.js index 1f4304e78a..f936a5a7a8 100644 --- a/src/RestQuery.js +++ b/src/RestQuery.js @@ -223,9 +223,6 @@ RestQuery.prototype.execute = function (executeOptions) { .then(() => { return this.runAfterFindTrigger(); }) - .then(() => { - return this.handleAuthAdapters(); - }) .then(() => { return this.response; }); @@ -845,15 +842,6 @@ RestQuery.prototype.runAfterFindTrigger = function () { }); }; -RestQuery.prototype.handleAuthAdapters = async function () { - if (this.className !== '_User' || this.findOptions.explain) { - return; - } - await Promise.all( - this.response.results.map(result => this.config.authDataManager.runAfterFind(result.authData)) - ); -}; - // Adds included values to the response. // Path is a list of field names. // Returns a promise for an augmented response. diff --git a/src/Routers/UsersRouter.js b/src/Routers/UsersRouter.js index e26d2ef141..4a72fdd73b 100644 --- a/src/Routers/UsersRouter.js +++ b/src/Routers/UsersRouter.js @@ -292,7 +292,6 @@ export class UsersRouter extends ClassesRouter { if (authDataResponse) { user.authDataResponse = authDataResponse; } - await req.config.authDataManager.runAfterFind(user.authData); return { response: user }; } diff --git a/src/index.js b/src/index.js index dcfe9b4c7e..38e0922d0a 100644 --- a/src/index.js +++ b/src/index.js @@ -6,8 +6,6 @@ import RedisCacheAdapter from './Adapters/Cache/RedisCacheAdapter'; import LRUCacheAdapter from './Adapters/Cache/LRUCache.js'; import * as TestUtils from './TestUtils'; import * as SchemaMigrations from './SchemaMigrations/Migrations'; -import AuthAdapter from './Adapters/Auth/AuthAdapter'; - import { useExternal } from './deprecated'; import { getLogger } from './logger'; import { PushWorker } from './Push/PushWorker'; @@ -44,5 +42,4 @@ export { ParseGraphQLServer, _ParseServer as ParseServer, SchemaMigrations, - AuthAdapter, };