Merge branch 'master' of github.com:ParsePlatform/parse-server into new-quickstart

This commit is contained in:
Héctor Ramos
2016-03-11 13:52:44 -08:00
32 changed files with 1352 additions and 256 deletions

View File

@@ -1 +1,3 @@
#!/usr/bin/env node
require("../lib/cli/parse-server");

View File

@@ -60,6 +60,7 @@
"build": "./node_modules/.bin/babel src/ -d lib/",
"pretest": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=3.0.8} ./node_modules/.bin/mongodb-runner start",
"test": "cross-env NODE_ENV=test TESTING=1 ./node_modules/.bin/babel-node $COVERAGE_OPTION ./node_modules/jasmine/bin/jasmine.js",
"test:win": "npm run pretest && cross-env NODE_ENV=test TESTING=1 ./node_modules/.bin/babel-node ./node_modules/babel-istanbul/lib/cli.js cover -x **/spec/** ./node_modules/jasmine/bin/jasmine.js && npm run posttest",
"posttest": "./node_modules/.bin/mongodb-runner stop",
"coverage": "cross-env COVERAGE_OPTION='./node_modules/babel-istanbul/lib/cli.js cover -x **/spec/**' npm test",
"start": "node ./bin/parse-server",

View File

@@ -0,0 +1,23 @@
'use strict';
let DatabaseAdapter = require('../src/DatabaseAdapter');
describe('DatabaseAdapter', () => {
it('options and URI are available to adapter', done => {
DatabaseAdapter.setAppDatabaseURI('optionsTest', 'mongodb://localhost:27017/optionsTest');
DatabaseAdapter.setAppDatabaseOptions('optionsTest', {foo: "bar"});
let optionsTestDatabaseConnection = DatabaseAdapter.getDatabaseConnection('optionsTest');
expect(optionsTestDatabaseConnection instanceof Object).toBe(true);
expect(optionsTestDatabaseConnection.adapter._options instanceof Object).toBe(true);
expect(optionsTestDatabaseConnection.adapter._options.foo).toBe("bar");
DatabaseAdapter.setAppDatabaseURI('noOptionsTest', 'mongodb://localhost:27017/noOptionsTest');
let noOptionsTestDatabaseConnection = DatabaseAdapter.getDatabaseConnection('noOptionsTest');
expect(noOptionsTestDatabaseConnection instanceof Object).toBe(true);
expect(noOptionsTestDatabaseConnection.adapter._options instanceof Object).toBe(false);
done();
});
});

View File

@@ -35,8 +35,13 @@ describe('info logs', () => {
size: 1,
level: 'info'
}, (results) => {
expect(results[0].message).toEqual('testing info logs');
done();
if(results.length == 0) {
fail('The adapter should return non-empty results');
done();
} else {
expect(results[0].message).toEqual('testing info logs');
done();
}
});
});
});
@@ -56,8 +61,14 @@ describe('error logs', () => {
size: 1,
level: 'error'
}, (results) => {
expect(results[0].message).toEqual('testing error logs');
done();
if(results.length == 0) {
fail('The adapter should return non-empty results');
done();
}
else {
expect(results[0].message).toEqual('testing error logs');
done();
}
});
});
});

View File

