Skip to content

Commit 02b56de

Browse files
committed
Merge branch 'android-installation-duplicate-token-test' into installation-handling-fix
2 parents c9d4f76 + aee968a commit 02b56de

13 files changed

+225
-22
lines changed

.github/ISSUE_TEMPLATE.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
Make sure these boxes are checked before submitting your issue -- thanks for reporting issues back to Parse Server!
2+
3+
-[ ] You've met the [prerequisites](https://github.com/ParsePlatform/parse-server/wiki/Parse-Server-Guide#prerequisites).
4+
5+
-[ ] You're running the [latest version](https://github.com/ParsePlatform/parse-server/releases) of Parse Server.
6+
7+
-[ ] You've searched through [existing issues](https://github.com/ParsePlatform/parse-server/issues?utf8=%E2%9C%93&q=). Chances are that your issue has been reported or resolved before.
8+
9+
#### Environment Setup
10+
11+
12+
#### Steps to reproduce
13+
14+
15+
#### Logs/Trace

spec/ParseInstallation.spec.js

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -445,6 +445,52 @@ describe('Installations', () => {
445445
});
446446
});
447447

448+
it('update android device token with duplicate device token', (done) => {
449+
var installId1 = '11111111-abcd-abcd-abcd-123456789abc';
450+
var installId2 = '22222222-abcd-abcd-abcd-123456789abc';
451+
var t = '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef';
452+
var input = {
453+
'installationId': installId1,
454+
'deviceToken': t,
455+
'deviceType': 'android'
456+
};
457+
var firstObject;
458+
var secondObject;
459+
rest.create(config, auth.nobody(config), '_Installation', input)
460+
.then(() => {
461+
input = {
462+
'installationId': installId2,
463+
'deviceType': 'android'
464+
};
465+
return rest.create(config, auth.nobody(config), '_Installation', input);
466+
}).then(() => {
467+
return database.mongoFind('_Installation',
468+
{installationId: installId1}, {});
469+
}).then((results) => {
470+
expect(results.length).toEqual(1);
471+
firstObject = results[0];
472+
return database.mongoFind('_Installation',
473+
{installationId: installId2}, {});
474+
}).then((results) => {
475+
expect(results.length).toEqual(1);
476+
secondObject = results[0];
477+
// Update second installation to conflict with first installation
478+
input = {
479+
'objectId': secondObject._id,
480+
'deviceToken': t
481+
};
482+
return rest.update(config, auth.nobody(config), '_Installation',
483+
secondObject._id, input);
484+
}).then(() => {
485+
// The first object should have been deleted
486+
return database.mongoFind('_Installation', {_id: firstObject._id}, {});
487+
}).then((results) => {
488+
expect(results.length).toEqual(0);
489+
done();
490+
}).catch((error) => { console.log(error); });
491+
});
492+
493+
448494
it('update ios device token with duplicate device token', (done) => {
449495
var installId1 = '11111111-abcd-abcd-abcd-123456789abc';
450496
var installId2 = '22222222-abcd-abcd-abcd-123456789abc';

spec/ParseUser.spec.js

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1720,7 +1720,17 @@ describe('Parse.User testing', () => {
17201720
expect(e.code).toEqual(Parse.Error.SESSION_MISSING);
17211721
done();
17221722
});
1723-
})
1723+
});
1724+
1725+
it('support user/password signup with empty authData block', (done) => {
1726+
// The android SDK can send an empty authData object along with username and password.
1727+
Parse.User.signUp('artof', 'thedeal', { authData: {} }).then((user) => {
1728+
done();
1729+
}, (error) => {
1730+
fail('Signup should have succeeded.');
1731+
done();
1732+
});
1733+
});
17241734

17251735
});
17261736

spec/RestCreate.spec.js

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// These tests check the "create" functionality of the REST API.
1+
// These tests check the "create" / "update" functionality of the REST API.
22
var auth = require('../src/Auth');
33
var cache = require('../src/cache');
44
var Config = require('../src/Config');
@@ -41,6 +41,52 @@ describe('rest create', () => {
4141
});
4242
});
4343

