build: Release (#8749)

This commit is contained in:
Manuel
2023-09-16 04:51:10 +02:00
committed by GitHub
26 changed files with 857 additions and 182 deletions

View File

@@ -13,7 +13,7 @@ The following is a list of deprecations, according to the [Deprecation Policy](h
| DEPPS7 | Remove file trigger syntax `Parse.Cloud.beforeSaveFile((request) => {})` | [#7966](https://github.com/parse-community/parse-server/pull/7966) | 5.3.0 (2022) | 7.0.0 (2024) | deprecated | - | | DEPPS7 | Remove file trigger syntax `Parse.Cloud.beforeSaveFile((request) => {})` | [#7966](https://github.com/parse-community/parse-server/pull/7966) | 5.3.0 (2022) | 7.0.0 (2024) | deprecated | - |
| DEPPS8 | Login with expired 3rd party authentication token defaults to `false` | [#7079](https://github.com/parse-community/parse-server/pull/7079) | 5.3.0 (2022) | 7.0.0 (2024) | deprecated | - | | DEPPS8 | Login with expired 3rd party authentication token defaults to `false` | [#7079](https://github.com/parse-community/parse-server/pull/7079) | 5.3.0 (2022) | 7.0.0 (2024) | deprecated | - |
| 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) | deprecated | - | | 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) | deprecated | - |
| DEPPS10 | Config option `encodeParseObjectInCloudFunction` defaults to `true` | [#8634](https://github.com/parse-community/parse-server/issues/8634) | 6.2.0 (2023) | 7.0.0 (2024) | deprecated | - | | DEPPS10 | Config option `encodeParseObjectInCloudFunction` defaults to `true` | [#8634](https://github.com/parse-community/parse-server/issues/8634) | 6.2.0 (2023) | 8.0.0 (2025) | deprecated | - |
[i_deprecation]: ## "The version and date of the deprecation." [i_deprecation]: ## "The version and date of the deprecation."
[i_removal]: ## "The version and date of the planned removal." [i_removal]: ## "The version and date of the planned removal."

View File

@@ -1,3 +1,52 @@
# [6.3.0-alpha.9](https://github.com/parse-community/parse-server/compare/6.3.0-alpha.8...6.3.0-alpha.9) (2023-09-13)
### Performance Improvements
* Improve performance of recursive pointer iterations ([#8741](https://github.com/parse-community/parse-server/issues/8741)) ([45a3ed0](https://github.com/parse-community/parse-server/commit/45a3ed0fcf2c0170607505a1550fb15896e705fd))
# [6.3.0-alpha.8](https://github.com/parse-community/parse-server/compare/6.3.0-alpha.7...6.3.0-alpha.8) (2023-08-30)
### Bug Fixes
* Redis 4 does not reconnect after unhandled error ([#8706](https://github.com/parse-community/parse-server/issues/8706)) ([2b3d4e5](https://github.com/parse-community/parse-server/commit/2b3d4e5d3c85cd142f85af68dec51a8523548d49))
# [6.3.0-alpha.7](https://github.com/parse-community/parse-server/compare/6.3.0-alpha.6...6.3.0-alpha.7) (2023-08-18)
### Bug Fixes
* Remove config logging when launching Parse Server via CLI ([#8710](https://github.com/parse-community/parse-server/issues/8710)) ([ae68f0c](https://github.com/parse-community/parse-server/commit/ae68f0c31b741eeb83379c905c7ddfaa124436ec))
# [6.3.0-alpha.6](https://github.com/parse-community/parse-server/compare/6.3.0-alpha.5...6.3.0-alpha.6) (2023-07-17)
### Bug Fixes
* Parse Server option `fileUpload.fileExtensions` does not work with an array of extensions ([#8688](https://github.com/parse-community/parse-server/issues/8688)) ([6a4a00c](https://github.com/parse-community/parse-server/commit/6a4a00ca7af1163ea74b047b85cd6817366b824b))
# [6.3.0-alpha.5](https://github.com/parse-community/parse-server/compare/6.3.0-alpha.4...6.3.0-alpha.5) (2023-07-05)
### Features
* Add property `Parse.Server.version` to determine current version of Parse Server in Cloud Code ([#8670](https://github.com/parse-community/parse-server/issues/8670)) ([a9d376b](https://github.com/parse-community/parse-server/commit/a9d376b61f5b07806eafbda91c4e36c322f09298))
# [6.3.0-alpha.4](https://github.com/parse-community/parse-server/compare/6.3.0-alpha.3...6.3.0-alpha.4) (2023-07-04)
### Bug Fixes
* Server does not start via CLI when `auth` option is set ([#8666](https://github.com/parse-community/parse-server/issues/8666)) ([4e2000b](https://github.com/parse-community/parse-server/commit/4e2000bc563324389584ace3c090a5c1a7796a64))
# [6.3.0-alpha.3](https://github.com/parse-community/parse-server/compare/6.3.0-alpha.2...6.3.0-alpha.3) (2023-06-23)
### Features
* Add TOTP authentication adapter ([#8457](https://github.com/parse-community/parse-server/issues/8457)) ([cc079a4](https://github.com/parse-community/parse-server/commit/cc079a40f6849a0e9bc6fdc811e8649ecb67b589))
# [6.3.0-alpha.2](https://github.com/parse-community/parse-server/compare/6.3.0-alpha.1...6.3.0-alpha.2) (2023-06-20) # [6.3.0-alpha.2](https://github.com/parse-community/parse-server/compare/6.3.0-alpha.1...6.3.0-alpha.2) (2023-06-20)

214
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{ {
"name": "parse-server", "name": "parse-server",
"version": "6.3.0", "version": "6.3.0-alpha.9",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "parse-server", "name": "parse-server",
"version": "6.3.0", "version": "6.3.0-alpha.9",
"hasInstallScript": true, "hasInstallScript": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
@@ -16,7 +16,7 @@
"@graphql-tools/utils": "8.12.0", "@graphql-tools/utils": "8.12.0",
"@graphql-yoga/node": "2.6.0", "@graphql-yoga/node": "2.6.0",
"@parse/fs-files-adapter": "1.2.2", "@parse/fs-files-adapter": "1.2.2",
"@parse/push-adapter": "4.1.3", "@parse/push-adapter": "4.2.0",
"bcryptjs": "2.4.3", "bcryptjs": "2.4.3",
"body-parser": "1.20.2", "body-parser": "1.20.2",
"commander": "10.0.1", "commander": "10.0.1",
@@ -39,6 +39,7 @@
"mime": "3.0.0", "mime": "3.0.0",
"mongodb": "4.10.0", "mongodb": "4.10.0",
"mustache": "4.2.0", "mustache": "4.2.0",
"otpauth": "9.1.2",
"parse": "4.1.0", "parse": "4.1.0",
"path-to-regexp": "6.2.1", "path-to-regexp": "6.2.1",
"pg-monitor": "2.0.0", "pg-monitor": "2.0.0",
@@ -46,7 +47,7 @@
"pluralize": "8.0.0", "pluralize": "8.0.0",
"rate-limit-redis": "3.0.2", "rate-limit-redis": "3.0.2",
"redis": "4.6.6", "redis": "4.6.6",
"semver": "^7.5.2", "semver": "7.5.2",
"subscriptions-transport-ws": "0.11.0", "subscriptions-transport-ws": "0.11.0",
"tv4": "1.3.0", "tv4": "1.3.0",
"uuid": "9.0.0", "uuid": "9.0.0",
@@ -2730,13 +2731,13 @@
"integrity": "sha512-VUsVZXgt53FULqUd9xqGDW6RXes62qHXTNOeRSlS1MOemiCdtQOUGgLHgjdYQXnZ1hPLkxZKph96AluZUb953g==" "integrity": "sha512-VUsVZXgt53FULqUd9xqGDW6RXes62qHXTNOeRSlS1MOemiCdtQOUGgLHgjdYQXnZ1hPLkxZKph96AluZUb953g=="
}, },
"node_modules/@parse/node-apn": { "node_modules/@parse/node-apn": {
"version": "5.1.3", "version": "5.2.1",
"resolved": "https://registry.npmjs.org/@parse/node-apn/-/node-apn-5.1.3.tgz", "resolved": "https://registry.npmjs.org/@parse/node-apn/-/node-apn-5.2.1.tgz",
"integrity": "sha512-Bwhmbm895lEIF2772PJ8dSvBjrtOG9/q/TDMxmX40IgZxQFoXS73+JUIKTq3CA7SUB/Szu5roJINQ0L2U/1MJw==", "integrity": "sha512-dwVCDv+G9YV01Ad1XslWQImnmfFDSnaNwxI4l+vuCjL+DbjsCl6DuV4nMqZpEZOpViAY0pGCRHBKUygsf+aAGg==",
"dependencies": { "dependencies": {
"debug": "4.3.3", "debug": "4.3.3",
"jsonwebtoken": "8.5.1", "jsonwebtoken": "9.0.0",
"node-forge": "1.3.0", "node-forge": "1.3.1",
"verror": "1.10.1" "verror": "1.10.1"
}, },
"engines": { "engines": {
@@ -2759,35 +2760,6 @@
} }
} }
}, },
"node_modules/@parse/node-apn/node_modules/jsonwebtoken": {
"version": "8.5.1",
"resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz",
"integrity": "sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w==",
"dependencies": {
"jws": "^3.2.2",
"lodash.includes": "^4.3.0",
"lodash.isboolean": "^3.0.3",
"lodash.isinteger": "^4.0.4",
"lodash.isnumber": "^3.0.3",
"lodash.isplainobject": "^4.0.6",
"lodash.isstring": "^4.0.1",
"lodash.once": "^4.0.0",
"ms": "^2.1.1",
"semver": "^5.6.0"
},
"engines": {
"node": ">=4",
"npm": ">=1.4.28"
}
},
"node_modules/@parse/node-apn/node_modules/semver": {
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
"bin": {
"semver": "bin/semver"
}
},
"node_modules/@parse/node-gcm": { "node_modules/@parse/node-gcm": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/@parse/node-gcm/-/node-gcm-1.0.2.tgz", "resolved": "https://registry.npmjs.org/@parse/node-gcm/-/node-gcm-1.0.2.tgz",
@@ -2810,11 +2782,11 @@
} }
}, },
"node_modules/@parse/push-adapter": { "node_modules/@parse/push-adapter": {
"version": "4.1.3", "version": "4.2.0",
"resolved": "https://registry.npmjs.org/@parse/push-adapter/-/push-adapter-4.1.3.tgz", "resolved": "https://registry.npmjs.org/@parse/push-adapter/-/push-adapter-4.2.0.tgz",
"integrity": "sha512-Oy53ag7DpUva5dUWwP6tNEsrxv2xU9QIk+rb84q1DIm1qVgo2yl4oXcZ3FPG2Ks/NYURbv4w+z9oaSgVfyBRfQ==", "integrity": "sha512-M6D9qk4KE9bJ2lMufTvgGmKOvsbj20lFhzg0kQRmHU10ootKt4XcL+QJRSTu/BmlRbIVZMGQEZ61UyUumWTOiQ==",
"dependencies": { "dependencies": {
"@parse/node-apn": "5.1.3", "@parse/node-apn": "5.2.1",
"@parse/node-gcm": "1.0.2", "@parse/node-gcm": "1.0.2",
"npmlog": "4.1.2", "npmlog": "4.1.2",
"parse": "3.4.0" "parse": "3.4.0"
@@ -10220,6 +10192,14 @@
"extsprintf": "^1.2.0" "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": { "node_modules/jwa": {
"version": "1.4.1", "version": "1.4.1",
"resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz",
@@ -10791,41 +10771,23 @@
"integrity": "sha512-aEXTF4d+m05rVOAUG3z4vZZ4xVexLKZGF0lIxuHZ1Hplpk/3B6Z1+/ICICYRLm7c41Z2xiejbkCkJoTlypoXhQ==", "integrity": "sha512-aEXTF4d+m05rVOAUG3z4vZZ4xVexLKZGF0lIxuHZ1Hplpk/3B6Z1+/ICICYRLm7c41Z2xiejbkCkJoTlypoXhQ==",
"dev": true "dev": true
}, },
"node_modules/lodash.includes": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
"integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w=="
},
"node_modules/lodash.isboolean": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
"integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg=="
},
"node_modules/lodash.isinteger": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
"integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA=="
},
"node_modules/lodash.ismatch": { "node_modules/lodash.ismatch": {
"version": "4.4.0", "version": "4.4.0",
"resolved": "https://registry.npmjs.org/lodash.ismatch/-/lodash.ismatch-4.4.0.tgz", "resolved": "https://registry.npmjs.org/lodash.ismatch/-/lodash.ismatch-4.4.0.tgz",
"integrity": "sha512-fPMfXjGQEV9Xsq/8MTSgUf255gawYRbjwMyDbcvDhXgV7enSZA0hynz6vMPnpAb5iONEzBHBPsT+0zes5Z301g==", "integrity": "sha512-fPMfXjGQEV9Xsq/8MTSgUf255gawYRbjwMyDbcvDhXgV7enSZA0hynz6vMPnpAb5iONEzBHBPsT+0zes5Z301g==",
"dev": true "dev": true
}, },
"node_modules/lodash.isnumber": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
"integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw=="
},
"node_modules/lodash.isplainobject": { "node_modules/lodash.isplainobject": {
"version": "4.0.6", "version": "4.0.6",
"resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
"integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==" "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==",
"dev": true
}, },
"node_modules/lodash.isstring": { "node_modules/lodash.isstring": {
"version": "4.0.1", "version": "4.0.1",
"resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
"integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==" "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==",
"dev": true
}, },
"node_modules/lodash.map": { "node_modules/lodash.map": {
"version": "4.6.0", "version": "4.6.0",
@@ -10838,11 +10800,6 @@
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
"integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==" "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="
}, },
"node_modules/lodash.once": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
"integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg=="
},
"node_modules/lodash.pad": { "node_modules/lodash.pad": {
"version": "4.5.1", "version": "4.5.1",
"resolved": "https://registry.npmjs.org/lodash.pad/-/lodash.pad-4.5.1.tgz", "resolved": "https://registry.npmjs.org/lodash.pad/-/lodash.pad-4.5.1.tgz",
@@ -12484,9 +12441,9 @@
} }
}, },
"node_modules/node-forge": { "node_modules/node-forge": {
"version": "1.3.0", "version": "1.3.1",
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.0.tgz", "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz",
"integrity": "sha512-08ARB91bUi6zNKzVmaj3QO7cr397uiDT2nJ63cHjyNtCTWIgvS47j3eT0WfzUwS9+6Z5YshRaoasFkXCKrIYbA==", "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==",
"engines": { "engines": {
"node": ">= 6.13.0" "node": ">= 6.13.0"
} }
@@ -15940,6 +15897,17 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/otpauth": {
"version": "9.1.2",
"resolved": "https://registry.npmjs.org/otpauth/-/otpauth-9.1.2.tgz",
"integrity": "sha512-iI5nlVvMFP3aTPdjG/fnC4mhVJ/KZOSnBrvo/VnYHUwlTp9jVLjAe2B3i3pyCH+3/E5jYQRSvuHk/8oas3870g==",
"dependencies": {
"jssha": "~3.3.0"
},
"funding": {
"url": "https://github.com/hectorm/otpauth?sponsor=1"
}
},
"node_modules/p-cancelable": { "node_modules/p-cancelable": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-3.0.0.tgz", "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-3.0.0.tgz",
@@ -20417,9 +20385,9 @@
} }
}, },
"node_modules/word-wrap": { "node_modules/word-wrap": {
"version": "1.2.3", "version": "1.2.5",
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
"integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==",
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=0.10.0"
} }
@@ -22584,13 +22552,13 @@
"integrity": "sha512-VUsVZXgt53FULqUd9xqGDW6RXes62qHXTNOeRSlS1MOemiCdtQOUGgLHgjdYQXnZ1hPLkxZKph96AluZUb953g==" "integrity": "sha512-VUsVZXgt53FULqUd9xqGDW6RXes62qHXTNOeRSlS1MOemiCdtQOUGgLHgjdYQXnZ1hPLkxZKph96AluZUb953g=="
}, },
"@parse/node-apn": { "@parse/node-apn": {
"version": "5.1.3", "version": "5.2.1",
"resolved": "https://registry.npmjs.org/@parse/node-apn/-/node-apn-5.1.3.tgz", "resolved": "https://registry.npmjs.org/@parse/node-apn/-/node-apn-5.2.1.tgz",
"integrity": "sha512-Bwhmbm895lEIF2772PJ8dSvBjrtOG9/q/TDMxmX40IgZxQFoXS73+JUIKTq3CA7SUB/Szu5roJINQ0L2U/1MJw==", "integrity": "sha512-dwVCDv+G9YV01Ad1XslWQImnmfFDSnaNwxI4l+vuCjL+DbjsCl6DuV4nMqZpEZOpViAY0pGCRHBKUygsf+aAGg==",
"requires": { "requires": {
"debug": "4.3.3", "debug": "4.3.3",
"jsonwebtoken": "8.5.1", "jsonwebtoken": "9.0.0",
"node-forge": "1.3.0", "node-forge": "1.3.1",
"verror": "1.10.1" "verror": "1.10.1"
}, },
"dependencies": { "dependencies": {
@@ -22601,28 +22569,6 @@
"requires": { "requires": {
"ms": "2.1.2" "ms": "2.1.2"
} }
},
"jsonwebtoken": {
"version": "8.5.1",
"resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz",
"integrity": "sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w==",
"requires": {
"jws": "^3.2.2",
"lodash.includes": "^4.3.0",
"lodash.isboolean": "^3.0.3",
"lodash.isinteger": "^4.0.4",
"lodash.isnumber": "^3.0.3",
"lodash.isplainobject": "^4.0.6",
"lodash.isstring": "^4.0.1",
"lodash.once": "^4.0.0",
"ms": "^2.1.1",
"semver": "^5.6.0"
}
},
"semver": {
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ=="
} }
} }
}, },
@@ -22647,11 +22593,11 @@
} }
}, },
"@parse/push-adapter": { "@parse/push-adapter": {
"version": "4.1.3", "version": "4.2.0",
"resolved": "https://registry.npmjs.org/@parse/push-adapter/-/push-adapter-4.1.3.tgz", "resolved": "https://registry.npmjs.org/@parse/push-adapter/-/push-adapter-4.2.0.tgz",
"integrity": "sha512-Oy53ag7DpUva5dUWwP6tNEsrxv2xU9QIk+rb84q1DIm1qVgo2yl4oXcZ3FPG2Ks/NYURbv4w+z9oaSgVfyBRfQ==", "integrity": "sha512-M6D9qk4KE9bJ2lMufTvgGmKOvsbj20lFhzg0kQRmHU10ootKt4XcL+QJRSTu/BmlRbIVZMGQEZ61UyUumWTOiQ==",
"requires": { "requires": {
"@parse/node-apn": "5.1.3", "@parse/node-apn": "5.2.1",
"@parse/node-gcm": "1.0.2", "@parse/node-gcm": "1.0.2",
"npmlog": "4.1.2", "npmlog": "4.1.2",
"parse": "3.4.0" "parse": "3.4.0"
@@ -28397,6 +28343,11 @@
} }
} }
}, },
"jssha": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/jssha/-/jssha-3.3.0.tgz",
"integrity": "sha512-w9OtT4ALL+fbbwG3gw7erAO0jvS5nfvrukGPMWIAoea359B26ALXGpzy4YJSp9yGnpUvuvOw1nSjSoHDfWSr1w=="
},
"jwa": { "jwa": {
"version": "1.4.1", "version": "1.4.1",
"resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz",
@@ -28860,41 +28811,23 @@
"integrity": "sha512-aEXTF4d+m05rVOAUG3z4vZZ4xVexLKZGF0lIxuHZ1Hplpk/3B6Z1+/ICICYRLm7c41Z2xiejbkCkJoTlypoXhQ==", "integrity": "sha512-aEXTF4d+m05rVOAUG3z4vZZ4xVexLKZGF0lIxuHZ1Hplpk/3B6Z1+/ICICYRLm7c41Z2xiejbkCkJoTlypoXhQ==",
"dev": true "dev": true
}, },
"lodash.includes": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
"integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w=="
},
"lodash.isboolean": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
"integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg=="
},
"lodash.isinteger": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
"integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA=="
},
"lodash.ismatch": { "lodash.ismatch": {
"version": "4.4.0", "version": "4.4.0",
"resolved": "https://registry.npmjs.org/lodash.ismatch/-/lodash.ismatch-4.4.0.tgz", "resolved": "https://registry.npmjs.org/lodash.ismatch/-/lodash.ismatch-4.4.0.tgz",
"integrity": "sha512-fPMfXjGQEV9Xsq/8MTSgUf255gawYRbjwMyDbcvDhXgV7enSZA0hynz6vMPnpAb5iONEzBHBPsT+0zes5Z301g==", "integrity": "sha512-fPMfXjGQEV9Xsq/8MTSgUf255gawYRbjwMyDbcvDhXgV7enSZA0hynz6vMPnpAb5iONEzBHBPsT+0zes5Z301g==",
"dev": true "dev": true
}, },
"lodash.isnumber": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
"integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw=="
},
"lodash.isplainobject": { "lodash.isplainobject": {
"version": "4.0.6", "version": "4.0.6",
"resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
"integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==" "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==",
"dev": true
}, },
"lodash.isstring": { "lodash.isstring": {
"version": "4.0.1", "version": "4.0.1",
"resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
"integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==" "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==",
"dev": true
}, },
"lodash.map": { "lodash.map": {
"version": "4.6.0", "version": "4.6.0",
@@ -28907,11 +28840,6 @@
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
"integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==" "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="
}, },
"lodash.once": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
"integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg=="
},
"lodash.pad": { "lodash.pad": {
"version": "4.5.1", "version": "4.5.1",
"resolved": "https://registry.npmjs.org/lodash.pad/-/lodash.pad-4.5.1.tgz", "resolved": "https://registry.npmjs.org/lodash.pad/-/lodash.pad-4.5.1.tgz",
@@ -30206,9 +30134,9 @@
} }
}, },
"node-forge": { "node-forge": {
"version": "1.3.0", "version": "1.3.1",
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.0.tgz", "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz",
"integrity": "sha512-08ARB91bUi6zNKzVmaj3QO7cr397uiDT2nJ63cHjyNtCTWIgvS47j3eT0WfzUwS9+6Z5YshRaoasFkXCKrIYbA==" "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA=="
}, },
"node-netstat": { "node-netstat": {
"version": "1.8.0", "version": "1.8.0",
@@ -32712,6 +32640,14 @@
} }
} }
}, },
"otpauth": {
"version": "9.1.2",
"resolved": "https://registry.npmjs.org/otpauth/-/otpauth-9.1.2.tgz",
"integrity": "sha512-iI5nlVvMFP3aTPdjG/fnC4mhVJ/KZOSnBrvo/VnYHUwlTp9jVLjAe2B3i3pyCH+3/E5jYQRSvuHk/8oas3870g==",
"requires": {
"jssha": "~3.3.0"
}
},
"p-cancelable": { "p-cancelable": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-3.0.0.tgz", "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-3.0.0.tgz",
@@ -36167,9 +36103,9 @@
} }
}, },
"word-wrap": { "word-wrap": {
"version": "1.2.3", "version": "1.2.5",
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
"integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==" "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="
}, },
"wordwrap": { "wordwrap": {
"version": "1.0.0", "version": "1.0.0",

View File

@@ -1,6 +1,6 @@
{ {
"name": "parse-server", "name": "parse-server",
"version": "6.3.0", "version": "6.3.0-alpha.9",
"description": "An express module providing a Parse-compatible API server", "description": "An express module providing a Parse-compatible API server",
"main": "lib/index.js", "main": "lib/index.js",
"repository": { "repository": {
@@ -25,7 +25,7 @@
"@graphql-tools/utils": "8.12.0", "@graphql-tools/utils": "8.12.0",
"@graphql-yoga/node": "2.6.0", "@graphql-yoga/node": "2.6.0",
"@parse/fs-files-adapter": "1.2.2", "@parse/fs-files-adapter": "1.2.2",
"@parse/push-adapter": "4.1.3", "@parse/push-adapter": "4.2.0",
"bcryptjs": "2.4.3", "bcryptjs": "2.4.3",
"body-parser": "1.20.2", "body-parser": "1.20.2",
"commander": "10.0.1", "commander": "10.0.1",
@@ -48,6 +48,7 @@
"mime": "3.0.0", "mime": "3.0.0",
"mongodb": "4.10.0", "mongodb": "4.10.0",
"mustache": "4.2.0", "mustache": "4.2.0",
"otpauth": "9.1.2",
"parse": "4.1.0", "parse": "4.1.0",
"path-to-regexp": "6.2.1", "path-to-regexp": "6.2.1",
"pg-monitor": "2.0.0", "pg-monitor": "2.0.0",

View File

@@ -2406,3 +2406,342 @@ describe('facebook limited auth adapter', () => {
} }
}); });
}); });
describe('OTP TOTP auth adatper', () => {
const headers = {
'Content-Type': 'application/json',
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
};
beforeEach(async () => {
await reconfigureServer({
auth: {
mfa: {
enabled: true,
options: ['TOTP'],
algorithm: 'SHA1',
digits: 6,
period: 30,
},
},
});
});
it('can enroll', async () => {
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.split(',').length).toEqual(2);
await user.fetch();
expect(user.get('authData').mfa).toEqual({ status: 'enabled' });
});
it('can login with valid token', async () => {
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 = await request({
headers,
method: 'POST',
url: 'http://localhost:8378/1/login',
body: JSON.stringify({
username: 'username',
password: 'password',
authData: {
mfa: {
token: totp.generate(),
},
},
}),
}).then(res => res.data);
expect(response.objectId).toEqual(user.id);
expect(response.sessionToken).toBeDefined();
expect(response.authData).toEqual({ mfa: { status: 'enabled' } });
expect(Object.keys(response).sort()).toEqual(
[
'objectId',
'username',
'createdAt',
'updatedAt',
'authData',
'ACL',
'sessionToken',
'authDataResponse',
].sort()
);
});
it('can change OTP with valid token', async () => {
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 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() }
);
await user.fetch({ useMasterKey: true });
expect(user.get('authData').mfa.secret).toEqual(new_secret.base32);
});
it('cannot change OTP with invalid token', async () => {
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 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 expectAsync(
user.save(
{
authData: { mfa: { secret: new_secret.base32, token: new_token, old: '123' } },
},
{ sessionToken: user.getSessionToken() }
)
).toBeRejectedWith(new Parse.Error(Parse.Error.OTHER_CAUSE, 'Invalid MFA token'));
await user.fetch({ useMasterKey: true });
expect(user.get('authData').mfa.secret).toEqual(secret.base32);
});
it('future logins require TOTP token', async () => {
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() }
);
await expectAsync(Parse.User.logIn('username', 'password')).toBeRejectedWith(
new Parse.Error(Parse.Error.OTHER_CAUSE, 'Missing additional authData mfa')
);
});
it('future logins reject incorrect TOTP token', async () => {
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() }
);
await expectAsync(
request({
headers,
method: 'POST',
url: 'http://localhost:8378/1/login',
body: JSON.stringify({
username: 'username',
password: 'password',
authData: {
mfa: {
token: 'abcd',
},
},
}),
}).catch(e => {
throw e.data;
})
).toBeRejectedWith({ code: Parse.Error.SCRIPT_FAILED, error: 'Invalid MFA token' });
});
});
describe('OTP SMS auth adatper', () => {
const headers = {
'Content-Type': 'application/json',
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
};
let code;
let mobile;
const 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,
};
beforeEach(async () => {
code = '';
mobile = '';
await reconfigureServer({
auth: {
mfa,
},
});
});
it('can enroll', async () => {
const user = await Parse.User.signUp('username', 'password');
const sessionToken = user.getSessionToken();
const spy = spyOn(mfa, 'sendSMS').and.callThrough();
await user.save({ authData: { mfa: { mobile: '+11111111111' } } }, { sessionToken });
await user.fetch({ sessionToken });
expect(user.get('authData')).toEqual({ mfa: { status: 'disabled' } });
expect(spy).toHaveBeenCalledWith(code, '+11111111111');
await user.fetch({ useMasterKey: true });
const authData = user.get('authData').mfa?.pending;
expect(authData).toBeDefined();
expect(authData['+11111111111']).toBeDefined();
expect(Object.keys(authData['+11111111111'])).toEqual(['token', 'expiry']);
await user.save({ authData: { mfa: { mobile, token: code } } }, { sessionToken });
await user.fetch({ sessionToken });
expect(user.get('authData')).toEqual({ mfa: { status: 'enabled' } });
});
it('future logins require SMS code', async () => {
const user = await Parse.User.signUp('username', 'password');
const spy = spyOn(mfa, 'sendSMS').and.callThrough();
await user.save(
{ authData: { mfa: { mobile: '+11111111111' } } },
{ sessionToken: user.getSessionToken() }
);
await user.save(
{ authData: { mfa: { mobile, token: code } } },
{ sessionToken: user.getSessionToken() }
);
spy.calls.reset();
await expectAsync(Parse.User.logIn('username', 'password')).toBeRejectedWith(
new Parse.Error(Parse.Error.OTHER_CAUSE, 'Missing additional authData mfa')
);
const res = await request({
headers,
method: 'POST',
url: 'http://localhost:8378/1/login',
body: JSON.stringify({
username: 'username',
password: 'password',
authData: {
mfa: {
token: 'request',
},
},
}),
}).catch(e => e.data);
expect(res).toEqual({ code: Parse.Error.SCRIPT_FAILED, error: 'Please enter the token' });
expect(spy).toHaveBeenCalledWith(code, '+11111111111');
const response = await request({
headers,
method: 'POST',
url: 'http://localhost:8378/1/login',
body: JSON.stringify({
username: 'username',
password: 'password',
authData: {
mfa: {
token: code,
},
},
}),
}).then(res => res.data);
expect(response.objectId).toEqual(user.id);
expect(response.sessionToken).toBeDefined();
expect(response.authData).toEqual({ mfa: { status: 'enabled' } });
expect(Object.keys(response).sort()).toEqual(
[
'objectId',
'username',
'createdAt',
'updatedAt',
'authData',
'ACL',
'sessionToken',
'authDataResponse',
].sort()
);
});
it('partially enrolled users can still login', async () => {
const user = await Parse.User.signUp('username', 'password');
await user.save({ authData: { mfa: { mobile: '+11111111111' } } });
const spy = spyOn(mfa, 'sendSMS').and.callThrough();
await Parse.User.logIn('username', 'password');
expect(spy).not.toHaveBeenCalled();
});
});

