Merge branch 'android-installation-duplicate-token-test' into installation-handling-fix
This commit is contained in:
15
.github/ISSUE_TEMPLATE.md
vendored
Normal file
15
.github/ISSUE_TEMPLATE.md
vendored
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
Make sure these boxes are checked before submitting your issue -- thanks for reporting issues back to Parse Server!
|
||||||
|
|
||||||
|
-[ ] You've met the [prerequisites](https://github.com/ParsePlatform/parse-server/wiki/Parse-Server-Guide#prerequisites).
|
||||||
|
|
||||||
|
-[ ] You're running the [latest version](https://github.com/ParsePlatform/parse-server/releases) of Parse Server.
|
||||||
|
|
||||||
|
-[ ] 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.
|
||||||
|
|
||||||
|
#### Environment Setup
|
||||||
|
|
||||||
|
|
||||||
|
#### Steps to reproduce
|
||||||
|
|
||||||
|
|
||||||
|
#### Logs/Trace
|
||||||
@@ -445,6 +445,52 @@ describe('Installations', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('update android device token with duplicate device token', (done) => {
|
||||||
|
var installId1 = '11111111-abcd-abcd-abcd-123456789abc';
|
||||||
|
var installId2 = '22222222-abcd-abcd-abcd-123456789abc';
|
||||||
|
var t = '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef';
|
||||||
|
var input = {
|
||||||
|
'installationId': installId1,
|
||||||
|
'deviceToken': t,
|
||||||
|
'deviceType': 'android'
|
||||||
|
};
|
||||||
|
var firstObject;
|
||||||
|
var secondObject;
|
||||||
|
rest.create(config, auth.nobody(config), '_Installation', input)
|
||||||
|
.then(() => {
|
||||||
|
input = {
|
||||||
|
'installationId': installId2,
|
||||||
|
'deviceType': 'android'
|
||||||
|
};
|
||||||
|
return rest.create(config, auth.nobody(config), '_Installation', input);
|
||||||
|
}).then(() => {
|
||||||
|
return database.mongoFind('_Installation',
|
||||||
|
{installationId: installId1}, {});
|
||||||
|
}).then((results) => {
|
||||||
|
expect(results.length).toEqual(1);
|
||||||
|
firstObject = results[0];
|
||||||
|
return database.mongoFind('_Installation',
|
||||||
|
{installationId: installId2}, {});
|
||||||
|
}).then((results) => {
|
||||||
|
expect(results.length).toEqual(1);
|
||||||
|
secondObject = results[0];
|
||||||
|
// Update second installation to conflict with first installation
|
||||||
|
input = {
|
||||||
|
'objectId': secondObject._id,
|
||||||
|
'deviceToken': t
|
||||||
|
};
|
||||||
|
return rest.update(config, auth.nobody(config), '_Installation',
|
||||||
|
secondObject._id, input);
|
||||||
|
}).then(() => {
|
||||||
|
// The first object should have been deleted
|
||||||
|
return database.mongoFind('_Installation', {_id: firstObject._id}, {});
|
||||||
|
}).then((results) => {
|
||||||
|
expect(results.length).toEqual(0);
|
||||||
|
done();
|
||||||
|
}).catch((error) => { console.log(error); });
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
it('update ios device token with duplicate device token', (done) => {
|
it('update ios device token with duplicate device token', (done) => {
|
||||||
var installId1 = '11111111-abcd-abcd-abcd-123456789abc';
|
var installId1 = '11111111-abcd-abcd-abcd-123456789abc';
|
||||||
var installId2 = '22222222-abcd-abcd-abcd-123456789abc';
|
var installId2 = '22222222-abcd-abcd-abcd-123456789abc';
|
||||||
|
|||||||
@@ -1720,7 +1720,17 @@ describe('Parse.User testing', () => {
|
|||||||
expect(e.code).toEqual(Parse.Error.SESSION_MISSING);
|
expect(e.code).toEqual(Parse.Error.SESSION_MISSING);
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
})
|
});
|
||||||
|
|
||||||
|
it('support user/password signup with empty authData block', (done) => {
|
||||||
|
// The android SDK can send an empty authData object along with username and password.
|
||||||
|
Parse.User.signUp('artof', 'thedeal', { authData: {} }).then((user) => {
|
||||||
|
done();
|
||||||
|
}, (error) => {
|
||||||
|
fail('Signup should have succeeded.');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// These tests check the "create" functionality of the REST API.
|
// These tests check the "create" / "update" functionality of the REST API.
|
||||||
var auth = require('../src/Auth');
|
var auth = require('../src/Auth');
|
||||||
var cache = require('../src/cache');
|
var cache = require('../src/cache');
|
||||||
var Config = require('../src/Config');
|
var Config = require('../src/Config');
|
||||||
@@ -41,6 +41,52 @@ describe('rest create', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('handles object and subdocument', (done) => {
|
||||||
|
var obj = {
|
||||||
|
subdoc: {foo: 'bar', wu: 'tan'},
|
||||||
|
};
|
||||||
|
rest.create(config, auth.nobody(config), 'MyClass', obj).then(() => {
|
||||||
|
return database.mongoFind('MyClass', {}, {});
|
||||||
|
}).then((results) => {
|
||||||
|
expect(results.length).toEqual(1);
|
||||||
|
var mob = results[0];
|
||||||
|
expect(typeof mob.subdoc).toBe('object');
|
||||||
|
expect(mob.subdoc.foo).toBe('bar');
|
||||||
|
expect(mob.subdoc.wu).toBe('tan');
|
||||||
|
expect(typeof mob._id).toEqual('string');
|
||||||
|
|
||||||
|
var obj = {
|
||||||
|
'subdoc.wu': 'clan',
|
||||||
|
};
|
||||||
|
|
||||||
|
rest.update(config, auth.nobody(config), 'MyClass', mob._id, obj).then(() => {
|
||||||
|
return database.mongoFind('MyClass', {}, {});
|
||||||
|
}).then((results) => {
|
||||||
|
expect(results.length).toEqual(1);
|
||||||
|
var mob = results[0];
|
||||||
|
expect(typeof mob.subdoc).toBe('object');
|
||||||
|
expect(mob.subdoc.foo).toBe('bar');
|
||||||
|
expect(mob.subdoc.wu).toBe('clan');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles create on non-existent class when disabled client class creation', (done) => {
|
||||||
|
var customConfig = Object.assign({}, config, {allowClientClassCreation: false});
|
||||||
|
rest.create(customConfig, auth.nobody(customConfig), 'ClientClassCreation', {})
|
||||||
|
.then(() => {
|
||||||
|
fail('Should throw an error');
|
||||||
|
done();
|
||||||
|
}, (err) => {
|
||||||
|
expect(err.code).toEqual(Parse.Error.OPERATION_FORBIDDEN);
|
||||||
|
expect(err.message).toEqual('This user is not allowed to access ' +
|
||||||
|
'non-existent class: ClientClassCreation');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('handles user signup', (done) => {
|
it('handles user signup', (done) => {
|
||||||
var user = {
|
var user = {
|
||||||
username: 'asdf',
|
username: 'asdf',
|
||||||
|
|||||||
@@ -95,6 +95,20 @@ describe('rest query', () => {
|
|||||||
}).catch((error) => { console.log(error); });
|
}).catch((error) => { console.log(error); });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('query non-existent class when disabled client class creation', (done) => {
|
||||||
|
var customConfig = Object.assign({}, config, {allowClientClassCreation: false});
|
||||||
|
rest.find(customConfig, auth.nobody(customConfig), 'ClientClassCreation', {})
|
||||||
|
.then(() => {
|
||||||
|
fail('Should throw an error');
|
||||||
|
done();
|
||||||
|
}, (err) => {
|
||||||
|
expect(err.code).toEqual(Parse.Error.OPERATION_FORBIDDEN);
|
||||||
|
expect(err.message).toEqual('This user is not allowed to access ' +
|
||||||
|
'non-existent class: ClientClassCreation');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('query with wrongly encoded parameter', (done) => {
|
it('query with wrongly encoded parameter', (done) => {
|
||||||
rest.create(config, nobody, 'TestParameterEncode', {foo: 'bar'}
|
rest.create(config, nobody, 'TestParameterEncode', {foo: 'bar'}
|
||||||
).then(() => {
|
).then(() => {
|
||||||
|
|||||||
@@ -29,6 +29,17 @@ describe('Schema', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('can validate one object with dot notation', (done) => {
|
||||||
|
config.database.loadSchema().then((schema) => {
|
||||||
|
return schema.validateObject('TestObjectWithSubDoc', {x: false, y: 'YY', z: 1, 'aObject.k1': 'newValue'});
|
||||||
|
}).then((schema) => {
|
||||||
|
done();
|
||||||
|
}, (error) => {
|
||||||
|
fail(error);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('can validate two objects in a row', (done) => {
|
it('can validate two objects in a row', (done) => {
|
||||||
config.database.loadSchema().then((schema) => {
|
config.database.loadSchema().then((schema) => {
|
||||||
return schema.validateObject('Foo', {x: true, y: 'yyy', z: 0});
|
return schema.validateObject('Foo', {x: true, y: 'yyy', z: 0});
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ export class Config {
|
|||||||
this.fileKey = cacheInfo.fileKey;
|
this.fileKey = cacheInfo.fileKey;
|
||||||
this.facebookAppIds = cacheInfo.facebookAppIds;
|
this.facebookAppIds = cacheInfo.facebookAppIds;
|
||||||
this.enableAnonymousUsers = cacheInfo.enableAnonymousUsers;
|
this.enableAnonymousUsers = cacheInfo.enableAnonymousUsers;
|
||||||
|
this.allowClientClassCreation = cacheInfo.allowClientClassCreation;
|
||||||
this.database = DatabaseAdapter.getDatabaseConnection(applicationId);
|
this.database = DatabaseAdapter.getDatabaseConnection(applicationId);
|
||||||
this.hooksController = cacheInfo.hooksController;
|
this.hooksController = cacheInfo.hooksController;
|
||||||
this.filesController = cacheInfo.filesController;
|
this.filesController = cacheInfo.filesController;
|
||||||
|
|||||||
@@ -115,6 +115,8 @@ RestQuery.prototype.execute = function() {
|
|||||||
return this.getUserAndRoleACL();
|
return this.getUserAndRoleACL();
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
return this.redirectClassNameForKey();
|
return this.redirectClassNameForKey();
|
||||||
|
}).then(() => {
|
||||||
|
return this.validateClientClassCreation();
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
return this.replaceSelect();
|
return this.replaceSelect();
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
@@ -161,6 +163,25 @@ RestQuery.prototype.redirectClassNameForKey = function() {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Validates this operation against the allowClientClassCreation config.
|
||||||
|
RestQuery.prototype.validateClientClassCreation = function() {
|
||||||
|
if (this.config.allowClientClassCreation === false && !this.auth.isMaster) {
|
||||||
|
return this.config.database.loadSchema().then((schema) => {
|
||||||
|
return schema.hasClass(this.className)
|
||||||
|
}).then((hasClass) => {
|
||||||
|
if (hasClass === true) {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Parse.Error(Parse.Error.OPERATION_FORBIDDEN,
|
||||||
|
'This user is not allowed to access ' +
|
||||||
|
'non-existent class: ' + this.className);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Replaces a $inQuery clause by running the subquery, if there is an
|
// Replaces a $inQuery clause by running the subquery, if there is an
|
||||||
// $inQuery clause.
|
// $inQuery clause.
|
||||||
// The $inQuery clause turns into an $in with values that are just
|
// The $inQuery clause turns into an $in with values that are just
|
||||||
|
|||||||
@@ -59,6 +59,8 @@ function RestWrite(config, auth, className, query, data, originalData) {
|
|||||||
RestWrite.prototype.execute = function() {
|
RestWrite.prototype.execute = function() {
|
||||||
return Promise.resolve().then(() => {
|
return Promise.resolve().then(() => {
|
||||||
return this.getUserAndRoleACL();
|
return this.getUserAndRoleACL();
|
||||||
|
}).then(() => {
|
||||||
|
return this.validateClientClassCreation();
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
return this.validateSchema();
|
return this.validateSchema();
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
@@ -105,6 +107,25 @@ RestWrite.prototype.getUserAndRoleACL = function() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Validates this operation against the allowClientClassCreation config.
|
||||||
|
RestWrite.prototype.validateClientClassCreation = function() {
|
||||||
|
if (this.config.allowClientClassCreation === false && !this.auth.isMaster) {
|
||||||
|
return this.config.database.loadSchema().then((schema) => {
|
||||||
|
return schema.hasClass(this.className)
|
||||||
|
}).then((hasClass) => {
|
||||||
|
if (hasClass === true) {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Parse.Error(Parse.Error.OPERATION_FORBIDDEN,
|
||||||
|
'This user is not allowed to access ' +
|
||||||
|
'non-existent class: ' + this.className);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Validates this operation against the schema.
|
// Validates this operation against the schema.
|
||||||
RestWrite.prototype.validateSchema = function() {
|
RestWrite.prototype.validateSchema = function() {
|
||||||
return this.config.database.validateObject(this.className, this.data, this.query);
|
return this.config.database.validateObject(this.className, this.data, this.query);
|
||||||
@@ -176,7 +197,7 @@ RestWrite.prototype.validateAuthData = function() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.data.authData) {
|
if (!this.data.authData || !Object.keys(this.data.authData).length) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -426,6 +426,12 @@ Schema.prototype.validateField = function(className, key, type, freeze) {
|
|||||||
// Just to check that the key is valid
|
// Just to check that the key is valid
|
||||||
transform.transformKey(this, className, key);
|
transform.transformKey(this, className, key);
|
||||||
|
|
||||||
|
if( key.indexOf(".") > 0 ) {
|
||||||
|
// subdocument key (x.y) => ok if x is of type 'object'
|
||||||
|
key = key.split(".")[ 0 ];
|
||||||
|
type = 'object';
|
||||||
|
}
|
||||||
|
|
||||||
var expected = this.data[className][key];
|
var expected = this.data[className][key];
|
||||||
if (expected) {
|
if (expected) {
|
||||||
expected = (expected === 'map' ? 'object' : expected);
|
expected = (expected === 'map' ? 'object' : expected);
|
||||||
|
|||||||
@@ -85,6 +85,16 @@ export default {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"allowClientClassCreation": {
|
||||||
|
env: "PARSE_SERVER_ALLOW_CLIENT_CLASS_CREATION",
|
||||||
|
help: "Enable (or disable) client class creation, defaults to true",
|
||||||
|
action: function(opt) {
|
||||||
|
if (opt == "true" || opt == "1") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
"mountPath": {
|
"mountPath": {
|
||||||
env: "PARSE_SERVER_MOUNT_PATH",
|
env: "PARSE_SERVER_MOUNT_PATH",
|
||||||
help: "Mount path for the server, defaults to /parse",
|
help: "Mount path for the server, defaults to /parse",
|
||||||
|
|||||||
12
src/index.js
12
src/index.js
@@ -79,20 +79,21 @@ function ParseServer({
|
|||||||
databaseURI,
|
databaseURI,
|
||||||
cloud,
|
cloud,
|
||||||
collectionPrefix = '',
|
collectionPrefix = '',
|
||||||
clientKey = '',
|
clientKey,
|
||||||
javascriptKey = randomString(20),
|
javascriptKey,
|
||||||
dotNetKey = '',
|
dotNetKey,
|
||||||
restAPIKey = '',
|
restAPIKey,
|
||||||
fileKey = 'invalid-file-key',
|
fileKey = 'invalid-file-key',
|
||||||
facebookAppIds = [],
|
facebookAppIds = [],
|
||||||
enableAnonymousUsers = true,
|
enableAnonymousUsers = true,
|
||||||
|
allowClientClassCreation = true,
|
||||||
oauth = {},
|
oauth = {},
|
||||||
serverURL = requiredParameter('You must provide a serverURL!'),
|
serverURL = requiredParameter('You must provide a serverURL!'),
|
||||||
maxUploadSize = '20mb'
|
maxUploadSize = '20mb'
|
||||||
}) {
|
}) {
|
||||||
|
|
||||||
// Initialize the node client SDK automatically
|
// Initialize the node client SDK automatically
|
||||||
Parse.initialize(appId, javascriptKey, masterKey);
|
Parse.initialize(appId, javascriptKey || 'unused', masterKey);
|
||||||
Parse.serverURL = serverURL;
|
Parse.serverURL = serverURL;
|
||||||
|
|
||||||
if (databaseAdapter) {
|
if (databaseAdapter) {
|
||||||
@@ -139,6 +140,7 @@ function ParseServer({
|
|||||||
loggerController: loggerController,
|
loggerController: loggerController,
|
||||||
hooksController: hooksController,
|
hooksController: hooksController,
|
||||||
enableAnonymousUsers: enableAnonymousUsers,
|
enableAnonymousUsers: enableAnonymousUsers,
|
||||||
|
allowClientClassCreation: allowClientClassCreation,
|
||||||
oauth: oauth,
|
oauth: oauth,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -99,20 +99,20 @@ function handleParseHeaders(req, res, next) {
|
|||||||
|
|
||||||
// Client keys are not required in parse-server, but if any have been configured in the server, validate them
|
// Client keys are not required in parse-server, but if any have been configured in the server, validate them
|
||||||
// to preserve original behavior.
|
// to preserve original behavior.
|
||||||
var keyRequired = (req.config.clientKey
|
let keys = ["clientKey", "javascriptKey", "dotNetKey", "restAPIKey"];
|
||||||
|| req.config.javascriptKey
|
|
||||||
|| req.config.dotNetKey
|
// We do it with mismatching keys to support no-keys config
|
||||||
|| req.config.restAPIKey);
|
var keyMismatch = keys.reduce(function(mismatch, key){
|
||||||
var keyHandled = false;
|
|
||||||
if (keyRequired
|
// check if set in the config and compare
|
||||||
&& ((info.clientKey && req.config.clientKey && info.clientKey === req.config.clientKey)
|
if (req.config[key] && info[key] !== req.config[key]) {
|
||||||
|| (info.javascriptKey && req.config.javascriptKey && info.javascriptKey === req.config.javascriptKey)
|
mismatch++;
|
||||||
|| (info.dotNetKey && req.config.dotNetKey && info.dotNetKey === req.config.dotNetKey)
|
}
|
||||||
|| (info.restAPIKey && req.config.restAPIKey && info.restAPIKey === req.config.restAPIKey)
|
return mismatch;
|
||||||
)) {
|
}, 0);
|
||||||
keyHandled = true;
|
|
||||||
}
|
// All keys mismatch
|
||||||
if (keyRequired && !keyHandled) {
|
if (keyMismatch == keys.length) {
|
||||||
return invalidRequest(req, res);
|
return invalidRequest(req, res);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user