@@ -1,4 +1,4 @@
var OAuth = require("../src/oauth/OAuth1Client");
var OAuth = require("../src/authDataManager/OAuth1Client");
var request = require('request');
describe('OAuth', function() {
@@ -138,7 +138,7 @@ describe('OAuth', function() {
["facebook", "github", "instagram", "google", "linkedin", "meetup", "twitter"].map(function(providerName){
it("Should validate structure of "+providerName, (done) => {
var provider = require("../src/oauth/"+providerName);
var provider = require("../src/authDataManager/"+providerName);
jequal(typeof provider.validateAuthData, "function");
jequal(typeof provider.validateAppId, "function");
jequal(provider.validateAuthData({}, {}).constructor, Promise.prototype.constructor);

View File

@@ -643,6 +643,7 @@ describe('miscellaneous', function() {
it('test afterSave get original object on update', function(done) {
var triggerTime = 0;
// Register a mock beforeSave hook
Parse.Cloud.afterSave('GameScore', function(req, res) {
var object = req.object;
expect(object instanceof Parse.Object).toBeTruthy();
@@ -693,6 +694,56 @@ describe('miscellaneous', function() {
});
});
it('test afterSave get full original object even req auth can not query it', (done) => {
var triggerTime = 0;
// Register a mock beforeSave hook
Parse.Cloud.afterSave('GameScore', function(req, res) {
var object = req.object;
var originalObject = req.original;
if (triggerTime == 0) {
// Create
} else if (triggerTime == 1) {
// Update
expect(object.get('foo')).toEqual('baz');
// Make sure we get the full originalObject
expect(originalObject instanceof Parse.Object).toBeTruthy();
expect(originalObject.get('fooAgain')).toEqual('barAgain');
expect(originalObject.id).not.toBeUndefined();
expect(originalObject.createdAt).not.toBeUndefined();
expect(originalObject.updatedAt).not.toBeUndefined();
expect(originalObject.get('foo')).toEqual('bar');
} else {
res.error();
}
triggerTime++;
res.success();
});
var obj = new Parse.Object('GameScore');
obj.set('foo', 'bar');
obj.set('fooAgain', 'barAgain');
var acl = new Parse.ACL();
// Make sure our update request can not query the object
acl.setPublicReadAccess(false);
acl.setPublicWriteAccess(true);
obj.setACL(acl);
obj.save().then(function() {
// We only update foo
obj.set('foo', 'baz');
return obj.save();
}).then(function() {
// Make sure the checking has been triggered
expect(triggerTime).toBe(2);
// Clear mock afterSave
Parse.Cloud._removeHook("Triggers", "afterSave", "GameScore");
done();
}, function(error) {
console.error(error);
fail(error);
done();
});
});
it('afterSave flattens custom operations', done => {
var triggerTime = 0;
// Register a mock beforeSave hook

View File

@@ -9,6 +9,7 @@
var request = require('request');
var passwordCrypto = require('../src/password');
var Config = require('../src/Config');
function verifyACL(user) {
const ACL = user.getACL();
@@ -904,6 +905,50 @@ describe('Parse.User testing', () => {
}
};
};
var getMockMyOauthProvider = function() {
return {
authData: {
id: "12345",
access_token: "12345",
expiration_date: new Date().toJSON(),
},
shouldError: false,
loggedOut: false,
synchronizedUserId: null,
synchronizedAuthToken: null,
synchronizedExpiration: null,
authenticate: function(options) {
if (this.shouldError) {
options.error(this, "An error occurred");
} else if (this.shouldCancel) {
options.error(this, null);
} else {
options.success(this, this.authData);
}
},
restoreAuthentication: function(authData) {
if (!authData) {
this.synchronizedUserId = null;
this.synchronizedAuthToken = null;
this.synchronizedExpiration = null;
return true;
}
this.synchronizedUserId = authData.id;
this.synchronizedAuthToken = authData.access_token;
this.synchronizedExpiration = authData.expiration_date;
return true;
},
getAuthType: function() {
return "myoauth";
},
deauthenticate: function() {
this.loggedOut = true;
this.restoreAuthentication(null);
}
};
};
var ExtendedUser = Parse.User.extend({
extended: function() {
@@ -1284,6 +1329,151 @@ describe('Parse.User testing', () => {
}
});
});
it("link multiple providers", (done) => {
var provider = getMockFacebookProvider();
var mockProvider = getMockMyOauthProvider();
Parse.User._registerAuthenticationProvider(provider);
Parse.User._logInWith("facebook", {
success: function(model) {
ok(model instanceof Parse.User, "Model should be a Parse.User");
strictEqual(Parse.User.current(), model);
ok(model.extended(), "Should have used the subclass.");
strictEqual(provider.authData.id, provider.synchronizedUserId);
strictEqual(provider.authData.access_token, provider.synchronizedAuthToken);
strictEqual(provider.authData.expiration_date, provider.synchronizedExpiration);
ok(model._isLinked("facebook"), "User should be linked to facebook");
Parse.User._registerAuthenticationProvider(mockProvider);
let objectId = model.id;
model._linkWith("myoauth", {
success: function(model) {
expect(model.id).toEqual(objectId);
ok(model._isLinked("facebook"), "User should be linked to facebook");
ok(model._isLinked("myoauth"), "User should be linked to myoauth");
done();
},
error: function(error) {
console.error(error);
fail('SHould not fail');
done();
}
})
},
error: function(model, error) {
ok(false, "linking should have worked");
done();
}
});
});
it("link multiple providers and update token", (done) => {
var provider = getMockFacebookProvider();
var mockProvider = getMockMyOauthProvider();
Parse.User._registerAuthenticationProvider(provider);
Parse.User._logInWith("facebook", {
success: function(model) {
ok(model instanceof Parse.User, "Model should be a Parse.User");
strictEqual(Parse.User.current(), model);
ok(model.extended(), "Should have used the subclass.");
strictEqual(provider.authData.id, provider.synchronizedUserId);
strictEqual(provider.authData.access_token, provider.synchronizedAuthToken);
strictEqual(provider.authData.expiration_date, provider.synchronizedExpiration);
ok(model._isLinked("facebook"), "User should be linked to facebook");
Parse.User._registerAuthenticationProvider(mockProvider);
let objectId = model.id;
model._linkWith("myoauth", {
success: function(model) {
expect(model.id).toEqual(objectId);
ok(model._isLinked("facebook"), "User should be linked to facebook");
ok(model._isLinked("myoauth"), "User should be linked to myoauth");
model._linkWith("facebook", {
success: () => {
ok(model._isLinked("facebook"), "User should be linked to facebook");
ok(model._isLinked("myoauth"), "User should be linked to myoauth");
done();
},
error: () => {
fail('should link again');
done();
}
})
},
error: function(error) {
console.error(error);
fail('SHould not fail');
done();
}
})
},
error: function(model, error) {
ok(false, "linking should have worked");
done();
}
});
});
it('should fail linking with existing', (done) => {
var provider = getMockFacebookProvider();
Parse.User._registerAuthenticationProvider(provider);
Parse.User._logInWith("facebook", {
success: function(model) {
Parse.User.logOut().then(() => {
let user = new Parse.User();
user.setUsername('user');
user.setPassword('password');
return user.signUp().then(() => {
// try to link here
user._linkWith('facebook', {
success: () => {
fail('should not succeed');
done();
},
error: (err) => {
done();
}
});
});
});
}
});
});
it('should have authData in beforeSave and afterSave', (done) => {
Parse.Cloud.beforeSave('_User', (request, response) => {
let authData = request.object.get('authData');
expect(authData).not.toBeUndefined();
if (authData) {
expect(authData.facebook.id).toEqual('8675309');
expect(authData.facebook.access_token).toEqual('jenny');
} else {
fail('authData should be set');
}
response.success();
});
Parse.Cloud.afterSave('_User', (request, response) => {
let authData = request.object.get('authData');
expect(authData).not.toBeUndefined();
if (authData) {
expect(authData.facebook.id).toEqual('8675309');
expect(authData.facebook.access_token).toEqual('jenny');
} else {
fail('authData should be set');
}
response.success();
});
var provider = getMockFacebookProvider();
Parse.User._registerAuthenticationProvider(provider);
Parse.User._logInWith("facebook", {
success: function(model) {
Parse.Cloud._removeHook('Triggers', 'beforeSave', Parse.User.className);
Parse.Cloud._removeHook('Triggers', 'afterSave', Parse.User.className);
done();
}
});
});
it('set password then change password', (done) => {
Parse.User.signUp('bob', 'barker').then((bob) => {
@@ -1780,5 +1970,41 @@ describe('Parse.User testing', () => {
}
});
});
// Sometimes the authData still has null on that keys
// https://github.com/ParsePlatform/parse-server/issues/935
it('should cleanup null authData keys', (done) => {
let database = new Config(Parse.applicationId).database;
database.create('_User', {
username: 'user',
password: '$2a$10$8/wZJyEuiEaobBBqzTG.jeY.XSFJd0rzaN//ososvEI4yLqI.4aie',
_auth_data_facebook: null
}, {}).then(() => {
return new Promise((resolve, reject) => {
request.get({
url: 'http://localhost:8378/1/login?username=user&password=test',
headers: {
'X-Parse-Application-Id': 'test',
'X-Parse-Master-Key': 'test',
},
json: true
}, (err, res, body) => {
if (err) {
reject(err);
} else {
resolve(body);
}
})
})
}).then((user) => {
let authData = user.authData;
expect(user.username).toEqual('user');
expect(authData).toBeUndefined();
done();
}).catch((err) => {
fail('this should not fail');
done();
})
});
});

View File

@@ -148,7 +148,8 @@ describe('rest create', () => {
});
it('handles no anonymous users config', (done) => {
var NoAnnonConfig = Object.assign({}, config, {enableAnonymousUsers: false});
var NoAnnonConfig = Object.assign({}, config);
NoAnnonConfig.authDataManager.setEnableAnonymousUsers(false);
var data1 = {
authData: {
anonymous: {
@@ -162,6 +163,7 @@ describe('rest create', () => {
}, (err) => {
expect(err.code).toEqual(Parse.Error.UNSUPPORTED_SERVICE);
expect(err.message).toEqual('This authentication method is unsupported.');
NoAnnonConfig.authDataManager.setEnableAnonymousUsers(true);
done();
})
});

View File

@@ -5,8 +5,9 @@ jasmine.DEFAULT_TIMEOUT_INTERVAL = 2000;
var cache = require('../src/cache').default;
var DatabaseAdapter = require('../src/DatabaseAdapter');
var express = require('express');
var facebook = require('../src/oauth/facebook');
var facebook = require('../src/authDataManager/facebook');
var ParseServer = require('../src/index').ParseServer;
var path = require('path');
var databaseURI = process.env.DATABASE_URI;
var cloudMain = process.env.CLOUD_CODE_MAIN || '../spec/cloud/main.js';
@@ -26,7 +27,7 @@ var defaultConfiguration = {
collectionPrefix: 'test_',
fileKey: 'test',
push: {
'ios': {
'ios': {
cert: 'prodCert.pem',
key: 'prodKey.pem',
production: true,
@@ -36,7 +37,7 @@ var defaultConfiguration = {
oauth: { // Override the facebook provider
facebook: mockFacebook(),
myoauth: {
module: "../spec/myoauth" // relative path as it's run from src
module: path.resolve(__dirname, "myoauth") // relative path as it's run from src
}
}
};
@@ -81,7 +82,7 @@ afterEach(function(done) {
Parse.User.logOut().then(() => {
return clearData();
}).then(() => {
DatabaseAdapter.clearDatabaseURIs();
DatabaseAdapter.clearDatabaseSettings();
done();
}, (error) => {
console.log('error in clearData', error);

View File

@@ -23,6 +23,27 @@ var hasAllPODobject = () => {
return obj;
};
let defaultClassLevelPermissions = {
find: {
'*': true
},
create: {
'*': true
},
get: {
'*': true
},
update: {
'*': true
},
addField: {
'*': true
},
delete: {
'*': true
}
}
var plainOldDataSchema = {
className: 'HasAllPOD',
fields: {
@@ -40,7 +61,8 @@ var plainOldDataSchema = {
aArray: {type: 'Array'},
aGeoPoint: {type: 'GeoPoint'},
aFile: {type: 'File'}
}
},
classLevelPermissions: defaultClassLevelPermissions
};
var pointersAndRelationsSchema = {
@@ -61,6 +83,7 @@ var pointersAndRelationsSchema = {
targetClass: 'HasAllPOD',
},
},
classLevelPermissions: defaultClassLevelPermissions
}
var noAuthHeaders = {
@@ -296,7 +319,8 @@ describe('schemas', () => {
objectId: {type: 'String'},
foo: {type: 'Number'},
ptr: {type: 'Pointer', targetClass: 'SomeClass'},
}
},
classLevelPermissions: defaultClassLevelPermissions
});
done();
});
@@ -318,7 +342,8 @@ describe('schemas', () => {
createdAt: {type: 'Date'},
updatedAt: {type: 'Date'},
objectId: {type: 'String'},
}
},
classLevelPermissions: defaultClassLevelPermissions
});
done();
});
@@ -490,7 +515,8 @@ describe('schemas', () => {
"objectId": {"type": "String"},
"updatedAt": {"type": "Date"},
"geo2": {"type": "GeoPoint"},
}
},
classLevelPermissions: defaultClassLevelPermissions
})).toEqual(undefined);
done();
});
@@ -539,6 +565,7 @@ describe('schemas', () => {
"updatedAt": {"type": "Date"},
"newField": {"type": "String"},
},
classLevelPermissions: defaultClassLevelPermissions
})).toEqual(undefined);
request.get({
url: 'http://localhost:8378/1/schemas/NewClass',
@@ -553,7 +580,8 @@ describe('schemas', () => {
updatedAt: {type: 'Date'},
objectId: {type: 'String'},
newField: {type: 'String'},
}
},
classLevelPermissions: defaultClassLevelPermissions
});
done();
});
@@ -590,7 +618,8 @@ describe('schemas', () => {
emailVerified: {type: 'Boolean'},
newField: {type: 'String'},
ACL: {type: 'ACL'}
}
},
classLevelPermissions: defaultClassLevelPermissions
});
request.get({
url: 'http://localhost:8378/1/schemas/_User',
@@ -610,7 +639,8 @@ describe('schemas', () => {
emailVerified: {type: 'Boolean'},
newField: {type: 'String'},
ACL: {type: 'ACL'}
}
},
classLevelPermissions: defaultClassLevelPermissions
});
done();
});
@@ -656,7 +686,8 @@ describe('schemas', () => {
aNewString: {type: 'String'},
aNewPointer: {type: 'Pointer', targetClass: 'HasAllPOD'},
aNewRelation: {type: 'Relation', targetClass: 'HasAllPOD'},
}
},
classLevelPermissions: defaultClassLevelPermissions
});
var obj2 = new Parse.Object('HasAllPOD');
obj2.set('aNewPointer', obj1);
@@ -872,4 +903,597 @@ describe('schemas', () => {
});
});
});
it('should set/get schema permissions', done => {
request.post({
url: 'http://localhost:8378/1/schemas/AClass',
headers: masterKeyHeaders,
json: true,
body: {
classLevelPermissions: {
find: {
'*': true
},
create: {
'role:admin': true
}
}
}
}, (error, response, body) => {
expect(error).toEqual(null);
request.get({
url: 'http://localhost:8378/1/schemas/AClass',
headers: masterKeyHeaders,
json: true,
}, (error, response, body) => {
expect(response.statusCode).toEqual(200);
expect(response.body.classLevelPermissions).toEqual({
find: {
'*': true
},
create: {
'role:admin': true
},
get: {
'*': true
},
update: {
'*': true
},
addField: {
'*': true
},
delete: {
'*': true
}
});
done();
});
});
});
it('should fail setting schema permissions with invalid key', done => {
let object = new Parse.Object('AClass');
object.save().then(() => {
request.put({
url: 'http://localhost:8378/1/schemas/AClass',
headers: masterKeyHeaders,
json: true,
body: {
classLevelPermissions: {
find: {
'*': true
},
create: {
'role:admin': true
},
dummy: {
'some': true
}
}
}
}, (error, response, body) => {
expect(error).toEqual(null);
expect(body.code).toEqual(107);
expect(body.error).toEqual('dummy is not a valid operation for class level permissions');
done();
});
});
});
it('should not be able to add a field', done => {
request.post({
url: 'http://localhost:8378/1/schemas/AClass',
headers: masterKeyHeaders,
json: true,
body: {
classLevelPermissions: {
find: {
'*': true
},
addField: {
'role:admin': true
}
}
}
}, (error, response, body) => {
expect(error).toEqual(null);
let object = new Parse.Object('AClass');
object.set('hello', 'world');
return object.save().then(() => {
fail('should not be able to add a field');
done();
}, (err) => {
expect(err.message).toEqual('Permission denied for this action.');
done();
})
})
});
it('should not be able to add a field', done => {
request.post({
url: 'http://localhost:8378/1/schemas/AClass',
headers: masterKeyHeaders,
json: true,
body: {
classLevelPermissions: {
find: {
'*': true
},
addField: {
'*': true
}
}
}
}, (error, response, body) => {
expect(error).toEqual(null);
let object = new Parse.Object('AClass');
object.set('hello', 'world');
return object.save().then(() => {
done();
}, (err) => {
fail('should be able to add a field');
done();
})
})
});
it('should throw with invalid userId (>10 chars)', done => {
request.post({
url: 'http://localhost:8378/1/schemas/AClass',
headers: masterKeyHeaders,
json: true,
body: {
classLevelPermissions: {
find: {
'1234567890A': true
},
}
}
}, (error, response, body) => {
expect(body.error).toEqual("'1234567890A' is not a valid key for class level permissions");
done();
})
});
it('should throw with invalid userId (<10 chars)', done => {
request.post({
url: 'http://localhost:8378/1/schemas/AClass',
headers: masterKeyHeaders,
json: true,
body: {
classLevelPermissions: {
find: {
'a12345678': true
},
}
}
}, (error, response, body) => {
expect(body.error).toEqual("'a12345678' is not a valid key for class level permissions");
done();
})
});
it('should throw with invalid userId (invalid char)', done => {
request.post({
url: 'http://localhost:8378/1/schemas/AClass',
headers: masterKeyHeaders,
json: true,
body: {
classLevelPermissions: {
find: {
'12345_6789': true
},
}
}
}, (error, response, body) => {
expect(body.error).toEqual("'12345_6789' is not a valid key for class level permissions");
done();
})
});
it('should throw with invalid * (spaces)', done => {
request.post({
url: 'http://localhost:8378/1/schemas/AClass',
headers: masterKeyHeaders,
json: true,
body: {
classLevelPermissions: {
find: {
' *': true
},
}
}
}, (error, response, body) => {
expect(body.error).toEqual("' *' is not a valid key for class level permissions");
done();
})
});
it('should throw with invalid * (spaces)', done => {
request.post({
url: 'http://localhost:8378/1/schemas/AClass',
headers: masterKeyHeaders,
json: true,
body: {
classLevelPermissions: {
find: {
'* ': true
},
}
}
}, (error, response, body) => {
expect(body.error).toEqual("'* ' is not a valid key for class level permissions");
done();
})
});
it('should throw with invalid value', done => {
request.post({
url: 'http://localhost:8378/1/schemas/AClass',
headers: masterKeyHeaders,
json: true,
body: {
classLevelPermissions: {
find: {
'*': 1
},
}
}
}, (error, response, body) => {
expect(body.error).toEqual("'1' is not a valid value for class level permissions find:*:1");
done();
})
});
it('should throw with invalid value', done => {
request.post({
url: 'http://localhost:8378/1/schemas/AClass',
headers: masterKeyHeaders,
json: true,
body: {
classLevelPermissions: {
find: {
'*': ""
},
}
}
}, (error, response, body) => {
expect(body.error).toEqual("'' is not a valid value for class level permissions find:*:");
done();
})
});
function setPermissionsOnClass(className, permissions, doPut) {
let op = request.post;
if (doPut)
{
op = request.put;
}
return new Promise((resolve, reject) => {
op({
url: 'http://localhost:8378/1/schemas/'+className,
headers: masterKeyHeaders,
json: true,
body: {
classLevelPermissions: permissions
}
}, (error, response, body) => {
if (error) {
return reject(error);
}
if (body.error) {
return reject(body);
}
return resolve(body);
})
});
}
it('validate CLP 1', done => {
let user = new Parse.User();
user.setUsername('user');
user.setPassword('user');
let admin = new Parse.User();
admin.setUsername('admin');
admin.setPassword('admin');
let role = new Parse.Role('admin', new Parse.ACL());
setPermissionsOnClass('AClass', {
'find': {
'role:admin': true
}
}).then(() => {
return Parse.Object.saveAll([user, admin, role], {useMasterKey: true});
}).then(()=> {
role.relation('users').add(admin);
return role.save(null, {useMasterKey: true});
}).then(() => {
return Parse.User.logIn('user', 'user').then(() => {
let obj = new Parse.Object('AClass');
return obj.save();
})
}).then(() => {
let query = new Parse.Query('AClass');
return query.find().then((err) => {
fail('Use should hot be able to find!')
}, (err) => {
expect(err.message).toEqual('Permission denied for this action.');
return Promise.resolve();
})
}).then(() => {
return Parse.User.logIn('admin', 'admin');
}).then( () => {
let query = new Parse.Query('AClass');
return query.find();
}).then((results) => {
expect(results.length).toBe(1);
done();
}, () => {
fail("should not fail!");
done();
}).catch( (err) => {
done();
})
});
it('validate CLP 2', done => {
let user = new Parse.User();
user.setUsername('user');
user.setPassword('user');
let admin = new Parse.User();
admin.setUsername('admin');
admin.setPassword('admin');
let role = new Parse.Role('admin', new Parse.ACL());
setPermissionsOnClass('AClass', {
'find': {
'role:admin': true
}
}).then(() => {
return Parse.Object.saveAll([user, admin, role], {useMasterKey: true});
}).then(()=> {
role.relation('users').add(admin);
return role.save(null, {useMasterKey: true});
}).then(() => {
return Parse.User.logIn('user', 'user').then(() => {
let obj = new Parse.Object('AClass');
return obj.save();
})
}).then(() => {
let query = new Parse.Query('AClass');
return query.find().then((err) => {
fail('User should not be able to find!')
}, (err) => {
expect(err.message).toEqual('Permission denied for this action.');
return Promise.resolve();
})
}).then(() => {
// let everyone see it now
return setPermissionsOnClass('AClass', {
'find': {
'role:admin': true,
'*': true
}
}, true);
}).then(() => {
let query = new Parse.Query('AClass');
return query.find().then((result) => {
expect(result.length).toBe(1);
}, (err) => {
fail('User should be able to find!')
done();
});
}).then(() => {
return Parse.User.logIn('admin', 'admin');
}).then( () => {
let query = new Parse.Query('AClass');
return query.find();
}).then((results) => {
expect(results.length).toBe(1);
done();
}, (err) => {
fail("should not fail!");
done();
}).catch( (err) => {
done();
})
});
it('validate CLP 3', done => {
let user = new Parse.User();
user.setUsername('user');
user.setPassword('user');
let admin = new Parse.User();
admin.setUsername('admin');
admin.setPassword('admin');
let role = new Parse.Role('admin', new Parse.ACL());
setPermissionsOnClass('AClass', {
'find': {
'role:admin': true
}
}).then(() => {
return Parse.Object.saveAll([user, admin, role], {useMasterKey: true});
}).then(()=> {
role.relation('users').add(admin);
return role.save(null, {useMasterKey: true});
}).then(() => {
return Parse.User.logIn('user', 'user').then(() => {
let obj = new Parse.Object('AClass');
return obj.save();
})
}).then(() => {
let query = new Parse.Query('AClass');
return query.find().then((err) => {
fail('User should not be able to find!')
}, (err) => {
expect(err.message).toEqual('Permission denied for this action.');
return Promise.resolve();
})
}).then(() => {
// delete all CLP
return setPermissionsOnClass('AClass', null, true);
}).then(() => {
let query = new Parse.Query('AClass');
return query.find().then((result) => {
expect(result.length).toBe(1);
}, (err) => {
fail('User should be able to find!')
done();
});
}).then(() => {
return Parse.User.logIn('admin', 'admin');
}).then( () => {
let query = new Parse.Query('AClass');
return query.find();
}).then((results) => {
expect(results.length).toBe(1);
done();
}, (err) => {
fail("should not fail!");
done();
});
});
it('validate CLP 4', done => {
let user = new Parse.User();
user.setUsername('user');
user.setPassword('user');
let admin = new Parse.User();
admin.setUsername('admin');
admin.setPassword('admin');
let role = new Parse.Role('admin', new Parse.ACL());
setPermissionsOnClass('AClass', {
'find': {
'role:admin': true
}
}).then(() => {
return Parse.Object.saveAll([user, admin, role], {useMasterKey: true});
}).then(()=> {
role.relation('users').add(admin);
return role.save(null, {useMasterKey: true});
}).then(() => {
return Parse.User.logIn('user', 'user').then(() => {
let obj = new Parse.Object('AClass');
return obj.save();
})
}).then(() => {
let query = new Parse.Query('AClass');
return query.find().then((err) => {
fail('User should not be able to find!')
}, (err) => {
expect(err.message).toEqual('Permission denied for this action.');
return Promise.resolve();
})
}).then(() => {
// borked CLP should not affec security
return setPermissionsOnClass('AClass', {
'found': {
'role:admin': true
}
}, true).then(() => {
fail("Should not be able to save a borked CLP");
}, () => {
return Promise.resolve();
})
}).then(() => {
let query = new Parse.Query('AClass');
return query.find().then((result) => {
fail('User should not be able to find!')
}, (err) => {
expect(err.message).toEqual('Permission denied for this action.');
return Promise.resolve();
});
}).then(() => {
return Parse.User.logIn('admin', 'admin');
}).then( () => {
let query = new Parse.Query('AClass');
return query.find();
}).then((results) => {
expect(results.length).toBe(1);
done();
}, (err) => {
fail("should not fail!");
done();
}).catch( (err) => {
done();
})
});
it('validate CLP 5', done => {
let user = new Parse.User();
user.setUsername('user');
user.setPassword('user');
let user2 = new Parse.User();
user2.setUsername('user2');
user2.setPassword('user2');
let admin = new Parse.User();
admin.setUsername('admin');
admin.setPassword('admin');
let role = new Parse.Role('admin', new Parse.ACL());
Promise.resolve().then(() => {
return Parse.Object.saveAll([user, user2, admin, role], {useMasterKey: true});
}).then(()=> {
role.relation('users').add(admin);
return role.save(null, {useMasterKey: true}).then(() => {
let perm = {
find: {}
};
// let the user find
perm['find'][user.id] = true;
return setPermissionsOnClass('AClass', perm);
})
}).then(() => {
return Parse.User.logIn('user', 'user').then(() => {
let obj = new Parse.Object('AClass');
return obj.save();
})
}).then(() => {
let query = new Parse.Query('AClass');
return query.find().then((res) => {
expect(res.length).toEqual(1);
}, (err) => {
fail('User should be able to find!')
return Promise.resolve();
})
}).then(() => {
return Parse.User.logIn('admin', 'admin');
}).then( () => {
let query = new Parse.Query('AClass');
return query.find();
}).then((results) => {
fail("should not be able to read!");
return Promise.resolve();
}, (err) => {
expect(err.message).toEqual('Permission denied for this action.');
return Promise.resolve();
}).then(() => {
return Parse.User.logIn('user2', 'user2');
}).then( () => {
let query = new Parse.Query('AClass');
return query.find();
}).then((results) => {
fail("should not be able to read!");
return Promise.resolve();
}, (err) => {
expect(err.message).toEqual('Permission denied for this action.');
return Promise.resolve();
}).then(() => {
done();
});
});
});

