mirror of
https://github.com/parse-community/parse-server
synced 2026-08-09 13:03:18 +00:00
Compare commits
32
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ff4639ab35 | ||
|
|
686a9f282d | ||
|
|
0bb63d8cc1 | ||
|
|
ef77b993bf | ||
|
|
6458ab072e | ||
|
|
62bb396ab4 | ||
|
|
c7fa3b92a1 | ||
|
|
9c0abe0120 | ||
|
|
601da1ee3c | ||
|
|
e6374e791d | ||
|
|
5fad2928fb | ||
|
|
a0360712c8 | ||
|
|
f5c6b3e8e5 | ||
|
|
733dc29e04 | ||
|
|
e13f7bbb18 | ||
|
|
81d51f3bde | ||
|
|
c83b343aae | ||
|
|
8e83cac02d | ||
|
|
d8bff571ae | ||
|
|
c0a9ff89af | ||
|
|
ac90cb8c35 | ||
|
|
196e05f047 | ||
|
|
e9ae4351a1 | ||
|
|
4f0f0ec4bb | ||
|
|
0ec923937b | ||
|
|
b905137278 | ||
|
|
2c19c2e4d4 | ||
|
|
30576f1091 | ||
|
|
e016d813e0 | ||
|
|
c8bc200729 | ||
|
|
09d04b0aad | ||
|
|
38f64be596 |
@@ -24,5 +24,8 @@
|
||||
"space-infix-ops": "error",
|
||||
"no-useless-escape": "off",
|
||||
"require-atomic-updates": "off"
|
||||
},
|
||||
"globals": {
|
||||
"Parse": true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
name: ci
|
||||
on:
|
||||
push:
|
||||
branches: [ release, alpha, beta ]
|
||||
branches: [ release*, alpha, beta ]
|
||||
pull_request:
|
||||
branches: [ release, alpha, beta ]
|
||||
branches: [ release*, alpha, beta ]
|
||||
env:
|
||||
NODE_VERSION: 18.1.0
|
||||
PARSE_SERVER_TEST_TIMEOUT: 20000
|
||||
@@ -32,7 +32,7 @@ jobs:
|
||||
check-ci:
|
||||
name: Node Engine Check
|
||||
timeout-minutes: 15
|
||||
runs-on: ubuntu-20.04
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Use Node.js ${{ matrix.NODE_VERSION }}
|
||||
@@ -53,7 +53,7 @@ jobs:
|
||||
check-lint:
|
||||
name: Lint
|
||||
timeout-minutes: 15
|
||||
runs-on: ubuntu-20.04
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Use Node.js ${{ matrix.NODE_VERSION }}
|
||||
@@ -73,7 +73,7 @@ jobs:
|
||||
check-circular:
|
||||
name: Circular Dependencies
|
||||
timeout-minutes: 5
|
||||
runs-on: ubuntu-20.04
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Use Node.js ${{ matrix.NODE_VERSION }}
|
||||
@@ -93,7 +93,7 @@ jobs:
|
||||
check-docker:
|
||||
name: Docker Build
|
||||
timeout-minutes: 15
|
||||
runs-on: ubuntu-20.04
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
@@ -110,7 +110,7 @@ jobs:
|
||||
check-lock-file-version:
|
||||
name: NPM Lock File Version
|
||||
timeout-minutes: 5
|
||||
runs-on: ubuntu-20.04
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Check NPM lock file version
|
||||
@@ -180,7 +180,7 @@ jobs:
|
||||
fail-fast: false
|
||||
name: ${{ matrix.name }}
|
||||
timeout-minutes: 15
|
||||
runs-on: ubuntu-20.04
|
||||
runs-on: ubuntu-latest
|
||||
services:
|
||||
redis:
|
||||
image: redis
|
||||
@@ -245,7 +245,7 @@ jobs:
|
||||
fail-fast: false
|
||||
name: ${{ matrix.name }}
|
||||
timeout-minutes: 15
|
||||
runs-on: ubuntu-20.04
|
||||
runs-on: ubuntu-latest
|
||||
services:
|
||||
redis:
|
||||
image: redis
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
name: release-automated
|
||||
on:
|
||||
push:
|
||||
branches: [ release, alpha, beta, next-major ]
|
||||
branches: [ release, alpha, beta, next-major, release-4.x.x, release-5.x.x ]
|
||||
jobs:
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -42,7 +42,7 @@ jobs:
|
||||
env:
|
||||
REGISTRY: docker.io
|
||||
IMAGE_NAME: parseplatform/parse-server
|
||||
runs-on: ubuntu-18.04
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
@@ -86,7 +86,7 @@ jobs:
|
||||
docs:
|
||||
needs: release
|
||||
if: needs.release.outputs.current_tag != '' && github.ref == 'refs/heads/release'
|
||||
runs-on: ubuntu-18.04
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
@@ -14,7 +14,7 @@ env:
|
||||
IMAGE_NAME: parseplatform/parse-server
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-18.04
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
+84
-1754
File diff suppressed because it is too large
Load Diff
Generated
+354
-325
File diff suppressed because it is too large
Load Diff
+13
-13
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "parse-server",
|
||||
"version": "5.4.0",
|
||||
"version": "5.5.6",
|
||||
"description": "An express module providing a Parse-compatible API server",
|
||||
"main": "lib/index.js",
|
||||
"repository": {
|
||||
@@ -19,25 +19,25 @@
|
||||
],
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"@graphql-yoga/node": "2.6.0",
|
||||
"@graphql-tools/utils": "8.12.0",
|
||||
"@graphql-tools/merge": "8.3.6",
|
||||
"@graphql-tools/merge": "8.4.1",
|
||||
"@graphql-tools/schema": "9.0.4",
|
||||
"@graphql-tools/utils": "8.12.0",
|
||||
"@graphql-yoga/node": "2.6.0",
|
||||
"@parse/fs-files-adapter": "1.2.2",
|
||||
"@parse/push-adapter": "4.1.2",
|
||||
"@parse/push-adapter": "4.1.3",
|
||||
"bcryptjs": "2.4.3",
|
||||
"body-parser": "1.20.1",
|
||||
"body-parser": "1.20.2",
|
||||
"commander": "5.1.0",
|
||||
"cors": "2.8.5",
|
||||
"deepcopy": "2.1.0",
|
||||
"express": "4.18.1",
|
||||
"express": "4.18.2",
|
||||
"follow-redirects": "1.15.2",
|
||||
"graphql": "16.6.0",
|
||||
"graphql": "16.8.1",
|
||||
"graphql-list-fields": "2.0.2",
|
||||
"graphql-tag": "2.12.6",
|
||||
"graphql-relay": "0.10.0",
|
||||
"graphql-tag": "2.12.6",
|
||||
"intersect": "1.0.1",
|
||||
"jsonwebtoken": "8.5.1",
|
||||
"jsonwebtoken": "9.0.0",
|
||||
"jwks-rsa": "2.1.4",
|
||||
"ldapjs": "2.3.3",
|
||||
"lodash": "4.17.21",
|
||||
@@ -50,13 +50,13 @@
|
||||
"pg-promise": "10.12.0",
|
||||
"pluralize": "8.0.0",
|
||||
"redis": "3.1.2",
|
||||
"semver": "7.3.8",
|
||||
"semver": "7.5.2",
|
||||
"subscriptions-transport-ws": "0.11.0",
|
||||
"tv4": "1.3.0",
|
||||
"uuid": "9.0.0",
|
||||
"winston": "3.8.1",
|
||||
"winston": "3.8.2",
|
||||
"winston-daily-rotate-file": "4.7.1",
|
||||
"ws": "8.9.0"
|
||||
"ws": "8.13.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"graphql-tag": "2.12.6",
|
||||
|
||||
+2
-15
@@ -28,7 +28,7 @@ async function config() {
|
||||
console.log(`Running on branch: ${branch}`);
|
||||
|
||||
// Set changelog file
|
||||
const changelogFile = `./changelogs/CHANGELOG_${branch}.md`;
|
||||
const changelogFile = `./changelogs/CHANGELOG_release.md`;
|
||||
console.log(`Changelog file output to: ${changelogFile}`);
|
||||
|
||||
// Load template file contents
|
||||
@@ -41,10 +41,7 @@ async function config() {
|
||||
{ name: 'beta', prerelease: true },
|
||||
'next-major',
|
||||
// Long-Term-Support branches
|
||||
// { name: 'release-1', range: '1.x.x', channel: '1.x' },
|
||||
// { name: 'release-2', range: '2.x.x', channel: '2.x' },
|
||||
// { name: 'release-3', range: '3.x.x', channel: '3.x' },
|
||||
// { name: 'release-4', range: '4.x.x', channel: '4.x' },
|
||||
{ name: 'release-5.x.x', range: '5.x.x', channel: '5.x.x' },
|
||||
],
|
||||
dryRun: false,
|
||||
debug: true,
|
||||
@@ -88,16 +85,6 @@ async function config() {
|
||||
labels: ['type:ci'],
|
||||
releasedLabels: ['state:released<%= nextRelease.channel ? `-\${nextRelease.channel}` : "" %>']
|
||||
}],
|
||||
// Back-merge module runs last because if it fails it should not impede the release process
|
||||
[
|
||||
"@saithodev/semantic-release-backmerge",
|
||||
{
|
||||
"branches": [
|
||||
{ from: "beta", to: "alpha" },
|
||||
{ from: "release", to: "beta" },
|
||||
]
|
||||
}
|
||||
],
|
||||
],
|
||||
};
|
||||
|
||||
|
||||
@@ -596,6 +596,7 @@ describe('instagram auth adapter', () => {
|
||||
describe('google auth adapter', () => {
|
||||
const google = require('../lib/Adapters/Auth/google');
|
||||
const jwt = require('jsonwebtoken');
|
||||
const authUtils = require('../lib/Adapters/Auth/utils');
|
||||
|
||||
it('should throw error with missing id_token', async () => {
|
||||
try {
|
||||
@@ -618,7 +619,7 @@ describe('google auth adapter', () => {
|
||||
// it('should throw error if public key used to encode token is not available', async () => {
|
||||
// const fakeDecodedToken = { header: { kid: '789', alg: 'RS256' } };
|
||||
// try {
|
||||
// spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken);
|
||||
// spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
|
||||
|
||||
// await google.validateAuthData({ id: 'the_user_id', id_token: 'the_token' }, {});
|
||||
// fail();
|
||||
@@ -637,7 +638,7 @@ describe('google auth adapter', () => {
|
||||
sub: 'the_user_id',
|
||||
};
|
||||
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
|
||||
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken);
|
||||
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
|
||||
spyOn(jwt, 'verify').and.callFake(() => fakeClaim);
|
||||
|
||||
const result = await google.validateAuthData(
|
||||
@@ -653,7 +654,7 @@ describe('google auth adapter', () => {
|
||||
sub: 'the_user_id',
|
||||
};
|
||||
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
|
||||
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken);
|
||||
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
|
||||
spyOn(jwt, 'verify').and.callFake(() => fakeClaim);
|
||||
|
||||
try {
|
||||
@@ -677,7 +678,7 @@ describe('google auth adapter', () => {
|
||||
sub: 'the_user_id',
|
||||
};
|
||||
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
|
||||
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken);
|
||||
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
|
||||
spyOn(jwt, 'verify').and.callFake(() => fakeClaim);
|
||||
|
||||
try {
|
||||
@@ -699,7 +700,7 @@ describe('google auth adapter', () => {
|
||||
sub: 'the_user_id',
|
||||
};
|
||||
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
|
||||
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken);
|
||||
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
|
||||
spyOn(jwt, 'verify').and.callFake(() => fakeClaim);
|
||||
|
||||
try {
|
||||
@@ -1402,6 +1403,7 @@ describe('apple signin auth adapter', () => {
|
||||
const apple = require('../lib/Adapters/Auth/apple');
|
||||
const jwt = require('jsonwebtoken');
|
||||
const util = require('util');
|
||||
const authUtils = require('../lib/Adapters/Auth/utils');
|
||||
|
||||
it('(using client id as string) should throw error with missing id_token', async () => {
|
||||
try {
|
||||
@@ -1436,7 +1438,7 @@ describe('apple signin auth adapter', () => {
|
||||
it('should throw error if public key used to encode token is not available', async () => {
|
||||
const fakeDecodedToken = { header: { kid: '789', alg: 'RS256' } };
|
||||
try {
|
||||
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken);
|
||||
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken.header);
|
||||
|
||||
await apple.validateAuthData(
|
||||
{ id: 'the_user_id', token: 'the_token' },
|
||||
@@ -1458,7 +1460,7 @@ describe('apple signin auth adapter', () => {
|
||||
sub: 'the_user_id',
|
||||
};
|
||||
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
|
||||
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken);
|
||||
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken.header);
|
||||
spyOn(jwt, 'verify').and.callFake(() => fakeClaim);
|
||||
const fakeGetSigningKeyAsyncFunction = () => {
|
||||
return { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
|
||||
@@ -1475,7 +1477,7 @@ describe('apple signin auth adapter', () => {
|
||||
|
||||
it('should not verify invalid id_token', async () => {
|
||||
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
|
||||
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken);
|
||||
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
|
||||
const fakeGetSigningKeyAsyncFunction = () => {
|
||||
return { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
|
||||
};
|
||||
@@ -1512,7 +1514,7 @@ describe('apple signin auth adapter', () => {
|
||||
sub: 'the_user_id',
|
||||
};
|
||||
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
|
||||
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken);
|
||||
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
|
||||
const fakeGetSigningKeyAsyncFunction = () => {
|
||||
return { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
|
||||
};
|
||||
@@ -1534,7 +1536,7 @@ describe('apple signin auth adapter', () => {
|
||||
sub: 'the_user_id',
|
||||
};
|
||||
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
|
||||
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken);
|
||||
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
|
||||
const fakeGetSigningKeyAsyncFunction = () => {
|
||||
return { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
|
||||
};
|
||||
@@ -1556,7 +1558,7 @@ describe('apple signin auth adapter', () => {
|
||||
sub: 'the_user_id',
|
||||
};
|
||||
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
|
||||
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken);
|
||||
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
|
||||
const fakeGetSigningKeyAsyncFunction = () => {
|
||||
return { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
|
||||
};
|
||||
@@ -1576,7 +1578,7 @@ describe('apple signin auth adapter', () => {
|
||||
sub: 'the_user_id',
|
||||
};
|
||||
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
|
||||
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken);
|
||||
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
|
||||
const fakeGetSigningKeyAsyncFunction = () => {
|
||||
return { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
|
||||
};
|
||||
@@ -1604,7 +1606,7 @@ describe('apple signin auth adapter', () => {
|
||||
sub: 'the_user_id',
|
||||
};
|
||||
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
|
||||
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken);
|
||||
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
|
||||
const fakeGetSigningKeyAsyncFunction = () => {
|
||||
return { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
|
||||
};
|
||||
@@ -1633,7 +1635,7 @@ describe('apple signin auth adapter', () => {
|
||||
sub: 'the_user_id',
|
||||
};
|
||||
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
|
||||
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken);
|
||||
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
|
||||
const fakeGetSigningKeyAsyncFunction = () => {
|
||||
return { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
|
||||
};
|
||||
@@ -1705,7 +1707,7 @@ describe('apple signin auth adapter', () => {
|
||||
sub: 'a_different_user_id',
|
||||
};
|
||||
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
|
||||
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken);
|
||||
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
|
||||
const fakeGetSigningKeyAsyncFunction = () => {
|
||||
return { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
|
||||
};
|
||||
@@ -1972,6 +1974,7 @@ describe('facebook limited auth adapter', () => {
|
||||
const facebook = require('../lib/Adapters/Auth/facebook');
|
||||
const jwt = require('jsonwebtoken');
|
||||
const util = require('util');
|
||||
const authUtils = require('../lib/Adapters/Auth/utils');
|
||||
|
||||
// TODO: figure out a way to run this test alongside facebook classic tests
|
||||
xit('(using client id as string) should throw error with missing id_token', async () => {
|
||||
@@ -2010,7 +2013,7 @@ describe('facebook limited auth adapter', () => {
|
||||
header: { kid: '789', alg: 'RS256' },
|
||||
};
|
||||
try {
|
||||
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken);
|
||||
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken.header);
|
||||
|
||||
await facebook.validateAuthData(
|
||||
{ id: 'the_user_id', token: 'the_token' },
|
||||
@@ -2034,7 +2037,7 @@ describe('facebook limited auth adapter', () => {
|
||||
const fakeDecodedToken = {
|
||||
header: { kid: '123', alg: 'RS256' },
|
||||
};
|
||||
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken);
|
||||
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken.header);
|
||||
spyOn(jwt, 'verify').and.callFake(() => fakeClaim);
|
||||
const fakeGetSigningKeyAsyncFunction = () => {
|
||||
return {
|
||||
@@ -2056,7 +2059,7 @@ describe('facebook limited auth adapter', () => {
|
||||
const fakeDecodedToken = {
|
||||
header: { kid: '123', alg: 'RS256' },
|
||||
};
|
||||
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken);
|
||||
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
|
||||
const fakeGetSigningKeyAsyncFunction = () => {
|
||||
return {
|
||||
kid: '123',
|
||||
@@ -2098,7 +2101,7 @@ describe('facebook limited auth adapter', () => {
|
||||
const fakeDecodedToken = {
|
||||
header: { kid: '123', alg: 'RS256' },
|
||||
};
|
||||
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken);
|
||||
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
|
||||
const fakeGetSigningKeyAsyncFunction = () => {
|
||||
return {
|
||||
kid: '123',
|
||||
@@ -2125,7 +2128,7 @@ describe('facebook limited auth adapter', () => {
|
||||
const fakeDecodedToken = {
|
||||
header: { kid: '123', alg: 'RS256' },
|
||||
};
|
||||
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken);
|
||||
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
|
||||
const fakeGetSigningKeyAsyncFunction = () => {
|
||||
return {
|
||||
kid: '123',
|
||||
@@ -2152,7 +2155,7 @@ describe('facebook limited auth adapter', () => {
|
||||
const fakeDecodedToken = {
|
||||
header: { kid: '123', alg: 'RS256' },
|
||||
};
|
||||
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken);
|
||||
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
|
||||
const fakeGetSigningKeyAsyncFunction = () => {
|
||||
return {
|
||||
kid: '123',
|
||||
@@ -2177,7 +2180,7 @@ describe('facebook limited auth adapter', () => {
|
||||
const fakeDecodedToken = {
|
||||
header: { kid: '123', alg: 'RS256' },
|
||||
};
|
||||
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken);
|
||||
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
|
||||
const fakeGetSigningKeyAsyncFunction = () => {
|
||||
return {
|
||||
kid: '123',
|
||||
@@ -2210,7 +2213,7 @@ describe('facebook limited auth adapter', () => {
|
||||
const fakeDecodedToken = {
|
||||
header: { kid: '123', alg: 'RS256' },
|
||||
};
|
||||
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken);
|
||||
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
|
||||
const fakeGetSigningKeyAsyncFunction = () => {
|
||||
return {
|
||||
kid: '123',
|
||||
@@ -2244,7 +2247,7 @@ describe('facebook limited auth adapter', () => {
|
||||
const fakeDecodedToken = {
|
||||
header: { kid: '123', alg: 'RS256' },
|
||||
};
|
||||
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken);
|
||||
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
|
||||
const fakeGetSigningKeyAsyncFunction = () => {
|
||||
return {
|
||||
kid: '123',
|
||||
@@ -2330,7 +2333,7 @@ describe('facebook limited auth adapter', () => {
|
||||
const fakeDecodedToken = {
|
||||
header: { kid: '123', alg: 'RS256' },
|
||||
};
|
||||
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken);
|
||||
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
|
||||
const fakeGetSigningKeyAsyncFunction = () => {
|
||||
return {
|
||||
kid: '123',
|
||||
|
||||
@@ -294,4 +294,18 @@ describe('execution', () => {
|
||||
done.fail(data.toString());
|
||||
});
|
||||
});
|
||||
|
||||
it('can start Parse Server with auth via CLI', done => {
|
||||
childProcess = spawn(binPath, ['./spec/configs/CLIConfigAuth.json'], { env: process.env });
|
||||
childProcess.stdout.on('data', data => {
|
||||
data = data.toString();
|
||||
if (data.includes('parse-server running on')) {
|
||||
done();
|
||||
}
|
||||
});
|
||||
childProcess.stderr.on('data', data => {
|
||||
data = data.toString();
|
||||
done.fail(data.toString());
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2342,6 +2342,35 @@ describe('beforeFind hooks', () => {
|
||||
})
|
||||
.then(() => done());
|
||||
});
|
||||
|
||||
it('should run beforeFind on pointers and array of pointers from an object', async () => {
|
||||
const obj1 = new Parse.Object('TestObject');
|
||||
const obj2 = new Parse.Object('TestObject2');
|
||||
const obj3 = new Parse.Object('TestObject');
|
||||
obj2.set('aField', 'aFieldValue');
|
||||
await obj2.save();
|
||||
obj1.set('pointerField', obj2);
|
||||
obj3.set('pointerFieldArray', [obj2]);
|
||||
await obj1.save();
|
||||
await obj3.save();
|
||||
const spy = jasmine.createSpy('beforeFindSpy');
|
||||
Parse.Cloud.beforeFind('TestObject2', spy);
|
||||
const query = new Parse.Query('TestObject');
|
||||
await query.get(obj1.id);
|
||||
// Pointer not included in query so we don't expect beforeFind to be called
|
||||
expect(spy).not.toHaveBeenCalled();
|
||||
const query2 = new Parse.Query('TestObject');
|
||||
query2.include('pointerField');
|
||||
const res = await query2.get(obj1.id);
|
||||
expect(res.get('pointerField').get('aField')).toBe('aFieldValue');
|
||||
// Pointer included in query so we expect beforeFind to be called
|
||||
expect(spy).toHaveBeenCalledTimes(1);
|
||||
const query3 = new Parse.Query('TestObject');
|
||||
query3.include('pointerFieldArray');
|
||||
const res2 = await query3.get(obj3.id);
|
||||
expect(res2.get('pointerFieldArray')[0].get('aField')).toBe('aFieldValue');
|
||||
expect(spy).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('afterFind hooks', () => {
|
||||
|
||||
+3
-118
@@ -158,78 +158,6 @@ describe('middlewares', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should not succeed if the connection.remoteAddress does not belong to masterKeyIps list', () => {
|
||||
AppCache.put(fakeReq.body._ApplicationId, {
|
||||
masterKey: 'masterKey',
|
||||
masterKeyIps: ['ip1', 'ip2'],
|
||||
});
|
||||
fakeReq.connection = { remoteAddress: 'ip3' };
|
||||
fakeReq.headers['x-parse-master-key'] = 'masterKey';
|
||||
middlewares.handleParseHeaders(fakeReq, fakeRes);
|
||||
expect(fakeRes.status).toHaveBeenCalledWith(403);
|
||||
});
|
||||
|
||||
it('should succeed if the connection.remoteAddress does belong to masterKeyIps list', done => {
|
||||
AppCache.put(fakeReq.body._ApplicationId, {
|
||||
masterKey: 'masterKey',
|
||||
masterKeyIps: ['ip1', 'ip2'],
|
||||
});
|
||||
fakeReq.connection = { remoteAddress: 'ip1' };
|
||||
fakeReq.headers['x-parse-master-key'] = 'masterKey';
|
||||
middlewares.handleParseHeaders(fakeReq, fakeRes, () => {
|
||||
expect(fakeRes.status).not.toHaveBeenCalled();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should not succeed if the socket.remoteAddress does not belong to masterKeyIps list', () => {
|
||||
AppCache.put(fakeReq.body._ApplicationId, {
|
||||
masterKey: 'masterKey',
|
||||
masterKeyIps: ['ip1', 'ip2'],
|
||||
});
|
||||
fakeReq.socket = { remoteAddress: 'ip3' };
|
||||
fakeReq.headers['x-parse-master-key'] = 'masterKey';
|
||||
middlewares.handleParseHeaders(fakeReq, fakeRes);
|
||||
expect(fakeRes.status).toHaveBeenCalledWith(403);
|
||||
});
|
||||
|
||||
it('should succeed if the socket.remoteAddress does belong to masterKeyIps list', done => {
|
||||
AppCache.put(fakeReq.body._ApplicationId, {
|
||||
masterKey: 'masterKey',
|
||||
masterKeyIps: ['ip1', 'ip2'],
|
||||
});
|
||||
fakeReq.socket = { remoteAddress: 'ip1' };
|
||||
fakeReq.headers['x-parse-master-key'] = 'masterKey';
|
||||
middlewares.handleParseHeaders(fakeReq, fakeRes, () => {
|
||||
expect(fakeRes.status).not.toHaveBeenCalled();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should not succeed if the connection.socket.remoteAddress does not belong to masterKeyIps list', () => {
|
||||
AppCache.put(fakeReq.body._ApplicationId, {
|
||||
masterKey: 'masterKey',
|
||||
masterKeyIps: ['ip1', 'ip2'],
|
||||
});
|
||||
fakeReq.connection = { socket: { remoteAddress: 'ip3' } };
|
||||
fakeReq.headers['x-parse-master-key'] = 'masterKey';
|
||||
middlewares.handleParseHeaders(fakeReq, fakeRes);
|
||||
expect(fakeRes.status).toHaveBeenCalledWith(403);
|
||||
});
|
||||
|
||||
it('should succeed if the connection.socket.remoteAddress does belong to masterKeyIps list', done => {
|
||||
AppCache.put(fakeReq.body._ApplicationId, {
|
||||
masterKey: 'masterKey',
|
||||
masterKeyIps: ['ip1', 'ip2'],
|
||||
});
|
||||
fakeReq.connection = { socket: { remoteAddress: 'ip1' } };
|
||||
fakeReq.headers['x-parse-master-key'] = 'masterKey';
|
||||
middlewares.handleParseHeaders(fakeReq, fakeRes, () => {
|
||||
expect(fakeRes.status).not.toHaveBeenCalled();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should allow any ip to use masterKey if masterKeyIps is empty', done => {
|
||||
AppCache.put(fakeReq.body._ApplicationId, {
|
||||
masterKey: 'masterKey',
|
||||
@@ -243,52 +171,9 @@ describe('middlewares', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should succeed if xff header does belong to masterKeyIps', done => {
|
||||
AppCache.put(fakeReq.body._ApplicationId, {
|
||||
masterKey: 'masterKey',
|
||||
masterKeyIps: ['ip1'],
|
||||
});
|
||||
fakeReq.headers['x-parse-master-key'] = 'masterKey';
|
||||
fakeReq.headers['x-forwarded-for'] = 'ip1, ip2, ip3';
|
||||
middlewares.handleParseHeaders(fakeReq, fakeRes, () => {
|
||||
expect(fakeRes.status).not.toHaveBeenCalled();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should succeed if xff header with one ip does belong to masterKeyIps', done => {
|
||||
AppCache.put(fakeReq.body._ApplicationId, {
|
||||
masterKey: 'masterKey',
|
||||
masterKeyIps: ['ip1'],
|
||||
});
|
||||
fakeReq.headers['x-parse-master-key'] = 'masterKey';
|
||||
fakeReq.headers['x-forwarded-for'] = 'ip1';
|
||||
middlewares.handleParseHeaders(fakeReq, fakeRes, () => {
|
||||
expect(fakeRes.status).not.toHaveBeenCalled();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should not succeed if xff header does not belong to masterKeyIps', () => {
|
||||
AppCache.put(fakeReq.body._ApplicationId, {
|
||||
masterKey: 'masterKey',
|
||||
masterKeyIps: ['ip4'],
|
||||
});
|
||||
fakeReq.headers['x-parse-master-key'] = 'masterKey';
|
||||
fakeReq.headers['x-forwarded-for'] = 'ip1, ip2, ip3';
|
||||
middlewares.handleParseHeaders(fakeReq, fakeRes);
|
||||
expect(fakeRes.status).toHaveBeenCalledWith(403);
|
||||
});
|
||||
|
||||
it('should not succeed if xff header is empty and masterKeyIps is set', () => {
|
||||
AppCache.put(fakeReq.body._ApplicationId, {
|
||||
masterKey: 'masterKey',
|
||||
masterKeyIps: ['ip1'],
|
||||
});
|
||||
fakeReq.headers['x-parse-master-key'] = 'masterKey';
|
||||
fakeReq.headers['x-forwarded-for'] = '';
|
||||
middlewares.handleParseHeaders(fakeReq, fakeRes);
|
||||
expect(fakeRes.status).toHaveBeenCalledWith(403);
|
||||
it('can set trust proxy', async () => {
|
||||
const server = await reconfigureServer({ trustProxy: 1 });
|
||||
expect(server.app.parent.settings['trust proxy']).toBe(1);
|
||||
});
|
||||
|
||||
it('should properly expose the headers', () => {
|
||||
|
||||
+191
-25
@@ -37,8 +37,14 @@ describe('Parse.File testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('works with _ContentType', done => {
|
||||
request({
|
||||
it('works with _ContentType', async () => {
|
||||
await reconfigureServer({
|
||||
fileUpload: {
|
||||
enableForPublic: true,
|
||||
fileExtensions: ['*'],
|
||||
},
|
||||
});
|
||||
let response = await request({
|
||||
method: 'POST',
|
||||
url: 'http://localhost:8378/1/files/file',
|
||||
body: JSON.stringify({
|
||||
@@ -47,21 +53,18 @@ describe('Parse.File testing', () => {
|
||||
_ContentType: 'text/html',
|
||||
base64: 'PGh0bWw+PC9odG1sPgo=',
|
||||
}),
|
||||
}).then(response => {
|
||||
const b = response.data;
|
||||
expect(b.name).toMatch(/_file.html/);
|
||||
expect(b.url).toMatch(/^http:\/\/localhost:8378\/1\/files\/test\/.*file.html$/);
|
||||
request({ url: b.url }).then(response => {
|
||||
const body = response.text;
|
||||
try {
|
||||
expect(response.headers['content-type']).toMatch('^text/html');
|
||||
expect(body).toEqual('<html></html>\n');
|
||||
} catch (e) {
|
||||
jfail(e);
|
||||
}
|
||||
done();
|
||||
});
|
||||
});
|
||||
const b = response.data;
|
||||
expect(b.name).toMatch(/_file.html/);
|
||||
expect(b.url).toMatch(/^http:\/\/localhost:8378\/1\/files\/test\/.*file.html$/);
|
||||
response = await request({ url: b.url });
|
||||
const body = response.text;
|
||||
try {
|
||||
expect(response.headers['content-type']).toMatch('^text/html');
|
||||
expect(body).toEqual('<html></html>\n');
|
||||
} catch (e) {
|
||||
jfail(e);
|
||||
}
|
||||
});
|
||||
|
||||
it('works without Content-Type', done => {
|
||||
@@ -351,25 +354,29 @@ describe('Parse.File testing', () => {
|
||||
ok(object.toJSON().file.url);
|
||||
});
|
||||
|
||||
it('content-type used with no extension', done => {
|
||||
it('content-type used with no extension', async () => {
|
||||
await reconfigureServer({
|
||||
fileUpload: {
|
||||
enableForPublic: true,
|
||||
fileExtensions: ['*'],
|
||||
},
|
||||
});
|
||||
const headers = {
|
||||
'Content-Type': 'text/html',
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-REST-API-Key': 'rest',
|
||||
};
|
||||
request({
|
||||
let response = await request({
|
||||
method: 'POST',
|
||||
headers: headers,
|
||||
url: 'http://localhost:8378/1/files/file',
|
||||
body: 'fee fi fo',
|
||||
}).then(response => {
|
||||
const b = response.data;
|
||||
expect(b.name).toMatch(/\.html$/);
|
||||
request({ url: b.url }).then(response => {
|
||||
expect(response.headers['content-type']).toMatch(/^text\/html/);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
const b = response.data;
|
||||
expect(b.name).toMatch(/\.html$/);
|
||||
response = await request({ url: b.url });
|
||||
expect(response.headers['content-type']).toMatch(/^text\/html/);
|
||||
});
|
||||
|
||||
it('filename is url encoded', done => {
|
||||
@@ -1298,6 +1305,165 @@ describe('Parse.File testing', () => {
|
||||
await expectAsync(reconfigureServer({ fileUpload: { [key]: value } })).toBeResolved();
|
||||
}
|
||||
}
|
||||
await expectAsync(
|
||||
reconfigureServer({
|
||||
fileUpload: {
|
||||
fileExtensions: 1,
|
||||
},
|
||||
})
|
||||
).toBeRejectedWith('fileUpload.fileExtensions must be an array.');
|
||||
});
|
||||
});
|
||||
|
||||
describe('fileExtensions', () => {
|
||||
it('works with _ContentType', async () => {
|
||||
await reconfigureServer({
|
||||
silent: false,
|
||||
fileUpload: {
|
||||
enableForPublic: true,
|
||||
fileExtensions: ['png'],
|
||||
},
|
||||
});
|
||||
await expectAsync(
|
||||
request({
|
||||
method: 'POST',
|
||||
url: 'http://localhost:8378/1/files/file',
|
||||
body: JSON.stringify({
|
||||
_ApplicationId: 'test',
|
||||
_JavaScriptKey: 'test',
|
||||
_ContentType: 'text/html',
|
||||
base64: 'PGh0bWw+PC9odG1sPgo=',
|
||||
}),
|
||||
}).catch(e => {
|
||||
throw new Error(e.data.error);
|
||||
})
|
||||
).toBeRejectedWith(
|
||||
new Parse.Error(Parse.Error.FILE_SAVE_ERROR, `File upload of extension html is disabled.`)
|
||||
);
|
||||
});
|
||||
|
||||
it('works without Content-Type', async () => {
|
||||
await reconfigureServer({
|
||||
fileUpload: {
|
||||
enableForPublic: true,
|
||||
},
|
||||
});
|
||||
const headers = {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-REST-API-Key': 'rest',
|
||||
};
|
||||
await expectAsync(
|
||||
request({
|
||||
method: 'POST',
|
||||
headers: headers,
|
||||
url: 'http://localhost:8378/1/files/file.html',
|
||||
body: '<html></html>\n',
|
||||
}).catch(e => {
|
||||
throw new Error(e.data.error);
|
||||
})
|
||||
).toBeRejectedWith(
|
||||
new Parse.Error(Parse.Error.FILE_SAVE_ERROR, `File upload of extension html is disabled.`)
|
||||
);
|
||||
});
|
||||
|
||||
it('allows file without extension', async () => {
|
||||
await reconfigureServer({
|
||||
fileUpload: {
|
||||
enableForPublic: true,
|
||||
fileExtensions: ['^[^hH][^tT][^mM][^lL]?$'],
|
||||
},
|
||||
});
|
||||
const headers = {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-REST-API-Key': 'rest',
|
||||
};
|
||||
|
||||
const values = ['filenamewithoutextension'];
|
||||
|
||||
for (const value of values) {
|
||||
await expectAsync(
|
||||
request({
|
||||
method: 'POST',
|
||||
headers: headers,
|
||||
url: `http://localhost:8378/1/files/${value}`,
|
||||
body: '<html></html>\n',
|
||||
}).catch(e => {
|
||||
throw new Error(e.data.error);
|
||||
})
|
||||
).toBeResolved();
|
||||
}
|
||||
});
|
||||
|
||||
it('works with array', async () => {
|
||||
await reconfigureServer({
|
||||
fileUpload: {
|
||||
enableForPublic: true,
|
||||
fileExtensions: ['jpg'],
|
||||
},
|
||||
});
|
||||
await expectAsync(
|
||||
request({
|
||||
method: 'POST',
|
||||
url: 'http://localhost:8378/1/files/file',
|
||||
body: JSON.stringify({
|
||||
_ApplicationId: 'test',
|
||||
_JavaScriptKey: 'test',
|
||||
_ContentType: 'text/html',
|
||||
base64: 'PGh0bWw+PC9odG1sPgo=',
|
||||
}),
|
||||
}).catch(e => {
|
||||
throw new Error(e.data.error);
|
||||
})
|
||||
).toBeRejectedWith(
|
||||
new Parse.Error(Parse.Error.FILE_SAVE_ERROR, `File upload of extension html is disabled.`)
|
||||
);
|
||||
});
|
||||
|
||||
it('works with array without Content-Type', async () => {
|
||||
await reconfigureServer({
|
||||
fileUpload: {
|
||||
enableForPublic: true,
|
||||
fileExtensions: ['jpg'],
|
||||
},
|
||||
});
|
||||
const headers = {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-REST-API-Key': 'rest',
|
||||
};
|
||||
await expectAsync(
|
||||
request({
|
||||
method: 'POST',
|
||||
headers: headers,
|
||||
url: 'http://localhost:8378/1/files/file.html',
|
||||
body: '<html></html>\n',
|
||||
}).catch(e => {
|
||||
throw new Error(e.data.error);
|
||||
})
|
||||
).toBeRejectedWith(
|
||||
new Parse.Error(Parse.Error.FILE_SAVE_ERROR, `File upload of extension html is disabled.`)
|
||||
);
|
||||
});
|
||||
|
||||
it('works with array with correct file type', async () => {
|
||||
await reconfigureServer({
|
||||
fileUpload: {
|
||||
enableForPublic: true,
|
||||
fileExtensions: ['html'],
|
||||
},
|
||||
});
|
||||
const response = await request({
|
||||
method: 'POST',
|
||||
url: 'http://localhost:8378/1/files/file',
|
||||
body: JSON.stringify({
|
||||
_ApplicationId: 'test',
|
||||
_JavaScriptKey: 'test',
|
||||
_ContentType: 'text/html',
|
||||
base64: 'PGh0bWw+PC9odG1sPgo=',
|
||||
}),
|
||||
});
|
||||
const b = response.data;
|
||||
expect(b.name).toMatch(/_file.html$/);
|
||||
expect(b.url).toMatch(/^http:\/\/localhost:8378\/1\/files\/test\/.*file.html$/);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5269,7 +5269,6 @@ describe('ParseGraphQLServer', () => {
|
||||
|
||||
it('should only count', async () => {
|
||||
await prepareData();
|
||||
|
||||
await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear();
|
||||
|
||||
const where = {
|
||||
|
||||
@@ -142,7 +142,7 @@ describe('Parse Role testing', () => {
|
||||
return Promise.all(promises);
|
||||
};
|
||||
|
||||
const restExecute = spyOn(RestQuery.prototype, 'execute').and.callThrough();
|
||||
const restExecute = spyOn(RestQuery._UnsafeRestQuery.prototype, 'execute').and.callThrough();
|
||||
|
||||
let user, auth, getAllRolesSpy;
|
||||
createTestUser()
|
||||
@@ -601,4 +601,12 @@ describe('Parse Role testing', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should save role when beforeSave hook for _Role is present.', async done => {
|
||||
Parse.Cloud.beforeSave('_Role', () => {});
|
||||
const role = new Parse.Role('roleName', new Parse.ACL());
|
||||
await role.save({}, { useMasterKey: true });
|
||||
expect(role.id).toBeDefined();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
+24
-20
@@ -398,15 +398,16 @@ describe('RestQuery.each', () => {
|
||||
}
|
||||
const config = Config.get('test');
|
||||
await Parse.Object.saveAll(objects);
|
||||
const query = new RestQuery(
|
||||
const query = await RestQuery({
|
||||
method: RestQuery.Method.find,
|
||||
config,
|
||||
auth.master(config),
|
||||
'Object',
|
||||
{ value: { $gt: 2 } },
|
||||
{ limit: 2 }
|
||||
);
|
||||
auth: auth.master(config),
|
||||
className: 'Object',
|
||||
restWhere: { value: { $gt: 2 } },
|
||||
restOptions: { limit: 2 },
|
||||
});
|
||||
const spy = spyOn(query, 'execute').and.callThrough();
|
||||
const classSpy = spyOn(RestQuery.prototype, 'execute').and.callThrough();
|
||||
const classSpy = spyOn(RestQuery._UnsafeRestQuery.prototype, 'execute').and.callThrough();
|
||||
const results = [];
|
||||
await query.each(result => {
|
||||
expect(result.value).toBeGreaterThan(2);
|
||||
@@ -437,34 +438,37 @@ describe('RestQuery.each', () => {
|
||||
* Two queries needed since objectId are sorted and we can't know which one
|
||||
* going to be the first and then skip by the $gt added by each
|
||||
*/
|
||||
const queryOne = new RestQuery(
|
||||
const queryOne = await RestQuery({
|
||||
method: RestQuery.Method.get,
|
||||
config,
|
||||
auth.master(config),
|
||||
'Letter',
|
||||
{
|
||||
auth: auth.master(config),
|
||||
className: 'Letter',
|
||||
restWhere: {
|
||||
numbers: {
|
||||
__type: 'Pointer',
|
||||
className: 'Number',
|
||||
objectId: object1.id,
|
||||
},
|
||||
},
|
||||
{ limit: 1 }
|
||||
);
|
||||
const queryTwo = new RestQuery(
|
||||
restOptions: { limit: 1 },
|
||||
});
|
||||
|
||||
const queryTwo = await RestQuery({
|
||||
method: RestQuery.Method.get,
|
||||
config,
|
||||
auth.master(config),
|
||||
'Letter',
|
||||
{
|
||||
auth: auth.master(config),
|
||||
className: 'Letter',
|
||||
restWhere: {
|
||||
numbers: {
|
||||
__type: 'Pointer',
|
||||
className: 'Number',
|
||||
objectId: object2.id,
|
||||
},
|
||||
},
|
||||
{ limit: 1 }
|
||||
);
|
||||
restOptions: { limit: 1 },
|
||||
});
|
||||
|
||||
const classSpy = spyOn(RestQuery.prototype, 'execute').and.callThrough();
|
||||
const classSpy = spyOn(RestQuery._UnsafeRestQuery.prototype, 'execute').and.callThrough();
|
||||
const resultsOne = [];
|
||||
const resultsTwo = [];
|
||||
await queryOne.each(result => {
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"appName": "test",
|
||||
"appId": "test",
|
||||
"masterKey": "test",
|
||||
"logLevel": "error",
|
||||
"auth": {
|
||||
"facebook": {
|
||||
"appIds": "test"
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
-1
@@ -553,6 +553,7 @@ describe('server', () => {
|
||||
|
||||
it('should not fail when Google signin is introduced without the optional clientId', done => {
|
||||
const jwt = require('jsonwebtoken');
|
||||
const authUtils = require('../lib/Adapters/Auth/utils');
|
||||
|
||||
reconfigureServer({
|
||||
auth: { google: {} },
|
||||
@@ -565,7 +566,7 @@ describe('server', () => {
|
||||
sub: 'the_user_id',
|
||||
};
|
||||
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
|
||||
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken);
|
||||
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
|
||||
spyOn(jwt, 'verify').and.callFake(() => fakeClaim);
|
||||
const user = new Parse.User();
|
||||
user
|
||||
|
||||
@@ -660,6 +660,38 @@ describe('rest create', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('cannot get object in volatileClasses if not masterKey through pointer', async () => {
|
||||
const masterKeyOnlyClassObject = new Parse.Object('_PushStatus');
|
||||
await masterKeyOnlyClassObject.save(null, { useMasterKey: true });
|
||||
const obj2 = new Parse.Object('TestObject');
|
||||
// Anyone is can basically create a pointer to any object
|
||||
// or some developers can use master key in some hook to link
|
||||
// private objects to standard objects
|
||||
obj2.set('pointer', masterKeyOnlyClassObject);
|
||||
await obj2.save();
|
||||
const query = new Parse.Query('TestObject');
|
||||
query.include('pointer');
|
||||
await expectAsync(query.get(obj2.id)).toBeRejectedWithError(
|
||||
"Clients aren't allowed to perform the get operation on the _PushStatus collection."
|
||||
);
|
||||
});
|
||||
|
||||
it('cannot get object in _GlobalConfig if not masterKey through pointer', async () => {
|
||||
await Parse.Config.save({ privateData: 'secret' }, { privateData: true });
|
||||
const obj2 = new Parse.Object('TestObject');
|
||||
obj2.set('globalConfigPointer', {
|
||||
__type: 'Pointer',
|
||||
className: '_GlobalConfig',
|
||||
objectId: 1,
|
||||
});
|
||||
await obj2.save();
|
||||
const query = new Parse.Query('TestObject');
|
||||
query.include('globalConfigPointer');
|
||||
await expectAsync(query.get(obj2.id)).toBeRejectedWithError(
|
||||
"Clients aren't allowed to perform the get operation on the _GlobalConfig collection."
|
||||
);
|
||||
});
|
||||
|
||||
it('locks down session', done => {
|
||||
let currentUser;
|
||||
Parse.User.signUp('foo', 'bar')
|
||||
|
||||
@@ -138,6 +138,71 @@ describe('Vulnerabilities', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('denies creating global config with polluted data', async () => {
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-Master-Key': 'test',
|
||||
};
|
||||
const params = {
|
||||
method: 'PUT',
|
||||
url: 'http://localhost:8378/1/config',
|
||||
json: true,
|
||||
body: {
|
||||
params: {
|
||||
welcomeMesssage: 'Welcome to Parse',
|
||||
foo: { _bsontype: 'Code', code: 'shell' },
|
||||
},
|
||||
},
|
||||
headers,
|
||||
};
|
||||
const response = await request(params).catch(e => e);
|
||||
expect(response.status).toBe(400);
|
||||
const text = JSON.parse(response.text);
|
||||
expect(text.code).toBe(Parse.Error.INVALID_KEY_NAME);
|
||||
expect(text.error).toBe(
|
||||
'Prohibited keyword in request data: {"key":"_bsontype","value":"Code"}.'
|
||||
);
|
||||
});
|
||||
|
||||
it('denies direct database write wih prohibited keys', async () => {
|
||||
const Config = require('../lib/Config');
|
||||
const config = Config.get(Parse.applicationId);
|
||||
const user = {
|
||||
objectId: '1234567890',
|
||||
username: 'hello',
|
||||
password: 'pass',
|
||||
_session_token: 'abc',
|
||||
foo: { _bsontype: 'Code', code: 'shell' },
|
||||
};
|
||||
await expectAsync(config.database.create('_User', user)).toBeRejectedWith(
|
||||
new Parse.Error(
|
||||
Parse.Error.INVALID_KEY_NAME,
|
||||
'Prohibited keyword in request data: {"key":"_bsontype","value":"Code"}.'
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
it('denies direct database update wih prohibited keys', async () => {
|
||||
const Config = require('../lib/Config');
|
||||
const config = Config.get(Parse.applicationId);
|
||||
const user = {
|
||||
objectId: '1234567890',
|
||||
username: 'hello',
|
||||
password: 'pass',
|
||||
_session_token: 'abc',
|
||||
foo: { _bsontype: 'Code', code: 'shell' },
|
||||
};
|
||||
await expectAsync(
|
||||
config.database.update('_User', { _id: user.objectId }, user)
|
||||
).toBeRejectedWith(
|
||||
new Parse.Error(
|
||||
Parse.Error.INVALID_KEY_NAME,
|
||||
'Prohibited keyword in request data: {"key":"_bsontype","value":"Code"}.'
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
it('denies creating a hook with polluted data', async () => {
|
||||
const express = require('express');
|
||||
const bodyParser = require('body-parser');
|
||||
|
||||
@@ -5,6 +5,7 @@ const Parse = require('parse/node').Parse;
|
||||
const jwksClient = require('jwks-rsa');
|
||||
const util = require('util');
|
||||
const jwt = require('jsonwebtoken');
|
||||
const authUtils = require('./utils');
|
||||
|
||||
const TOKEN_ISSUER = 'https://appleid.apple.com';
|
||||
|
||||
@@ -30,21 +31,12 @@ const getAppleKeyByKeyId = async (keyId, cacheMaxEntries, cacheMaxAge) => {
|
||||
return key;
|
||||
};
|
||||
|
||||
const getHeaderFromToken = token => {
|
||||
const decodedToken = jwt.decode(token, { complete: true });
|
||||
if (!decodedToken) {
|
||||
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, `provided token does not decode as JWT`);
|
||||
}
|
||||
|
||||
return decodedToken.header;
|
||||
};
|
||||
|
||||
const verifyIdToken = async ({ token, id }, { clientId, cacheMaxEntries, cacheMaxAge }) => {
|
||||
if (!token) {
|
||||
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, `id token is invalid for this user.`);
|
||||
}
|
||||
|
||||
const { kid: keyId, alg: algorithm } = getHeaderFromToken(token);
|
||||
const { kid: keyId, alg: algorithm } = authUtils.getHeaderFromToken(token);
|
||||
const ONE_HOUR_IN_MS = 3600000;
|
||||
let jwtClaims;
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ const jwksClient = require('jwks-rsa');
|
||||
const util = require('util');
|
||||
const jwt = require('jsonwebtoken');
|
||||
const httpsRequest = require('./httpsRequest');
|
||||
const authUtils = require('./utils');
|
||||
|
||||
const TOKEN_ISSUER = 'https://facebook.com';
|
||||
|
||||
@@ -73,21 +74,12 @@ const getFacebookKeyByKeyId = async (keyId, cacheMaxEntries, cacheMaxAge) => {
|
||||
return key;
|
||||
};
|
||||
|
||||
const getHeaderFromToken = token => {
|
||||
const decodedToken = jwt.decode(token, { complete: true });
|
||||
if (!decodedToken) {
|
||||
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'provided token does not decode as JWT');
|
||||
}
|
||||
|
||||
return decodedToken.header;
|
||||
};
|
||||
|
||||
const verifyIdToken = async ({ token, id }, { clientId, cacheMaxEntries, cacheMaxAge }) => {
|
||||
if (!token) {
|
||||
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'id token is invalid for this user.');
|
||||
}
|
||||
|
||||
const { kid: keyId, alg: algorithm } = getHeaderFromToken(token);
|
||||
const { kid: keyId, alg: algorithm } = authUtils.getHeaderFromToken(token);
|
||||
const ONE_HOUR_IN_MS = 3600000;
|
||||
let jwtClaims;
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ var Parse = require('parse/node').Parse;
|
||||
|
||||
const https = require('https');
|
||||
const jwt = require('jsonwebtoken');
|
||||
const authUtils = require('./utils');
|
||||
|
||||
const TOKEN_ISSUER = 'accounts.google.com';
|
||||
const HTTPS_TOKEN_ISSUER = 'https://accounts.google.com';
|
||||
@@ -51,22 +52,12 @@ function getGoogleKeyByKeyId(keyId) {
|
||||
});
|
||||
}
|
||||
|
||||
function getHeaderFromToken(token) {
|
||||
const decodedToken = jwt.decode(token, { complete: true });
|
||||
|
||||
if (!decodedToken) {
|
||||
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, `provided token does not decode as JWT`);
|
||||
}
|
||||
|
||||
return decodedToken.header;
|
||||
}
|
||||
|
||||
async function verifyIdToken({ id_token: token, id }, { clientId }) {
|
||||
if (!token) {
|
||||
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, `id token is invalid for this user.`);
|
||||
}
|
||||
|
||||
const { kid: keyId, alg: algorithm } = getHeaderFromToken(token);
|
||||
const { kid: keyId, alg: algorithm } = authUtils.getHeaderFromToken(token);
|
||||
let jwtClaims;
|
||||
const googleKey = await getGoogleKeyByKeyId(keyId);
|
||||
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
const jwt = require('jsonwebtoken');
|
||||
const Parse = require('parse/node').Parse;
|
||||
const getHeaderFromToken = token => {
|
||||
const decodedToken = jwt.decode(token, { complete: true });
|
||||
if (!decodedToken) {
|
||||
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, `provided token does not decode as JWT`);
|
||||
}
|
||||
|
||||
return decodedToken.header;
|
||||
};
|
||||
module.exports = {
|
||||
getHeaderFromToken,
|
||||
};
|
||||
+39
-9
@@ -84,7 +84,15 @@ const getAuthForSessionToken = async function ({
|
||||
include: 'user',
|
||||
};
|
||||
|
||||
const query = new RestQuery(config, master(config), '_Session', { sessionToken }, restOptions);
|
||||
const query = await RestQuery({
|
||||
method: RestQuery.Method.get,
|
||||
runBeforeFind: false,
|
||||
config,
|
||||
auth: master(config),
|
||||
className: '_Session',
|
||||
restWhere: { sessionToken },
|
||||
restOptions,
|
||||
});
|
||||
results = (await query.execute()).results;
|
||||
} else {
|
||||
results = (
|
||||
@@ -121,11 +129,19 @@ const getAuthForSessionToken = async function ({
|
||||
});
|
||||
};
|
||||
|
||||
var getAuthForLegacySessionToken = function ({ config, sessionToken, installationId }) {
|
||||
var getAuthForLegacySessionToken = async function ({ config, sessionToken, installationId }) {
|
||||
var restOptions = {
|
||||
limit: 1,
|
||||
};
|
||||
var query = new RestQuery(config, master(config), '_User', { sessionToken }, restOptions);
|
||||
var query = await RestQuery({
|
||||
method: RestQuery.Method.get,
|
||||
runBeforeFind: false,
|
||||
config,
|
||||
auth: master(config),
|
||||
className: '_User',
|
||||
restWhere: { sessionToken },
|
||||
restOptions,
|
||||
});
|
||||
return query.execute().then(response => {
|
||||
var results = response.results;
|
||||
if (results.length !== 1) {
|
||||
@@ -169,9 +185,16 @@ Auth.prototype.getRolesForUser = async function () {
|
||||
objectId: this.user.id,
|
||||
},
|
||||
};
|
||||
await new RestQuery(this.config, master(this.config), '_Role', restWhere, {}).each(result =>
|
||||
results.push(result)
|
||||
);
|
||||
const query = await RestQuery({
|
||||
method: RestQuery.Method.find,
|
||||
config: this.config,
|
||||
auth: master(this.config),
|
||||
runBeforeFind: false,
|
||||
className: '_Role',
|
||||
restWhere,
|
||||
restOptions: {},
|
||||
});
|
||||
await query.each(result => results.push(result));
|
||||
} else {
|
||||
await new Parse.Query(Parse.Role)
|
||||
.equalTo('users', this.user)
|
||||
@@ -262,9 +285,16 @@ Auth.prototype.getRolesByIds = async function (ins) {
|
||||
};
|
||||
});
|
||||
const restWhere = { roles: { $in: roles } };
|
||||
await new RestQuery(this.config, master(this.config), '_Role', restWhere, {}).each(result =>
|
||||
results.push(result)
|
||||
);
|
||||
const query = await RestQuery({
|
||||
method: RestQuery.Method.find,
|
||||
runBeforeFind: false,
|
||||
config: this.config,
|
||||
auth: master(this.config),
|
||||
className: '_Role',
|
||||
restWhere,
|
||||
restOptions: {},
|
||||
});
|
||||
await query.each(result => results.push(result));
|
||||
}
|
||||
return results;
|
||||
};
|
||||
|
||||
@@ -424,6 +424,11 @@ export class Config {
|
||||
} else if (typeof fileUpload.enableForAuthenticatedUser !== 'boolean') {
|
||||
throw 'fileUpload.enableForAuthenticatedUser must be a boolean value.';
|
||||
}
|
||||
if (fileUpload.fileExtensions === undefined) {
|
||||
fileUpload.fileExtensions = FileUploadOptions.fileExtensions.default;
|
||||
} else if (!Array.isArray(fileUpload.fileExtensions)) {
|
||||
throw 'fileUpload.fileExtensions must be an array.';
|
||||
}
|
||||
}
|
||||
|
||||
static validateMasterKeyIps(masterKeyIps) {
|
||||
|
||||
@@ -467,6 +467,11 @@ class DatabaseController {
|
||||
validateOnly: boolean = false,
|
||||
validSchemaController: SchemaController.SchemaController
|
||||
): Promise<any> {
|
||||
try {
|
||||
Utils.checkProhibitedKeywords(this.options, update);
|
||||
} catch (error) {
|
||||
return Promise.reject(new Parse.Error(Parse.Error.INVALID_KEY_NAME, error));
|
||||
}
|
||||
const originalQuery = query;
|
||||
const originalUpdate = update;
|
||||
// Make a copy of the object, so we don't mutate the incoming data.
|
||||
@@ -797,6 +802,11 @@ class DatabaseController {
|
||||
validateOnly: boolean = false,
|
||||
validSchemaController: SchemaController.SchemaController
|
||||
): Promise<any> {
|
||||
try {
|
||||
Utils.checkProhibitedKeywords(this.options, object);
|
||||
} catch (error) {
|
||||
return Promise.reject(new Parse.Error(Parse.Error.INVALID_KEY_NAME, error));
|
||||
}
|
||||
// Make a copy of the object, so we don't mutate the incoming data.
|
||||
const originalObject = object;
|
||||
object = transformObjectACL(object);
|
||||
|
||||
@@ -58,9 +58,16 @@ export class PushController {
|
||||
|
||||
// Force filtering on only valid device tokens
|
||||
const updateWhere = applyDeviceTokenExists(where);
|
||||
badgeUpdate = () => {
|
||||
badgeUpdate = async () => {
|
||||
// Build a real RestQuery so we can use it in RestWrite
|
||||
const restQuery = new RestQuery(config, master(config), '_Installation', updateWhere);
|
||||
const restQuery = await RestQuery({
|
||||
method: RestQuery.Method.find,
|
||||
config,
|
||||
runBeforeFind: false,
|
||||
auth: master(config),
|
||||
className: '_Installation',
|
||||
restWhere: updateWhere,
|
||||
});
|
||||
return restQuery.buildRestWhere().then(() => {
|
||||
const write = new RestWrite(
|
||||
config,
|
||||
|
||||
@@ -48,7 +48,7 @@ export class UserController extends AdaptableController {
|
||||
}
|
||||
}
|
||||
|
||||
verifyEmail(username, token) {
|
||||
async verifyEmail(username, token) {
|
||||
if (!this.shouldVerifyEmails) {
|
||||
// Trying to verify email when not enabled
|
||||
// TODO: Better error here.
|
||||
@@ -70,12 +70,14 @@ export class UserController extends AdaptableController {
|
||||
updateFields._email_verify_token_expires_at = { __op: 'Delete' };
|
||||
}
|
||||
const masterAuth = Auth.master(this.config);
|
||||
var findUserForEmailVerification = new RestQuery(
|
||||
this.config,
|
||||
Auth.master(this.config),
|
||||
'_User',
|
||||
{ username: username }
|
||||
);
|
||||
var findUserForEmailVerification = await RestQuery({
|
||||
method: RestQuery.Method.get,
|
||||
config: this.config,
|
||||
runBeforeFind: false,
|
||||
auth: Auth.master(this.config),
|
||||
className: '_User',
|
||||
restWhere: { username },
|
||||
});
|
||||
return findUserForEmailVerification.execute().then(result => {
|
||||
if (result.results.length && result.results[0].emailVerified) {
|
||||
return Promise.resolve(result.results.length[0]);
|
||||
@@ -112,7 +114,7 @@ export class UserController extends AdaptableController {
|
||||
});
|
||||
}
|
||||
|
||||
getUserIfNeeded(user) {
|
||||
async getUserIfNeeded(user) {
|
||||
if (user.username && user.email) {
|
||||
return Promise.resolve(user);
|
||||
}
|
||||
@@ -124,7 +126,14 @@ export class UserController extends AdaptableController {
|
||||
where.email = user.email;
|
||||
}
|
||||
|
||||
var query = new RestQuery(this.config, Auth.master(this.config), '_User', where);
|
||||
var query = await RestQuery({
|
||||
method: RestQuery.Method.get,
|
||||
config: this.config,
|
||||
runBeforeFind: false,
|
||||
auth: Auth.master(this.config),
|
||||
className: '_User',
|
||||
restWhere: where,
|
||||
});
|
||||
return query.execute().then(function (result) {
|
||||
if (result.results.length != 1) {
|
||||
throw undefined;
|
||||
|
||||
@@ -95,7 +95,6 @@ module.exports.ParseServerOptions = {
|
||||
env: 'PARSE_SERVER_AUTH_PROVIDERS',
|
||||
help:
|
||||
'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: {
|
||||
env: 'PARSE_SERVER_CACHE_ADAPTER',
|
||||
@@ -477,6 +476,13 @@ module.exports.ParseServerOptions = {
|
||||
help: 'Starts the liveQuery server',
|
||||
action: parsers.booleanParser,
|
||||
},
|
||||
trustProxy: {
|
||||
env: 'PARSE_SERVER_TRUST_PROXY',
|
||||
help:
|
||||
'The trust proxy settings. It is important to understand the exact setup of the reverse proxy, since this setting will trust values provided in the Parse Server API request. See the <a href="https://expressjs.com/en/guide/behind-proxies.html">express trust proxy settings</a> documentation. Defaults to `false`.',
|
||||
action: parsers.objectParser,
|
||||
default: [],
|
||||
},
|
||||
userSensitiveFields: {
|
||||
env: 'PARSE_SERVER_USER_SENSITIVE_FIELDS',
|
||||
help:
|
||||
@@ -873,6 +879,13 @@ module.exports.FileUploadOptions = {
|
||||
action: parsers.booleanParser,
|
||||
default: false,
|
||||
},
|
||||
fileExtensions: {
|
||||
env: 'PARSE_SERVER_FILE_UPLOAD_FILE_EXTENSIONS',
|
||||
help:
|
||||
"Sets the allowed file extensions for uploading files. The extension is defined as an array of file extensions, or a regex pattern.<br><br>It is recommended to restrict the file upload extensions as much as possible. HTML files are especially problematic as they may be used by an attacker who uploads a HTML form to look legitimate under your app's domain name, or to compromise the session token of another user via accessing the browser's local storage.<br><br>Defaults to `^[^hH][^tT][^mM][^lL]?$` which allows any file extension except HTML files.",
|
||||
action: parsers.arrayParser,
|
||||
default: ['^[^hH][^tT][^mM][^lL]?$'],
|
||||
},
|
||||
};
|
||||
module.exports.DatabaseOptions = {
|
||||
enableSchemaHooks: {
|
||||
|
||||
+3
-1
@@ -19,7 +19,7 @@
|
||||
* @property {Adapter<AnalyticsAdapter>} analyticsAdapter Adapter module for the analytics
|
||||
* @property {String} appId Your Parse Application ID
|
||||
* @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 {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)
|
||||
@@ -88,6 +88,7 @@
|
||||
* @property {Number} sessionLength Session duration, in seconds, defaults to 1 year
|
||||
* @property {Boolean} silent Disables console output
|
||||
* @property {Boolean} startLiveQueryServer Starts the liveQuery server
|
||||
* @property {Any} trustProxy The trust proxy settings. It is important to understand the exact setup of the reverse proxy, since this setting will trust values provided in the Parse Server API request. See the <a href="https://expressjs.com/en/guide/behind-proxies.html">express trust proxy settings</a> documentation. Defaults to `false`.
|
||||
* @property {String[]} userSensitiveFields Personally identifiable information fields in the user table the should be removed for non-authorized users. Deprecated @see protectedFields
|
||||
* @property {Boolean} verbose Set the logging to verbose
|
||||
* @property {Boolean} verifyUserEmails Set to `true` to require users to verify their email address to complete the sign-up process.<br><br>Default is `false`.
|
||||
@@ -203,6 +204,7 @@
|
||||
* @property {Boolean} enableForAnonymousUser Is true if file upload should be allowed for anonymous users.
|
||||
* @property {Boolean} enableForAuthenticatedUser Is true if file upload should be allowed for authenticated users.
|
||||
* @property {Boolean} enableForPublic Is true if file upload should be allowed for anyone, regardless of user authentication.
|
||||
* @property {String[]} fileExtensions Sets the allowed file extensions for uploading files. The extension is defined as an array of file extensions, or a regex pattern.<br><br>It is recommended to restrict the file upload extensions as much as possible. HTML files are especially problematic as they may be used by an attacker who uploads a HTML form to look legitimate under your app's domain name, or to compromise the session token of another user via accessing the browser's local storage.<br><br>Defaults to `^[^hH][^tT][^mM][^lL]?$` which allows any file extension except HTML files.
|
||||
*/
|
||||
|
||||
/**
|
||||
|
||||
@@ -140,7 +140,7 @@ export interface ParseServerOptions {
|
||||
allowCustomObjectId: ?boolean;
|
||||
/* 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 */
|
||||
auth: ?(AuthAdapter[]);
|
||||
auth: ?{ [string]: AuthAdapter };
|
||||
/* Max file size for uploads, defaults to 20mb
|
||||
:DEFAULT: 20mb */
|
||||
maxUploadSize: ?string;
|
||||
@@ -238,6 +238,9 @@ export interface ParseServerOptions {
|
||||
cluster: ?NumberOrBoolean;
|
||||
/* middleware for express server, can be string or function */
|
||||
middleware: ?((() => void) | string);
|
||||
/* The trust proxy settings. It is important to understand the exact setup of the reverse proxy, since this setting will trust values provided in the Parse Server API request. See the <a href="https://expressjs.com/en/guide/behind-proxies.html">express trust proxy settings</a> documentation. Defaults to `false`.
|
||||
:DEFAULT: false */
|
||||
trustProxy: ?any;
|
||||
/* Starts the liveQuery server */
|
||||
startLiveQueryServer: ?boolean;
|
||||
/* Live query server configuration options (will start the liveQuery server) */
|
||||
@@ -493,6 +496,9 @@ export interface PasswordPolicyOptions {
|
||||
}
|
||||
|
||||
export interface FileUploadOptions {
|
||||
/* Sets the allowed file extensions for uploading files. The extension is defined as an array of file extensions, or a regex pattern.<br><br>It is recommended to restrict the file upload extensions as much as possible. HTML files are especially problematic as they may be used by an attacker who uploads a HTML form to look legitimate under your app's domain name, or to compromise the session token of another user via accessing the browser's local storage.<br><br>Defaults to `^[^hH][^tT][^mM][^lL]?$` which allows any file extension except HTML files.
|
||||
:DEFAULT: ["^[^hH][^tT][^mM][^lL]?$"] */
|
||||
fileExtensions: ?(string[]);
|
||||
/* Is true if file upload should be allowed for anonymous users.
|
||||
:DEFAULT: false */
|
||||
enableForAnonymousUser: ?boolean;
|
||||
|
||||
@@ -304,6 +304,9 @@ class ParseServer {
|
||||
options
|
||||
);
|
||||
}
|
||||
if (options.trustProxy) {
|
||||
app.set('trust proxy', options.trustProxy);
|
||||
}
|
||||
/* istanbul ignore next */
|
||||
if (!process.env.TESTING) {
|
||||
configureListeners(this);
|
||||
|
||||
+145
-52
@@ -6,6 +6,8 @@ var Parse = require('parse/node').Parse;
|
||||
const triggers = require('./triggers');
|
||||
const { continueWhile } = require('parse/lib/node/promiseUtils');
|
||||
const AlwaysSelectedKeys = ['objectId', 'createdAt', 'updatedAt', 'ACL'];
|
||||
const { enforceRoleSecurity } = require('./SharedRest');
|
||||
|
||||
// restOptions can include:
|
||||
// skip
|
||||
// limit
|
||||
@@ -18,7 +20,80 @@ const AlwaysSelectedKeys = ['objectId', 'createdAt', 'updatedAt', 'ACL'];
|
||||
// readPreference
|
||||
// includeReadPreference
|
||||
// subqueryReadPreference
|
||||
function RestQuery(
|
||||
/**
|
||||
* Use to perform a query on a class. It will run security checks and triggers.
|
||||
* @param options
|
||||
* @param options.method {RestQuery.Method} The type of query to perform
|
||||
* @param options.config {ParseServerConfiguration} The server configuration
|
||||
* @param options.auth {Auth} The auth object for the request
|
||||
* @param options.className {string} The name of the class to query
|
||||
* @param options.restWhere {object} The where object for the query
|
||||
* @param options.restOptions {object} The options object for the query
|
||||
* @param options.clientSDK {string} The client SDK that is performing the query
|
||||
* @param options.runAfterFind {boolean} Whether to run the afterFind trigger
|
||||
* @param options.runBeforeFind {boolean} Whether to run the beforeFind trigger
|
||||
* @param options.context {object} The context object for the query
|
||||
* @returns {Promise<_UnsafeRestQuery>} A promise that is resolved with the _UnsafeRestQuery object
|
||||
*/
|
||||
async function RestQuery({
|
||||
method,
|
||||
config,
|
||||
auth,
|
||||
className,
|
||||
restWhere = {},
|
||||
restOptions = {},
|
||||
clientSDK,
|
||||
runAfterFind = true,
|
||||
runBeforeFind = true,
|
||||
context,
|
||||
}) {
|
||||
if (![RestQuery.Method.find, RestQuery.Method.get].includes(method)) {
|
||||
throw new Parse.Error(Parse.Error.INVALID_QUERY, 'bad query type');
|
||||
}
|
||||
enforceRoleSecurity(method, className, auth);
|
||||
const result = runBeforeFind
|
||||
? await triggers.maybeRunQueryTrigger(
|
||||
triggers.Types.beforeFind,
|
||||
className,
|
||||
restWhere,
|
||||
restOptions,
|
||||
config,
|
||||
auth,
|
||||
context,
|
||||
method === RestQuery.Method.get
|
||||
)
|
||||
: Promise.resolve({ restWhere, restOptions });
|
||||
|
||||
return new _UnsafeRestQuery(
|
||||
config,
|
||||
auth,
|
||||
className,
|
||||
result.restWhere || restWhere,
|
||||
result.restOptions || restOptions,
|
||||
clientSDK,
|
||||
runAfterFind,
|
||||
context
|
||||
);
|
||||
}
|
||||
|
||||
RestQuery.Method = Object.freeze({
|
||||
get: 'get',
|
||||
find: 'find',
|
||||
});
|
||||
|
||||
/**
|
||||
* _UnsafeRestQuery is meant for specific internal usage only. When you need to skip security checks or some triggers.
|
||||
* Don't use it if you don't know what you are doing.
|
||||
* @param config
|
||||
* @param auth
|
||||
* @param className
|
||||
* @param restWhere
|
||||
* @param restOptions
|
||||
* @param clientSDK
|
||||
* @param runAfterFind
|
||||
* @param context
|
||||
*/
|
||||
function _UnsafeRestQuery(
|
||||
config,
|
||||
auth,
|
||||
className,
|
||||
@@ -197,7 +272,7 @@ function RestQuery(
|
||||
// Returns a promise for the response - an object with optional keys
|
||||
// 'results' and 'count'.
|
||||
// TODO: consolidate the replaceX functions
|
||||
RestQuery.prototype.execute = function (executeOptions) {
|
||||
_UnsafeRestQuery.prototype.execute = function (executeOptions) {
|
||||
return Promise.resolve()
|
||||
.then(() => {
|
||||
return this.buildRestWhere();
|
||||
@@ -228,7 +303,7 @@ RestQuery.prototype.execute = function (executeOptions) {
|
||||
});
|
||||
};
|
||||
|
||||
RestQuery.prototype.each = function (callback) {
|
||||
_UnsafeRestQuery.prototype.each = function (callback) {
|
||||
const { config, auth, className, restWhere, restOptions, clientSDK } = this;
|
||||
// if the limit is set, use it
|
||||
restOptions.limit = restOptions.limit || 100;
|
||||
@@ -240,7 +315,9 @@ RestQuery.prototype.each = function (callback) {
|
||||
return !finished;
|
||||
},
|
||||
async () => {
|
||||
const query = new RestQuery(
|
||||
// Safe here to use _UnsafeRestQuery because the security was already
|
||||
// checked during "await RestQuery()"
|
||||
const query = new _UnsafeRestQuery(
|
||||
config,
|
||||
auth,
|
||||
className,
|
||||
@@ -262,7 +339,7 @@ RestQuery.prototype.each = function (callback) {
|
||||
);
|
||||
};
|
||||
|
||||
RestQuery.prototype.buildRestWhere = function () {
|
||||
_UnsafeRestQuery.prototype.buildRestWhere = function () {
|
||||
return Promise.resolve()
|
||||
.then(() => {
|
||||
return this.getUserAndRoleACL();
|
||||
@@ -291,7 +368,7 @@ RestQuery.prototype.buildRestWhere = function () {
|
||||
};
|
||||
|
||||
// Uses the Auth object to get the list of roles, adds the user id
|
||||
RestQuery.prototype.getUserAndRoleACL = function () {
|
||||
_UnsafeRestQuery.prototype.getUserAndRoleACL = function () {
|
||||
if (this.auth.isMaster) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
@@ -310,7 +387,7 @@ RestQuery.prototype.getUserAndRoleACL = function () {
|
||||
|
||||
// Changes the className if redirectClassNameForKey is set.
|
||||
// Returns a promise.
|
||||
RestQuery.prototype.redirectClassNameForKey = function () {
|
||||
_UnsafeRestQuery.prototype.redirectClassNameForKey = function () {
|
||||
if (!this.redirectKey) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
@@ -325,7 +402,7 @@ RestQuery.prototype.redirectClassNameForKey = function () {
|
||||
};
|
||||
|
||||
// Validates this operation against the allowClientClassCreation config.
|
||||
RestQuery.prototype.validateClientClassCreation = function () {
|
||||
_UnsafeRestQuery.prototype.validateClientClassCreation = function () {
|
||||
if (
|
||||
this.config.allowClientClassCreation === false &&
|
||||
!this.auth.isMaster &&
|
||||
@@ -368,7 +445,7 @@ function transformInQuery(inQueryObject, className, results) {
|
||||
// $inQuery clause.
|
||||
// The $inQuery clause turns into an $in with values that are just
|
||||
// pointers to the objects returned in the subquery.
|
||||
RestQuery.prototype.replaceInQuery = function () {
|
||||
_UnsafeRestQuery.prototype.replaceInQuery = async function () {
|
||||
var inQueryObject = findObjectWithKey(this.restWhere, '$inQuery');
|
||||
if (!inQueryObject) {
|
||||
return;
|
||||
@@ -391,13 +468,14 @@ RestQuery.prototype.replaceInQuery = function () {
|
||||
additionalOptions.readPreference = this.restOptions.readPreference;
|
||||
}
|
||||
|
||||
var subquery = new RestQuery(
|
||||
this.config,
|
||||
this.auth,
|
||||
inQueryValue.className,
|
||||
inQueryValue.where,
|
||||
additionalOptions
|
||||
);
|
||||
const subquery = await RestQuery({
|
||||
method: RestQuery.Method.find,
|
||||
config: this.config,
|
||||
auth: this.auth,
|
||||
className: inQueryValue.className,
|
||||
restWhere: inQueryValue.where,
|
||||
restOptions: additionalOptions,
|
||||
});
|
||||
return subquery.execute().then(response => {
|
||||
transformInQuery(inQueryObject, subquery.className, response.results);
|
||||
// Recurse to repeat
|
||||
@@ -426,7 +504,7 @@ function transformNotInQuery(notInQueryObject, className, results) {
|
||||
// $notInQuery clause.
|
||||
// The $notInQuery clause turns into a $nin with values that are just
|
||||
// pointers to the objects returned in the subquery.
|
||||
RestQuery.prototype.replaceNotInQuery = function () {
|
||||
_UnsafeRestQuery.prototype.replaceNotInQuery = async function () {
|
||||
var notInQueryObject = findObjectWithKey(this.restWhere, '$notInQuery');
|
||||
if (!notInQueryObject) {
|
||||
return;
|
||||
@@ -449,13 +527,15 @@ RestQuery.prototype.replaceNotInQuery = function () {
|
||||
additionalOptions.readPreference = this.restOptions.readPreference;
|
||||
}
|
||||
|
||||
var subquery = new RestQuery(
|
||||
this.config,
|
||||
this.auth,
|
||||
notInQueryValue.className,
|
||||
notInQueryValue.where,
|
||||
additionalOptions
|
||||
);
|
||||
const subquery = await RestQuery({
|
||||
method: RestQuery.Method.find,
|
||||
config: this.config,
|
||||
auth: this.auth,
|
||||
className: notInQueryValue.className,
|
||||
restWhere: notInQueryValue.where,
|
||||
restOptions: additionalOptions,
|
||||
});
|
||||
|
||||
return subquery.execute().then(response => {
|
||||
transformNotInQuery(notInQueryObject, subquery.className, response.results);
|
||||
// Recurse to repeat
|
||||
@@ -489,7 +569,7 @@ const transformSelect = (selectObject, key, objects) => {
|
||||
// The $select clause turns into an $in with values selected out of
|
||||
// the subquery.
|
||||
// Returns a possible-promise.
|
||||
RestQuery.prototype.replaceSelect = function () {
|
||||
_UnsafeRestQuery.prototype.replaceSelect = async function () {
|
||||
var selectObject = findObjectWithKey(this.restWhere, '$select');
|
||||
if (!selectObject) {
|
||||
return;
|
||||
@@ -519,13 +599,15 @@ RestQuery.prototype.replaceSelect = function () {
|
||||
additionalOptions.readPreference = this.restOptions.readPreference;
|
||||
}
|
||||
|
||||
var subquery = new RestQuery(
|
||||
this.config,
|
||||
this.auth,
|
||||
selectValue.query.className,
|
||||
selectValue.query.where,
|
||||
additionalOptions
|
||||
);
|
||||
const subquery = await RestQuery({
|
||||
method: RestQuery.Method.find,
|
||||
config: this.config,
|
||||
auth: this.auth,
|
||||
className: selectValue.query.className,
|
||||
restWhere: selectValue.query.where,
|
||||
restOptions: additionalOptions,
|
||||
});
|
||||
|
||||
return subquery.execute().then(response => {
|
||||
transformSelect(selectObject, selectValue.key, response.results);
|
||||
// Keep replacing $select clauses
|
||||
@@ -551,7 +633,7 @@ const transformDontSelect = (dontSelectObject, key, objects) => {
|
||||
// The $dontSelect clause turns into an $nin with values selected out of
|
||||
// the subquery.
|
||||
// Returns a possible-promise.
|
||||
RestQuery.prototype.replaceDontSelect = function () {
|
||||
_UnsafeRestQuery.prototype.replaceDontSelect = async function () {
|
||||
var dontSelectObject = findObjectWithKey(this.restWhere, '$dontSelect');
|
||||
if (!dontSelectObject) {
|
||||
return;
|
||||
@@ -579,13 +661,15 @@ RestQuery.prototype.replaceDontSelect = function () {
|
||||
additionalOptions.readPreference = this.restOptions.readPreference;
|
||||
}
|
||||
|
||||
var subquery = new RestQuery(
|
||||
this.config,
|
||||
this.auth,
|
||||
dontSelectValue.query.className,
|
||||
dontSelectValue.query.where,
|
||||
additionalOptions
|
||||
);
|
||||
const subquery = await RestQuery({
|
||||
method: RestQuery.Method.find,
|
||||
config: this.config,
|
||||
auth: this.auth,
|
||||
className: dontSelectValue.query.className,
|
||||
restWhere: dontSelectValue.query.where,
|
||||
restOptions: additionalOptions,
|
||||
});
|
||||
|
||||
return subquery.execute().then(response => {
|
||||
transformDontSelect(dontSelectObject, dontSelectValue.key, response.results);
|
||||
// Keep replacing $dontSelect clauses
|
||||
@@ -593,7 +677,7 @@ RestQuery.prototype.replaceDontSelect = function () {
|
||||
});
|
||||
};
|
||||
|
||||
const cleanResultAuthData = function (result) {
|
||||
_UnsafeRestQuery.prototype.cleanResultAuthData = function (result) {
|
||||
delete result.password;
|
||||
if (result.authData) {
|
||||
Object.keys(result.authData).forEach(provider => {
|
||||
@@ -632,7 +716,7 @@ const replaceEqualityConstraint = constraint => {
|
||||
return constraint;
|
||||
};
|
||||
|
||||
RestQuery.prototype.replaceEquality = function () {
|
||||
_UnsafeRestQuery.prototype.replaceEquality = function () {
|
||||
if (typeof this.restWhere !== 'object') {
|
||||
return;
|
||||
}
|
||||
@@ -643,7 +727,7 @@ RestQuery.prototype.replaceEquality = function () {
|
||||
|
||||
// Returns a promise for whether it was successful.
|
||||
// Populates this.response with an object that only has 'results'.
|
||||
RestQuery.prototype.runFind = function (options = {}) {
|
||||
_UnsafeRestQuery.prototype.runFind = function (options = {}) {
|
||||
if (this.findOptions.limit === 0) {
|
||||
this.response = { results: [] };
|
||||
return Promise.resolve();
|
||||
@@ -662,7 +746,7 @@ RestQuery.prototype.runFind = function (options = {}) {
|
||||
.then(results => {
|
||||
if (this.className === '_User' && !findOptions.explain) {
|
||||
for (var result of results) {
|
||||
cleanResultAuthData(result);
|
||||
this.cleanResultAuthData(result);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -679,7 +763,7 @@ RestQuery.prototype.runFind = function (options = {}) {
|
||||
|
||||
// Returns a promise for whether it was successful.
|
||||
// Populates this.response.count with the count
|
||||
RestQuery.prototype.runCount = function () {
|
||||
_UnsafeRestQuery.prototype.runCount = function () {
|
||||
if (!this.doCount) {
|
||||
return;
|
||||
}
|
||||
@@ -691,7 +775,7 @@ RestQuery.prototype.runCount = function () {
|
||||
});
|
||||
};
|
||||
|
||||
RestQuery.prototype.denyProtectedFields = async function () {
|
||||
_UnsafeRestQuery.prototype.denyProtectedFields = async function () {
|
||||
if (this.auth.isMaster) {
|
||||
return;
|
||||
}
|
||||
@@ -716,7 +800,7 @@ RestQuery.prototype.denyProtectedFields = async function () {
|
||||
};
|
||||
|
||||
// Augments this.response with all pointers on an object
|
||||
RestQuery.prototype.handleIncludeAll = function () {
|
||||
_UnsafeRestQuery.prototype.handleIncludeAll = function () {
|
||||
if (!this.includeAll) {
|
||||
return;
|
||||
}
|
||||
@@ -745,7 +829,7 @@ RestQuery.prototype.handleIncludeAll = function () {
|
||||
};
|
||||
|
||||
// Updates property `this.keys` to contain all keys but the ones unselected.
|
||||
RestQuery.prototype.handleExcludeKeys = function () {
|
||||
_UnsafeRestQuery.prototype.handleExcludeKeys = function () {
|
||||
if (!this.excludeKeys) {
|
||||
return;
|
||||
}
|
||||
@@ -763,7 +847,7 @@ RestQuery.prototype.handleExcludeKeys = function () {
|
||||
};
|
||||
|
||||
// Augments this.response with data at the paths provided in this.include.
|
||||
RestQuery.prototype.handleInclude = function () {
|
||||
_UnsafeRestQuery.prototype.handleInclude = function () {
|
||||
if (this.include.length == 0) {
|
||||
return;
|
||||
}
|
||||
@@ -790,7 +874,7 @@ RestQuery.prototype.handleInclude = function () {
|
||||
};
|
||||
|
||||
//Returns a promise of a processed set of results
|
||||
RestQuery.prototype.runAfterFindTrigger = function () {
|
||||
_UnsafeRestQuery.prototype.runAfterFindTrigger = function () {
|
||||
if (!this.response) {
|
||||
return;
|
||||
}
|
||||
@@ -910,7 +994,7 @@ function includePath(config, auth, response, path, restOptions = {}) {
|
||||
includeRestOptions.readPreference = restOptions.readPreference;
|
||||
}
|
||||
|
||||
const queryPromises = Object.keys(pointersHash).map(className => {
|
||||
const queryPromises = Object.keys(pointersHash).map(async className => {
|
||||
const objectIds = Array.from(pointersHash[className]);
|
||||
let where;
|
||||
if (objectIds.length === 1) {
|
||||
@@ -918,7 +1002,14 @@ function includePath(config, auth, response, path, restOptions = {}) {
|
||||
} else {
|
||||
where = { objectId: { $in: objectIds } };
|
||||
}
|
||||
var query = new RestQuery(config, auth, className, where, includeRestOptions);
|
||||
const query = await RestQuery({
|
||||
method: objectIds.length === 1 ? RestQuery.Method.get : RestQuery.Method.find,
|
||||
config,
|
||||
auth,
|
||||
className,
|
||||
restWhere: where,
|
||||
restOptions: includeRestOptions,
|
||||
});
|
||||
return query.execute({ op: 'get' }).then(results => {
|
||||
results.className = className;
|
||||
return Promise.resolve(results);
|
||||
@@ -1049,3 +1140,5 @@ function findObjectWithKey(root, key) {
|
||||
}
|
||||
|
||||
module.exports = RestQuery;
|
||||
// For tests
|
||||
module.exports._UnsafeRestQuery = _UnsafeRestQuery;
|
||||
|
||||
+37
-39
@@ -65,8 +65,6 @@ function RestWrite(config, auth, className, query, data, originalData, clientSDK
|
||||
}
|
||||
}
|
||||
|
||||
this.checkProhibitedKeywords(data);
|
||||
|
||||
// When the operation is complete, this.response may have several
|
||||
// fields.
|
||||
// response: the actual data to be returned
|
||||
@@ -87,7 +85,10 @@ function RestWrite(config, auth, className, query, data, originalData, clientSDK
|
||||
// Shared SchemaController to be reused to reduce the number of loadSchema() calls per request
|
||||
// Once set the schemaData should be immutable
|
||||
this.validSchemaController = null;
|
||||
this.pendingOps = {};
|
||||
this.pendingOps = {
|
||||
operations: null,
|
||||
identifier: null,
|
||||
};
|
||||
}
|
||||
|
||||
// A convenient method to perform all the steps of processing the
|
||||
@@ -219,10 +220,13 @@ RestWrite.prototype.runBeforeSaveTrigger = function () {
|
||||
}
|
||||
|
||||
const { originalObject, updatedObject } = this.buildParseObjects();
|
||||
|
||||
const identifier = updatedObject._getStateIdentifier();
|
||||
const stateController = Parse.CoreManager.getObjectStateController();
|
||||
const [pending] = stateController.getPendingOps(updatedObject._getStateIdentifier());
|
||||
this.pendingOps = { ...pending };
|
||||
const [pending] = stateController.getPendingOps(identifier);
|
||||
this.pendingOps = {
|
||||
operations: { ...pending },
|
||||
identifier,
|
||||
};
|
||||
|
||||
return Promise.resolve()
|
||||
.then(() => {
|
||||
@@ -282,7 +286,11 @@ RestWrite.prototype.runBeforeSaveTrigger = function () {
|
||||
delete this.data.objectId;
|
||||
}
|
||||
}
|
||||
this.checkProhibitedKeywords(this.data);
|
||||
try {
|
||||
Utils.checkProhibitedKeywords(this.config, this.data);
|
||||
} catch (error) {
|
||||
throw new Parse.Error(Parse.Error.INVALID_KEY_NAME, error);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -575,7 +583,7 @@ RestWrite.prototype.handleAuthData = function (authData) {
|
||||
};
|
||||
|
||||
// The non-third-party parts of User transformation
|
||||
RestWrite.prototype.transformUser = function () {
|
||||
RestWrite.prototype.transformUser = async function () {
|
||||
var promise = Promise.resolve();
|
||||
|
||||
if (this.className !== '_User') {
|
||||
@@ -591,19 +599,25 @@ RestWrite.prototype.transformUser = function () {
|
||||
if (this.query && this.objectId()) {
|
||||
// If we're updating a _User object, we need to clear out the cache for that user. Find all their
|
||||
// session tokens, and remove them from the cache.
|
||||
promise = new RestQuery(this.config, Auth.master(this.config), '_Session', {
|
||||
user: {
|
||||
__type: 'Pointer',
|
||||
className: '_User',
|
||||
objectId: this.objectId(),
|
||||
const query = await RestQuery({
|
||||
method: RestQuery.Method.find,
|
||||
config: this.config,
|
||||
auth: Auth.master(this.config),
|
||||
className: '_Session',
|
||||
runBeforeFind: false,
|
||||
restWhere: {
|
||||
user: {
|
||||
__type: 'Pointer',
|
||||
className: '_User',
|
||||
objectId: this.objectId(),
|
||||
},
|
||||
},
|
||||
})
|
||||
.execute()
|
||||
.then(results => {
|
||||
results.results.forEach(session =>
|
||||
this.config.cacheController.user.del(session.sessionToken)
|
||||
);
|
||||
});
|
||||
});
|
||||
promise = query.execute().then(results => {
|
||||
results.results.forEach(session =>
|
||||
this.config.cacheController.user.del(session.sessionToken)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
return promise
|
||||
@@ -1569,7 +1583,7 @@ RestWrite.prototype.runAfterSaveTrigger = function () {
|
||||
.then(result => {
|
||||
const jsonReturned = result && !result._toFullJSON;
|
||||
if (jsonReturned) {
|
||||
this.pendingOps = {};
|
||||
this.pendingOps.operations = {};
|
||||
this.response.response = result;
|
||||
} else {
|
||||
this.response.response = this._updateResponseWithData(
|
||||
@@ -1673,10 +1687,9 @@ RestWrite.prototype.cleanUserAuthData = function () {
|
||||
};
|
||||
|
||||
RestWrite.prototype._updateResponseWithData = function (response, data) {
|
||||
const { updatedObject } = this.buildParseObjects();
|
||||
const stateController = Parse.CoreManager.getObjectStateController();
|
||||
const [pending] = stateController.getPendingOps(updatedObject._getStateIdentifier());
|
||||
for (const key in this.pendingOps) {
|
||||
const [pending] = stateController.getPendingOps(this.pendingOps.identifier);
|
||||
for (const key in this.pendingOps.operations) {
|
||||
if (!pending[key]) {
|
||||
data[key] = this.originalData ? this.originalData[key] : { __op: 'Delete' };
|
||||
this.storage.fieldsChangedByTrigger.push(key);
|
||||
@@ -1725,20 +1738,5 @@ RestWrite.prototype._updateResponseWithData = function (response, data) {
|
||||
return response;
|
||||
};
|
||||
|
||||
RestWrite.prototype.checkProhibitedKeywords = function (data) {
|
||||
if (this.config.requestKeywordDenylist) {
|
||||
// Scan request data for denied keywords
|
||||
for (const keyword of this.config.requestKeywordDenylist) {
|
||||
const match = Utils.objectContainsKeyValue(data, keyword.key, keyword.value);
|
||||
if (match) {
|
||||
throw new Parse.Error(
|
||||
Parse.Error.INVALID_KEY_NAME,
|
||||
`Prohibited keyword in request data: ${JSON.stringify(keyword)}.`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default RestWrite;
|
||||
module.exports = RestWrite;
|
||||
|
||||
+38
-15
@@ -138,25 +138,48 @@ export class FilesRouter {
|
||||
return;
|
||||
}
|
||||
|
||||
const fileExtensions = config.fileUpload?.fileExtensions;
|
||||
if (!isMaster && fileExtensions) {
|
||||
const isValidExtension = extension => {
|
||||
return fileExtensions.some(ext => {
|
||||
if (ext === '*') {
|
||||
return true;
|
||||
}
|
||||
const regex = new RegExp(fileExtensions);
|
||||
if (regex.test(extension)) {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
};
|
||||
let extension = contentType;
|
||||
if (filename && filename.includes('.')) {
|
||||
extension = filename.split('.')[1];
|
||||
} else if (contentType && contentType.includes('/')) {
|
||||
extension = contentType.split('/')[1];
|
||||
}
|
||||
extension = extension?.split(' ')?.join('');
|
||||
|
||||
if (extension && !isValidExtension(extension)) {
|
||||
next(
|
||||
new Parse.Error(
|
||||
Parse.Error.FILE_SAVE_ERROR,
|
||||
`File upload of extension ${extension} is disabled.`
|
||||
)
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const base64 = req.body.toString('base64');
|
||||
const file = new Parse.File(filename, { base64 }, contentType);
|
||||
const { metadata = {}, tags = {} } = req.fileData || {};
|
||||
if (req.config && req.config.requestKeywordDenylist) {
|
||||
try {
|
||||
// Scan request data for denied keywords
|
||||
for (const keyword of req.config.requestKeywordDenylist) {
|
||||
const match =
|
||||
Utils.objectContainsKeyValue(metadata, keyword.key, keyword.value) ||
|
||||
Utils.objectContainsKeyValue(tags, keyword.key, keyword.value);
|
||||
if (match) {
|
||||
next(
|
||||
new Parse.Error(
|
||||
Parse.Error.INVALID_KEY_NAME,
|
||||
`Prohibited keyword in request data: ${JSON.stringify(keyword)}.`
|
||||
)
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
Utils.checkProhibitedKeywords(config, metadata);
|
||||
Utils.checkProhibitedKeywords(config, tags);
|
||||
} catch (error) {
|
||||
next(new Parse.Error(Parse.Error.INVALID_KEY_NAME, error));
|
||||
return;
|
||||
}
|
||||
file.setTags(tags);
|
||||
file.setMetadata(metadata);
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
const classesWithMasterOnlyAccess = [
|
||||
'_JobStatus',
|
||||
'_PushStatus',
|
||||
'_Hooks',
|
||||
'_GlobalConfig',
|
||||
'_JobSchedule',
|
||||
'_Idempotency',
|
||||
];
|
||||
// Disallowing access to the _Role collection except by master key
|
||||
function enforceRoleSecurity(method, className, auth) {
|
||||
if (className === '_Installation' && !auth.isMaster) {
|
||||
if (method === 'delete' || method === 'find') {
|
||||
const error = `Clients aren't allowed to perform the ${method} operation on the installation collection.`;
|
||||
throw new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, error);
|
||||
}
|
||||
}
|
||||
|
||||
//all volatileClasses are masterKey only
|
||||
if (classesWithMasterOnlyAccess.indexOf(className) >= 0 && !auth.isMaster) {
|
||||
const error = `Clients aren't allowed to perform the ${method} operation on the ${className} collection.`;
|
||||
throw new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, error);
|
||||
}
|
||||
|
||||
// readOnly masterKey is not allowed
|
||||
if (auth.isReadOnly && (method === 'delete' || method === 'create' || method === 'update')) {
|
||||
const error = `read-only masterKey isn't allowed to perform the ${method} operation.`;
|
||||
throw new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, error);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
enforceRoleSecurity,
|
||||
};
|
||||
@@ -358,6 +358,18 @@ class Utils {
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static checkProhibitedKeywords(config, data) {
|
||||
if (config?.requestKeywordDenylist) {
|
||||
// Scan request data for denied keywords
|
||||
for (const keyword of config.requestKeywordDenylist) {
|
||||
const match = Utils.objectContainsKeyValue(data, keyword.key, keyword.value);
|
||||
if (match) {
|
||||
throw `Prohibited keyword in request data: ${JSON.stringify(keyword)}.`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Utils;
|
||||
|
||||
+1
-16
@@ -280,22 +280,7 @@ export function handleParseHeaders(req, res, next) {
|
||||
}
|
||||
|
||||
function getClientIp(req) {
|
||||
if (req.headers['x-forwarded-for']) {
|
||||
// try to get from x-forwared-for if it set (behind reverse proxy)
|
||||
return req.headers['x-forwarded-for'].split(',')[0];
|
||||
} else if (req.connection && req.connection.remoteAddress) {
|
||||
// no proxy, try getting from connection.remoteAddress
|
||||
return req.connection.remoteAddress;
|
||||
} else if (req.socket) {
|
||||
// try to get it from req.socket
|
||||
return req.socket.remoteAddress;
|
||||
} else if (req.connection && req.connection.socket) {
|
||||
// try to get it form the connection.socket
|
||||
return req.connection.socket.remoteAddress;
|
||||
} else {
|
||||
// if non above, fallback.
|
||||
return req.ip;
|
||||
}
|
||||
return req.ip;
|
||||
}
|
||||
|
||||
function httpAuth(req) {
|
||||
|
||||
+63
-117
@@ -12,6 +12,7 @@ var Parse = require('parse/node').Parse;
|
||||
var RestQuery = require('./RestQuery');
|
||||
var RestWrite = require('./RestWrite');
|
||||
var triggers = require('./triggers');
|
||||
const { enforceRoleSecurity } = require('./SharedRest');
|
||||
|
||||
function checkTriggers(className, config, types) {
|
||||
return types.some(triggerType => {
|
||||
@@ -24,65 +25,34 @@ function checkLiveQuery(className, config) {
|
||||
}
|
||||
|
||||
// Returns a promise for an object with optional keys 'results' and 'count'.
|
||||
function find(config, auth, className, restWhere, restOptions, clientSDK, context) {
|
||||
enforceRoleSecurity('find', className, auth);
|
||||
return triggers
|
||||
.maybeRunQueryTrigger(
|
||||
triggers.Types.beforeFind,
|
||||
className,
|
||||
restWhere,
|
||||
restOptions,
|
||||
config,
|
||||
auth,
|
||||
context
|
||||
)
|
||||
.then(result => {
|
||||
restWhere = result.restWhere || restWhere;
|
||||
restOptions = result.restOptions || restOptions;
|
||||
const query = new RestQuery(
|
||||
config,
|
||||
auth,
|
||||
className,
|
||||
restWhere,
|
||||
restOptions,
|
||||
clientSDK,
|
||||
true,
|
||||
context
|
||||
);
|
||||
return query.execute();
|
||||
});
|
||||
}
|
||||
const find = async (config, auth, className, restWhere, restOptions, clientSDK, context) => {
|
||||
const query = await RestQuery({
|
||||
method: RestQuery.Method.find,
|
||||
config,
|
||||
auth,
|
||||
className,
|
||||
restWhere,
|
||||
restOptions,
|
||||
clientSDK,
|
||||
context,
|
||||
});
|
||||
return query.execute();
|
||||
};
|
||||
|
||||
// get is just like find but only queries an objectId.
|
||||
const get = (config, auth, className, objectId, restOptions, clientSDK, context) => {
|
||||
const get = async (config, auth, className, objectId, restOptions, clientSDK, context) => {
|
||||
var restWhere = { objectId };
|
||||
enforceRoleSecurity('get', className, auth);
|
||||
return triggers
|
||||
.maybeRunQueryTrigger(
|
||||
triggers.Types.beforeFind,
|
||||
className,
|
||||
restWhere,
|
||||
restOptions,
|
||||
config,
|
||||
auth,
|
||||
context,
|
||||
true
|
||||
)
|
||||
.then(result => {
|
||||
restWhere = result.restWhere || restWhere;
|
||||
restOptions = result.restOptions || restOptions;
|
||||
const query = new RestQuery(
|
||||
config,
|
||||
auth,
|
||||
className,
|
||||
restWhere,
|
||||
restOptions,
|
||||
clientSDK,
|
||||
true,
|
||||
context
|
||||
);
|
||||
return query.execute();
|
||||
});
|
||||
const query = await RestQuery({
|
||||
method: RestQuery.Method.get,
|
||||
config,
|
||||
auth,
|
||||
className,
|
||||
restWhere,
|
||||
restOptions,
|
||||
clientSDK,
|
||||
context,
|
||||
});
|
||||
return query.execute();
|
||||
};
|
||||
|
||||
// Returns a promise that doesn't resolve to any useful value.
|
||||
@@ -101,35 +71,40 @@ function del(config, auth, className, objectId, context) {
|
||||
let schemaController;
|
||||
|
||||
return Promise.resolve()
|
||||
.then(() => {
|
||||
.then(async () => {
|
||||
const hasTriggers = checkTriggers(className, config, ['beforeDelete', 'afterDelete']);
|
||||
const hasLiveQuery = checkLiveQuery(className, config);
|
||||
if (hasTriggers || hasLiveQuery || className == '_Session') {
|
||||
return new RestQuery(config, auth, className, { objectId })
|
||||
.execute({ op: 'delete' })
|
||||
.then(response => {
|
||||
if (response && response.results && response.results.length) {
|
||||
const firstResult = response.results[0];
|
||||
firstResult.className = className;
|
||||
if (className === '_Session' && !auth.isMaster) {
|
||||
if (!auth.user || firstResult.user.objectId !== auth.user.id) {
|
||||
throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, 'Invalid session token');
|
||||
}
|
||||
const query = await RestQuery({
|
||||
method: RestQuery.Method.get,
|
||||
config,
|
||||
auth,
|
||||
className,
|
||||
restWhere: { objectId },
|
||||
});
|
||||
return query.execute({ op: 'delete' }).then(response => {
|
||||
if (response && response.results && response.results.length) {
|
||||
const firstResult = response.results[0];
|
||||
firstResult.className = className;
|
||||
if (className === '_Session' && !auth.isMaster) {
|
||||
if (!auth.user || firstResult.user.objectId !== auth.user.id) {
|
||||
throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, 'Invalid session token');
|
||||
}
|
||||
var cacheAdapter = config.cacheController;
|
||||
cacheAdapter.user.del(firstResult.sessionToken);
|
||||
inflatedObject = Parse.Object.fromJSON(firstResult);
|
||||
return triggers.maybeRunTrigger(
|
||||
triggers.Types.beforeDelete,
|
||||
auth,
|
||||
inflatedObject,
|
||||
null,
|
||||
config,
|
||||
context
|
||||
);
|
||||
}
|
||||
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Object not found for delete.');
|
||||
});
|
||||
var cacheAdapter = config.cacheController;
|
||||
cacheAdapter.user.del(firstResult.sessionToken);
|
||||
inflatedObject = Parse.Object.fromJSON(firstResult);
|
||||
return triggers.maybeRunTrigger(
|
||||
triggers.Types.beforeDelete,
|
||||
auth,
|
||||
inflatedObject,
|
||||
null,
|
||||
config,
|
||||
context
|
||||
);
|
||||
}
|
||||
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Object not found for delete.');
|
||||
});
|
||||
}
|
||||
return Promise.resolve({});
|
||||
})
|
||||
@@ -193,21 +168,22 @@ function update(config, auth, className, restWhere, restObject, clientSDK, conte
|
||||
enforceRoleSecurity('update', className, auth);
|
||||
|
||||
return Promise.resolve()
|
||||
.then(() => {
|
||||
.then(async () => {
|
||||
const hasTriggers = checkTriggers(className, config, ['beforeSave', 'afterSave']);
|
||||
const hasLiveQuery = checkLiveQuery(className, config);
|
||||
if (hasTriggers || hasLiveQuery) {
|
||||
// Do not use find, as it runs the before finds
|
||||
return new RestQuery(
|
||||
const query = await RestQuery({
|
||||
method: RestQuery.Method.get,
|
||||
config,
|
||||
auth,
|
||||
className,
|
||||
restWhere,
|
||||
undefined,
|
||||
undefined,
|
||||
false,
|
||||
context
|
||||
).execute({
|
||||
runAfterFind: false,
|
||||
runBeforeFind: false,
|
||||
context,
|
||||
});
|
||||
return query.execute({
|
||||
op: 'update',
|
||||
});
|
||||
}
|
||||
@@ -243,36 +219,6 @@ function handleSessionMissingError(error, className, auth) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
const classesWithMasterOnlyAccess = [
|
||||
'_JobStatus',
|
||||
'_PushStatus',
|
||||
'_Hooks',
|
||||
'_GlobalConfig',
|
||||
'_JobSchedule',
|
||||
'_Idempotency',
|
||||
];
|
||||
// Disallowing access to the _Role collection except by master key
|
||||
function enforceRoleSecurity(method, className, auth) {
|
||||
if (className === '_Installation' && !auth.isMaster) {
|
||||
if (method === 'delete' || method === 'find') {
|
||||
const error = `Clients aren't allowed to perform the ${method} operation on the installation collection.`;
|
||||
throw new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, error);
|
||||
}
|
||||
}
|
||||
|
||||
//all volatileClasses are masterKey only
|
||||
if (classesWithMasterOnlyAccess.indexOf(className) >= 0 && !auth.isMaster) {
|
||||
const error = `Clients aren't allowed to perform the ${method} operation on the ${className} collection.`;
|
||||
throw new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, error);
|
||||
}
|
||||
|
||||
// readOnly masterKey is not allowed
|
||||
if (auth.isReadOnly && (method === 'delete' || method === 'create' || method === 'update')) {
|
||||
const error = `read-only masterKey isn't allowed to perform the ${method} operation.`;
|
||||
throw new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, error);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
create,
|
||||
del,
|
||||
|
||||
Reference in New Issue
Block a user