Skip to content

Commit 073f0fc

Browse files
committed
Merge remote-tracking branch 'upstream/master'
2 parents 3b337cd + 97c3046 commit 073f0fc

File tree

12 files changed

+1042
-595
lines changed

12 files changed

+1042
-595
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,11 @@
33
### master
44
[Full Changelog](https://github.com/parse-community/parse-server/compare/4.5.0...master)
55

6+
__BREAKING CHANGES:__
7+
- NEW: Added file upload restriction. File upload is now only allowed for authenticated users by default for improved security. To allow file upload also for Anonymous Users or Public, set the `fileUpload` parameter in the [Parse Server Options](https://parseplatform.org/parse-server/api/master/ParseServerOptions.html). [#7071](https://github.com/parse-community/parse-server/pull/7071). Thanks to [dblythy](https://github.com/dblythy).
8+
___
9+
- IMPROVE: Optimize queries on classes with pointer permissions. [#7061](https://github.com/parse-community/parse-server/pull/7061). Thanks to [Pedro Diaz](https://github.com/pdiaz)
10+
611
### 4.5.0
712
[Full Changelog](https://github.com/parse-community/parse-server/compare/4.4.0...4.5.0)
813

resources/buildConfigDefinitions.js

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,8 @@ function getENVPrefix(iface) {
4747
'LiveQueryOptions' : 'PARSE_SERVER_LIVEQUERY_',
4848
'IdempotencyOptions' : 'PARSE_SERVER_EXPERIMENTAL_IDEMPOTENCY_',
4949
'AccountLockoutOptions' : 'PARSE_SERVER_ACCOUNT_LOCKOUT_',
50-
'PasswordPolicyOptions' : 'PARSE_SERVER_PASSWORD_POLICY_'
50+
'PasswordPolicyOptions' : 'PARSE_SERVER_PASSWORD_POLICY_',
51+
'FileUploadOptions' : 'PARSE_SERVER_FILE_UPLOAD_'
5152
}
5253
if (options[iface.id.name]) {
5354
return options[iface.id.name]
@@ -163,14 +164,8 @@ function parseDefaultValue(elt, value, t) {
163164
if (type == 'NumberOrBoolean') {
164165
literalValue = t.numericLiteral(parsers.numberOrBoolParser('')(value));
165166
}
166-
if (type == 'CustomPagesOptions') {
167-
const object = parsers.objectParser(value);
168-
const props = Object.keys(object).map((key) => {
169-
return t.objectProperty(key, object[value]);
170-
});
171-
literalValue = t.objectExpression(props);
172-
}
173-
if (type == 'IdempotencyOptions') {
167+
const literalTypes = ['IdempotencyOptions','FileUploadOptions','CustomPagesOptions'];
168+
if (literalTypes.includes(type)) {
174169
const object = parsers.objectParser(value);
175170
const props = Object.keys(object).map((key) => {
176171
return t.objectProperty(key, object[value]);

spec/DatabaseController.spec.js

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,57 @@ describe('DatabaseController', function () {
236236
done();
237237
});
238238

239+
it('should not return a $or operation if the query involves one of the two fields also used as array/pointer permissions', done => {
240+
const clp = buildCLP(['users', 'user']);
241+
const query = { a: 'b', user: createUserPointer(USER_ID) };
242+
schemaController.testPermissionsForClassName
243+
.withArgs(CLASS_NAME, ACL_GROUP, OPERATION)
244+
.and.returnValue(false);
245+
schemaController.getClassLevelPermissions.withArgs(CLASS_NAME).and.returnValue(clp);
246+
schemaController.getExpectedType
247+
.withArgs(CLASS_NAME, 'user')
248+
.and.returnValue({ type: 'Pointer' });
249+
schemaController.getExpectedType
250+
.withArgs(CLASS_NAME, 'users')
251+
.and.returnValue({ type: 'Array' });
252+
const output = databaseController.addPointerPermissions(
253+
schemaController,
254+
CLASS_NAME,
255+
OPERATION,
256+
query,
257+
ACL_GROUP
258+
);
259+
expect(output).toEqual({ ...query, user: createUserPointer(USER_ID) });
260+
done();
261+
});
262+
263+
it('should not return a $or operation if the query involves one of the fields also used as array/pointer permissions', done => {
264+
const clp = buildCLP(['user', 'users', 'userObject']);
265+
const query = { a: 'b', user: createUserPointer(USER_ID) };
266+
schemaController.testPermissionsForClassName
267+
.withArgs(CLASS_NAME, ACL_GROUP, OPERATION)
268+
.and.returnValue(false);
269+
schemaController.getClassLevelPermissions.withArgs(CLASS_NAME).and.returnValue(clp);
270+
schemaController.getExpectedType
271+
.withArgs(CLASS_NAME, 'user')
272+
.and.returnValue({ type: 'Pointer' });
273+
schemaController.getExpectedType
274+
.withArgs(CLASS_NAME, 'users')
275+
.and.returnValue({ type: 'Array' });
276+
schemaController.getExpectedType
277+
.withArgs(CLASS_NAME, 'userObject')
278+
.and.returnValue({ type: 'Object' });
279+
const output = databaseController.addPointerPermissions(
280+
schemaController,
281+
CLASS_NAME,
282+
OPERATION,
283+
query,
284+
ACL_GROUP
285+
);
286+
expect(output).toEqual({ ...query, user: createUserPointer(USER_ID) });
287+
done();
288+
});
289+
239290
it('should throw an error if for some unexpected reason the property specified in the CLP is neither a pointer nor an array', done => {
240291
const clp = buildCLP(['user']);
241292
const query = { a: 'b' };
@@ -265,6 +316,51 @@ describe('DatabaseController', function () {
265316
done();
266317
});
267318
});
319+
320+
describe('reduceOperations', function () {
321+
const databaseController = new DatabaseController();
322+
323+
it('objectToEntriesStrings', done => {
324+
const output = databaseController.objectToEntriesStrings({ a: 1, b: 2, c: 3 });
325+
expect(output).toEqual(['"a":1', '"b":2', '"c":3']);
326+
done();
327+
});
328+
329+
it('reduceOrOperation', done => {
330+
expect(databaseController.reduceOrOperation({ a: 1 })).toEqual({ a: 1 });
331+
expect(databaseController.reduceOrOperation({ $or: [{ a: 1 }, { b: 2 }] })).toEqual({
332+
$or: [{ a: 1 }, { b: 2 }],
333+
});
334+
expect(databaseController.reduceOrOperation({ $or: [{ a: 1 }, { a: 2 }] })).toEqual({
335+
$or: [{ a: 1 }, { a: 2 }],
336+
});
337+
expect(databaseController.reduceOrOperation({ $or: [{ a: 1 }, { a: 1 }] })).toEqual({ a: 1 });
338+
expect(
339+
databaseController.reduceOrOperation({ $or: [{ a: 1, b: 2, c: 3 }, { a: 1 }] })
340+
).toEqual({ a: 1 });
341+
expect(
342+
databaseController.reduceOrOperation({ $or: [{ b: 2 }, { a: 1, b: 2, c: 3 }] })
343+
).toEqual({ b: 2 });
344+
done();
345+
});
346+
347+
it('reduceAndOperation', done => {
348+
expect(databaseController.reduceAndOperation({ a: 1 })).toEqual({ a: 1 });
349+
expect(databaseController.reduceAndOperation({ $and: [{ a: 1 }, { b: 2 }] })).toEqual({
350+
$and: [{ a: 1 }, { b: 2 }],
351+
});
352+
expect(databaseController.reduceAndOperation({ $and: [{ a: 1 }, { a: 2 }] })).toEqual({
353+
$and: [{ a: 1 }, { a: 2 }],
354+
});
355+
expect(databaseController.reduceAndOperation({ $and: [{ a: 1 }, { a: 1 }] })).toEqual({
356+
a: 1,
357+
});
358+
expect(
359+
databaseController.reduceAndOperation({ $and: [{ a: 1, b: 2, c: 3 }, { b: 2 }] })
360+
).toEqual({ a: 1, b: 2, c: 3 });
361+
done();
362+
});
363+
});
268364
});
269365

270366
function buildCLP(pointerNames) {

spec/ParseFile.spec.js

Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
'use strict';
55

66
const request = require('../lib/request');
7+
const Definitions = require('../src/Options/Definitions');
78

89
const str = 'Hello World!';
910
const data = [];
@@ -860,4 +861,196 @@ describe('Parse.File testing', () => {
860861
});
861862
});
862863
});
864+
865+
describe('file upload configuration', () => {
866+
it('allows file upload only for authenticated user by default', async () => {
867+
await reconfigureServer({
868+
fileUpload: {
869+
enableForPublic: Definitions.FileUploadOptions.enableForPublic.default,
870+
enableForAnonymousUser: Definitions.FileUploadOptions.enableForAnonymousUser.default,
871+
enableForAuthenticatedUser: Definitions.FileUploadOptions.enableForAuthenticatedUser.default,
872+
}
873+
});
874+
let file = new Parse.File('hello.txt', data, 'text/plain');
875+
await expectAsync(file.save()).toBeRejectedWith(
876+
new Parse.Error(Parse.Error.FILE_SAVE_ERROR, 'File upload by public is disabled.')
877+
);
878+
file = new Parse.File('hello.txt', data, 'text/plain');
879+
const anonUser = await Parse.AnonymousUtils.logIn();
880+
await expectAsync(file.save({ sessionToken: anonUser.getSessionToken() })).toBeRejectedWith(
881+
new Parse.Error(Parse.Error.FILE_SAVE_ERROR, 'File upload by anonymous user is disabled.')
882+
);
883+
file = new Parse.File('hello.txt', data, 'text/plain');
884+
const authUser = await Parse.User.signUp('user', 'password');
885+
await expectAsync(file.save({ sessionToken: authUser.getSessionToken() })).toBeResolved();
886+
});
887+
888+
it('allows file upload with master key', async () => {
889+
await reconfigureServer({
890+
fileUpload: {
891+
enableForPublic: false,
892+
enableForAnonymousUser: false,
893+
enableForAuthenticatedUser: false,
894+
},
895+
});
896+
const file = new Parse.File('hello.txt', data, 'text/plain');
897+
await expectAsync(file.save({ useMasterKey: true })).toBeResolved();
898+
});
899+
900+
it('rejects all file uploads', async () => {
901+
await reconfigureServer({
902+
fileUpload: {
903+
enableForPublic: false,
904+
enableForAnonymousUser: false,
905+
enableForAuthenticatedUser: false,
906+
},
907+
});
908+
let file = new Parse.File('hello.txt', data, 'text/plain');
909+
await expectAsync(file.save()).toBeRejectedWith(
910+
new Parse.Error(Parse.Error.FILE_SAVE_ERROR, 'File upload by public is disabled.')
911+
);
912+
file = new Parse.File('hello.txt', data, 'text/plain');
913+
const anonUser = await Parse.AnonymousUtils.logIn();
914+
await expectAsync(file.save({ sessionToken: anonUser.getSessionToken() })).toBeRejectedWith(
915+
new Parse.Error(Parse.Error.FILE_SAVE_ERROR, 'File upload by anonymous user is disabled.')
916+
);
917+
file = new Parse.File('hello.txt', data, 'text/plain');
918+
const authUser = await Parse.User.signUp('user', 'password');
919+
await expectAsync(file.save({ sessionToken: authUser.getSessionToken() })).toBeRejectedWith(
920+
new Parse.Error(Parse.Error.FILE_SAVE_ERROR, 'File upload by authenticated user is disabled.')
921+
);
922+
});
923+
924+
it('allows all file uploads', async () => {
925+
await reconfigureServer({
926+
fileUpload: {
927+
enableForPublic: true,
928+
enableForAnonymousUser: true,
929+
enableForAuthenticatedUser: true,
930+
},
931+
});
932+
let file = new Parse.File('hello.txt', data, 'text/plain');
933+
await expectAsync(file.save()).toBeResolved();
934+
file = new Parse.File('hello.txt', data, 'text/plain');
935+
const anonUser = await Parse.AnonymousUtils.logIn();
936+
await expectAsync(file.save({ sessionToken: anonUser.getSessionToken() })).toBeResolved();
937+
file = new Parse.File('hello.txt', data, 'text/plain');
938+
const authUser = await Parse.User.signUp('user', 'password');
939+
await expectAsync(file.save({ sessionToken: authUser.getSessionToken() })).toBeResolved();
940+
});
941+
942+
it('allows file upload only for public', async () => {
943+
await reconfigureServer({
944+
fileUpload: {
945+
enableForPublic: true,
946+
enableForAnonymousUser: false,
947+
enableForAuthenticatedUser: false,
948+
},
949+
});
950+
let file = new Parse.File('hello.txt', data, 'text/plain');
951+
await expectAsync(file.save()).toBeResolved();
952+
file = new Parse.File('hello.txt', data, 'text/plain');
953+
const anonUser = await Parse.AnonymousUtils.logIn();
954+
await expectAsync(file.save({ sessionToken: anonUser.getSessionToken() })).toBeRejectedWith(
955+
new Parse.Error(Parse.Error.FILE_SAVE_ERROR, 'File upload by anonymous user is disabled.')
956+
);
957+
file = new Parse.File('hello.txt', data, 'text/plain');
958+
const authUser = await Parse.User.signUp('user', 'password');
959+
await expectAsync(file.save({ sessionToken: authUser.getSessionToken() })).toBeRejectedWith(
960+
new Parse.Error(Parse.Error.FILE_SAVE_ERROR, 'File upload by authenticated user is disabled.')
961+
);
962+
});
963+
964+
it('allows file upload only for anonymous user', async () => {
965+
await reconfigureServer({
966+
fileUpload: {
967+
enableForPublic: false,
968+
enableForAnonymousUser: true,
969+
enableForAuthenticatedUser: false,
970+
},
971+
});
972+
let file = new Parse.File('hello.txt', data, 'text/plain');
973+
await expectAsync(file.save()).toBeRejectedWith(
974+
new Parse.Error(Parse.Error.FILE_SAVE_ERROR, 'File upload by public is disabled.')
975+
);
976+
file = new Parse.File('hello.txt', data, 'text/plain');
977+
const anonUser = await Parse.AnonymousUtils.logIn();
978+
await expectAsync(file.save({ sessionToken: anonUser.getSessionToken() })).toBeResolved();
979+
file = new Parse.File('hello.txt', data, 'text/plain');
980+
const authUser = await Parse.User.signUp('user', 'password');
981+
await expectAsync(file.save({ sessionToken: authUser.getSessionToken() })).toBeRejectedWith(
982+
new Parse.Error(Parse.Error.FILE_SAVE_ERROR, 'File upload by authenticated user is disabled.')
983+
);
984+
});
985+
986+
it('allows file upload only for authenticated user', async () => {
987+
await reconfigureServer({
988+
fileUpload: {
989+
enableForPublic: false,
990+
enableForAnonymousUser: false,
991+
enableForAuthenticatedUser: true,
992+
},
993+
});
994+
let file = new Parse.File('hello.txt', data, 'text/plain');
995+
await expectAsync(file.save()).toBeRejectedWith(
996+
new Parse.Error(Parse.Error.FILE_SAVE_ERROR, 'File upload by public is disabled.')
997+
);
998+
file = new Parse.File('hello.txt', data, 'text/plain');
999+
const anonUser = await Parse.AnonymousUtils.logIn();
1000+
await expectAsync(file.save({ sessionToken: anonUser.getSessionToken() })).toBeRejectedWith(
1001+
new Parse.Error(Parse.Error.FILE_SAVE_ERROR, 'File upload by anonymous user is disabled.')
1002+
);
1003+
file = new Parse.File('hello.txt', data, 'text/plain');
1004+
const authUser = await Parse.User.signUp('user', 'password');
1005+
await expectAsync(file.save({ sessionToken: authUser.getSessionToken() })).toBeResolved();
1006+
});
1007+
1008+
it('rejects invalid fileUpload configuration', async () => {
1009+
const invalidConfigs = [
1010+
{ fileUpload: [] },
1011+
{ fileUpload: 1 },
1012+
{ fileUpload: "string" },
1013+
];
1014+
const validConfigs = [
1015+
{ fileUpload: {} },
1016+
{ fileUpload: null },
1017+
{ fileUpload: undefined },
1018+
];
1019+
const keys = [
1020+
"enableForPublic",
1021+
"enableForAnonymousUser",
1022+
"enableForAuthenticatedUser",
1023+
];
1024+
const invalidValues = [
1025+
[],
1026+
{},
1027+
1,
1028+
"string",
1029+
null,
1030+
];
1031+
const validValues = [
1032+
undefined,
1033+
true,
1034+
false,
1035+
];
1036+
for (const config of invalidConfigs) {
1037+
await expectAsync(reconfigureServer(config)).toBeRejectedWith(
1038+
'fileUpload must be an object value.'
1039+
);
1040+
}
1041+
for (const config of validConfigs) {
1042+
await expectAsync(reconfigureServer(config)).toBeResolved();
1043+
}
1044+
for (const key of keys) {
1045+
for (const value of invalidValues) {
1046+
await expectAsync(reconfigureServer({ fileUpload: { [key]: value }})).toBeRejectedWith(
1047+
`fileUpload.${key} must be a boolean value.`
1048+
);
1049+
}
1050+
for (const value of validValues) {
1051+
await expectAsync(reconfigureServer({ fileUpload: { [key]: value }})).toBeResolved();
1052+
}
1053+
}
1054+
});
1055+
});
8631056
});

spec/helper.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,11 @@ const defaultConfiguration = {
8888
fileKey: 'test',
8989
silent,
9090
logLevel,
91+
fileUpload: {
92+
enableForPublic: true,
93+
enableForAnonymousUser: true,
94+
enableForAuthenticatedUser: true,
95+
},
9196
push: {
9297
android: {
9398
senderId: 'yolo',

0 commit comments

Comments
 (0)