View File

@@ -10,12 +10,14 @@ const MongoSchemaCollectionName = '_SCHEMA';
export class MongoStorageAdapter {
// Private
_uri: string;
_options: Object;
// Public
connectionPromise;
database;
constructor(uri: string) {
constructor(uri: string, options: Object) {
this._uri = uri;
this._options = options;
}
connect() {
@@ -23,7 +25,7 @@ export class MongoStorageAdapter {
return this.connectionPromise;
}
this.connectionPromise = MongoClient.connect(this._uri).then(database => {
this.connectionPromise = MongoClient.connect(this._uri, this._options).then(database => {
this.database = database;
});
return this.connectionPromise;

View File

@@ -20,7 +20,6 @@ export class Config {
this.restAPIKey = cacheInfo.restAPIKey;
this.fileKey = cacheInfo.fileKey;
this.facebookAppIds = cacheInfo.facebookAppIds;
this.enableAnonymousUsers = cacheInfo.enableAnonymousUsers;
this.allowClientClassCreation = cacheInfo.allowClientClassCreation;
this.database = DatabaseAdapter.getDatabaseConnection(applicationId, cacheInfo.collectionPrefix);
@@ -34,7 +33,7 @@ export class Config {
this.pushController = cacheInfo.pushController;
this.loggerController = cacheInfo.loggerController;
this.userController = cacheInfo.userController;
this.oauth = cacheInfo.oauth;
this.authDataManager = cacheInfo.authDataManager;
this.customPages = cacheInfo.customPages || {};
this.mount = mount;
}

View File

@@ -101,8 +101,12 @@ DatabaseController.prototype.redirectClassNameForKey = function(className, key)
// Returns a promise that resolves to the new schema.
// This does not update this.schema, because in a situation like a
// batch request, that could confuse other users of the schema.
DatabaseController.prototype.validateObject = function(className, object, query) {
return this.loadSchema().then((schema) => {
DatabaseController.prototype.validateObject = function(className, object, query, options) {
let schema;
return this.loadSchema().then(s => {
schema = s;
return this.canAddField(schema, className, object, options.acl || []);
}).then(() => {
return schema.validateObject(className, object, query);
});
};
@@ -332,6 +336,22 @@ DatabaseController.prototype.create = function(className, object, options) {
});
};
DatabaseController.prototype.canAddField = function(schema, className, object, aclGroup) {
let classSchema = schema.data[className];
if (!classSchema) {
return Promise.resolve();
}
let fields = Object.keys(object);
let schemaFields = Object.keys(classSchema);
let newKeys = fields.filter((field) => {
return schemaFields.indexOf(field) < 0;
})
if (newKeys.length > 0) {
return schema.validatePermission(className, aclGroup, 'addField');
}
return Promise.resolve();
}
// Runs a mongo query on the database.
// This should only be used for testing - use 'find' for normal code
// to avoid Mongo-format dependencies.

View File

@@ -24,6 +24,7 @@ let adapter = MongoStorageAdapter;
let dbConnections = {};
let databaseURI = DefaultDatabaseURI;
let appDatabaseURIs = {};
let appDatabaseOptions = {};
function setAdapter(databaseAdapter) {
adapter = databaseAdapter;
@@ -37,10 +38,15 @@ function setAppDatabaseURI(appId, uri) {
appDatabaseURIs[appId] = uri;
}
function setAppDatabaseOptions(appId: string, options: Object) {
appDatabaseOptions[appId] = options;
}
//Used by tests
function clearDatabaseURIs() {
function clearDatabaseSettings() {
appDatabaseURIs = {};
dbConnections = {};
appDatabaseOptions = {};
}
function getDatabaseConnection(appId: string, collectionPrefix: string) {
@@ -50,7 +56,7 @@ function getDatabaseConnection(appId: string, collectionPrefix: string) {
var dbURI = (appDatabaseURIs[appId] ? appDatabaseURIs[appId] : databaseURI);
let storageAdapter = new adapter(dbURI);
let storageAdapter = new adapter(dbURI, appDatabaseOptions[appId]);
dbConnections[appId] = new DatabaseController(storageAdapter, {
collectionPrefix: collectionPrefix
});
@@ -62,7 +68,8 @@ module.exports = {
getDatabaseConnection: getDatabaseConnection,
setAdapter: setAdapter,
setDatabaseURI: setDatabaseURI,
setAppDatabaseOptions: setAppDatabaseOptions,
setAppDatabaseURI: setAppDatabaseURI,
clearDatabaseURIs: clearDatabaseURIs,
clearDatabaseSettings: clearDatabaseSettings,
defaultDatabaseURI: databaseURI
};

View File

@@ -9,7 +9,6 @@ var Auth = require('./Auth');
var Config = require('./Config');
var cryptoUtils = require('./cryptoUtils');
var passwordCrypto = require('./password');
var oauth = require("./oauth");
var Parse = require('parse/node');
var triggers = require('./triggers');
@@ -33,7 +32,7 @@ function RestWrite(config, auth, className, query, data, originalData) {
throw new Parse.Error(Parse.Error.INVALID_KEY_NAME, 'objectId ' +
'is an invalid field name.');
}
// When the operation is complete, this.response may have several
// fields.
// response: the actual data to be returned
@@ -128,7 +127,7 @@ RestWrite.prototype.validateClientClassCreation = function() {
// Validates this operation against the schema.
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, this.runOptions);
};
// Runs any beforeSave triggers against this operation.
@@ -211,170 +210,96 @@ RestWrite.prototype.validateAuthData = function() {
}
var authData = this.data.authData;
var anonData = this.data.authData.anonymous;
if (this.config.enableAnonymousUsers === true && (anonData === null ||
(anonData && anonData.id))) {
return this.handleAnonymousAuthData();
}
// Not anon, try other providers
var providers = Object.keys(authData);
if (!anonData && providers.length == 1) {
var provider = providers[0];
var providerAuthData = authData[provider];
var hasToken = (providerAuthData && providerAuthData.id);
if (providerAuthData === null || hasToken) {
return this.handleOAuthAuthData(provider);
if (providers.length > 0) {
let canHandleAuthData = providers.reduce((canHandle, provider) => {
var providerAuthData = authData[provider];
var hasToken = (providerAuthData && providerAuthData.id);
return canHandle && (hasToken || providerAuthData == null);
}, true);
if (canHandleAuthData) {
return this.handleAuthData(authData);
}
}
throw new Parse.Error(Parse.Error.UNSUPPORTED_SERVICE,
'This authentication method is unsupported.');
};
RestWrite.prototype.handleAnonymousAuthData = function() {
var anonData = this.data.authData.anonymous;
if (anonData === null && this.query) {
// We are unlinking the user from the anonymous provider
this.data._auth_data_anonymous = null;
return;
}
// Check if this user already exists
return this.config.database.find(
this.className,
{'authData.anonymous.id': anonData.id}, {})
.then((results) => {
if (results.length > 0) {
if (!this.query) {
// We're signing up, but this user already exists. Short-circuit
delete results[0].password;
this.response = {
response: results[0],
location: this.location()
};
return;
}
// If this is a PUT for the same user, allow the linking
if (results[0].objectId === this.query.objectId) {
// Delete the rest format key before saving
delete this.data.authData;
return;
}
// We're trying to create a duplicate account. Forbid it
throw new Parse.Error(Parse.Error.ACCOUNT_ALREADY_LINKED,
'this auth is already used');
}
// This anonymous user does not already exist, so transform it
// to a saveable format
this.data._auth_data_anonymous = anonData;
// Delete the rest format key before saving
delete this.data.authData;
})
};
RestWrite.prototype.handleOAuthAuthData = function(provider) {
var authData = this.data.authData[provider];
if (authData === null && this.query) {
// We are unlinking from the provider.
this.data["_auth_data_" + provider ] = null;
return;
}
var appIds;
var oauthOptions = this.config.oauth[provider];
if (oauthOptions) {
appIds = oauthOptions.appIds;
} else if (provider == "facebook") {
appIds = this.config.facebookAppIds;
}
var validateAuthData;
var validateAppId;
if (oauth[provider]) {
validateAuthData = oauth[provider].validateAuthData;
validateAppId = oauth[provider].validateAppId;
}
// Try the configuration methods
if (oauthOptions) {
if (oauthOptions.module) {
validateAuthData = require(oauthOptions.module).validateAuthData;
validateAppId = require(oauthOptions.module).validateAppId;
};
if (oauthOptions.validateAuthData) {
validateAuthData = oauthOptions.validateAuthData;
}
if (oauthOptions.validateAppId) {
validateAppId = oauthOptions.validateAppId;
}
}
// try the custom provider first, fallback on the oauth implementation
if (!validateAuthData || !validateAppId) {
return false;
};
return validateAuthData(authData, oauthOptions)
.then(() => {
if (appIds && typeof validateAppId === "function") {
return validateAppId(appIds, authData, oauthOptions);
}
// No validation required by the developer
RestWrite.prototype.handleAuthDataValidation = function(authData) {
let validations = Object.keys(authData).map((provider) => {
if (authData[provider] === null) {
return Promise.resolve();
}
let validateAuthData = this.config.authDataManager.getValidatorForProvider(provider);
if (!validateAuthData) {
throw new Parse.Error(Parse.Error.UNSUPPORTED_SERVICE,
'This authentication method is unsupported.');
};
return validateAuthData(authData[provider]);
});
return Promise.all(validations);
}
}).then(() => {
// Check if this user already exists
// TODO: does this handle re-linking correctly?
var query = {};
query['authData.' + provider + '.id'] = authData.id;
return this.config.database.find(
RestWrite.prototype.findUsersWithAuthData = function(authData) {
let providers = Object.keys(authData);
let query = providers.reduce((memo, provider) => {
if (!authData[provider]) {
return memo;
}
let queryKey = `authData.${provider}.id`;
let query = {};
query[queryKey] = authData[provider].id;
memo.push(query);
return memo;
}, []).filter((q) => {
return typeof q !== undefined;
});
let findPromise = Promise.resolve([]);
if (query.length > 0) {
findPromise = this.config.database.find(
this.className,
query, {});
}).then((results) => {
this.storage['authProvider'] = provider;
if (results.length > 0) {
if (!this.query) {
// We're signing up, but this user already exists. Short-circuit
delete results[0].password;
this.response = {
response: results[0],
location: this.location()
};
this.data.objectId = results[0].objectId;
return;
}
{'$or': query}, {})
}
return findPromise;
}
// If this is a PUT for the same user, allow the linking
if (results[0].objectId === this.query.objectId) {
// Delete the rest format key before saving
delete this.data.authData;
return;
}
// We're trying to create a duplicate oauth auth. Forbid it
throw new Parse.Error(Parse.Error.ACCOUNT_ALREADY_LINKED,
RestWrite.prototype.handleAuthData = function(authData) {
let results;
return this.handleAuthDataValidation(authData).then(() => {
return this.findUsersWithAuthData(authData);
}).then((r) => {
results = r;
if (results.length > 1) {
// More than 1 user with the passed id's
throw new Parse.Error(Parse.Error.ACCOUNT_ALREADY_LINKED,
'this auth is already used');
} else {
this.data.username = cryptoUtils.newToken();
}
this.storage['authProvider'] = Object.keys(authData).join(',');
if (results.length == 0) {
this.data.username = cryptoUtils.newToken();
} else if (!this.query) {
// Login with auth data
// Short circuit
delete results[0].password;
this.response = {
response: results[0],
location: this.location()
};
this.data.objectId = results[0].objectId;
} else if (this.query && this.query.objectId) {
// Trying to update auth data but users
// are different
if (results[0].objectId !== this.query.objectId) {
throw new Parse.Error(Parse.Error.ACCOUNT_ALREADY_LINKED,
'this auth is already used');
}
// This FB auth does not already exist, so transform it to a
// saveable format
this.data["_auth_data_" + provider ] = authData;
// Delete the rest format key before saving
delete this.data.authData;
});
}
return Promise.resolve();
});
}
// The non-third-party parts of User transformation

View File

@@ -46,7 +46,7 @@ function createSchema(req) {
}
return req.config.database.loadSchema()
.then(schema => schema.addClassIfNotExists(className, req.body.fields))
.then(schema => schema.addClassIfNotExists(className, req.body.fields, req.body.classLevelPermissions))
.then(result => ({ response: Schema.mongoSchemaToSchemaAPIResponse(result) }));
}
@@ -60,52 +60,20 @@ function modifySchema(req) {
return req.config.database.loadSchema()
.then(schema => {
if (!schema.data[className]) {
throw new Parse.Error(Parse.Error.INVALID_CLASS_NAME, `Class ${req.params.className} does not exist.`);
}
let existingFields = Object.assign(schema.data[className], { _id: className });
Object.keys(submittedFields).forEach(name => {
let field = submittedFields[name];
if (existingFields[name] && field.__op !== 'Delete') {
throw new Parse.Error(255, `Field ${name} exists, cannot update.`);
}
if (!existingFields[name] && field.__op === 'Delete') {
throw new Parse.Error(255, `Field ${name} does not exist, cannot delete.`);
}
});
let newSchema = Schema.buildMergedSchemaObject(existingFields, submittedFields);
let mongoObject = Schema.mongoSchemaFromFieldsAndClassName(newSchema, className);
if (!mongoObject.result) {
throw new Parse.Error(mongoObject.code, mongoObject.error);
}
// Finally we have checked to make sure the request is valid and we can start deleting fields.
// Do all deletions first, then add fields to avoid duplicate geopoint error.
let deletePromises = [];
let insertedFields = [];
Object.keys(submittedFields).forEach(fieldName => {
if (submittedFields[fieldName].__op === 'Delete') {
const promise = schema.deleteField(fieldName, className, req.config.database);
deletePromises.push(promise);
} else {
insertedFields.push(fieldName);
}
});
return Promise.all(deletePromises) // Delete Everything
.then(() => schema.reloadData()) // Reload our Schema, so we have all the new values
.then(() => {
let promises = insertedFields.map(fieldName => {
const mongoType = mongoObject.result[fieldName];
return schema.validateField(className, fieldName, mongoType);
});
return Promise.all(promises);
})
.then(() => ({ response: Schema.mongoSchemaToSchemaAPIResponse(mongoObject.result) }));
return schema.updateClass(className, submittedFields, req.body.classLevelPermissions, req.config.database);
}).then((result) => {
return Promise.resolve({response: result});
});
}
function getSchemaPermissions(req) {
var className = req.params.className;
return req.config.database.loadSchema()
.then(schema => {
return Promise.resolve({response: schema.perms[className]});
});
}
// A helper function that removes all join tables for a schema. Returns a promise.
var removeJoinTables = (database, mongoSchema) => {
return Promise.all(Object.keys(mongoSchema)

View File

@@ -102,7 +102,20 @@ export class UsersRouter extends ClassesRouter {
let token = 'r:' + cryptoUtils.newToken();
user.sessionToken = token;
delete user.password;
// Sometimes the authData still has null on that keys
// https://github.com/ParsePlatform/parse-server/issues/935
if (user.authData) {
Object.keys(user.authData).forEach((provider) => {
if (user.authData[provider] === null) {
delete user.authData[provider];
}
});
if (Object.keys(user.authData).length == 0) {
delete user.authData;
}
}
req.config.filesController.expandFilesInObject(req.config, user);
let expiresAt = new Date();

View File

@@ -76,6 +76,50 @@ var requiredColumns = {
_Role: ["name", "ACL"]
}
// 10 alpha numberic chars + uppercase
const userIdRegex = /^[a-zA-Z0-9]{10}$/;
// Anything that start with role
const roleRegex = /^role:.*/;
// * permission
const publicRegex = /^\*$/
const permissionKeyRegex = [userIdRegex, roleRegex, publicRegex];
function verifyPermissionKey(key) {
let result = permissionKeyRegex.reduce((isGood, regEx) => {
isGood = isGood || key.match(regEx) != null;
return isGood;
}, false);
if (!result) {
throw new Parse.Error(Parse.Error.INVALID_JSON, `'${key}' is not a valid key for class level permissions`);
}
}
let CLPValidKeys = ['find', 'get', 'create', 'update', 'delete', 'addField'];
let DefaultClassLevelPermissions = CLPValidKeys.reduce((perms, key) => {
perms[key] = {
'*': true
};
return perms;
}, {});
function validateCLP(perms) {
if (!perms) {
return;
}
Object.keys(perms).forEach((operation) => {
if (CLPValidKeys.indexOf(operation) == -1) {
throw new Parse.Error(Parse.Error.INVALID_JSON, `${operation} is not a valid operation for class level permissions`);
}
Object.keys(perms[operation]).forEach((key) => {
verifyPermissionKey(key);
let perm = perms[operation][key];
if (perm !== true) {
throw new Parse.Error(Parse.Error.INVALID_JSON, `'${perm}' is not a valid value for class level permissions ${operation}:${key}:${perm}`);
}
});
});
}
// Valid classes must:
// Be one of _User, _Installation, _Role, _Session OR
// Be a join table OR
@@ -221,12 +265,12 @@ class Schema {
// on success, and rejects with an error on fail. Ensure you
// have authorization (master key, or client class creation
// enabled) before calling this function.
addClassIfNotExists(className, fields) {
addClassIfNotExists(className, fields, classLevelPermissions) {
if (this.data[className]) {
throw new Parse.Error(Parse.Error.INVALID_CLASS_NAME, `Class ${className} already exists.`);
}
let mongoObject = mongoSchemaFromFieldsAndClassName(fields, className);
let mongoObject = mongoSchemaFromFieldsAndClassNameAndCLP(fields, className, classLevelPermissions);
if (!mongoObject.result) {
return Promise.reject(mongoObject);
}
@@ -240,6 +284,54 @@ class Schema {
return Promise.reject(error);
});
}
updateClass(className, submittedFields, classLevelPermissions, database) {
if (!this.data[className]) {
throw new Parse.Error(Parse.Error.INVALID_CLASS_NAME, `Class ${className} does not exist.`);
}
let existingFields = Object.assign(this.data[className], {_id: className});
Object.keys(submittedFields).forEach(name => {
let field = submittedFields[name];
if (existingFields[name] && field.__op !== 'Delete') {
throw new Parse.Error(255, `Field ${name} exists, cannot update.`);
}
if (!existingFields[name] && field.__op === 'Delete') {
throw new Parse.Error(255, `Field ${name} does not exist, cannot delete.`);
}
});
let newSchema = buildMergedSchemaObject(existingFields, submittedFields);
let mongoObject = mongoSchemaFromFieldsAndClassNameAndCLP(newSchema, className, classLevelPermissions);
if (!mongoObject.result) {
throw new Parse.Error(mongoObject.code, mongoObject.error);
}
// Finally we have checked to make sure the request is valid and we can start deleting fields.
// Do all deletions first, then a single save to _SCHEMA collection to handle all additions.
let deletePromises = [];
let insertedFields = [];
Object.keys(submittedFields).forEach(fieldName => {
if (submittedFields[fieldName].__op === 'Delete') {
const promise = this.deleteField(fieldName, className, database);
deletePromises.push(promise);
} else {
insertedFields.push(fieldName);
}
});
return Promise.all(deletePromises) // Delete Everything
.then(() => this.reloadData()) // Reload our Schema, so we have all the new values
.then(() => {
let promises = insertedFields.map(fieldName => {
const mongoType = mongoObject.result[fieldName];
return this.validateField(className, fieldName, mongoType);
});
return Promise.all(promises);
})
.then(() => {
return this.setPermissions(className, classLevelPermissions)
})
.then(() => { return mongoSchemaToSchemaAPIResponse(mongoObject.result) });
}
// Returns whether the schema knows the type of all these keys.
@@ -288,6 +380,10 @@ class Schema {
// Sets the Class-level permissions for a given className, which must exist.
setPermissions(className, perms) {
if (typeof perms === 'undefined') {
return Promise.resolve();
}
validateCLP(perms);
var update = {
_metadata: {
class_permissions: perms
@@ -548,7 +644,7 @@ function load(collection) {
// Returns { code, error } if invalid, or { result }, an object
// suitable for inserting into _SCHEMA collection, otherwise
function mongoSchemaFromFieldsAndClassName(fields, className) {
function mongoSchemaFromFieldsAndClassNameAndCLP(fields, className, classLevelPermissions) {
if (!classNameIsValid(className)) {
return {
code: Parse.Error.INVALID_CLASS_NAME,
@@ -601,6 +697,16 @@ function mongoSchemaFromFieldsAndClassName(fields, className) {
error: 'currently, only one GeoPoint field may exist in an object. Adding ' + geoPoints[1] + ' when ' + geoPoints[0] + ' already exists.',
};
}
validateCLP(classLevelPermissions);
if (typeof classLevelPermissions !== 'undefined') {
mongoObject._metadata = mongoObject._metadata || {};
if (!classLevelPermissions) {
delete mongoObject._metadata.class_permissions;
} else {
mongoObject._metadata.class_permissions = classLevelPermissions;
}
}
return { result: mongoObject };
}
@@ -776,17 +882,23 @@ function mongoSchemaAPIResponseFields(schema) {
}
function mongoSchemaToSchemaAPIResponse(schema) {
return {
let result = {
className: schema._id,
fields: mongoSchemaAPIResponseFields(schema),
};
let classLevelPermissions = DefaultClassLevelPermissions;
if (schema._metadata && schema._metadata.class_permissions) {
classLevelPermissions = Object.assign(classLevelPermissions, schema._metadata.class_permissions);
}
result.classLevelPermissions = classLevelPermissions;
return result;
}
module.exports = {
load: load,
classNameIsValid: classNameIsValid,
invalidClassNameMessage: invalidClassNameMessage,
mongoSchemaFromFieldsAndClassName: mongoSchemaFromFieldsAndClassName,
schemaAPITypeToMongoFieldType: schemaAPITypeToMongoFieldType,
buildMergedSchemaObject: buildMergedSchemaObject,
mongoFieldTypeToSchemaAPIType: mongoFieldTypeToSchemaAPIType,

View File

@@ -0,0 +1,94 @@
let facebook = require('./facebook');
let instagram = require("./instagram");
let linkedin = require("./linkedin");
let meetup = require("./meetup");
let google = require("./google");
let github = require("./github");
let twitter = require("./twitter");
let anonymous = {
validateAuthData: () => {
return Promise.resolve();
},
validateAppId: () => {
return Promise.resolve();
}
}
let providers = {
facebook,
instagram,
linkedin,
meetup,
google,
github,
twitter,
anonymous
}
module.exports = function(oauthOptions = {}, enableAnonymousUsers = true) {
let _enableAnonymousUsers = enableAnonymousUsers;
let setEnableAnonymousUsers = function(enable) {
_enableAnonymousUsers = enable;
}
// To handle the test cases on configuration
let getValidatorForProvider = function(provider) {
if (provider === 'anonymous' && !_enableAnonymousUsers) {
return;
}
let defaultProvider = providers[provider];
let optionalProvider = oauthOptions[provider];
if (!defaultProvider && !optionalProvider) {
return;
}
let appIds;
if (optionalProvider) {
appIds = optionalProvider.appIds;
}
var validateAuthData;
var validateAppId;
if (defaultProvider) {
validateAuthData = defaultProvider.validateAuthData;
validateAppId = defaultProvider.validateAppId;
}
// Try the configuration methods
if (optionalProvider) {
if (optionalProvider.module) {
validateAuthData = require(optionalProvider.module).validateAuthData;
validateAppId = require(optionalProvider.module).validateAppId;
};
if (optionalProvider.validateAuthData) {
validateAuthData = optionalProvider.validateAuthData;
}
if (optionalProvider.validateAppId) {
validateAppId = optionalProvider.validateAppId;
}
}
if (!validateAuthData || !validateAppId) {
return;
}
return function(authData) {
return validateAuthData(authData, optionalProvider).then(() => {
if (appIds) {
return validateAppId(appIds, authData, optionalProvider);
}
return Promise.resolve();
})
}
}
return Object.freeze({
getValidatorForProvider,
setEnableAnonymousUsers,
})
}

View File

@@ -65,7 +65,7 @@ const app = express();
const api = new ParseServer(options);
app.use(options.mountPath, api);
app.listen(options.port, function() {
var server = app.listen(options.port, function() {
for (let key in options) {
let value = options[key];
@@ -77,3 +77,12 @@ app.listen(options.port, function() {
console.log('');
console.log('parse-server running on '+options.serverURL);
});
var handleShutdown = function() {
console.log('Termination signal received. Shutting down.');
server.close(function () {
process.exit(0);
});
};
process.on('SIGTERM', handleShutdown);
process.on('SIGINT', handleShutdown);

View File

@@ -8,7 +8,8 @@ var batch = require('./batch'),
express = require('express'),
middlewares = require('./middlewares'),
multer = require('multer'),
Parse = require('parse/node').Parse;
Parse = require('parse/node').Parse,
authDataManager = require('./authDataManager');
//import passwordReset from './passwordReset';
import cache from './cache';
@@ -84,6 +85,7 @@ function ParseServer({
push,
loggerAdapter,
databaseURI = DatabaseAdapter.defaultDatabaseURI,
databaseOptions,
cloud,
collectionPrefix = '',
clientKey,
@@ -120,6 +122,10 @@ function ParseServer({
DatabaseAdapter.setAppDatabaseURI(appId, databaseURI);
}
if (databaseOptions) {
DatabaseAdapter.setAppDatabaseOptions(appId, databaseOptions);
}
if (cloud) {
addParseCloud();
if (typeof cloud === 'function') {
@@ -163,9 +169,8 @@ function ParseServer({
hooksController: hooksController,
userController: userController,
verifyUserEmails: verifyUserEmails,
enableAnonymousUsers: enableAnonymousUsers,
allowClientClassCreation: allowClientClassCreation,
oauth: oauth,
authDataManager: authDataManager(oauth, enableAnonymousUsers),
appName: appName,
publicServerURL: publicServerURL,
customPages: customPages,

View File

@@ -1,17 +0,0 @@
var facebook = require('./facebook');
var instagram = require("./instagram");
var linkedin = require("./linkedin");
var meetup = require("./meetup");
var google = require("./google");
var github = require("./github");
var twitter = require("./twitter");
module.exports = {
facebook: facebook,
github: github,
google: google,
instagram: instagram,
linkedin: linkedin,
meetup: meetup,
twitter: twitter
}

View File

@@ -9,6 +9,7 @@
var Parse = require('parse/node').Parse;
import cache from './cache';
import Auth from './Auth';
var RestQuery = require('./RestQuery');
var RestWrite = require('./RestWrite');
@@ -42,7 +43,7 @@ function del(config, auth, className, objectId) {
if (triggers.getTrigger(className, triggers.Types.beforeDelete, config.applicationId) ||
triggers.getTrigger(className, triggers.Types.afterDelete, config.applicationId) ||
className == '_Session') {
return find(config, auth, className, {objectId: objectId})
return find(config, Auth.master(config), className, {objectId: objectId})
.then((response) => {
if (response && response.results && response.results.length) {
response.results[0].className = className;
@@ -97,7 +98,7 @@ function update(config, auth, className, objectId, restObject) {
return Promise.resolve().then(() => {
if (triggers.getTrigger(className, triggers.Types.beforeSave, config.applicationId) ||
triggers.getTrigger(className, triggers.Types.afterSave, config.applicationId)) {
return find(config, auth, className, {objectId: objectId});
return find(config, Auth.master(config), className, {objectId: objectId});
}
return Promise.resolve({});
}).then((response) => {

View File

@@ -87,7 +87,7 @@ export function transformKeyValue(schema, className, restKey, restValue, options
return transformWhere(schema, className, s);
});
return {key: '$and', value: mongoSubqueries};
default:
default:
// Other auth data
var authDataMatch = key.match(/^authData\.([a-zA-Z0-9_]+)\.id$/);
if (authDataMatch) {
@@ -203,6 +203,9 @@ function transformWhere(schema, className, restWhere) {
// restCreate is the "create" clause in REST API form.
// Returns the mongo form of the object.
function transformCreate(schema, className, restCreate) {
if (className == '_User') {
restCreate = transformAuthData(restCreate);
}
var mongoCreate = transformACL(restCreate);
for (var restKey in restCreate) {
var out = transformKeyValue(schema, className, restKey, restCreate[restKey]);
@@ -218,6 +221,10 @@ function transformUpdate(schema, className, restUpdate) {
if (!restUpdate) {
throw 'got empty restUpdate';
}
if (className == '_User') {
restUpdate = transformAuthData(restUpdate);
}
var mongoUpdate = {};
var acl = transformACL(restUpdate);
if (acl._rperm || acl._wperm) {
@@ -250,6 +257,16 @@ function transformUpdate(schema, className, restUpdate) {
return mongoUpdate;
}
function transformAuthData(restObject) {
if (restObject.authData) {
Object.keys(restObject.authData).forEach((provider) => {
restObject[`_auth_data_${provider}`] = restObject.authData[provider];
});
delete restObject.authData;
}
return restObject;
}
// Transforms a REST API formatted ACL object to our two-field mongo format.
// This mutates the restObject passed in to remove the ACL key.
function transformACL(restObject) {