build: Release (#9169)

This commit is contained in:
Manuel
2024-06-30 03:56:49 +02:00
committed by GitHub
47 changed files with 3448 additions and 2376 deletions

View File

@@ -24,7 +24,8 @@
"prefer-const": "error", "prefer-const": "error",
"space-infix-ops": "error", "space-infix-ops": "error",
"no-useless-escape": "off", "no-useless-escape": "off",
"require-atomic-updates": "off" "require-atomic-updates": "off",
"object-curly-spacing": ["error", "always"]
}, },
"globals": { "globals": {
"Parse": true "Parse": true

View File

@@ -8,16 +8,10 @@ assignees: ''
--- ---
### New Issue Checklist ### New Issue Checklist
<!--
Check every following box [x] before submitting your issue.
Click the "Preview" tab for better readability.
Thanks for contributing to Parse Platform!
-->
- [ ] I am not disclosing a [vulnerability](https://github.com/parse-community/parse-server/blob/master/SECURITY.md). - Report security issues [confidentially](https://github.com/parse-community/parse-server/security/policy).
- [ ] I am not just asking a [question](https://github.com/parse-community/.github/blob/master/SUPPORT.md). - Any contribution is under this [license](https://github.com/parse-community/parse-server/blob/alpha/LICENSE).
- [ ] I have searched through [existing issues](https://github.com/parse-community/parse-server/issues?q=is%3Aissue). - Before posting search [existing issues](https://github.com/parse-community/parse-server/issues?q=is%3Aissue).
- [ ] I can reproduce the issue with the [latest version of Parse Server](https://github.com/parse-community/parse-server/releases). <!-- We don't investigate issues for outdated releases. -->
### Issue Description ### Issue Description
<!-- What is the specific issue with Parse Server? --> <!-- What is the specific issue with Parse Server? -->
@@ -30,6 +24,7 @@ assignees: ''
### Expected Outcome ### Expected Outcome
<!-- What outcome, for example query result, did you expect? --> <!-- What outcome, for example query result, did you expect? -->
### Environment ### Environment
<!-- Be specific with versions, don't use "latest" or semver ranges like "~x.y.z" or "^x.y.z". --> <!-- Be specific with versions, don't use "latest" or semver ranges like "~x.y.z" or "^x.y.z". -->

View File

@@ -8,15 +8,10 @@ assignees: ''
--- ---
### New Feature / Enhancement Checklist ### New Feature / Enhancement Checklist
<!--
Check every following box [x] before submitting your issue.
Click the "Preview" tab for better readability.
Thanks for contributing to Parse Platform!
-->
- [ ] I am not disclosing a [vulnerability](https://github.com/parse-community/parse-server/blob/master/SECURITY.md). - Report security issues [confidentially](https://github.com/parse-community/parse-server/security/policy).
- [ ] I am not just asking a [question](https://github.com/parse-community/.github/blob/master/SUPPORT.md). - Any contribution is under this [license](https://github.com/parse-community/parse-server/blob/alpha/LICENSE).
- [ ] I have searched through [existing issues](https://github.com/parse-community/parse-server/issues?q=is%3Aissue). - Before posting search [existing issues](https://github.com/parse-community/parse-server/issues?q=is%3Aissue).
### Current Limitation ### Current Limitation
<!-- Which current limitation is the feature or enhancement addressing? --> <!-- Which current limitation is the feature or enhancement addressing? -->

View File

@@ -17,14 +17,8 @@ jobs:
- name: Setup Node - name: Setup Node
uses: actions/setup-node@v2 uses: actions/setup-node@v2
with: with:
node-version: 14 node-version: 20
- name: Cache Node.js modules cache: 'npm'
uses: actions/cache@v4
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
- name: Install dependencies - name: Install dependencies
run: npm ci run: npm ci
- name: CI Environments Check - name: CI Environments Check

View File

@@ -8,7 +8,7 @@ on:
paths-ignore: paths-ignore:
- '**/**.md' - '**/**.md'
env: env:
NODE_VERSION: 20.11.1 NODE_VERSION: 20.12.0
PARSE_SERVER_TEST_TIMEOUT: 20000 PARSE_SERVER_TEST_TIMEOUT: 20000
jobs: jobs:
check-code-analysis: check-code-analysis:
@@ -146,34 +146,34 @@ jobs:
matrix: matrix:
include: include:
- name: MongoDB 4.2, ReplicaSet - name: MongoDB 4.2, ReplicaSet
MONGODB_VERSION: 4.2.19 MONGODB_VERSION: 4.2.25
MONGODB_TOPOLOGY: replset MONGODB_TOPOLOGY: replset
NODE_VERSION: 20.11.1 NODE_VERSION: 20.12.0
- name: MongoDB 4.4, ReplicaSet - name: MongoDB 4.4, ReplicaSet
MONGODB_VERSION: 4.4.13 MONGODB_VERSION: 4.4.29
MONGODB_TOPOLOGY: replset MONGODB_TOPOLOGY: replset
NODE_VERSION: 20.11.1 NODE_VERSION: 20.12.0
- name: MongoDB 5, ReplicaSet - name: MongoDB 5, ReplicaSet
MONGODB_VERSION: 5.3.2 MONGODB_VERSION: 5.0.26
MONGODB_TOPOLOGY: replset MONGODB_TOPOLOGY: replset
NODE_VERSION: 20.11.1 NODE_VERSION: 20.12.0
- name: MongoDB 6, ReplicaSet - name: MongoDB 6, ReplicaSet
MONGODB_VERSION: 6.0.2 MONGODB_VERSION: 6.0.14
MONGODB_TOPOLOGY: replset MONGODB_TOPOLOGY: replset
NODE_VERSION: 20.11.1 NODE_VERSION: 20.12.0
- name: MongoDB 7, ReplicaSet - name: MongoDB 7, ReplicaSet
MONGODB_VERSION: 7.0.1 MONGODB_VERSION: 7.0.8
MONGODB_TOPOLOGY: replset MONGODB_TOPOLOGY: replset
NODE_VERSION: 20.11.1 NODE_VERSION: 20.12.0
- name: Redis Cache - name: Redis Cache
PARSE_SERVER_TEST_CACHE: redis PARSE_SERVER_TEST_CACHE: redis
MONGODB_VERSION: 4.4.13 MONGODB_VERSION: 7.0.8
MONGODB_TOPOLOGY: standalone MONGODB_TOPOLOGY: standalone
NODE_VERSION: 20.11.1 NODE_VERSION: 20.12.0
- name: Node 18 - name: Node 18
MONGODB_VERSION: 4.4.13 MONGODB_VERSION: 7.0.8
MONGODB_TOPOLOGY: standalone MONGODB_TOPOLOGY: standalone
NODE_VERSION: 18.19.1 NODE_VERSION: 18.20.0
fail-fast: false fail-fast: false
name: ${{ matrix.name }} name: ${{ matrix.name }}
timeout-minutes: 15 timeout-minutes: 15
@@ -210,32 +210,37 @@ jobs:
- run: npm run coverage - run: npm run coverage
env: env:
CI: true CI: true
- run: bash <(curl -s https://codecov.io/bash) - name: Upload code coverage
uses: codecov/codecov-action@v4
with:
# Set to `true` once codecov token bug is fixed; https://github.com/parse-community/parse-server/issues/9129
fail_ci_if_error: false
token: ${{ secrets.CODECOV_TOKEN }}
check-postgres: check-postgres:
strategy: strategy:
matrix: matrix:
include: include:
- name: PostgreSQL 13, PostGIS 3.1 - name: PostgreSQL 13, PostGIS 3.1
POSTGRES_IMAGE: postgis/postgis:13-3.1 POSTGRES_IMAGE: postgis/postgis:13-3.1
NODE_VERSION: 20.11.1 NODE_VERSION: 20.12.0
- name: PostgreSQL 13, PostGIS 3.2 - name: PostgreSQL 13, PostGIS 3.2
POSTGRES_IMAGE: postgis/postgis:13-3.2 POSTGRES_IMAGE: postgis/postgis:13-3.2
NODE_VERSION: 20.11.1 NODE_VERSION: 20.12.0
- name: PostgreSQL 13, PostGIS 3.3 - name: PostgreSQL 13, PostGIS 3.3
POSTGRES_IMAGE: postgis/postgis:13-3.3 POSTGRES_IMAGE: postgis/postgis:13-3.3
NODE_VERSION: 20.11.1 NODE_VERSION: 20.12.0
- name: PostgreSQL 13, PostGIS 3.4 - name: PostgreSQL 13, PostGIS 3.4
POSTGRES_IMAGE: postgis/postgis:13-3.4 POSTGRES_IMAGE: postgis/postgis:13-3.4
NODE_VERSION: 20.11.1 NODE_VERSION: 20.12.0
- name: PostgreSQL 14, PostGIS 3.4 - name: PostgreSQL 14, PostGIS 3.4
POSTGRES_IMAGE: postgis/postgis:14-3.4 POSTGRES_IMAGE: postgis/postgis:14-3.4
NODE_VERSION: 20.11.1 NODE_VERSION: 20.12.0
- name: PostgreSQL 15, PostGIS 3.4 - name: PostgreSQL 15, PostGIS 3.4
POSTGRES_IMAGE: postgis/postgis:15-3.4 POSTGRES_IMAGE: postgis/postgis:15-3.4
NODE_VERSION: 20.11.1 NODE_VERSION: 20.12.0
- name: PostgreSQL 16, PostGIS 3.4 - name: PostgreSQL 16, PostGIS 3.4
POSTGRES_IMAGE: postgis/postgis:15-3.4 POSTGRES_IMAGE: postgis/postgis:15-3.4
NODE_VERSION: 20.11.1 NODE_VERSION: 20.12.0
fail-fast: false fail-fast: false
name: ${{ matrix.name }} name: ${{ matrix.name }}
timeout-minutes: 15 timeout-minutes: 15
@@ -281,7 +286,13 @@ jobs:
- run: npm run coverage - run: npm run coverage
env: env:
CI: true CI: true
- run: bash <(curl -s https://codecov.io/bash) - name: Upload code coverage
uses: codecov/codecov-action@v4
with:
fail_ci_if_error: false
token: ${{ secrets.CODECOV_TOKEN }}
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
concurrency: concurrency:
group: ${{ github.workflow }}-${{ github.ref }} group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true cancel-in-progress: true

View File

@@ -17,7 +17,7 @@ jobs:
persist-credentials: false persist-credentials: false
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
with: with:
node-version: 18.19.1 node-version: 18.20.0
registry-url: https://registry.npmjs.org/ registry-url: https://registry.npmjs.org/
- name: Cache Node.js modules - name: Cache Node.js modules
uses: actions/cache@v4 uses: actions/cache@v4
@@ -93,7 +93,7 @@ jobs:
- name: Use Node.js - name: Use Node.js
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
node-version: 18.19.1 node-version: 18.20.0
- name: Cache Node.js modules - name: Cache Node.js modules
uses: actions/cache@v4 uses: actions/cache@v4
with: with:

View File

@@ -1,9 +1,13 @@
############################################################ ############################################################
# Build stage # Build stage
############################################################ ############################################################
FROM node:lts-alpine AS build FROM node:20.14.0-alpine3.20 AS build
RUN apk --no-cache add \
build-base \
git \
python3
RUN apk --no-cache add git
WORKDIR /tmp WORKDIR /tmp
# Copy package.json first to benefit from layer caching # Copy package.json first to benefit from layer caching
@@ -24,7 +28,7 @@ RUN npm ci --omit=dev --ignore-scripts \
############################################################ ############################################################
# Release stage # Release stage
############################################################ ############################################################
FROM node:lts-alpine AS release FROM node:20.14.0-alpine3.20 AS release
VOLUME /parse-server/cloud /parse-server/config VOLUME /parse-server/cloud /parse-server/config

View File

@@ -6,11 +6,11 @@
[![Build Status](https://github.com/parse-community/parse-server/workflows/ci/badge.svg?branch=beta)](https://github.com/parse-community/parse-server/actions?query=workflow%3Aci+branch%3Abeta) [![Build Status](https://github.com/parse-community/parse-server/workflows/ci/badge.svg?branch=beta)](https://github.com/parse-community/parse-server/actions?query=workflow%3Aci+branch%3Abeta)
[![Build Status](https://github.com/parse-community/parse-server/workflows/ci/badge.svg?branch=release)](https://github.com/parse-community/parse-server/actions?query=workflow%3Aci+branch%3Arelease) [![Build Status](https://github.com/parse-community/parse-server/workflows/ci/badge.svg?branch=release)](https://github.com/parse-community/parse-server/actions?query=workflow%3Aci+branch%3Arelease)
[![Snyk Badge](https://snyk.io/test/github/parse-community/parse-server/badge.svg)](https://snyk.io/test/github/parse-community/parse-server) [![Snyk Badge](https://snyk.io/test/github/parse-community/parse-server/badge.svg)](https://snyk.io/test/github/parse-community/parse-server)
[![Coverage](https://img.shields.io/codecov/c/github/parse-community/parse-server/alpha.svg)](https://codecov.io/github/parse-community/parse-server?branch=alpha) [![Coverage](https://codecov.io/github/parse-community/parse-server/branch/alpha/graph/badge.svg)](https://app.codecov.io/github/parse-community/parse-server/tree/alpha)
[![auto-release](https://img.shields.io/badge/%F0%9F%9A%80-auto--release-9e34eb.svg)](https://github.com/parse-community/parse-dashboard/releases) [![auto-release](https://img.shields.io/badge/%F0%9F%9A%80-auto--release-9e34eb.svg)](https://github.com/parse-community/parse-dashboard/releases)
[![Node Version](https://img.shields.io/badge/nodejs-18,_20-green.svg?logo=node.js&style=flat)](https://nodejs.org) [![Node Version](https://img.shields.io/badge/nodejs-18,_20-green.svg?logo=node.js&style=flat)](https://nodejs.org)
[![MongoDB Version](https://img.shields.io/badge/mongodb-4.0,_4.2,_4.4,_5,_6-green.svg?logo=mongodb&style=flat)](https://www.mongodb.com) [![MongoDB Version](https://img.shields.io/badge/mongodb-4.2,_4.4,_5,_6,_7-green.svg?logo=mongodb&style=flat)](https://www.mongodb.com)
[![Postgres Version](https://img.shields.io/badge/postgresql-13,_14,_15,_16-green.svg?logo=postgresql&style=flat)](https://www.postgresql.org) [![Postgres Version](https://img.shields.io/badge/postgresql-13,_14,_15,_16-green.svg?logo=postgresql&style=flat)](https://www.postgresql.org)
[![npm latest version](https://img.shields.io/npm/v/parse-server/latest.svg)](https://www.npmjs.com/package/parse-server) [![npm latest version](https://img.shields.io/npm/v/parse-server/latest.svg)](https://www.npmjs.com/package/parse-server)
@@ -129,21 +129,20 @@ Parse Server is continuously tested with the most recent releases of Node.js to
| Version | Latest Version | End-of-Life | Compatible | | Version | Latest Version | End-of-Life | Compatible |
|------------|----------------|-------------|------------| |------------|----------------|-------------|------------|
| Node.js 18 | 18.19.1 | April 2025 | ✅ Yes | | Node.js 18 | 18.20.0 | April 2025 | ✅ Yes |
| Node.js 20 | 20.11.1 | April 2026 | ✅ Yes | | Node.js 20 | 20.12.0 | April 2026 | ✅ Yes |
#### MongoDB #### MongoDB
Parse Server is continuously tested with the most recent releases of MongoDB to ensure compatibility. We follow the [MongoDB support schedule](https://www.mongodb.com/support-policy) and [MongoDB lifecycle schedule](https://www.mongodb.com/support-policy/lifecycles) and only test against versions that are officially supported and have not reached their end-of-life date. We consider the end-of-life date of a MongoDB "rapid release" to be the same as its major version release. Parse Server is continuously tested with the most recent releases of MongoDB to ensure compatibility. We follow the [MongoDB support schedule](https://www.mongodb.com/support-policy) and [MongoDB lifecycle schedule](https://www.mongodb.com/support-policy/lifecycles) and only test against versions that are officially supported and have not reached their end-of-life date. MongoDB "rapid releases" are ignored as these are considered pre-releases of the next major version.
| Version | Latest Version | End-of-Life | Compatible | | Version | Latest Version | End-of-Life | Compatible |
| ----------- | -------------- | ------------- | ---------- | | ----------- | -------------- | ------------- | ---------- |
| MongoDB 4.0 | 4.0.28 | April 2022 | ✅ Yes | | MongoDB 4.2 | 4.2.25 | April 2023 | ✅ Yes |
| MongoDB 4.2 | 4.2.19 | April 2023 | ✅ Yes | | MongoDB 4.4 | 4.4.29 | February 2024 | ✅ Yes |
| MongoDB 4.4 | 4.4.13 | February 2024 | ✅ Yes | | MongoDB 5 | 5.0.26 | October 2024 | ✅ Yes |
| MongoDB 5 | 5.3.2 | October 2024 | ✅ Yes | | MongoDB 6 | 6.0.14 | July 2025 | ✅ Yes |
| MongoDB 6 | 6.0.2 | July 2025 | ✅ Yes | | MongoDB 7 | 7.0.8 | TDB | ✅ Yes |
| MongoDB 7 | 7.0.1 | TDB | ✅ Yes |
#### PostgreSQL #### PostgreSQL

View File

@@ -1,3 +1,105 @@
# [7.1.0-alpha.12](https://github.com/parse-community/parse-server/compare/7.1.0-alpha.11...7.1.0-alpha.12) (2024-06-30)
### Bug Fixes
* SQL injection when using Parse Server with PostgreSQL; fixes security vulnerability [GHSA-c2hr-cqg6-8j6r](https://github.com/parse-community/parse-server/security/advisories/GHSA-c2hr-cqg6-8j6r) ([#9167](https://github.com/parse-community/parse-server/issues/9167)) ([2edf1e4](https://github.com/parse-community/parse-server/commit/2edf1e4c0363af01e97a7fbc97694f851b7d1ff3))
# [7.1.0-alpha.11](https://github.com/parse-community/parse-server/compare/7.1.0-alpha.10...7.1.0-alpha.11) (2024-06-29)
### Features
* Upgrade to Parse JS SDK 5.2.0 ([#9128](https://github.com/parse-community/parse-server/issues/9128)) ([665b8d5](https://github.com/parse-community/parse-server/commit/665b8d52d6cf5275179a5e1fb132c934edb53ecc))
# [7.1.0-alpha.10](https://github.com/parse-community/parse-server/compare/7.1.0-alpha.9...7.1.0-alpha.10) (2024-06-11)
### Bug Fixes
* Live query throws error when constraint `notEqualTo` is set to `null` ([#8835](https://github.com/parse-community/parse-server/issues/8835)) ([11d3e48](https://github.com/parse-community/parse-server/commit/11d3e484df862224c15d20f6171514948981ea90))
# [7.1.0-alpha.9](https://github.com/parse-community/parse-server/compare/7.1.0-alpha.8...7.1.0-alpha.9) (2024-05-27)
### Bug Fixes
* Parse Server option `extendSessionOnUse` not working for session lengths < 24 hours ([#9113](https://github.com/parse-community/parse-server/issues/9113)) ([0a054e6](https://github.com/parse-community/parse-server/commit/0a054e6b541fd5ab470bf025665f5f7d2acedaa0))
# [7.1.0-alpha.8](https://github.com/parse-community/parse-server/compare/7.1.0-alpha.7...7.1.0-alpha.8) (2024-05-16)
### Features
* Upgrade to @parse/push-adapter 6.2.0 ([#9127](https://github.com/parse-community/parse-server/issues/9127)) ([ca20496](https://github.com/parse-community/parse-server/commit/ca20496f28e5ec1294a7a23c8559df82b79b2a04))
# [7.1.0-alpha.7](https://github.com/parse-community/parse-server/compare/7.1.0-alpha.6...7.1.0-alpha.7) (2024-05-16)
### Bug Fixes
* Facebook Limited Login not working due to incorrect domain in JWT validation ([#9122](https://github.com/parse-community/parse-server/issues/9122)) ([9d0bd2b](https://github.com/parse-community/parse-server/commit/9d0bd2badd6e5f7429d1af00b118225752e5d86a))
# [7.1.0-alpha.6](https://github.com/parse-community/parse-server/compare/7.1.0-alpha.5...7.1.0-alpha.6) (2024-04-14)
### Bug Fixes
* `Parse.Cloud.startJob` and `Parse.Push.send` not returning status ID when setting Parse Server option `directAccess: true` ([#8766](https://github.com/parse-community/parse-server/issues/8766)) ([5b0efb2](https://github.com/parse-community/parse-server/commit/5b0efb22efe94c47f243cf8b1e6407ed5c5a67d3))
# [7.1.0-alpha.5](https://github.com/parse-community/parse-server/compare/7.1.0-alpha.4...7.1.0-alpha.5) (2024-04-07)
### Features
* Prevent Parse Server start in case of unknown option in server configuration ([#8987](https://github.com/parse-community/parse-server/issues/8987)) ([8758e6a](https://github.com/parse-community/parse-server/commit/8758e6abb9dbb68757bddcbd332ad25100c24a0e))
# [7.1.0-alpha.4](https://github.com/parse-community/parse-server/compare/7.1.0-alpha.3...7.1.0-alpha.4) (2024-03-31)
### Features
* Upgrade to @parse/push-adapter 6.0.0 ([#9066](https://github.com/parse-community/parse-server/issues/9066)) ([18bdbf8](https://github.com/parse-community/parse-server/commit/18bdbf89c53a57648891ef582614ba7c2941e587))
# [7.1.0-alpha.3](https://github.com/parse-community/parse-server/compare/7.1.0-alpha.2...7.1.0-alpha.3) (2024-03-24)
### Bug Fixes
* Rate limiting can fail when using Parse Server option `rateLimit.redisUrl` with clusters ([#8632](https://github.com/parse-community/parse-server/issues/8632)) ([c277739](https://github.com/parse-community/parse-server/commit/c27773962399f8e27691e3b8087e7e1d59516efd))
# [7.1.0-alpha.2](https://github.com/parse-community/parse-server/compare/7.1.0-alpha.1...7.1.0-alpha.2) (2024-03-24)
### Features
* Add server security check status `security.enableCheck` to Features Router ([#8679](https://github.com/parse-community/parse-server/issues/8679)) ([b07ec15](https://github.com/parse-community/parse-server/commit/b07ec153825882e97cc48dc84072c7f549f3238b))
# [7.1.0-alpha.1](https://github.com/parse-community/parse-server/compare/7.0.0...7.1.0-alpha.1) (2024-03-23)
### Bug Fixes
* `Required` option not handled correctly for special fields (File, GeoPoint, Polygon) on GraphQL API mutations ([#8915](https://github.com/parse-community/parse-server/issues/8915)) ([907ad42](https://github.com/parse-community/parse-server/commit/907ad4267c228d26cfcefe7848b30ce85ba7ff8f))
### Features
* Add `silent` log level for Cloud Code ([#8803](https://github.com/parse-community/parse-server/issues/8803)) ([5f81efb](https://github.com/parse-community/parse-server/commit/5f81efb42964c4c2fa8bcafee9446a0122e3ce21))
# [7.0.0-alpha.31](https://github.com/parse-community/parse-server/compare/7.0.0-alpha.30...7.0.0-alpha.31) (2024-03-21)
### Features
* Add `silent` log level for Cloud Code ([#8803](https://github.com/parse-community/parse-server/issues/8803)) ([5f81efb](https://github.com/parse-community/parse-server/commit/5f81efb42964c4c2fa8bcafee9446a0122e3ce21))
# [7.0.0-alpha.30](https://github.com/parse-community/parse-server/compare/7.0.0-alpha.29...7.0.0-alpha.30) (2024-03-20)
### Bug Fixes
* `Required` option not handled correctly for special fields (File, GeoPoint, Polygon) on GraphQL API mutations ([#8915](https://github.com/parse-community/parse-server/issues/8915)) ([907ad42](https://github.com/parse-community/parse-server/commit/907ad4267c228d26cfcefe7848b30ce85ba7ff8f))
# [7.0.0-alpha.29](https://github.com/parse-community/parse-server/compare/7.0.0-alpha.28...7.0.0-alpha.29) (2024-03-19) # [7.0.0-alpha.29](https://github.com/parse-community/parse-server/compare/7.0.0-alpha.28...7.0.0-alpha.29) (2024-03-19)

View File

@@ -1,8 +1,7 @@
'use strict'; 'use strict';
const CiVersionCheck = require('./CiVersionCheck'); const CiVersionCheck = require('./CiVersionCheck');
const mongoVersionList = require('mongodb-version-list'); const { exec } = require('child_process');
const allNodeVersions = require('all-node-versions');
async function check() { async function check() {
// Run checks // Run checks
@@ -14,14 +13,16 @@ async function check() {
* Check the MongoDB versions used in test environments. * Check the MongoDB versions used in test environments.
*/ */
async function checkMongoDbVersions() { async function checkMongoDbVersions() {
const releasedVersions = await new Promise((resolve, reject) => { let latestStableVersions = await new Promise((resolve, reject) => {
mongoVersionList(function (error, versions) { exec('m ls', (error, stdout) => {
if (error) { if (error) {
reject(error); reject(error);
return;
} }
resolve(versions); resolve(stdout.trim());
}); });
}); });
latestStableVersions = latestStableVersions.split('\n').map(version => version.trim());
await new CiVersionCheck({ await new CiVersionCheck({
packageName: 'MongoDB', packageName: 'MongoDB',
@@ -29,13 +30,14 @@ async function checkMongoDbVersions() {
yamlFilePath: './.github/workflows/ci.yml', yamlFilePath: './.github/workflows/ci.yml',
ciEnvironmentsKeyPath: 'jobs.check-mongo.strategy.matrix.include', ciEnvironmentsKeyPath: 'jobs.check-mongo.strategy.matrix.include',
ciVersionKey: 'MONGODB_VERSION', ciVersionKey: 'MONGODB_VERSION',
releasedVersions, releasedVersions: latestStableVersions,
latestComponent: CiVersionCheck.versionComponents.minor, latestComponent: CiVersionCheck.versionComponents.patch,
ignoreReleasedVersions: [ ignoreReleasedVersions: [
'<4.0.0', // Versions reached their MongoDB end-of-life support date '<4.2.0', // These versions have reached their end-of-life support date
'~4.1.0', // Development release according to MongoDB support '>=4.3.0 <5.0.0', // Unsupported rapid release versions
'~4.3.0', // Development release according to MongoDB support '>=5.1.0 <6.0.0', // Unsupported rapid release versions
'~4.7.0', // Development release according to MongoDB support '>=6.1.0 <7.0.0', // Unsupported rapid release versions
'>=7.1.0 <8.0.0', // Unsupported rapid release versions
], ],
}).check(); }).check();
} }
@@ -44,8 +46,9 @@ async function checkMongoDbVersions() {
* Check the Nodejs versions used in test environments. * Check the Nodejs versions used in test environments.
*/ */
async function checkNodeVersions() { async function checkNodeVersions() {
const allVersions = await allNodeVersions(); const allVersions = (await import('all-node-versions')).default;
const releasedVersions = allVersions.versions; const { versions } = await allVersions();
const nodeVersions = versions.map(version => version.node);
await new CiVersionCheck({ await new CiVersionCheck({
packageName: 'Node.js', packageName: 'Node.js',
@@ -53,13 +56,12 @@ async function checkNodeVersions() {
yamlFilePath: './.github/workflows/ci.yml', yamlFilePath: './.github/workflows/ci.yml',
ciEnvironmentsKeyPath: 'jobs.check-mongo.strategy.matrix.include', ciEnvironmentsKeyPath: 'jobs.check-mongo.strategy.matrix.include',
ciVersionKey: 'NODE_VERSION', ciVersionKey: 'NODE_VERSION',
releasedVersions, releasedVersions: nodeVersions,
latestComponent: CiVersionCheck.versionComponents.minor, latestComponent: CiVersionCheck.versionComponents.minor,
ignoreReleasedVersions: [ ignoreReleasedVersions: [
'<12.0.0', // These versions have reached their end-of-life support date '<18.0.0', // These versions have reached their end-of-life support date
'>=13.0.0 <14.0.0', // These versions have reached their end-of-life support date '>=19.0.0 <20.0.0', // These versions have reached their end-of-life support date
'>=15.0.0 <16.0.0', // These versions have reached their end-of-life support date '>=21.0.0', // These versions are not officially supported yet
'>=19.0.0', // These versions are not officially supported yet
], ],
}).check(); }).check();
} }

View File

@@ -29,7 +29,7 @@
"template": "./node_modules/clean-jsdoc-theme", "template": "./node_modules/clean-jsdoc-theme",
"theme_opts": { "theme_opts": {
"default_theme": "dark", "default_theme": "dark",
"title": "<img src='../.github/parse-server-logo.png' class='logo'/>", "title": "<img src='https://raw.githubusercontent.com/parse-community/parse-server/alpha/.github/parse-server-logo.png' class='logo'/>",
"create_style": "header, .sidebar-section-title, .sidebar-title { color: #139cee !important } .logo { margin-left : 40px; margin-right: 40px }" "create_style": "header, .sidebar-section-title, .sidebar-title { color: #139cee !important } .logo { margin-left : 40px; margin-right: 40px }"
} }
}, },

4821
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{ {
"name": "parse-server", "name": "parse-server",
"version": "7.0.0", "version": "7.1.0-alpha.12",
"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": {
@@ -21,40 +21,40 @@
"dependencies": { "dependencies": {
"@apollo/server": "4.10.1", "@apollo/server": "4.10.1",
"@babel/eslint-parser": "7.21.8", "@babel/eslint-parser": "7.21.8",
"@graphql-tools/merge": "8.4.1", "@graphql-tools/merge": "9.0.3",
"@graphql-tools/schema": "10.0.3", "@graphql-tools/schema": "10.0.3",
"@graphql-tools/utils": "8.12.0", "@graphql-tools/utils": "8.12.0",
"@parse/fs-files-adapter": "2.0.1", "@parse/fs-files-adapter": "3.0.0",
"@parse/push-adapter": "5.1.1", "@parse/push-adapter": "6.2.0",
"bcryptjs": "2.4.3", "bcryptjs": "2.4.3",
"body-parser": "1.20.2", "body-parser": "1.20.2",
"commander": "12.0.0", "commander": "12.0.0",
"cors": "2.8.5", "cors": "2.8.5",
"deepcopy": "2.1.0", "deepcopy": "2.1.0",
"express": "4.18.2", "express": "4.19.2",
"express-rate-limit": "6.11.2", "express-rate-limit": "6.11.2",
"follow-redirects": "1.15.6", "follow-redirects": "1.15.6",
"graphql": "16.8.1", "graphql": "16.8.1",
"graphql-list-fields": "2.0.4", "graphql-list-fields": "2.0.4",
"graphql-relay": "0.10.0", "graphql-relay": "0.10.1",
"graphql-tag": "2.12.6", "graphql-tag": "2.12.6",
"graphql-upload": "15.0.2", "graphql-upload": "15.0.2",
"intersect": "1.0.1", "intersect": "1.0.1",
"jsonwebtoken": "9.0.0", "jsonwebtoken": "9.0.2",
"jwks-rsa": "3.1.0", "jwks-rsa": "3.1.0",
"ldapjs": "3.0.7", "ldapjs": "3.0.7",
"lodash": "4.17.21", "lodash": "4.17.21",
"lru-cache": "10.1.0", "lru-cache": "10.2.2",
"mime": "3.0.0", "mime": "3.0.0",
"mongodb": "5.9.0", "mongodb": "5.9.0",
"mustache": "4.2.0", "mustache": "4.2.0",
"otpauth": "9.2.2", "otpauth": "9.2.2",
"parse": "5.0.0", "parse": "5.2.0",
"path-to-regexp": "6.2.1", "path-to-regexp": "6.2.1",
"pg-monitor": "2.0.0", "pg-monitor": "2.0.0",
"pg-promise": "11.5.4", "pg-promise": "11.7.8",
"pluralize": "8.0.0", "pluralize": "8.0.0",
"rate-limit-redis": "3.0.2", "rate-limit-redis": "4.2.0",
"redis": "4.6.13", "redis": "4.6.13",
"semver": "7.6.0", "semver": "7.6.0",
"subscriptions-transport-ws": "0.11.0", "subscriptions-transport-ws": "0.11.0",
@@ -62,13 +62,13 @@
"uuid": "9.0.1", "uuid": "9.0.1",
"winston": "3.12.0", "winston": "3.12.0",
"winston-daily-rotate-file": "5.0.0", "winston-daily-rotate-file": "5.0.0",
"ws": "8.16.0" "ws": "8.17.1"
}, },
"devDependencies": { "devDependencies": {
"@actions/core": "1.9.1", "@actions/core": "1.10.1",
"@apollo/client": "3.9.5", "@apollo/client": "3.9.11",
"@babel/cli": "7.23.9", "@babel/cli": "7.23.9",
"@babel/core": "7.24.0", "@babel/core": "7.24.7",
"@babel/plugin-proposal-object-rest-spread": "7.10.0", "@babel/plugin-proposal-object-rest-spread": "7.10.0",
"@babel/plugin-transform-flow-strip-types": "7.23.3", "@babel/plugin-transform-flow-strip-types": "7.23.3",
"@babel/preset-env": "7.24.0", "@babel/preset-env": "7.24.0",
@@ -79,9 +79,9 @@
"@semantic-release/github": "7.2.3", "@semantic-release/github": "7.2.3",
"@semantic-release/npm": "7.1.3", "@semantic-release/npm": "7.1.3",
"@semantic-release/release-notes-generator": "9.0.3", "@semantic-release/release-notes-generator": "9.0.3",
"all-node-versions": "11.3.0", "all-node-versions": "12.1.0",
"apollo-upload-client": "17.0.0", "apollo-upload-client": "17.0.0",
"clean-jsdoc-theme": "4.2.7", "clean-jsdoc-theme": "4.2.18",
"cross-env": "7.0.2", "cross-env": "7.0.2",
"deep-diff": "1.0.2", "deep-diff": "1.0.2",
"eslint": "8.26.0", "eslint": "8.26.0",
@@ -95,12 +95,12 @@
"jsdoc": "4.0.2", "jsdoc": "4.0.2",
"jsdoc-babel": "0.5.0", "jsdoc-babel": "0.5.0",
"lint-staged": "10.2.3", "lint-staged": "10.2.3",
"m": "1.9.0",
"madge": "6.1.0", "madge": "6.1.0",
"mock-files-adapter": "file:spec/dependencies/mock-files-adapter", "mock-files-adapter": "file:spec/dependencies/mock-files-adapter",
"mock-mail-adapter": "file:spec/dependencies/mock-mail-adapter", "mock-mail-adapter": "file:spec/dependencies/mock-mail-adapter",
"mongodb-runner": "5.4.4", "mongodb-runner": "5.5.4",
"mongodb-version-list": "1.0.0", "node-abort-controller": "3.1.1",
"node-abort-controller": "3.0.1",
"node-fetch": "3.2.10", "node-fetch": "3.2.10",
"nyc": "15.1.0", "nyc": "15.1.0",
"prettier": "2.0.5", "prettier": "2.0.5",
@@ -125,6 +125,7 @@
"test:mongodb:5.3.2": "npm run test:mongodb --dbversion=5.3.2", "test:mongodb:5.3.2": "npm run test:mongodb --dbversion=5.3.2",
"test:mongodb:6.0.2": "npm run test:mongodb --dbversion=6.0.2", "test:mongodb:6.0.2": "npm run test:mongodb --dbversion=6.0.2",
"test:mongodb:7.0.1": "npm run test:mongodb --dbversion=7.0.1", "test:mongodb:7.0.1": "npm run test:mongodb --dbversion=7.0.1",
"test:postgres:testonly": "cross-env PARSE_SERVER_TEST_DB=postgres PARSE_SERVER_TEST_DATABASE_URI=postgres://postgres:password@localhost:5432/parse_server_postgres_adapter_test_database npm run testonly",
"pretest": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=5.3.2} MONGODB_TOPOLOGY=${MONGODB_TOPOLOGY:=standalone} mongodb-runner start -t ${MONGODB_TOPOLOGY} --version ${MONGODB_VERSION} -- --port 27017", "pretest": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=5.3.2} MONGODB_TOPOLOGY=${MONGODB_TOPOLOGY:=standalone} mongodb-runner start -t ${MONGODB_TOPOLOGY} --version ${MONGODB_VERSION} -- --port 27017",
"testonly": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=5.3.2} MONGODB_TOPOLOGY=${MONGODB_TOPOLOGY:=standalone} TESTING=1 jasmine", "testonly": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=5.3.2} MONGODB_TOPOLOGY=${MONGODB_TOPOLOGY:=standalone} TESTING=1 jasmine",
"test": "npm run testonly", "test": "npm run testonly",
@@ -143,7 +144,7 @@
"parse-server": "bin/parse-server" "parse-server": "bin/parse-server"
}, },
"optionalDependencies": { "optionalDependencies": {
"@node-rs/bcrypt": "1.1.0" "@node-rs/bcrypt": "1.10.1"
}, },
"collective": { "collective": {
"type": "opencollective", "type": "opencollective",

View File

@@ -1,10 +1,17 @@
#!/bin/sh -e #!/bin/sh -e
set -x set -x
# GITHUB_ACTIONS=true SOURCE_TAG=test ./release_docs.sh
if [ "${GITHUB_ACTIONS}" = "" ]; if [ "${GITHUB_ACTIONS}" = "" ];
then then
echo "Cannot release docs without GITHUB_ACTIONS set" echo "Cannot release docs without GITHUB_ACTIONS set"
exit 0; exit 0;
fi fi
if [ "${SOURCE_TAG}" = "" ];
then
echo "Cannot release docs without SOURCE_TAG set"
exit 0;
fi
REPO="https://github.com/parse-community/parse-server" REPO="https://github.com/parse-community/parse-server"
rm -rf docs rm -rf docs
@@ -13,20 +20,20 @@ cd docs
git pull origin gh-pages git pull origin gh-pages
cd .. cd ..
DEST="master" RELEASE="release"
VERSION="${SOURCE_TAG}"
if [ "${SOURCE_TAG}" != "" ]; # change the default page to the latest
then echo "<meta http-equiv='refresh' content='0; url=/parse-server/api/${VERSION}'>" > "docs/api/index.html"
DEST="${SOURCE_TAG}"
# change the default page to the latest
echo "<meta http-equiv='refresh' content='0; url=/parse-server/api/${DEST}'>" > "docs/api/index.html"
fi
npm run definitions npm run definitions
npm run docs npm run docs
mkdir -p "docs/api/${DEST}" mkdir -p "docs/api/${RELEASE}"
cp -R out/* "docs/api/${DEST}" cp -R out/* "docs/api/${RELEASE}"
mkdir -p "docs/api/${VERSION}"
cp -R out/* "docs/api/${VERSION}"
# Copy other resources # Copy other resources
RESOURCE_DIR=".github" RESOURCE_DIR=".github"

View File

@@ -254,6 +254,23 @@ function inject(t, list) {
if (action) { if (action) {
props.push(t.objectProperty(t.stringLiteral('action'), action)); props.push(t.objectProperty(t.stringLiteral('action'), action));
} }
if (t.isGenericTypeAnnotation(elt)) {
if (elt.typeAnnotation.id.name in nestedOptionEnvPrefix) {
props.push(
t.objectProperty(t.stringLiteral('type'), t.stringLiteral(elt.typeAnnotation.id.name))
);
}
} else if (t.isArrayTypeAnnotation(elt)) {
const elementType = elt.typeAnnotation.elementType;
if (t.isGenericTypeAnnotation(elementType)) {
if (elementType.id.name in nestedOptionEnvPrefix) {
props.push(
t.objectProperty(t.stringLiteral('type'), t.stringLiteral(elementType.id.name + '[]'))
);
}
}
}
if (elt.defaultValue) { if (elt.defaultValue) {
let parsedValue = parseDefaultValue(elt, elt.defaultValue, t); let parsedValue = parseDefaultValue(elt, elt.defaultValue, t);
if (!parsedValue) { if (!parsedValue) {

View File

@@ -142,6 +142,25 @@ describe('AdapterLoader', () => {
}).not.toThrow(); }).not.toThrow();
}); });
it('should load custom database adapter from config', done => {
const adapterPath = require('path').resolve('./spec/support/MockDatabaseAdapter');
const options = {
databaseURI: 'oracledb://user:password@localhost:1521/freepdb1',
collectionPrefix: '',
};
const databaseAdapterOptions = {
adapter: adapterPath,
options,
};
expect(() => {
const databaseAdapter = loadAdapter(databaseAdapterOptions);
expect(databaseAdapter).not.toBe(undefined);
expect(databaseAdapter.options).toEqual(options);
expect(databaseAdapter.getDatabaseURI()).toEqual(options.databaseURI);
}).not.toThrow();
done();
});
it('should load file adapter from direct passing', done => { it('should load file adapter from direct passing', done => {
spyOn(console, 'warn').and.callFake(() => {}); spyOn(console, 'warn').and.callFake(() => {});
const mockFilesAdapter = new MockFilesAdapter('key', 'secret', 'bucket'); const mockFilesAdapter = new MockFilesAdapter('key', 'secret', 'bucket');

View File

@@ -260,3 +260,24 @@ describe('Auth', () => {
}); });
}); });
}); });
describe('extendSessionOnUse', () => {
it(`shouldUpdateSessionExpiry()`, async () => {
const { shouldUpdateSessionExpiry } = require('../lib/Auth');
let update = new Date(Date.now() - 86410 * 1000);
const res = shouldUpdateSessionExpiry(
{ sessionLength: 86460 },
{ updatedAt: update }
);
update = new Date(Date.now() - 43210 * 1000);
const res2 = shouldUpdateSessionExpiry(
{ sessionLength: 86460 },
{ updatedAt: update }
);
expect(res).toBe(true);
expect(res2).toBe(false);
});
});

View File

@@ -2047,7 +2047,7 @@ describe('facebook limited auth adapter', () => {
it('should use algorithm from key header to verify id_token', async () => { it('should use algorithm from key header to verify id_token', async () => {
const fakeClaim = { const fakeClaim = {
iss: 'https://facebook.com', iss: 'https://www.facebook.com',
aud: 'secret', aud: 'secret',
exp: Date.now(), exp: Date.now(),
sub: 'the_user_id', sub: 'the_user_id',
@@ -2097,7 +2097,7 @@ describe('facebook limited auth adapter', () => {
it('(using client id as string) should verify id_token', async () => { it('(using client id as string) should verify id_token', async () => {
const fakeClaim = { const fakeClaim = {
iss: 'https://facebook.com', iss: 'https://www.facebook.com',
aud: 'secret', aud: 'secret',
exp: Date.now(), exp: Date.now(),
sub: 'the_user_id', sub: 'the_user_id',
@@ -2117,7 +2117,7 @@ describe('facebook limited auth adapter', () => {
it('(using client id as array) should verify id_token', async () => { it('(using client id as array) should verify id_token', async () => {
const fakeClaim = { const fakeClaim = {
iss: 'https://facebook.com', iss: 'https://www.facebook.com',
aud: 'secret', aud: 'secret',
exp: Date.now(), exp: Date.now(),
sub: 'the_user_id', sub: 'the_user_id',
@@ -2137,7 +2137,7 @@ describe('facebook limited auth adapter', () => {
it('(using client id as array with multiple items) should verify id_token', async () => { it('(using client id as array with multiple items) should verify id_token', async () => {
const fakeClaim = { const fakeClaim = {
iss: 'https://facebook.com', iss: 'https://www.facebook.com',
aud: 'secret', aud: 'secret',
exp: Date.now(), exp: Date.now(),
sub: 'the_user_id', sub: 'the_user_id',
@@ -2174,7 +2174,7 @@ describe('facebook limited auth adapter', () => {
fail(); fail();
} catch (e) { } catch (e) {
expect(e.message).toBe( expect(e.message).toBe(
'id token not issued by correct OpenID provider - expected: https://facebook.com | from: https://not.facebook.com' 'id token not issued by correct OpenID provider - expected: https://www.facebook.com | from: https://not.facebook.com'
); );
} }
}); });
@@ -2203,7 +2203,7 @@ describe('facebook limited auth adapter', () => {
fail(); fail();
} catch (e) { } catch (e) {
expect(e.message).toBe( expect(e.message).toBe(
'id token not issued by correct OpenID provider - expected: https://facebook.com | from: https://not.facebook.com' 'id token not issued by correct OpenID provider - expected: https://www.facebook.com | from: https://not.facebook.com'
); );
} }
}); });
@@ -2230,7 +2230,7 @@ describe('facebook limited auth adapter', () => {
fail(); fail();
} catch (e) { } catch (e) {
expect(e.message).toBe( expect(e.message).toBe(
'id token not issued by correct OpenID provider - expected: https://facebook.com | from: https://not.facebook.com' 'id token not issued by correct OpenID provider - expected: https://www.facebook.com | from: https://not.facebook.com'
); );
} }
}); });
@@ -2288,7 +2288,7 @@ describe('facebook limited auth adapter', () => {
it('should throw error with with invalid user id', async () => { it('should throw error with with invalid user id', async () => {
const fakeClaim = { const fakeClaim = {
iss: 'https://facebook.com', iss: 'https://www.facebook.com',
aud: 'invalid_client_id', aud: 'invalid_client_id',
sub: 'a_different_user_id', sub: 'a_different_user_id',
}; };

View File

@@ -487,6 +487,33 @@ describe('Auth Adapter features', () => {
expect(baseAdapter2.validateAuthData).toHaveBeenCalledTimes(2); expect(baseAdapter2.validateAuthData).toHaveBeenCalledTimes(2);
}); });
it('should not perform authData validation twice when data mutated', async () => {
spyOn(baseAdapter, 'validateAuthData').and.resolveTo({});
await reconfigureServer({
auth: { baseAdapter },
allowExpiredAuthDataToken: false,
});
const user = new Parse.User();
await user.save({
authData: {
baseAdapter: { id: 'baseAdapter', token: "sometoken1" },
},
});
expect(baseAdapter.validateAuthData).toHaveBeenCalledTimes(1);
const user2 = new Parse.User();
await user2.save({
authData: {
baseAdapter: { id: 'baseAdapter', token: "sometoken2" },
},
});
expect(baseAdapter.validateAuthData).toHaveBeenCalledTimes(2);
});
it('should require additional provider if configured', async () => { it('should require additional provider if configured', async () => {
await reconfigureServer({ await reconfigureServer({
auth: { baseAdapter, additionalAdapter }, auth: { baseAdapter, additionalAdapter },

View File

@@ -15,6 +15,13 @@ describe('Cloud Code Logger', () => {
// useful to flip to false for fine tuning :). // useful to flip to false for fine tuning :).
silent: true, silent: true,
logLevel: undefined, logLevel: undefined,
logLevels: {
cloudFunctionError: 'error',
cloudFunctionSuccess: 'info',
triggerAfter: 'info',
triggerBeforeError: 'error',
triggerBeforeSuccess: 'info',
},
}) })
.then(() => { .then(() => {
return Parse.User.signUp('tester', 'abc') return Parse.User.signUp('tester', 'abc')
@@ -334,4 +341,53 @@ describe('Cloud Code Logger', () => {
expect(args[0]).toBe('Parse error: '); expect(args[0]).toBe('Parse error: ');
expect(args[1].message).toBe('Object not found.'); expect(args[1].message).toBe('Object not found.');
}); });
it('should log cloud function execution using the silent log level', async () => {
await reconfigureServer({
logLevels: {
cloudFunctionSuccess: 'silent',
cloudFunctionError: 'silent',
},
});
Parse.Cloud.define('aFunction', () => {
return 'it worked!';
});
Parse.Cloud.define('bFunction', () => {
throw new Error('Failed');
});
spy = spyOn(Config.get('test').loggerController.adapter, 'log').and.callThrough();
await Parse.Cloud.run('aFunction', { foo: 'bar' });
expect(spy).toHaveBeenCalledTimes(0);
await expectAsync(Parse.Cloud.run('bFunction', { foo: 'bar' })).toBeRejected();
// Not "Failed running cloud function message..."
expect(spy).toHaveBeenCalledTimes(1);
});
it('should log cloud function triggers using the silent log level', async () => {
await reconfigureServer({
logLevels: {
triggerAfter: 'silent',
triggerBeforeSuccess: 'silent',
triggerBeforeError: 'silent',
},
});
Parse.Cloud.beforeSave('TestClassError', () => {
throw new Error('Failed');
});
Parse.Cloud.beforeSave('TestClass', () => {});
Parse.Cloud.afterSave('TestClass', () => {});
spy = spyOn(Config.get('test').loggerController.adapter, 'log').and.callThrough();
const obj = new Parse.Object('TestClass');
await obj.save();
expect(spy).toHaveBeenCalledTimes(0);
const objError = new Parse.Object('TestClassError');
await expectAsync(objError.save()).toBeRejected();
// Not "beforeSave failed for TestClassError for user ..."
expect(spy).toHaveBeenCalledTimes(1);
});
}); });

View File

@@ -87,7 +87,7 @@ describe('OAuth', function () {
done(); done();
} }
it('GET request for a resource that requires OAuth should fail with invalid credentials', done => { xit('GET request for a resource that requires OAuth should fail with invalid credentials', done => {
/* /*
This endpoint has been chosen to make a request to an endpoint that requires OAuth which fails due to missing authentication. This endpoint has been chosen to make a request to an endpoint that requires OAuth which fails due to missing authentication.
Any other endpoint from the Twitter API that requires OAuth can be used instead in case the currently used endpoint deprecates. Any other endpoint from the Twitter API that requires OAuth can be used instead in case the currently used endpoint deprecates.
@@ -105,7 +105,7 @@ describe('OAuth', function () {
}); });
}); });
it('POST request for a resource that requires OAuth should fail with invalid credentials', done => { xit('POST request for a resource that requires OAuth should fail with invalid credentials', done => {
/* /*
This endpoint has been chosen to make a request to an endpoint that requires OAuth which fails due to missing authentication. This endpoint has been chosen to make a request to an endpoint that requires OAuth which fails due to missing authentication.
Any other endpoint from the Twitter API that requires OAuth can be used instead in case the currently used endpoint deprecates. Any other endpoint from the Twitter API that requires OAuth can be used instead in case the currently used endpoint deprecates.

View File

@@ -0,0 +1,52 @@
const Config = require('../lib/Config');
const ParseServer = require('../lib/index').ParseServer;
describe('Config Keys', () => {
const tests = [
{
name: 'Invalid Root Keys',
options: { unknow: 'val', masterKeyIPs: '' },
error: 'unknow, masterKeyIPs',
},
{ name: 'Invalid Schema Keys', options: { schema: { Strict: 'val' } }, error: 'schema.Strict' },
{
name: 'Invalid Pages Keys',
options: { pages: { customUrls: { EmailVerificationSendFail: 'val' } } },
error: 'pages.customUrls.EmailVerificationSendFail',
},
{
name: 'Invalid LiveQueryServerOptions Keys',
options: { liveQueryServerOptions: { MasterKey: 'value' } },
error: 'liveQueryServerOptions.MasterKey',
},
{
name: 'Invalid RateLimit Keys - Array Item',
options: { rateLimit: [{ RequestPath: '' }, { RequestTimeWindow: '' }] },
error: 'rateLimit[0].RequestPath, rateLimit[1].RequestTimeWindow',
},
];
tests.forEach(test => {
it(test.name, async () => {
const logger = require('../lib/logger').logger;
spyOn(logger, 'error').and.callThrough();
spyOn(Config, 'validateOptions').and.callFake(() => {});
new ParseServer({
...defaultConfiguration,
...test.options,
});
expect(logger.error).toHaveBeenCalledWith(`Invalid Option Keys Found: ${test.error}`);
});
});
it('should run fine', async () => {
try {
await reconfigureServer({
...defaultConfiguration,
});
} catch (err) {
fail('Should run without error');
}
});
});

View File

@@ -9548,6 +9548,71 @@ describe('ParseGraphQLServer', () => {
} }
}); });
it('should support files on required file', async () => {
try {
parseServer = await global.reconfigureServer({
publicServerURL: 'http://localhost:13377/parse',
});
const schemaController = await parseServer.config.databaseController.loadSchema();
await schemaController.addClassIfNotExists('SomeClassWithRequiredFile', {
someField: { type: 'File', required: true },
});
await resetGraphQLCache();
await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear();
const body = new FormData();
body.append(
'operations',
JSON.stringify({
query: `
mutation CreateSomeObject(
$fields: CreateSomeClassWithRequiredFileFieldsInput
) {
createSomeClassWithRequiredFile(
input: { fields: $fields }
) {
someClassWithRequiredFile {
id
someField {
name
url
}
}
}
}
`,
variables: {
fields: {
someField: { upload: null },
},
},
})
);
body.append('map', JSON.stringify({ 1: ['variables.fields.someField.upload'] }));
body.append('1', 'My File Content', {
filename: 'myFileName.txt',
contentType: 'text/plain',
});
const res = await fetch('http://localhost:13377/graphql', {
method: 'POST',
headers,
body,
});
expect(res.status).toEqual(200);
const resText = await res.text();
const result = JSON.parse(resText);
expect(
result.data.createSomeClassWithRequiredFile.someClassWithRequiredFile.someField.name
).toEqual(jasmine.stringMatching(/_myFileName.txt$/));
expect(
result.data.createSomeClassWithRequiredFile.someClassWithRequiredFile.someField.url
).toEqual(jasmine.stringMatching(/_myFileName.txt$/));
} catch (e) {
handleError(e);
}
});
it('should support file upload for on fly creation through pointer and relation', async () => { it('should support file upload for on fly creation through pointer and relation', async () => {
parseServer = await global.reconfigureServer({ parseServer = await global.reconfigureServer({
publicServerURL: 'http://localhost:13377/parse', publicServerURL: 'http://localhost:13377/parse',

View File

@@ -1269,4 +1269,33 @@ describe('ParseLiveQuery', function () {
expect(object2.id).toBeDefined(); expect(object2.id).toBeDefined();
expect(object3.id).toBeDefined(); expect(object3.id).toBeDefined();
}); });
it('triggers query event with constraint not equal to null', async () => {
await reconfigureServer({
liveQuery: {
classNames: ['TestObject'],
},
startLiveQueryServer: true,
verbose: false,
silent: true,
});
const spy = {
create(obj) {
expect(obj.attributes.foo).toEqual('bar');
},
};
const createSpy = spyOn(spy, 'create');
const query = new Parse.Query(TestObject);
query.notEqualTo('foo', null);
const subscription = await query.subscribe();
subscription.on('create', spy.create);
const object1 = new TestObject();
object1.set('foo', 'bar');
await object1.save();
await new Promise(resolve => setTimeout(resolve, 100));
expect(createSpy).toHaveBeenCalledTimes(1);
});
}); });

View File

@@ -631,4 +631,58 @@ describe('ParseServerRESTController', () => {
expect(sessions[0].get('installationId')).toBe(installationId); expect(sessions[0].get('installationId')).toBe(installationId);
expect(sessions[0].get('sessionToken')).toBe(loggedUser.sessionToken); expect(sessions[0].get('sessionToken')).toBe(loggedUser.sessionToken);
}); });
it('returns a statusId when running jobs', async () => {
Parse.Cloud.job('CloudJob', () => {
return 'Cloud job completed';
});
const res = await RESTController.request(
'POST',
'/jobs/CloudJob',
{},
{ useMasterKey: true, returnStatus: true }
);
const jobStatusId = res._headers['X-Parse-Job-Status-Id'];
expect(jobStatusId).toBeDefined();
const result = await Parse.Cloud.getJobStatus(jobStatusId);
expect(result.id).toBe(jobStatusId);
});
it('returns a statusId when running push notifications', async () => {
const payload = {
data: { alert: 'We return status!' },
where: { deviceType: 'ios' },
};
const res = await RESTController.request('POST', '/push', payload, {
useMasterKey: true,
returnStatus: true,
});
const pushStatusId = res._headers['X-Parse-Push-Status-Id'];
expect(pushStatusId).toBeDefined();
const result = await Parse.Push.getPushStatus(pushStatusId);
expect(result.id).toBe(pushStatusId);
});
it('returns a statusId when running batch push notifications', async () => {
const payload = {
data: { alert: 'We return status!' },
where: { deviceType: 'ios' },
};
const res = await RESTController.request('POST', 'batch', {
requests: [{
method: 'POST',
path: '/push',
body: payload,
}],
}, {
useMasterKey: true,
returnStatus: true,
});
const pushStatusId = res[0]._headers['X-Parse-Push-Status-Id'];
expect(pushStatusId).toBeDefined();
const result = await Parse.Push.getPushStatus(pushStatusId);
expect(result.id).toBe(pushStatusId);
});
}); });

View File

@@ -337,5 +337,33 @@ describe('Security Check', () => {
expect(logSpy.calls.all()[0].args[0]).toContain(title); expect(logSpy.calls.all()[0].args[0]).toContain(title);
} }
}); });
it('does update featuresRouter', async () => {
let response = await request({
url: 'http://localhost:8378/1/serverInfo',
json: true,
headers: {
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
'X-Parse-Master-Key': 'test',
},
});
expect(response.data.features.settings.securityCheck).toBeTrue();
await reconfigureServer({
security: {
enableCheck: false,
},
});
response = await request({
url: 'http://localhost:8378/1/serverInfo',
json: true,
headers: {
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
'X-Parse-Master-Key': 'test',
},
});
expect(response.data.features.settings.securityCheck).toBeFalse();
});
}); });
}); });

View File

@@ -35,6 +35,7 @@ process.noDeprecation = true;
const cache = require('../lib/cache').default; const cache = require('../lib/cache').default;
const defaults = require('../lib/defaults').default; const defaults = require('../lib/defaults').default;
const ParseServer = require('../lib/index').ParseServer; const ParseServer = require('../lib/index').ParseServer;
const loadAdapter = require('../lib/Adapters/AdapterLoader').loadAdapter;
const path = require('path'); const path = require('path');
const TestUtils = require('../lib/TestUtils'); const TestUtils = require('../lib/TestUtils');
const GridFSBucketAdapter = require('../lib/Adapters/Files/GridFSBucketAdapter') const GridFSBucketAdapter = require('../lib/Adapters/Files/GridFSBucketAdapter')
@@ -53,7 +54,10 @@ let databaseAdapter;
let databaseURI; let databaseURI;
// need to bind for mocking mocha // need to bind for mocking mocha
if (process.env.PARSE_SERVER_TEST_DB === 'postgres') { if (process.env.PARSE_SERVER_DATABASE_ADAPTER) {
databaseAdapter = JSON.parse(process.env.PARSE_SERVER_DATABASE_ADAPTER);
databaseAdapter = loadAdapter(databaseAdapter);
} else if (process.env.PARSE_SERVER_TEST_DB === 'postgres') {
databaseURI = process.env.PARSE_SERVER_TEST_DATABASE_URI || postgresURI; databaseURI = process.env.PARSE_SERVER_TEST_DATABASE_URI || postgresURI;
databaseAdapter = new PostgresStorageAdapter({ databaseAdapter = new PostgresStorageAdapter({
uri: databaseURI, uri: databaseURI,
@@ -132,6 +136,16 @@ const defaultConfiguration = {
allowClientClassCreation: true, allowClientClassCreation: true,
}; };
if (silent) {
defaultConfiguration.logLevels = {
cloudFunctionSuccess: 'silent',
cloudFunctionError: 'silent',
triggerAfter: 'silent',
triggerBeforeError: 'silent',
triggerBeforeSuccess: 'silent',
};
}
if (process.env.PARSE_SERVER_TEST_CACHE === 'redis') { if (process.env.PARSE_SERVER_TEST_CACHE === 'redis') {
defaultConfiguration.cacheAdapter = new RedisCacheAdapter(); defaultConfiguration.cacheAdapter = new RedisCacheAdapter();
} }
@@ -434,8 +448,8 @@ try {
// Fetch test exclusion list // Fetch test exclusion list
testExclusionList = require('./testExclusionList.json'); testExclusionList = require('./testExclusionList.json');
console.log(`Using test exclusion list with ${testExclusionList.length} entries`); console.log(`Using test exclusion list with ${testExclusionList.length} entries`);
} catch(error) { } catch (error) {
if(error.code !== 'MODULE_NOT_FOUND') { if (error.code !== 'MODULE_NOT_FOUND') {
throw error; throw error;
} }
} }
@@ -445,10 +459,7 @@ global.it_id = (id, func) => {
if (testExclusionList.includes(id)) { if (testExclusionList.includes(id)) {
return xit; return xit;
} else { } else {
if(func === undefined) return func || it;
return it;
else
return func;
} }
}; };

View File

@@ -0,0 +1,9 @@
module.exports = function (options) {
return {
options: options,
send: function () {},
getDatabaseURI: function () {
return options.databaseURI;
},
};
};

View File

@@ -6,7 +6,7 @@ const jwt = require('jsonwebtoken');
const httpsRequest = require('./httpsRequest'); const httpsRequest = require('./httpsRequest');
const authUtils = require('./utils'); const authUtils = require('./utils');
const TOKEN_ISSUER = 'https://facebook.com'; const TOKEN_ISSUER = 'https://www.facebook.com';
function getAppSecretPath(authData, options = {}) { function getAppSecretPath(authData, options = {}) {
const appSecret = options.appSecret; const appSecret = options.appSecret;

View File

@@ -2614,16 +2614,16 @@ function isAnyValueRegexStartsWith(values) {
}); });
} }
function createLiteralRegex(remaining) { function createLiteralRegex(remaining: string) {
return remaining return remaining
.split('') .split('')
.map(c => { .map(c => {
const regex = RegExp('[0-9 ]|\\p{L}', 'u'); // Support all unicode letter chars const regex = RegExp('[0-9 ]|\\p{L}', 'u'); // Support all Unicode letter chars
if (c.match(regex) !== null) { if (c.match(regex) !== null) {
// don't escape alphanumeric characters // Don't escape alphanumeric characters
return c; return c;
} }
// escape everything else (single quotes with single quotes, everything else with a backslash) // Escape everything else (single quotes with single quotes, everything else with a backslash)
return c === `'` ? `''` : `\\${c}`; return c === `'` ? `''` : `\\${c}`;
}) })
.join(''); .join('');
@@ -2633,14 +2633,14 @@ function literalizeRegexPart(s: string) {
const matcher1 = /\\Q((?!\\E).*)\\E$/; const matcher1 = /\\Q((?!\\E).*)\\E$/;
const result1: any = s.match(matcher1); const result1: any = s.match(matcher1);
if (result1 && result1.length > 1 && result1.index > -1) { if (result1 && result1.length > 1 && result1.index > -1) {
// process regex that has a beginning and an end specified for the literal text // Process Regex that has a beginning and an end specified for the literal text
const prefix = s.substring(0, result1.index); const prefix = s.substring(0, result1.index);
const remaining = result1[1]; const remaining = result1[1];
return literalizeRegexPart(prefix) + createLiteralRegex(remaining); return literalizeRegexPart(prefix) + createLiteralRegex(remaining);
} }
// process regex that has a beginning specified for the literal text // Process Regex that has a beginning specified for the literal text
const matcher2 = /\\Q((?!\\E).*)$/; const matcher2 = /\\Q((?!\\E).*)$/;
const result2: any = s.match(matcher2); const result2: any = s.match(matcher2);
if (result2 && result2.length > 1 && result2.index > -1) { if (result2 && result2.length > 1 && result2.index > -1) {
@@ -2650,14 +2650,18 @@ function literalizeRegexPart(s: string) {
return literalizeRegexPart(prefix) + createLiteralRegex(remaining); return literalizeRegexPart(prefix) + createLiteralRegex(remaining);
} }
// remove all instances of \Q and \E from the remaining text & escape single quotes // Remove problematic chars from remaining text
return s return s
// Remove all instances of \Q and \E
.replace(/([^\\])(\\E)/, '$1') .replace(/([^\\])(\\E)/, '$1')
.replace(/([^\\])(\\Q)/, '$1') .replace(/([^\\])(\\Q)/, '$1')
.replace(/^\\E/, '') .replace(/^\\E/, '')
.replace(/^\\Q/, '') .replace(/^\\Q/, '')
.replace(/([^'])'/g, `$1''`) // Ensure even number of single quote sequences by adding an extra single quote if needed;
.replace(/^'([^'])/, `''$1`); // this ensures that every single quote is escaped
.replace(/'+/g, match => {
return match.length % 2 === 0 ? match : match + "'";
});
} }
var GeoPointCoder = { var GeoPointCoder = {

View File

@@ -67,6 +67,17 @@ function nobody(config) {
return new Auth({ config, isMaster: false }); return new Auth({ config, isMaster: false });
} }
/**
* Checks whether session should be updated based on last update time & session length.
*/
function shouldUpdateSessionExpiry(config, session) {
const resetAfter = config.sessionLength / 2;
const lastUpdated = new Date(session?.updatedAt);
const skipRange = new Date();
skipRange.setTime(skipRange.getTime() - resetAfter * 1000);
return lastUpdated <= skipRange;
}
const throttle = {}; const throttle = {};
const renewSessionIfNeeded = async ({ config, session, sessionToken }) => { const renewSessionIfNeeded = async ({ config, session, sessionToken }) => {
if (!config?.extendSessionOnUse) { if (!config?.extendSessionOnUse) {
@@ -88,10 +99,7 @@ const renewSessionIfNeeded = async ({ config, session, sessionToken }) => {
const { results } = await query.execute(); const { results } = await query.execute();
session = results[0]; session = results[0];
} }
const lastUpdated = new Date(session?.updatedAt); if (!shouldUpdateSessionExpiry(config, session) || !session) {
const yesterday = new Date();
yesterday.setDate(yesterday.getDate() - 1);
if (lastUpdated > yesterday || !session) {
return; return;
} }
const expiresAt = config.generateSessionExpiresAt(); const expiresAt = config.generateSessionExpiresAt();
@@ -579,6 +587,7 @@ module.exports = {
maintenance, maintenance,
nobody, nobody,
readOnly, readOnly,
shouldUpdateSessionExpiry,
getAuthForSessionToken, getAuthForSessionToken,
getAuthForLegacySessionToken, getAuthForLegacySessionToken,
findUsersWithAuthData, findUsersWithAuthData,

View File

@@ -64,6 +64,7 @@ export class Config {
} }
static validateOptions({ static validateOptions({
customPages,
publicServerURL, publicServerURL,
revokeSessionOnPasswordReset, revokeSessionOnPasswordReset,
expireInactiveSessions, expireInactiveSessions,
@@ -133,9 +134,18 @@ export class Config {
this.validateRateLimit(rateLimit); this.validateRateLimit(rateLimit);
this.validateLogLevels(logLevels); this.validateLogLevels(logLevels);
this.validateDatabaseOptions(databaseOptions); this.validateDatabaseOptions(databaseOptions);
this.validateCustomPages(customPages);
this.validateAllowClientClassCreation(allowClientClassCreation); this.validateAllowClientClassCreation(allowClientClassCreation);
} }
static validateCustomPages(customPages) {
if (!customPages) return;
if (Object.prototype.toString.call(customPages) !== '[object Object]') {
throw Error('Parse Server option customPages must be an object.');
}
}
static validateControllers({ static validateControllers({
verifyUserEmails, verifyUserEmails,
userController, userController,
@@ -569,6 +579,7 @@ export class Config {
if (Object.prototype.toString.call(databaseOptions) !== '[object Object]') { if (Object.prototype.toString.call(databaseOptions) !== '[object Object]') {
throw `databaseOptions must be an object`; throw `databaseOptions must be an object`;
} }
if (databaseOptions.enableSchemaHooks === undefined) { if (databaseOptions.enableSchemaHooks === undefined) {
databaseOptions.enableSchemaHooks = DatabaseOptions.enableSchemaHooks.default; databaseOptions.enableSchemaHooks = DatabaseOptions.enableSchemaHooks.default;
} else if (typeof databaseOptions.enableSchemaHooks !== 'boolean') { } else if (typeof databaseOptions.enableSchemaHooks !== 'boolean') {

View File

@@ -16,7 +16,7 @@ export const LogOrder = {
ASCENDING: 'asc', ASCENDING: 'asc',
}; };
export const logLevels = ['error', 'warn', 'info', 'debug', 'verbose', 'silly']; export const logLevels = ['error', 'warn', 'info', 'debug', 'verbose', 'silly', 'silent'];
export class LoggerController extends AdaptableController { export class LoggerController extends AdaptableController {
constructor(adapter, appId, options = { logLevel: 'info' }) { constructor(adapter, appId, options = { logLevel: 'info' }) {

View File

@@ -15,6 +15,4 @@
* *
* If there are no deprecations, this must return an empty array. * If there are no deprecations, this must return an empty array.
*/ */
module.exports = [ module.exports = [{ optionKey: 'encodeParseObjectInCloudFunction', changeNewDefault: 'true' }];
{ optionKey: 'encodeParseObjectInCloudFunction', changeNewDefault: 'true' },
];

View File

@@ -1,7 +1,6 @@
import Parse from 'parse/node'; import Parse from 'parse/node';
import { fromGlobalId } from 'graphql-relay'; import { fromGlobalId } from 'graphql-relay';
import { handleUpload } from '../loaders/filesMutations'; import { handleUpload } from '../loaders/filesMutations';
import * as defaultGraphQLTypes from '../loaders/defaultGraphQLTypes';
import * as objectsMutations from '../helpers/objectsMutations'; import * as objectsMutations from '../helpers/objectsMutations';
const transformTypes = async ( const transformTypes = async (
@@ -28,27 +27,28 @@ const transformTypes = async (
inputTypeField = classGraphQLUpdateTypeFields[field]; inputTypeField = classGraphQLUpdateTypeFields[field];
} }
if (inputTypeField) { if (inputTypeField) {
switch (true) { const parseFieldType = parseClass.fields[field].type;
case inputTypeField.type === defaultGraphQLTypes.GEO_POINT_INPUT: switch (parseFieldType) {
case 'GeoPoint':
if (fields[field] === null) { if (fields[field] === null) {
fields[field] = { __op: 'Delete' }; fields[field] = { __op: 'Delete' };
break; break;
} }
fields[field] = transformers.geoPoint(fields[field]); fields[field] = transformers.geoPoint(fields[field]);
break; break;
case inputTypeField.type === defaultGraphQLTypes.POLYGON_INPUT: case 'Polygon':
if (fields[field] === null) { if (fields[field] === null) {
fields[field] = { __op: 'Delete' }; fields[field] = { __op: 'Delete' };
break; break;
} }
fields[field] = transformers.polygon(fields[field]); fields[field] = transformers.polygon(fields[field]);
break; break;
case inputTypeField.type === defaultGraphQLTypes.FILE_INPUT: case 'File':
// Use `originalFields` to handle file upload since fields are a deepcopy and do not // We need to use the originalFields to handle the file upload
// keep the file object // since fields are a deepcopy and do not keep the file object
fields[field] = await transformers.file(originalFields[field], req); fields[field] = await transformers.file(originalFields[field], req);
break; break;
case parseClass.fields[field].type === 'Relation': case 'Relation':
fields[field] = await transformers.relation( fields[field] = await transformers.relation(
parseClass.fields[field].targetClass, parseClass.fields[field].targetClass,
field, field,
@@ -58,7 +58,7 @@ const transformTypes = async (
req req
); );
break; break;
case parseClass.fields[field].type === 'Pointer': case 'Pointer':
if (fields[field] === null) { if (fields[field] === null) {
fields[field] = { __op: 'Delete' }; fields[field] = { __op: 'Delete' };
break; break;

View File

@@ -223,7 +223,7 @@ function matchesKeyConstraints(object, key, constraints) {
// More complex cases // More complex cases
for (var condition in constraints) { for (var condition in constraints) {
compareTo = constraints[condition]; compareTo = constraints[condition];
if (compareTo.__type) { if (compareTo?.__type) {
compareTo = Parse._decode(key, compareTo); compareTo = Parse._decode(key, compareTo);
} }
switch (condition) { switch (condition) {

View File

@@ -54,6 +54,7 @@ module.exports.ParseServerOptions = {
env: 'PARSE_SERVER_ACCOUNT_LOCKOUT', env: 'PARSE_SERVER_ACCOUNT_LOCKOUT',
help: 'The account lockout policy for failed login attempts.', help: 'The account lockout policy for failed login attempts.',
action: parsers.objectParser, action: parsers.objectParser,
type: 'AccountLockoutOptions',
}, },
allowClientClassCreation: { allowClientClassCreation: {
env: 'PARSE_SERVER_ALLOW_CLIENT_CLASS_CREATION', env: 'PARSE_SERVER_ALLOW_CLIENT_CLASS_CREATION',
@@ -157,6 +158,7 @@ module.exports.ParseServerOptions = {
env: 'PARSE_SERVER_CUSTOM_PAGES', env: 'PARSE_SERVER_CUSTOM_PAGES',
help: 'custom pages for password validation and reset', help: 'custom pages for password validation and reset',
action: parsers.objectParser, action: parsers.objectParser,
type: 'CustomPagesOptions',
default: {}, default: {},
}, },
databaseAdapter: { databaseAdapter: {
@@ -169,6 +171,7 @@ module.exports.ParseServerOptions = {
env: 'PARSE_SERVER_DATABASE_OPTIONS', env: 'PARSE_SERVER_DATABASE_OPTIONS',
help: 'Options to pass to the database client', help: 'Options to pass to the database client',
action: parsers.objectParser, action: parsers.objectParser,
type: 'DatabaseOptions',
}, },
databaseURI: { databaseURI: {
env: 'PARSE_SERVER_DATABASE_URI', env: 'PARSE_SERVER_DATABASE_URI',
@@ -256,7 +259,8 @@ module.exports.ParseServerOptions = {
}, },
extendSessionOnUse: { extendSessionOnUse: {
env: 'PARSE_SERVER_EXTEND_SESSION_ON_USE', env: 'PARSE_SERVER_EXTEND_SESSION_ON_USE',
help: 'Whether Parse Server should automatically extend a valid session by the sessionLength', help:
"Whether Parse Server should automatically extend a valid session by the sessionLength. In order to reduce the number of session updates in the database, a session will only be extended when a request is received after at least half of the current session's lifetime has passed.",
action: parsers.booleanParser, action: parsers.booleanParser,
default: false, default: false,
}, },
@@ -273,6 +277,7 @@ module.exports.ParseServerOptions = {
env: 'PARSE_SERVER_FILE_UPLOAD_OPTIONS', env: 'PARSE_SERVER_FILE_UPLOAD_OPTIONS',
help: 'Options for file uploads', help: 'Options for file uploads',
action: parsers.objectParser, action: parsers.objectParser,
type: 'FileUploadOptions',
default: {}, default: {},
}, },
graphQLPath: { graphQLPath: {
@@ -294,6 +299,7 @@ module.exports.ParseServerOptions = {
help: help:
'Options for request idempotency to deduplicate identical requests that may be caused by network issues. Caution, this is an experimental feature that may not be appropriate for production.', 'Options for request idempotency to deduplicate identical requests that may be caused by network issues. Caution, this is an experimental feature that may not be appropriate for production.',
action: parsers.objectParser, action: parsers.objectParser,
type: 'IdempotencyOptions',
default: {}, default: {},
}, },
javascriptKey: { javascriptKey: {
@@ -309,11 +315,13 @@ module.exports.ParseServerOptions = {
env: 'PARSE_SERVER_LIVE_QUERY', env: 'PARSE_SERVER_LIVE_QUERY',
help: "parse-server's LiveQuery configuration object", help: "parse-server's LiveQuery configuration object",
action: parsers.objectParser, action: parsers.objectParser,
type: 'LiveQueryOptions',
}, },
liveQueryServerOptions: { liveQueryServerOptions: {
env: 'PARSE_SERVER_LIVE_QUERY_SERVER_OPTIONS', env: 'PARSE_SERVER_LIVE_QUERY_SERVER_OPTIONS',
help: 'Live query server configuration options (will start the liveQuery server)', help: 'Live query server configuration options (will start the liveQuery server)',
action: parsers.objectParser, action: parsers.objectParser,
type: 'LiveQueryServerOptions',
}, },
loggerAdapter: { loggerAdapter: {
env: 'PARSE_SERVER_LOGGER_ADAPTER', env: 'PARSE_SERVER_LOGGER_ADAPTER',
@@ -328,6 +336,7 @@ module.exports.ParseServerOptions = {
env: 'PARSE_SERVER_LOG_LEVELS', env: 'PARSE_SERVER_LOG_LEVELS',
help: '(Optional) Overrides the log levels used internally by Parse Server to log events.', help: '(Optional) Overrides the log levels used internally by Parse Server to log events.',
action: parsers.objectParser, action: parsers.objectParser,
type: 'LogLevels',
default: {}, default: {},
}, },
logsFolder: { logsFolder: {
@@ -408,12 +417,14 @@ module.exports.ParseServerOptions = {
help: help:
'The options for pages such as password reset and email verification. Caution, this is an experimental feature that may not be appropriate for production.', 'The options for pages such as password reset and email verification. Caution, this is an experimental feature that may not be appropriate for production.',
action: parsers.objectParser, action: parsers.objectParser,
type: 'PagesOptions',
default: {}, default: {},
}, },
passwordPolicy: { passwordPolicy: {
env: 'PARSE_SERVER_PASSWORD_POLICY', env: 'PARSE_SERVER_PASSWORD_POLICY',
help: 'The password policy for enforcing password related rules.', help: 'The password policy for enforcing password related rules.',
action: parsers.objectParser, action: parsers.objectParser,
type: 'PasswordPolicyOptions',
}, },
playgroundPath: { playgroundPath: {
env: 'PARSE_SERVER_PLAYGROUND_PATH', env: 'PARSE_SERVER_PLAYGROUND_PATH',
@@ -471,6 +482,7 @@ module.exports.ParseServerOptions = {
help: help:
"Options to limit repeated requests to Parse Server APIs. This can be used to protect sensitive endpoints such as `/requestPasswordReset` from brute-force attacks or Parse Server as a whole from denial-of-service (DoS) attacks.<br><br>\u2139\uFE0F Mind the following limitations:<br>- rate limits applied per IP address; this limits protection against distributed denial-of-service (DDoS) attacks where many requests are coming from various IP addresses<br>- if multiple Parse Server instances are behind a load balancer or ran in a cluster, each instance will calculate it's own request rates, independent from other instances; this limits the applicability of this feature when using a load balancer and another rate limiting solution that takes requests across all instances into account may be more suitable<br>- this feature provides basic protection against denial-of-service attacks, but a more sophisticated solution works earlier in the request flow and prevents a malicious requests to even reach a server instance; it's therefore recommended to implement a solution according to architecture and user case.", "Options to limit repeated requests to Parse Server APIs. This can be used to protect sensitive endpoints such as `/requestPasswordReset` from brute-force attacks or Parse Server as a whole from denial-of-service (DoS) attacks.<br><br>\u2139\uFE0F Mind the following limitations:<br>- rate limits applied per IP address; this limits protection against distributed denial-of-service (DDoS) attacks where many requests are coming from various IP addresses<br>- if multiple Parse Server instances are behind a load balancer or ran in a cluster, each instance will calculate it's own request rates, independent from other instances; this limits the applicability of this feature when using a load balancer and another rate limiting solution that takes requests across all instances into account may be more suitable<br>- this feature provides basic protection against denial-of-service attacks, but a more sophisticated solution works earlier in the request flow and prevents a malicious requests to even reach a server instance; it's therefore recommended to implement a solution according to architecture and user case.",
action: parsers.arrayParser, action: parsers.arrayParser,
type: 'RateLimitOptions[]',
default: [], default: [],
}, },
readOnlyMasterKey: { readOnlyMasterKey: {
@@ -516,11 +528,13 @@ module.exports.ParseServerOptions = {
env: 'PARSE_SERVER_SCHEMA', env: 'PARSE_SERVER_SCHEMA',
help: 'Defined schema', help: 'Defined schema',
action: parsers.objectParser, action: parsers.objectParser,
type: 'SchemaOptions',
}, },
security: { security: {
env: 'PARSE_SERVER_SECURITY', env: 'PARSE_SERVER_SECURITY',
help: 'The security options to identify and report weak security settings.', help: 'The security options to identify and report weak security settings.',
action: parsers.objectParser, action: parsers.objectParser,
type: 'SecurityOptions',
default: {}, default: {},
}, },
sendUserEmailVerification: { sendUserEmailVerification: {
@@ -665,12 +679,14 @@ module.exports.PagesOptions = {
env: 'PARSE_SERVER_PAGES_CUSTOM_ROUTES', env: 'PARSE_SERVER_PAGES_CUSTOM_ROUTES',
help: 'The custom routes.', help: 'The custom routes.',
action: parsers.arrayParser, action: parsers.arrayParser,
type: 'PagesRoute[]',
default: [], default: [],
}, },
customUrls: { customUrls: {
env: 'PARSE_SERVER_PAGES_CUSTOM_URLS', env: 'PARSE_SERVER_PAGES_CUSTOM_URLS',
help: 'The URLs to the custom pages.', help: 'The URLs to the custom pages.',
action: parsers.objectParser, action: parsers.objectParser,
type: 'PagesCustomUrlsOptions',
default: {}, default: {},
}, },
enableLocalization: { enableLocalization: {

View File

@@ -47,7 +47,7 @@
* @property {String} encryptionKey Key for encrypting your files * @property {String} encryptionKey Key for encrypting your files
* @property {Boolean} enforcePrivateUsers Set to true if new users should be created without public read and write access. * @property {Boolean} enforcePrivateUsers Set to true if new users should be created without public read and write access.
* @property {Boolean} expireInactiveSessions Sets whether we should expire the inactive sessions, defaults to true. If false, all new sessions are created with no expiration date. * @property {Boolean} expireInactiveSessions Sets whether we should expire the inactive sessions, defaults to true. If false, all new sessions are created with no expiration date.
* @property {Boolean} extendSessionOnUse Whether Parse Server should automatically extend a valid session by the sessionLength * @property {Boolean} extendSessionOnUse Whether Parse Server should automatically extend a valid session by the sessionLength. In order to reduce the number of session updates in the database, a session will only be extended when a request is received after at least half of the current session's lifetime has passed.
* @property {String} fileKey Key for your files * @property {String} fileKey Key for your files
* @property {Adapter<FilesAdapter>} filesAdapter Adapter module for the files sub-system * @property {Adapter<FilesAdapter>} filesAdapter Adapter module for the files sub-system
* @property {FileUploadOptions} fileUpload Options for file uploads * @property {FileUploadOptions} fileUpload Options for file uploads

View File

@@ -228,7 +228,7 @@ export interface ParseServerOptions {
/* Session duration, in seconds, defaults to 1 year /* Session duration, in seconds, defaults to 1 year
:DEFAULT: 31536000 */ :DEFAULT: 31536000 */
sessionLength: ?number; sessionLength: ?number;
/* Whether Parse Server should automatically extend a valid session by the sessionLength /* Whether Parse Server should automatically extend a valid session by the sessionLength. In order to reduce the number of session updates in the database, a session will only be extended when a request is received after at least half of the current session's lifetime has passed.
:DEFAULT: false */ :DEFAULT: false */
extendSessionOnUse: ?boolean; extendSessionOnUse: ?boolean;
/* Default value for limit option on queries, defaults to `100`. /* Default value for limit option on queries, defaults to `100`.

View File

@@ -45,6 +45,7 @@ import { SecurityRouter } from './Routers/SecurityRouter';
import CheckRunner from './Security/CheckRunner'; import CheckRunner from './Security/CheckRunner';
import Deprecator from './Deprecator/Deprecator'; import Deprecator from './Deprecator/Deprecator';
import { DefinedSchemas } from './SchemaMigrations/DefinedSchemas'; import { DefinedSchemas } from './SchemaMigrations/DefinedSchemas';
import OptionsDefinitions from './Options/Definitions';
// Mutate the Parse object to add the Cloud Code handlers // Mutate the Parse object to add the Cloud Code handlers
addParseCloud(); addParseCloud();
@@ -59,6 +60,58 @@ class ParseServer {
constructor(options: ParseServerOptions) { constructor(options: ParseServerOptions) {
// Scan for deprecated Parse Server options // Scan for deprecated Parse Server options
Deprecator.scanParseServerOptions(options); Deprecator.scanParseServerOptions(options);
const interfaces = JSON.parse(JSON.stringify(OptionsDefinitions));
function getValidObject(root) {
const result = {};
for (const key in root) {
if (Object.prototype.hasOwnProperty.call(root[key], 'type')) {
if (root[key].type.endsWith('[]')) {
result[key] = [getValidObject(interfaces[root[key].type.slice(0, -2)])];
} else {
result[key] = getValidObject(interfaces[root[key].type]);
}
} else {
result[key] = '';
}
}
return result;
}
const optionsBlueprint = getValidObject(interfaces['ParseServerOptions']);
function validateKeyNames(original, ref, name = '') {
let result = [];
const prefix = name + (name !== '' ? '.' : '');
for (const key in original) {
if (!Object.prototype.hasOwnProperty.call(ref, key)) {
result.push(prefix + key);
} else {
if (ref[key] === '') continue;
let res = [];
if (Array.isArray(original[key]) && Array.isArray(ref[key])) {
const type = ref[key][0];
original[key].forEach((item, idx) => {
if (typeof item === 'object' && item !== null) {
res = res.concat(validateKeyNames(item, type, prefix + key + `[${idx}]`));
}
});
} else if (typeof original[key] === 'object' && typeof ref[key] === 'object') {
res = validateKeyNames(original[key], ref[key], prefix + key);
}
result = result.concat(res);
}
}
return result;
}
const diff = validateKeyNames(options, optionsBlueprint);
if (diff.length > 0) {
const logger = logging.logger;
logger.error(`Invalid Option Keys Found: ${diff.join(', ')}`);
}
// Set option defaults // Set option defaults
injectDefaults(options); injectDefaults(options);
const { const {
@@ -70,9 +123,9 @@ class ParseServer {
// Initialize the node client SDK automatically // Initialize the node client SDK automatically
Parse.initialize(appId, javascriptKey || 'unused', masterKey); Parse.initialize(appId, javascriptKey || 'unused', masterKey);
Parse.serverURL = serverURL; Parse.serverURL = serverURL;
Config.validateOptions(options); Config.validateOptions(options);
const allControllers = controllers.getControllers(options); const allControllers = controllers.getControllers(options);
options.state = 'initialized'; options.state = 'initialized';
this.config = Config.put(Object.assign({}, options, allControllers)); this.config = Config.put(Object.assign({}, options, allControllers));
this.config.masterKeyIpsStore = new Map(); this.config.masterKeyIpsStore = new Map();

View File

@@ -58,8 +58,10 @@ function ParseServerRESTController(applicationId, router) {
response => { response => {
if (options.returnStatus) { if (options.returnStatus) {
const status = response._status; const status = response._status;
const headers = response._headers;
delete response._status; delete response._status;
return { success: response, _status: status }; delete response._headers;
return { success: response, _status: status, _headers: headers };
} }
return { success: response }; return { success: response };
}, },
@@ -128,9 +130,9 @@ function ParseServerRESTController(applicationId, router) {
}) })
.then( .then(
resp => { resp => {
const { response, status } = resp; const { response, status, headers = {} } = resp;
if (options.returnStatus) { if (options.returnStatus) {
resolve({ ...response, _status: status }); resolve({ ...response, _status: status, _headers: headers });
} else { } else {
resolve(response); resolve(response);
} }

View File

@@ -523,10 +523,14 @@ RestWrite.prototype.handleAuthData = async function (authData) {
const r = await Auth.findUsersWithAuthData(this.config, authData); const r = await Auth.findUsersWithAuthData(this.config, authData);
const results = this.filteredObjectsByACL(r); const results = this.filteredObjectsByACL(r);
if (results.length > 1) { const userId = this.getUserId();
const userResult = results[0];
const foundUserIsNotCurrentUser = userId && userResult && userId !== userResult.objectId;
if (results.length > 1 || foundUserIsNotCurrentUser) {
// To avoid https://github.com/parse-community/parse-server/security/advisories/GHSA-8w3j-g983-8jh5 // To avoid https://github.com/parse-community/parse-server/security/advisories/GHSA-8w3j-g983-8jh5
// Let's run some validation before throwing // Let's run some validation before throwing
await Auth.handleAuthDataValidation(authData, this, results[0]); await Auth.handleAuthDataValidation(authData, this, userResult);
throw new Parse.Error(Parse.Error.ACCOUNT_ALREADY_LINKED, 'this auth is already used'); throw new Parse.Error(Parse.Error.ACCOUNT_ALREADY_LINKED, 'this auth is already used');
} }
@@ -544,13 +548,6 @@ RestWrite.prototype.handleAuthData = async function (authData) {
// User found with provided authData // User found with provided authData
if (results.length === 1) { if (results.length === 1) {
const userId = this.getUserId();
const userResult = results[0];
// Prevent duplicate authData id
if (userId && userId !== userResult.objectId) {
await Auth.handleAuthDataValidation(authData, this, results[0]);
throw new Parse.Error(Parse.Error.ACCOUNT_ALREADY_LINKED, 'this auth is already used');
}
this.storage.authProvider = Object.keys(authData).join(','); this.storage.authProvider = Object.keys(authData).join(',');

View File

@@ -46,6 +46,9 @@ export class FeaturesRouter extends PromiseRouter {
editClassLevelPermissions: true, editClassLevelPermissions: true,
editPointerPermissions: true, editPointerPermissions: true,
}, },
settings: {
securityCheck: !!config.security?.enableCheck,
},
}; };
return { return {

View File

@@ -141,10 +141,11 @@ export class FunctionsRouter extends PromiseRouter {
return new Promise(function (resolve, reject) { return new Promise(function (resolve, reject) {
const userString = req.auth && req.auth.user ? req.auth.user.id : undefined; const userString = req.auth && req.auth.user ? req.auth.user.id : undefined;
const cleanInput = logger.truncateLogMessage(JSON.stringify(params));
const { success, error } = FunctionsRouter.createResponseObject( const { success, error } = FunctionsRouter.createResponseObject(
result => { result => {
try { try {
if (req.config.logLevels.cloudFunctionSuccess !== 'silent') {
const cleanInput = logger.truncateLogMessage(JSON.stringify(params));
const cleanResult = logger.truncateLogMessage(JSON.stringify(result.response.result)); const cleanResult = logger.truncateLogMessage(JSON.stringify(result.response.result));
logger[req.config.logLevels.cloudFunctionSuccess]( logger[req.config.logLevels.cloudFunctionSuccess](
`Ran cloud function ${functionName} for user ${userString} with:\n Input: ${cleanInput}\n Result: ${cleanResult}`, `Ran cloud function ${functionName} for user ${userString} with:\n Input: ${cleanInput}\n Result: ${cleanResult}`,
@@ -154,6 +155,7 @@ export class FunctionsRouter extends PromiseRouter {
user: userString, user: userString,
} }
); );
}
resolve(result); resolve(result);
} catch (e) { } catch (e) {
reject(e); reject(e);
@@ -161,6 +163,8 @@ export class FunctionsRouter extends PromiseRouter {
}, },
error => { error => {
try { try {
if (req.config.logLevels.cloudFunctionError !== 'silent') {
const cleanInput = logger.truncateLogMessage(JSON.stringify(params));
logger[req.config.logLevels.cloudFunctionError]( logger[req.config.logLevels.cloudFunctionError](
`Failed running cloud function ${functionName} for user ${userString} with:\n Input: ${cleanInput}\n Error: ` + `Failed running cloud function ${functionName} for user ${userString} with:\n Input: ${cleanInput}\n Error: ` +
JSON.stringify(error), JSON.stringify(error),
@@ -171,6 +175,7 @@ export class FunctionsRouter extends PromiseRouter {
user: userString, user: userString,
} }
); );
}
reject(error); reject(error);
} catch (e) { } catch (e) {
reject(e); reject(e);

View File

@@ -6,6 +6,7 @@ function logStartupOptions(options) {
} }
// Keys that may include sensitive information that will be redacted in logs // Keys that may include sensitive information that will be redacted in logs
const keysToRedact = [ const keysToRedact = [
'databaseAdapter',
'databaseURI', 'databaseURI',
'masterKey', 'masterKey',
'maintenanceKey', 'maintenanceKey',

View File

@@ -531,19 +531,17 @@ export const addRateLimit = (route, config, cloud) => {
const redisStore = { const redisStore = {
connectionPromise: Promise.resolve(), connectionPromise: Promise.resolve(),
store: null, store: null,
connected: false,
}; };
if (route.redisUrl) { if (route.redisUrl) {
const client = createClient({ const client = createClient({
url: route.redisUrl, url: route.redisUrl,
}); });
redisStore.connectionPromise = async () => { redisStore.connectionPromise = async () => {
if (redisStore.connected) { if (client.isOpen) {
return; return;
} }
try { try {
await client.connect(); await client.connect();
redisStore.connected = true;
} catch (e) { } catch (e) {
const log = config?.loggerController || defaultLogger; const log = config?.loggerController || defaultLogger;
log.error(`Could not connect to redisURL in rate limit: ${e}`); log.error(`Could not connect to redisURL in rate limit: ${e}`);

View File

@@ -382,6 +382,9 @@ function userIdForLog(auth) {
} }
function logTriggerAfterHook(triggerType, className, input, auth, logLevel) { function logTriggerAfterHook(triggerType, className, input, auth, logLevel) {
if (logLevel === 'silent') {
return;
}
const cleanInput = logger.truncateLogMessage(JSON.stringify(input)); const cleanInput = logger.truncateLogMessage(JSON.stringify(input));
logger[logLevel]( logger[logLevel](
`${triggerType} triggered for ${className} for user ${userIdForLog( `${triggerType} triggered for ${className} for user ${userIdForLog(
@@ -396,6 +399,9 @@ function logTriggerAfterHook(triggerType, className, input, auth, logLevel) {
} }
function logTriggerSuccessBeforeHook(triggerType, className, input, result, auth, logLevel) { function logTriggerSuccessBeforeHook(triggerType, className, input, result, auth, logLevel) {
if (logLevel === 'silent') {
return;
}
const cleanInput = logger.truncateLogMessage(JSON.stringify(input)); const cleanInput = logger.truncateLogMessage(JSON.stringify(input));
const cleanResult = logger.truncateLogMessage(JSON.stringify(result)); const cleanResult = logger.truncateLogMessage(JSON.stringify(result));
logger[logLevel]( logger[logLevel](
@@ -411,6 +417,9 @@ function logTriggerSuccessBeforeHook(triggerType, className, input, result, auth
} }
function logTriggerErrorBeforeHook(triggerType, className, input, auth, error, logLevel) { function logTriggerErrorBeforeHook(triggerType, className, input, auth, error, logLevel) {
if (logLevel === 'silent') {
return;
}
const cleanInput = logger.truncateLogMessage(JSON.stringify(input)); const cleanInput = logger.truncateLogMessage(JSON.stringify(input));
logger[logLevel]( logger[logLevel](
`${triggerType} failed for ${className} for user ${userIdForLog( `${triggerType} failed for ${className} for user ${userIdForLog(