View File

@@ -302,4 +302,25 @@ describe('execution', () => {
done.fail(data.toString()); done.fail(data.toString());
}); });
}); });
it('can start Parse Server with auth via CLI', done => {
const env = { ...process.env };
env.NODE_OPTIONS = '--dns-result-order=ipv4first';
childProcess = spawn(
binPath,
['--databaseURI', databaseURI, './spec/configs/CLIConfigAuth.json'],
{ env }
);
childProcess.stdout.on('data', data => {
data = data.toString();
console.log(data);
if (data.includes('parse-server running on')) {
done();
}
});
childProcess.stderr.on('data', data => {
data = data.toString();
done.fail(data.toString());
});
});
}); });

View File

@@ -103,6 +103,14 @@ describe('Cloud Code', () => {
expect(currentConfig.silent).toBeFalse(); expect(currentConfig.silent).toBeFalse();
}); });
it('can get curent version', () => {
const version = require('../package.json').version;
const currentConfig = Config.get('test');
expect(Parse.Server.version).toBeDefined();
expect(currentConfig.version).toBeDefined();
expect(Parse.Server.version).toEqual(version);
});
it('show warning on duplicate cloud functions', done => { it('show warning on duplicate cloud functions', done => {
const logger = require('../lib/logger').logger; const logger = require('../lib/logger').logger;
spyOn(logger, 'warn').and.callFake(() => {}); spyOn(logger, 'warn').and.callFake(() => {});
@@ -2390,6 +2398,56 @@ describe('beforeFind hooks', () => {
}); });
}); });
it('sets correct beforeFind trigger isGet parameter for Parse.Object.fetch request', async () => {
const hook = {
method: req => {
expect(req.isGet).toEqual(true);
return Promise.resolve();
},
};
spyOn(hook, 'method').and.callThrough();
Parse.Cloud.beforeFind('MyObject', hook.method);
const obj = new Parse.Object('MyObject');
await obj.save();
const getObj = await obj.fetch();
expect(getObj).toBeInstanceOf(Parse.Object);
expect(hook.method).toHaveBeenCalledTimes(1);
});
it('sets correct beforeFind trigger isGet parameter for Parse.Query.get request', async () => {
const hook = {
method: req => {
expect(req.isGet).toEqual(false);
return Promise.resolve();
},
};
spyOn(hook, 'method').and.callThrough();
Parse.Cloud.beforeFind('MyObject', hook.method);
const obj = new Parse.Object('MyObject');
await obj.save();
const query = new Parse.Query('MyObject');
const getObj = await query.get(obj.id);
expect(getObj).toBeInstanceOf(Parse.Object);
expect(hook.method).toHaveBeenCalledTimes(1);
});
it('sets correct beforeFind trigger isGet parameter for Parse.Query.find request', async () => {
const hook = {
method: req => {
expect(req.isGet).toEqual(false);
return Promise.resolve();
},
};
spyOn(hook, 'method').and.callThrough();
Parse.Cloud.beforeFind('MyObject', hook.method);
const obj = new Parse.Object('MyObject');
await obj.save();
const query = new Parse.Query('MyObject');
const findObjs = await query.find();
expect(findObjs?.[0]).toBeInstanceOf(Parse.Object);
expect(hook.method).toHaveBeenCalledTimes(1);
});
it('should have request headers', done => { it('should have request headers', done => {
Parse.Cloud.beforeFind('MyObject', req => { Parse.Cloud.beforeFind('MyObject', req => {
expect(req.headers).toBeDefined(); expect(req.headers).toBeDefined();

View File

@@ -554,7 +554,7 @@ describe('DefinedSchemas', () => {
}); });
}); });
it('should not delete automatically classes', async () => { it('should not delete classes automatically', async () => {
await reconfigureServer({ await reconfigureServer({
schema: { definitions: [{ className: '_User' }, { className: 'Test' }] }, schema: { definitions: [{ className: '_User' }, { className: 'Test' }] },
}); });

View File

@@ -1368,7 +1368,7 @@ describe('Parse.File testing', () => {
await reconfigureServer({ await reconfigureServer({
fileUpload: { fileUpload: {
enableForPublic: true, enableForPublic: true,
fileExtensions: ['jpg'], fileExtensions: ['jpg', 'wav'],
}, },
}); });
await expectAsync( await expectAsync(
@@ -1387,6 +1387,30 @@ describe('Parse.File testing', () => {
).toBeRejectedWith( ).toBeRejectedWith(
new Parse.Error(Parse.Error.FILE_SAVE_ERROR, `File upload of extension html is disabled.`) new Parse.Error(Parse.Error.FILE_SAVE_ERROR, `File upload of extension html is disabled.`)
); );
await expectAsync(
request({
method: 'POST',
url: 'http://localhost:8378/1/files/file',
body: JSON.stringify({
_ApplicationId: 'test',
_JavaScriptKey: 'test',
_ContentType: 'image/jpg',
base64: 'PGh0bWw+PC9odG1sPgo=',
}),
})
).toBeResolved();
await expectAsync(
request({
method: 'POST',
url: 'http://localhost:8378/1/files/file',
body: JSON.stringify({
_ApplicationId: 'test',
_JavaScriptKey: 'test',
_ContentType: 'audio/wav',
base64: 'UklGRigAAABXQVZFZm10IBIAAAABAAEARKwAAIhYAQACABAAAABkYXRhAgAAAAEA',
}),
})
).toBeResolved();
}); });
it('works with array without Content-Type', async () => { it('works with array without Content-Type', async () => {

View File

@@ -0,0 +1,11 @@
{
"appName": "test",
"appId": "test",
"masterKey": "test",
"logLevel": "error",
"auth": {
"facebook": {
"appIds": "test"
}
}
}

View File

@@ -21,7 +21,9 @@ export class AuthAdapter {
* Usage policy * Usage policy
* @type {AuthPolicy} * @type {AuthPolicy}
*/ */
this.policy = 'default'; if (!this.policy) {
this.policy = 'default';
}
} }
/** /**
* @param appIds The specified app IDs in the configuration * @param appIds The specified app IDs in the configuration

View File

@@ -9,6 +9,7 @@ const facebook = require('./facebook');
const instagram = require('./instagram'); const instagram = require('./instagram');
const linkedin = require('./linkedin'); const linkedin = require('./linkedin');
const meetup = require('./meetup'); const meetup = require('./meetup');
import mfa from './mfa';
const google = require('./google'); const google = require('./google');
const github = require('./github'); const github = require('./github');
const twitter = require('./twitter'); const twitter = require('./twitter');
@@ -44,6 +45,7 @@ const providers = {
instagram, instagram,
linkedin, linkedin,
meetup, meetup,
mfa,
google, google,
github, github,
twitter, twitter,
@@ -75,7 +77,11 @@ function authDataValidator(provider, adapter, appIds, options) {
if (appIds && typeof adapter.validateAppId === 'function') { if (appIds && typeof adapter.validateAppId === 'function') {
await Promise.resolve(adapter.validateAppId(appIds, authData, options, requestObject)); await Promise.resolve(adapter.validateAppId(appIds, authData, options, requestObject));
} }
if (adapter.policy && !authAdapterPolicies[adapter.policy]) { if (
adapter.policy &&
!authAdapterPolicies[adapter.policy] &&
typeof adapter.policy !== 'function'
) {
throw new Parse.Error( throw new Parse.Error(
Parse.Error.OTHER_CAUSE, Parse.Error.OTHER_CAUSE,
'AuthAdapter policy is not configured correctly. The value must be either "solo", "additional", "default" or undefined (will be handled as "default")' 'AuthAdapter policy is not configured correctly. The value must be either "solo", "additional", "default" or undefined (will be handled as "default")'
@@ -225,17 +231,20 @@ module.exports = function (authOptions = {}, enableAnonymousUsers = true) {
if (!authAdapter) { if (!authAdapter) {
return; return;
} }
const { const { adapter, providerOptions } = authAdapter;
adapter: { afterFind }, const afterFind = adapter.afterFind;
providerOptions,
} = authAdapter;
if (afterFind && typeof afterFind === 'function') { if (afterFind && typeof afterFind === 'function') {
const requestObject = { const requestObject = {
ip: req.config.ip, ip: req.config.ip,
user: req.auth.user, user: req.auth.user,
master: req.auth.isMaster, master: req.auth.isMaster,
}; };
const result = afterFind(requestObject, authData[provider], providerOptions); const result = afterFind.call(
adapter,
requestObject,
authData[provider],
providerOptions
);
if (result) { if (result) {
authData[provider] = result; authData[provider] = result;
} }

213
src/Adapters/Auth/mfa.js Normal file
View File

@@ -0,0 +1,213 @@
import { TOTP, Secret } from 'otpauth';
import { randomString } from '../../cryptoUtils';
import AuthAdapter from './AuthAdapter';
class MFAAdapter extends AuthAdapter {
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(loginData, _, req) {
const saveResponse = {
doNotSave: true,
};
const token = loginData.token;
const auth = req.original.get('authData') || {};
const { secret, recovery, mobile, token: saved, expiry } = auth.mfa || {};
if (this.sms && mobile) {
if (token === 'request') {
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;
}
async 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) {
await this.validateLogin({ token: authData.old }, null, req);
return this.validateSetUp(authData);
}
throw 'Invalid MFA data';
}
afterFind(req, authData) {
if (req.master) {
return;
}
if (this.totp && authData.secret) {
return {
status: 'enabled',
};
}
if (this.sms && authData.mobile) {
return {
status: 'enabled',
};
}
return {
status: 'disabled',
};
}
policy(req, auth) {
if (this.sms && auth?.pending && Object.keys(auth).length === 1) {
return 'default';
}
return 'additional';
}
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: recovery.join(', ') },
save: { secret, recovery },
};
}
}
export default new MFAAdapter();

View File

@@ -17,6 +17,10 @@ export class RedisCacheAdapter {
this.ttl = isValidTTL(ttl) ? ttl : DEFAULT_REDIS_TTL; this.ttl = isValidTTL(ttl) ? ttl : DEFAULT_REDIS_TTL;
this.client = createClient(redisCtx); this.client = createClient(redisCtx);
this.queue = new KeyPromiseQueue(); this.queue = new KeyPromiseQueue();
this.client.on('error', err => { logger.error('RedisCacheAdapter client error', { error: err }) });
this.client.on('connect', () => {});
this.client.on('reconnecting', () => {});
this.client.on('ready', () => {});
} }
async connect() { async connect() {

View File

@@ -438,6 +438,7 @@ const hasMutatedAuthData = (authData, userAuthData) => {
}; };
const checkIfUserHasProvidedConfiguredProvidersForLogin = ( const checkIfUserHasProvidedConfiguredProvidersForLogin = (
req = {},
authData = {}, authData = {},
userAuthData = {}, userAuthData = {},
config config
@@ -461,7 +462,16 @@ const checkIfUserHasProvidedConfiguredProvidersForLogin = (
const additionProvidersNotFound = []; const additionProvidersNotFound = [];
const hasProvidedAtLeastOneAdditionalProvider = savedUserProviders.some(provider => { const hasProvidedAtLeastOneAdditionalProvider = savedUserProviders.some(provider => {
if (provider && provider.adapter && provider.adapter.policy === 'additional') { let policy = provider.adapter.policy;
if (typeof policy === 'function') {
const requestObject = {
ip: req.config.ip,
user: req.auth.user,
master: req.auth.isMaster,
};
policy = policy.call(provider.adapter, requestObject, userAuthData[provider.name]);
}
if (policy === 'additional') {
if (authData[provider.name]) { if (authData[provider.name]) {
return true; return true;
} else { } else {
@@ -498,14 +508,8 @@ const handleAuthDataValidation = async (authData, req, foundUser) => {
await user.fetch({ useMasterKey: true }); await user.fetch({ useMasterKey: true });
} }
const { originalObject, updatedObject } = req.buildParseObjects(); const { updatedObject } = req.buildParseObjects();
const requestObject = getRequestObject( const requestObject = getRequestObject(undefined, req.auth, updatedObject, user, req.config);
undefined,
req.auth,
updatedObject,
originalObject || user,
req.config
);
// Perform validation as step-by-step pipeline for better error consistency // 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 // and also to avoid to trigger a provider (like OTP SMS) if another one fails
const acc = { authData: {}, authDataResponse: {} }; const acc = { authData: {}, authDataResponse: {} };

View File

@@ -7,6 +7,7 @@ import net from 'net';
import AppCache from './cache'; import AppCache from './cache';
import DatabaseController from './Controllers/DatabaseController'; import DatabaseController from './Controllers/DatabaseController';
import { logLevels as validLogLevels } from './Controllers/LoggerController'; import { logLevels as validLogLevels } from './Controllers/LoggerController';
import { version } from '../package.json';
import { import {
AccountLockoutOptions, AccountLockoutOptions,
DatabaseOptions, DatabaseOptions,
@@ -50,6 +51,7 @@ export class Config {
config.generateEmailVerifyTokenExpiresAt = config.generateEmailVerifyTokenExpiresAt.bind( config.generateEmailVerifyTokenExpiresAt = config.generateEmailVerifyTokenExpiresAt.bind(
config config
); );
config.version = version;
return config; return config;
} }

View File

@@ -1716,12 +1716,6 @@ class DatabaseController {
throw error; throw error;
}); });
await this.adapter
.ensureIndex('_User', requiredUserFields, ['username'], 'case_insensitive_username', true)
.catch(error => {
logger.warn('Unable to create case insensitive username index: ', error);
throw error;
});
await this.adapter await this.adapter
.ensureIndex('_User', requiredUserFields, ['username'], 'case_insensitive_username', true) .ensureIndex('_User', requiredUserFields, ['username'], 'case_insensitive_username', true)
.catch(error => { .catch(error => {

View File

@@ -103,7 +103,6 @@ module.exports.ParseServerOptions = {
env: 'PARSE_SERVER_AUTH_PROVIDERS', env: 'PARSE_SERVER_AUTH_PROVIDERS',
help: help:
'Configuration for your authentication providers, as stringified JSON. See http://docs.parseplatform.org/parse-server/guide/#oauth-and-3rd-party-authentication', 'Configuration for your authentication providers, as stringified JSON. See http://docs.parseplatform.org/parse-server/guide/#oauth-and-3rd-party-authentication',
action: parsers.arrayParser,
}, },
cacheAdapter: { cacheAdapter: {
env: 'PARSE_SERVER_CACHE_ADAPTER', env: 'PARSE_SERVER_CACHE_ADAPTER',

View File

@@ -20,7 +20,7 @@
* @property {Adapter<AnalyticsAdapter>} analyticsAdapter Adapter module for the analytics * @property {Adapter<AnalyticsAdapter>} analyticsAdapter Adapter module for the analytics
* @property {String} appId Your Parse Application ID * @property {String} appId Your Parse Application ID
* @property {String} appName Sets the app name * @property {String} appName Sets the app name
* @property {AuthAdapter[]} auth Configuration for your authentication providers, as stringified JSON. See http://docs.parseplatform.org/parse-server/guide/#oauth-and-3rd-party-authentication * @property {Object} auth Configuration for your authentication providers, as stringified JSON. See http://docs.parseplatform.org/parse-server/guide/#oauth-and-3rd-party-authentication
* @property {Adapter<CacheAdapter>} cacheAdapter Adapter module for the cache * @property {Adapter<CacheAdapter>} cacheAdapter Adapter module for the cache
* @property {Number} cacheMaxSize Sets the maximum size for the in memory cache, defaults to 10000 * @property {Number} cacheMaxSize Sets the maximum size for the in memory cache, defaults to 10000
* @property {Number} cacheTTL Sets the TTL for the in memory cache (in ms), defaults to 5000 (5 seconds) * @property {Number} cacheTTL Sets the TTL for the in memory cache (in ms), defaults to 5000 (5 seconds)

View File

@@ -149,7 +149,7 @@ export interface ParseServerOptions {
allowCustomObjectId: ?boolean; allowCustomObjectId: ?boolean;
/* Configuration for your authentication providers, as stringified JSON. See http://docs.parseplatform.org/parse-server/guide/#oauth-and-3rd-party-authentication /* Configuration for your authentication providers, as stringified JSON. See http://docs.parseplatform.org/parse-server/guide/#oauth-and-3rd-party-authentication
:ENV: PARSE_SERVER_AUTH_PROVIDERS */ :ENV: PARSE_SERVER_AUTH_PROVIDERS */
auth: ?(AuthAdapter[]); auth: ?{ [string]: AuthAdapter };
/* Max file size for uploads, defaults to 20mb /* Max file size for uploads, defaults to 20mb
:DEFAULT: 20mb */ :DEFAULT: 20mb */
maxUploadSize: ?string; maxUploadSize: ?string;

View File

@@ -1066,11 +1066,7 @@ function includePath(config, auth, response, path, restOptions = {}) {
// Returns a list of pointers in REST format. // Returns a list of pointers in REST format.
function findPointers(object, path) { function findPointers(object, path) {
if (object instanceof Array) { if (object instanceof Array) {
var answer = []; return object.map(x => findPointers(x, path)).flat();
for (var x of object) {
answer = answer.concat(findPointers(x, path));
}
return answer;
} }
if (typeof object !== 'object' || !object) { if (typeof object !== 'object' || !object) {

View File

@@ -558,6 +558,7 @@ RestWrite.prototype.handleAuthData = async function (authData) {
// we need to be sure that the user has provided // we need to be sure that the user has provided
// required authData // required authData
Auth.checkIfUserHasProvidedConfiguredProvidersForLogin( Auth.checkIfUserHasProvidedConfiguredProvidersForLogin(
{ config: this.config, auth: this.auth },
authData, authData,
userResult.authData, userResult.authData,
this.config this.config

View File

@@ -147,7 +147,7 @@ export class FilesRouter {
if (ext === '*') { if (ext === '*') {
return true; return true;
} }
const regex = new RegExp(fileExtensions); const regex = new RegExp(ext);
if (regex.test(extension)) { if (regex.test(extension)) {
return true; return true;
} }
@@ -176,6 +176,7 @@ export class FilesRouter {
const file = new Parse.File(filename, { base64 }, contentType); const file = new Parse.File(filename, { base64 }, contentType);
const { metadata = {}, tags = {} } = req.fileData || {}; const { metadata = {}, tags = {} } = req.fileData || {};
try { try {
// Scan request data for denied keywords
Utils.checkProhibitedKeywords(config, metadata); Utils.checkProhibitedKeywords(config, metadata);
Utils.checkProhibitedKeywords(config, tags); Utils.checkProhibitedKeywords(config, tags);
} catch (error) { } catch (error) {

View File

@@ -189,7 +189,12 @@ export class UsersRouter extends ClassesRouter {
const user = await this._authenticateUserFromRequest(req); const user = await this._authenticateUserFromRequest(req);
const authData = req.body && req.body.authData; const authData = req.body && req.body.authData;
// Check if user has provided their required auth providers // Check if user has provided their required auth providers
Auth.checkIfUserHasProvidedConfiguredProvidersForLogin(authData, user.authData, req.config); Auth.checkIfUserHasProvidedConfiguredProvidersForLogin(
req,
authData,
user.authData,
req.config
);
let authDataResponse; let authDataResponse;
let validatedAuthData; let validatedAuthData;

View File

@@ -1,13 +1,20 @@
import program from './commander'; import program from './commander';
function logStartupOptions(options) { function logStartupOptions(options) {
if (!options.verbose) {
return;
}
// Keys that may include sensitive information that will be redacted in logs
const keysToRedact = [
'databaseURI',
'masterKey',
'maintenanceKey',
'push',
];
for (const key in options) { for (const key in options) {
let value = options[key]; let value = options[key];
if (key == 'masterKey') { if (keysToRedact.includes(key)) {
value = '***REDACTED***'; value = '<REDACTED>';
}
if (key == 'push' && options.verbose != true) {
value = '***REDACTED***';
} }
if (typeof value === 'object') { if (typeof value === 'object') {
try { try {

View File

@@ -7,7 +7,6 @@ import LRUCacheAdapter from './Adapters/Cache/LRUCache.js';
import * as TestUtils from './TestUtils'; import * as TestUtils from './TestUtils';
import * as SchemaMigrations from './SchemaMigrations/Migrations'; import * as SchemaMigrations from './SchemaMigrations/Migrations';
import AuthAdapter from './Adapters/Auth/AuthAdapter'; import AuthAdapter from './Adapters/Auth/AuthAdapter';
import { useExternal } from './deprecated'; import { useExternal } from './deprecated';
import { getLogger } from './logger'; import { getLogger } from './logger';
import { PushWorker } from './Push/PushWorker'; import { PushWorker } from './Push/PushWorker';