44+
it('handles object and subdocument', (done) => {
45+
var obj = {
46+
subdoc: {foo: 'bar', wu: 'tan'},
47+
};
48+
rest.create(config, auth.nobody(config), 'MyClass', obj).then(() => {
49+
return database.mongoFind('MyClass', {}, {});
50+
}).then((results) => {
51+
expect(results.length).toEqual(1);
52+
var mob = results[0];
53+
expect(typeof mob.subdoc).toBe('object');
54+
expect(mob.subdoc.foo).toBe('bar');
55+
expect(mob.subdoc.wu).toBe('tan');
56+
expect(typeof mob._id).toEqual('string');
57+
58+
var obj = {
59+
'subdoc.wu': 'clan',
60+
};
61+
62+
rest.update(config, auth.nobody(config), 'MyClass', mob._id, obj).then(() => {
63+
return database.mongoFind('MyClass', {}, {});
64+
}).then((results) => {
65+
expect(results.length).toEqual(1);
66+
var mob = results[0];
67+
expect(typeof mob.subdoc).toBe('object');
68+
expect(mob.subdoc.foo).toBe('bar');
69+
expect(mob.subdoc.wu).toBe('clan');
70+
done();
71+
});
72+
73+
});
74+
});
75+
76+
it('handles create on non-existent class when disabled client class creation', (done) => {
77+
var customConfig = Object.assign({}, config, {allowClientClassCreation: false});
78+
rest.create(customConfig, auth.nobody(customConfig), 'ClientClassCreation', {})
79+
.then(() => {
80+
fail('Should throw an error');
81+
done();
82+
}, (err) => {
83+
expect(err.code).toEqual(Parse.Error.OPERATION_FORBIDDEN);
84+
expect(err.message).toEqual('This user is not allowed to access ' +
85+
'non-existent class: ClientClassCreation');
86+
done();
87+
});
88+
});
89+
4490
it('handles user signup', (done) => {
4591
var user = {
4692
username: 'asdf',

spec/RestQuery.spec.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,20 @@ describe('rest query', () => {
9595
}).catch((error) => { console.log(error); });
9696
});
9797

98+
it('query non-existent class when disabled client class creation', (done) => {
99+
var customConfig = Object.assign({}, config, {allowClientClassCreation: false});
100+
rest.find(customConfig, auth.nobody(customConfig), 'ClientClassCreation', {})
101+
.then(() => {
102+
fail('Should throw an error');
103+
done();
104+
}, (err) => {
105+
expect(err.code).toEqual(Parse.Error.OPERATION_FORBIDDEN);
106+
expect(err.message).toEqual('This user is not allowed to access ' +
107+
'non-existent class: ClientClassCreation');
108+
done();
109+
});
110+
});
111+
98112
it('query with wrongly encoded parameter', (done) => {
99113
rest.create(config, nobody, 'TestParameterEncode', {foo: 'bar'}
100114
).then(() => {

spec/Schema.spec.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,17 @@ describe('Schema', () => {
2929
});
3030
});
3131

32+
it('can validate one object with dot notation', (done) => {
33+
config.database.loadSchema().then((schema) => {
34+
return schema.validateObject('TestObjectWithSubDoc', {x: false, y: 'YY', z: 1, 'aObject.k1': 'newValue'});
35+
}).then((schema) => {
36+
done();
37+
}, (error) => {
38+
fail(error);
39+
done();
40+
});
41+
});
42+
3243
it('can validate two objects in a row', (done) => {
3344
config.database.loadSchema().then((schema) => {
3445
return schema.validateObject('Foo', {x: true, y: 'yyy', z: 0});

src/Config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ export class Config {
2626
this.fileKey = cacheInfo.fileKey;
2727
this.facebookAppIds = cacheInfo.facebookAppIds;
2828
this.enableAnonymousUsers = cacheInfo.enableAnonymousUsers;
29+
this.allowClientClassCreation = cacheInfo.allowClientClassCreation;
2930
this.database = DatabaseAdapter.getDatabaseConnection(applicationId);
3031
this.hooksController = cacheInfo.hooksController;
3132
this.filesController = cacheInfo.filesController;

src/RestQuery.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,8 @@ RestQuery.prototype.execute = function() {
115115
return this.getUserAndRoleACL();
116116
}).then(() => {
117117
return this.redirectClassNameForKey();
118+
}).then(() => {
119+
return this.validateClientClassCreation();
118120
}).then(() => {
119121
return this.replaceSelect();
120122
}).then(() => {
@@ -161,6 +163,25 @@ RestQuery.prototype.redirectClassNameForKey = function() {
161163
});
162164
};
163165

166+
// Validates this operation against the allowClientClassCreation config.
167+
RestQuery.prototype.validateClientClassCreation = function() {
168+
if (this.config.allowClientClassCreation === false && !this.auth.isMaster) {
169+
return this.config.database.loadSchema().then((schema) => {
170+
return schema.hasClass(this.className)
171+
}).then((hasClass) => {
172+
if (hasClass === true) {
173+
return Promise.resolve();
174+
}
175+
176+
throw new Parse.Error(Parse.Error.OPERATION_FORBIDDEN,
177+
'This user is not allowed to access ' +
178+
'non-existent class: ' + this.className);
179+
});
180+
} else {
181+
return Promise.resolve();
182+
}
183+
};
184+
164185
// Replaces a $inQuery clause by running the subquery, if there is an
165186
// $inQuery clause.
166187
// The $inQuery clause turns into an $in with values that are just

src/RestWrite.js

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,8 @@ function RestWrite(config, auth, className, query, data, originalData) {
5959
RestWrite.prototype.execute = function() {
6060
return Promise.resolve().then(() => {
6161
return this.getUserAndRoleACL();
62+
}).then(() => {
63+
return this.validateClientClassCreation();
6264
}).then(() => {
6365
return this.validateSchema();
6466
}).then(() => {
@@ -105,6 +107,25 @@ RestWrite.prototype.getUserAndRoleACL = function() {
105107
}
106108
};
107109

110+
// Validates this operation against the allowClientClassCreation config.
111+
RestWrite.prototype.validateClientClassCreation = function() {
112+
if (this.config.allowClientClassCreation === false && !this.auth.isMaster) {
113+
return this.config.database.loadSchema().then((schema) => {
114+
return schema.hasClass(this.className)
115+
}).then((hasClass) => {
116+
if (hasClass === true) {
117+
return Promise.resolve();
118+
}
119+
120+
throw new Parse.Error(Parse.Error.OPERATION_FORBIDDEN,
121+
'This user is not allowed to access ' +
122+
'non-existent class: ' + this.className);
123+
});
124+
} else {
125+
return Promise.resolve();
126+
}
127+
};
128+
108129
// Validates this operation against the schema.
109130
RestWrite.prototype.validateSchema = function() {
110131
return this.config.database.validateObject(this.className, this.data, this.query);
@@ -176,7 +197,7 @@ RestWrite.prototype.validateAuthData = function() {
176197
}
177198
}
178199

179-
if (!this.data.authData) {
200+
if (!this.data.authData || !Object.keys(this.data.authData).length) {
180201
return;
181202
}
182203

src/Schema.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -426,6 +426,12 @@ Schema.prototype.validateField = function(className, key, type, freeze) {
426426
// Just to check that the key is valid
427427
transform.transformKey(this, className, key);
428428

429+
if( key.indexOf(".") > 0 ) {
430+
// subdocument key (x.y) => ok if x is of type 'object'
431+
key = key.split(".")[ 0 ];
432+
type = 'object';
433+
}
434+
429435
var expected = this.data[className][key];
430436
if (expected) {
431437
expected = (expected === 'map' ? 'object' : expected);

src/cli/cli-definitions.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,16 @@ export default {
8585
return false;
8686
}
8787
},
88+
"allowClientClassCreation": {
89+
env: "PARSE_SERVER_ALLOW_CLIENT_CLASS_CREATION",
90+
help: "Enable (or disable) client class creation, defaults to true",
91+
action: function(opt) {
92+
if (opt == "true" || opt == "1") {
93+
return true;
94+
}
95+
return false;
96+
}
97+
},
8898
"mountPath": {
8999
env: "PARSE_SERVER_MOUNT_PATH",
90100
help: "Mount path for the server, defaults to /parse",

src/index.js

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -79,20 +79,21 @@ function ParseServer({
7979
databaseURI,
8080
cloud,
8181
collectionPrefix = '',
82-
clientKey = '',
83-
javascriptKey = randomString(20),
84-
dotNetKey = '',
85-
restAPIKey = '',
82+
clientKey,
83+
javascriptKey,
84+
dotNetKey,
85+
restAPIKey,
8686
fileKey = 'invalid-file-key',
8787
facebookAppIds = [],
8888
enableAnonymousUsers = true,
89+
allowClientClassCreation = true,
8990
oauth = {},
9091
serverURL = requiredParameter('You must provide a serverURL!'),
9192
maxUploadSize = '20mb'
9293
}) {
9394

9495
// Initialize the node client SDK automatically
95-
Parse.initialize(appId, javascriptKey, masterKey);
96+
Parse.initialize(appId, javascriptKey || 'unused', masterKey);
9697
Parse.serverURL = serverURL;
9798

9899
if (databaseAdapter) {
@@ -139,6 +140,7 @@ function ParseServer({
139140
loggerController: loggerController,
140141
hooksController: hooksController,
141142
enableAnonymousUsers: enableAnonymousUsers,
143+
allowClientClassCreation: allowClientClassCreation,
142144
oauth: oauth,
143145
};
144146

src/middlewares.js

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -99,20 +99,20 @@ function handleParseHeaders(req, res, next) {
9999

100100
// Client keys are not required in parse-server, but if any have been configured in the server, validate them
101101
// to preserve original behavior.
102-
var keyRequired = (req.config.clientKey
103-
|| req.config.javascriptKey
104-
|| req.config.dotNetKey
105-
|| req.config.restAPIKey);
106-
var keyHandled = false;
107-
if (keyRequired
108-
&& ((info.clientKey && req.config.clientKey && info.clientKey === req.config.clientKey)
109-
|| (info.javascriptKey && req.config.javascriptKey && info.javascriptKey === req.config.javascriptKey)
110-
|| (info.dotNetKey && req.config.dotNetKey && info.dotNetKey === req.config.dotNetKey)
111-
|| (info.restAPIKey && req.config.restAPIKey && info.restAPIKey === req.config.restAPIKey)
112-
)) {
113-
keyHandled = true;
114-
}
115-
if (keyRequired && !keyHandled) {
102+
let keys = ["clientKey", "javascriptKey", "dotNetKey", "restAPIKey"];
103+
104+
// We do it with mismatching keys to support no-keys config
105+
var keyMismatch = keys.reduce(function(mismatch, key){
106+
107+
// check if set in the config and compare
108+
if (req.config[key] && info[key] !== req.config[key]) {
109+
mismatch++;
110+
}
111+
return mismatch;
112+
}, 0);
113+
114+
// All keys mismatch
115+
if (keyMismatch == keys.length) {
116116
return invalidRequest(req, res);
117117
}
118118

0 commit comments

Comments
 (0)