Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: parse-community/parse-server
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: 3.2.1
Choose a base ref
...
head repository: parse-community/parse-server
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: 3.2.2
Choose a head ref
  • 2 commits
  • 4 files changed
  • 2 contributors

Commits on Mar 30, 2019

  1. Protected fields fix (#5463)

    * fix minor spelling mistake
    
    * Always process userSensitiveFields if they exist
    
    * Cover change to protectedFields
    Add start of some more tests for protectedFields
    which i need to do to document the feature.
    
    * re-arrange promise deck chairs to not
    swallow errors.
    
    * remove noop code
    
    * protect agains the case where options.protectedFields
    is set without a _User permission.
    acinader authored Mar 30, 2019

    Verified

    This commit was created on github.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    edf5b51 View commit details
  2. Allow test credentials for Facebook Auth (#5466)

    * Allow test credentials for Facebook Auth
    
    * node_env testing
    dplewis authored Mar 30, 2019

    Verified

    This commit was created on github.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    730f5c5 View commit details
Showing with 280 additions and 162 deletions.
  1. +141 −0 spec/ProtectedFields.spec.js
  2. +114 −157 spec/UserPII.spec.js
  3. +7 −1 src/Adapters/Auth/facebook.js
  4. +18 −4 src/ParseServer.js
141 changes: 141 additions & 0 deletions spec/ProtectedFields.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
describe('ProtectedFields', function() {
it('should handle and empty protectedFields', async function() {
const protectedFields = {};
await reconfigureServer({ protectedFields });

const user = new Parse.User();
user.setUsername('Alice');
user.setPassword('sekrit');
user.set('email', 'alice@aol.com');
user.set('favoriteColor', 'yellow');
await user.save();

const fetched = await new Parse.Query(Parse.User).get(user.id);
expect(fetched.has('email')).toBeFalsy();
expect(fetched.has('favoriteColor')).toBeTruthy();
});

describe('interaction with legacy userSensitiveFields', function() {
it('should fall back on sensitive fields if protected fields are not configured', async function() {
const userSensitiveFields = ['phoneNumber', 'timeZone'];

const protectedFields = { _User: { '*': ['email'] } };

await reconfigureServer({ userSensitiveFields, protectedFields });
const user = new Parse.User();
user.setUsername('Alice');
user.setPassword('sekrit');
user.set('email', 'alice@aol.com');
user.set('phoneNumber', 8675309);
user.set('timeZone', 'America/Los_Angeles');
user.set('favoriteColor', 'yellow');
user.set('favoriteFood', 'pizza');
await user.save();

const fetched = await new Parse.Query(Parse.User).get(user.id);
expect(fetched.has('email')).toBeFalsy();
expect(fetched.has('phoneNumber')).toBeFalsy();
expect(fetched.has('favoriteColor')).toBeTruthy();
});

it('should merge protected and sensitive for extra safety', async function() {
const userSensitiveFields = ['phoneNumber', 'timeZone'];

const protectedFields = { _User: { '*': ['email', 'favoriteFood'] } };

await reconfigureServer({ userSensitiveFields, protectedFields });
const user = new Parse.User();
user.setUsername('Alice');
user.setPassword('sekrit');
user.set('email', 'alice@aol.com');
user.set('phoneNumber', 8675309);
user.set('timeZone', 'America/Los_Angeles');
user.set('favoriteColor', 'yellow');
user.set('favoriteFood', 'pizza');
await user.save();

const fetched = await new Parse.Query(Parse.User).get(user.id);
expect(fetched.has('email')).toBeFalsy();
expect(fetched.has('phoneNumber')).toBeFalsy();
expect(fetched.has('favoriteFood')).toBeFalsy();
expect(fetched.has('favoriteColor')).toBeTruthy();
});
});

describe('non user class', function() {
it('should hide fields in a non user class', async function() {
const protectedFields = {
ClassA: { '*': ['foo'] },
ClassB: { '*': ['bar'] },
};
await reconfigureServer({ protectedFields });

const objA = await new Parse.Object('ClassA')
.set('foo', 'zzz')
.set('bar', 'yyy')
.save();

const objB = await new Parse.Object('ClassB')
.set('foo', 'zzz')
.set('bar', 'yyy')
.save();

const [fetchedA, fetchedB] = await Promise.all([
new Parse.Query('ClassA').get(objA.id),
new Parse.Query('ClassB').get(objB.id),
]);

expect(fetchedA.has('foo')).toBeFalsy();
expect(fetchedA.has('bar')).toBeTruthy();

expect(fetchedB.has('foo')).toBeTruthy();
expect(fetchedB.has('bar')).toBeFalsy();
});

it('should hide fields in non user class and non standard user field at same time', async function() {
const protectedFields = {
_User: { '*': ['phoneNumber'] },
ClassA: { '*': ['foo'] },
ClassB: { '*': ['bar'] },
};

await reconfigureServer({ protectedFields });

const user = new Parse.User();
user.setUsername('Alice');
user.setPassword('sekrit');
user.set('email', 'alice@aol.com');
user.set('phoneNumber', 8675309);
user.set('timeZone', 'America/Los_Angeles');
user.set('favoriteColor', 'yellow');
user.set('favoriteFood', 'pizza');
await user.save();

const objA = await new Parse.Object('ClassA')
.set('foo', 'zzz')
.set('bar', 'yyy')
.save();

const objB = await new Parse.Object('ClassB')
.set('foo', 'zzz')
.set('bar', 'yyy')
.save();

const [fetchedUser, fetchedA, fetchedB] = await Promise.all([
new Parse.Query(Parse.User).get(user.id),
new Parse.Query('ClassA').get(objA.id),
new Parse.Query('ClassB').get(objB.id),
]);

expect(fetchedA.has('foo')).toBeFalsy();
expect(fetchedA.has('bar')).toBeTruthy();

expect(fetchedB.has('foo')).toBeTruthy();
expect(fetchedB.has('bar')).toBeFalsy();

expect(fetchedUser.has('email')).toBeFalsy();
expect(fetchedUser.has('phoneNumber')).toBeFalsy();
expect(fetchedUser.has('favoriteColor')).toBeTruthy();
});
});
});
271 changes: 114 additions & 157 deletions spec/UserPII.spec.js
Original file line number Diff line number Diff line change
@@ -12,37 +12,31 @@ const SSN = '999-99-9999';
describe('Personally Identifiable Information', () => {
let user;

beforeEach(done => {
return Parse.User.signUp('tester', 'abc')
.then(loggedInUser => (user = loggedInUser))
.then(() => Parse.User.logIn(user.get('username'), 'abc'))
.then(() =>
user
.set('email', EMAIL)
.set('zip', ZIP)
.set('ssn', SSN)
.save()
)
.then(() => done());
beforeEach(async done => {
user = await Parse.User.signUp('tester', 'abc');
user = await Parse.User.logIn(user.get('username'), 'abc');
await user
.set('email', EMAIL)
.set('zip', ZIP)
.set('ssn', SSN)
.save();
done();
});

it('should be able to get own PII via API with object', done => {
const userObj = new (Parse.Object.extend(Parse.User))();
userObj.id = user.id;
userObj
return userObj
.fetch()
.then(
fetchedUser => {
expect(fetchedUser.get('email')).toBe(EMAIL);
},
e => console.error('error', e)
)
.then(fetchedUser => {
expect(fetchedUser.get('email')).toBe(EMAIL);
})
.then(done)
.catch(done.fail);
});

it('should not be able to get PII via API with object', done => {
Parse.User.logOut().then(() => {
return Parse.User.logOut().then(() => {
const userObj = new (Parse.Object.extend(Parse.User))();
userObj.id = user.id;
userObj
@@ -60,24 +54,19 @@ describe('Personally Identifiable Information', () => {
});

it('should be able to get PII via API with object using master key', done => {
Parse.User.logOut().then(() => {
return Parse.User.logOut().then(() => {
const userObj = new (Parse.Object.extend(Parse.User))();
userObj.id = user.id;
userObj
.fetch({ useMasterKey: true })
.then(
fetchedUser => {
expect(fetchedUser.get('email')).toBe(EMAIL);
},
e => console.error('error', e)
)
.then(fetchedUser => expect(fetchedUser.get('email')).toBe(EMAIL))
.then(done)
.catch(done.fail);
});
});

it('should be able to get own PII via API with Find', done => {
new Parse.Query(Parse.User).first().then(fetchedUser => {
return new Parse.Query(Parse.User).first().then(fetchedUser => {
expect(fetchedUser.get('email')).toBe(EMAIL);
expect(fetchedUser.get('zip')).toBe(ZIP);
expect(fetchedUser.get('ssn')).toBe(SSN);
@@ -86,7 +75,7 @@ describe('Personally Identifiable Information', () => {
});

it('should not get PII via API with Find', done => {
Parse.User.logOut().then(() =>
return Parse.User.logOut().then(() =>
new Parse.Query(Parse.User).first().then(fetchedUser => {
expect(fetchedUser.get('email')).toBe(undefined);
expect(fetchedUser.get('zip')).toBe(ZIP);
@@ -97,7 +86,7 @@ describe('Personally Identifiable Information', () => {
});

it('should get PII via API with Find using master key', done => {
Parse.User.logOut().then(() =>
return Parse.User.logOut().then(() =>
new Parse.Query(Parse.User)
.first({ useMasterKey: true })
.then(fetchedUser => {
@@ -110,7 +99,7 @@ describe('Personally Identifiable Information', () => {
});

it('should be able to get own PII via API with Get', done => {
new Parse.Query(Parse.User).get(user.id).then(fetchedUser => {
return new Parse.Query(Parse.User).get(user.id).then(fetchedUser => {
expect(fetchedUser.get('email')).toBe(EMAIL);
expect(fetchedUser.get('zip')).toBe(ZIP);
expect(fetchedUser.get('ssn')).toBe(SSN);
@@ -119,7 +108,7 @@ describe('Personally Identifiable Information', () => {
});

it('should not get PII via API with Get', done => {
Parse.User.logOut().then(() =>
return Parse.User.logOut().then(() =>
new Parse.Query(Parse.User).get(user.id).then(fetchedUser => {
expect(fetchedUser.get('email')).toBe(undefined);
expect(fetchedUser.get('zip')).toBe(ZIP);
@@ -130,7 +119,7 @@ describe('Personally Identifiable Information', () => {
});

it('should get PII via API with Get using master key', done => {
Parse.User.logOut().then(() =>
return Parse.User.logOut().then(() =>
new Parse.Query(Parse.User)
.get(user.id, { useMasterKey: true })
.then(fetchedUser => {
@@ -143,28 +132,25 @@ describe('Personally Identifiable Information', () => {
});

it('should not get PII via REST', done => {
request({
return request({
url: 'http://localhost:8378/1/classes/_User',
headers: {
'X-Parse-Application-Id': 'test',
'X-Parse-Javascript-Key': 'test',
},
})
.then(
response => {
const result = response.data;
const fetchedUser = result.results[0];
expect(fetchedUser.zip).toBe(ZIP);
expect(fetchedUser.email).toBe(undefined);
},
e => console.error('error', e.message)
)
.then(response => {
const result = response.data;
const fetchedUser = result.results[0];
expect(fetchedUser.zip).toBe(ZIP);
return expect(fetchedUser.email).toBe(undefined);
})
.then(done)
.catch(done.fail);
});

it('should get PII via REST with self credentials', done => {
request({
return request({
url: 'http://localhost:8378/1/classes/_User',
json: true,
headers: {
@@ -173,16 +159,14 @@ describe('Personally Identifiable Information', () => {
'X-Parse-Session-Token': user.getSessionToken(),
},
})
.then(
response => {
const result = response.data;
const fetchedUser = result.results[0];
expect(fetchedUser.zip).toBe(ZIP);
expect(fetchedUser.email).toBe(EMAIL);
},
e => console.error('error', e.message)
)
.then(done);
.then(response => {
const result = response.data;
const fetchedUser = result.results[0];
expect(fetchedUser.zip).toBe(ZIP);
return expect(fetchedUser.email).toBe(EMAIL);
})
.then(done)
.catch(done.fail);
});

it('should get PII via REST using master key', done => {
@@ -194,16 +178,14 @@ describe('Personally Identifiable Information', () => {
'X-Parse-Master-Key': 'test',
},
})
.then(
response => {
const result = response.data;
const fetchedUser = result.results[0];
expect(fetchedUser.zip).toBe(ZIP);
expect(fetchedUser.email).toBe(EMAIL);
},
e => console.error('error', e.message)
)
.then(() => done());
.then(response => {
const result = response.data;
const fetchedUser = result.results[0];
expect(fetchedUser.zip).toBe(ZIP);
return expect(fetchedUser.email).toBe(EMAIL);
})
.then(done)
.catch(done.fail);
});

it('should not get PII via REST by ID', done => {
@@ -235,16 +217,14 @@ describe('Personally Identifiable Information', () => {
'X-Parse-Session-Token': user.getSessionToken(),
},
})
.then(
response => {
const result = response.data;
const fetchedUser = result;
expect(fetchedUser.zip).toBe(ZIP);
expect(fetchedUser.email).toBe(EMAIL);
},
e => console.error('error', e.message)
)
.then(() => done());
.then(response => {
const result = response.data;
const fetchedUser = result;
expect(fetchedUser.zip).toBe(ZIP);
return expect(fetchedUser.email).toBe(EMAIL);
})
.then(done)
.catch(done.fail);
});

it('should get PII via REST by ID with master key', done => {
@@ -257,37 +237,35 @@ describe('Personally Identifiable Information', () => {
'X-Parse-Master-Key': 'test',
},
})
.then(
response => {
const result = response.data;
const fetchedUser = result;
expect(fetchedUser.zip).toBe(ZIP);
expect(fetchedUser.email).toBe(EMAIL);
},
e => console.error('error', e.message)
)
.then(() => done());
.then(response => {
const result = response.data;
const fetchedUser = result;
expect(fetchedUser.zip).toBe(ZIP);
expect(fetchedUser.email).toBe(EMAIL);
})
.then(done)
.catch(done.fail);
});

describe('with deprecated configured sensitive fields', () => {
beforeEach(done => {
reconfigureServer({ userSensitiveFields: ['ssn', 'zip'] }).then(() =>
done()
return reconfigureServer({ userSensitiveFields: ['ssn', 'zip'] }).then(
done
);
});

it('should be able to get own PII via API with object', done => {
const userObj = new (Parse.Object.extend(Parse.User))();
userObj.id = user.id;
userObj.fetch().then(
fetchedUser => {
return userObj
.fetch()
.then(fetchedUser => {
expect(fetchedUser.get('email')).toBe(EMAIL);
expect(fetchedUser.get('zip')).toBe(ZIP);
expect(fetchedUser.get('ssn')).toBe(SSN);
done();
},
e => done.fail(e)
);
})
.catch(done.fail);
});

it('should not be able to get PII via API with object', done => {
@@ -296,14 +274,11 @@ describe('Personally Identifiable Information', () => {
userObj.id = user.id;
userObj
.fetch()
.then(
fetchedUser => {
expect(fetchedUser.get('email')).toBe(undefined);
expect(fetchedUser.get('zip')).toBe(undefined);
expect(fetchedUser.get('ssn')).toBe(undefined);
},
e => console.error('error', e)
)
.then(fetchedUser => {
expect(fetchedUser.get('email')).toBe(undefined);
expect(fetchedUser.get('zip')).toBe(undefined);
expect(fetchedUser.get('ssn')).toBe(undefined);
})
.then(done)
.catch(done.fail);
});
@@ -420,16 +395,13 @@ describe('Personally Identifiable Information', () => {
'X-Parse-Session-Token': user.getSessionToken(),
},
})
.then(
response => {
const result = response.data;
const fetchedUser = result.results[0];
expect(fetchedUser.zip).toBe(ZIP);
expect(fetchedUser.email).toBe(EMAIL);
expect(fetchedUser.ssn).toBe(SSN);
},
() => {}
)
.then(response => {
const result = response.data;
const fetchedUser = result.results[0];
expect(fetchedUser.zip).toBe(ZIP);
expect(fetchedUser.email).toBe(EMAIL);
return expect(fetchedUser.ssn).toBe(SSN);
})
.then(done)
.catch(done.fail);
});
@@ -553,7 +525,7 @@ describe('Personally Identifiable Information', () => {
done();
});

it('privilaged user should not be able to get user PII via API with object', done => {
it('privileged user should not be able to get user PII via API with object', done => {
const userObj = new (Parse.Object.extend(Parse.User))();
userObj.id = user.id;
userObj
@@ -565,7 +537,7 @@ describe('Personally Identifiable Information', () => {
.catch(done.fail);
});

it('privilaged user should not be able to get user PII via API with Find', done => {
it('privileged user should not be able to get user PII via API with Find', done => {
new Parse.Query(Parse.User)
.equalTo('objectId', user.id)
.find()
@@ -579,7 +551,7 @@ describe('Personally Identifiable Information', () => {
.catch(done.fail);
});

it('privilaged user should not be able to get user PII via API with Get', done => {
it('privileged user should not be able to get user PII via API with Get', done => {
new Parse.Query(Parse.User)
.get(user.id)
.then(fetchedUser => {
@@ -591,7 +563,7 @@ describe('Personally Identifiable Information', () => {
.catch(done.fail);
});

it('privilaged user should not get user PII via REST by ID', done => {
it('privileged user should not get user PII via REST by ID', done => {
request({
url: `http://localhost:8378/1/classes/_User/${user.id}`,
json: true,
@@ -601,15 +573,12 @@ describe('Personally Identifiable Information', () => {
'X-Parse-Session-Token': adminUser.getSessionToken(),
},
})
.then(
response => {
const result = response.data;
const fetchedUser = result;
expect(fetchedUser.zip).toBe(undefined);
expect(fetchedUser.email).toBe(undefined);
},
e => console.error('error', e.message)
)
.then(response => {
const result = response.data;
const fetchedUser = result;
expect(fetchedUser.zip).toBe(undefined);
expect(fetchedUser.email).toBe(undefined);
})
.then(() => done())
.catch(done.fail);
});
@@ -703,12 +672,9 @@ describe('Personally Identifiable Information', () => {
userObj.id = user.id;
userObj
.fetch()
.then(
fetchedUser => {
expect(fetchedUser.get('email')).toBe(undefined);
},
e => console.error('error', e)
)
.then(fetchedUser => {
expect(fetchedUser.get('email')).toBe(undefined);
})
.then(done)
.catch(done.fail);
});
@@ -768,14 +734,11 @@ describe('Personally Identifiable Information', () => {
userObj.id = user.id;
userObj
.fetch()
.then(
fetchedUser => {
expect(fetchedUser.get('email')).toBe(undefined);
expect(fetchedUser.get('zip')).toBe(undefined);
expect(fetchedUser.get('ssn')).toBe(undefined);
},
e => console.error('error', e)
)
.then(fetchedUser => {
expect(fetchedUser.get('email')).toBe(undefined);
expect(fetchedUser.get('zip')).toBe(undefined);
expect(fetchedUser.get('ssn')).toBe(undefined);
})
.then(done)
.catch(done.fail);
});
@@ -995,7 +958,7 @@ describe('Personally Identifiable Information', () => {
});

// Explicit ACL should be able to read sensitive information
describe('with privilaged user CLP', () => {
describe('with privileged user CLP', () => {
let adminUser;

beforeEach(async done => {
@@ -1025,7 +988,7 @@ describe('Personally Identifiable Information', () => {
done();
});

it('privilaged user should be able to get user PII via API with object', done => {
it('privileged user should be able to get user PII via API with object', done => {
const userObj = new (Parse.Object.extend(Parse.User))();
userObj.id = user.id;
userObj
@@ -1037,7 +1000,7 @@ describe('Personally Identifiable Information', () => {
.catch(done.fail);
});

it('privilaged user should be able to get user PII via API with Find', done => {
it('privileged user should be able to get user PII via API with Find', done => {
new Parse.Query(Parse.User)
.equalTo('objectId', user.id)
.find()
@@ -1051,7 +1014,7 @@ describe('Personally Identifiable Information', () => {
.catch(done.fail);
});

it('privilaged user should be able to get user PII via API with Get', done => {
it('privileged user should be able to get user PII via API with Get', done => {
new Parse.Query(Parse.User)
.get(user.id)
.then(fetchedUser => {
@@ -1063,7 +1026,7 @@ describe('Personally Identifiable Information', () => {
.catch(done.fail);
});

it('privilaged user should get user PII via REST by ID', done => {
it('privileged user should get user PII via REST by ID', done => {
request({
url: `http://localhost:8378/1/classes/_User/${user.id}`,
json: true,
@@ -1073,16 +1036,13 @@ describe('Personally Identifiable Information', () => {
'X-Parse-Session-Token': adminUser.getSessionToken(),
},
})
.then(
response => {
const result = response.data;
const fetchedUser = result;
expect(fetchedUser.zip).toBe(ZIP);
expect(fetchedUser.email).toBe(EMAIL);
},
e => console.error('error', e.message)
)
.then(() => done())
.then(response => {
const result = response.data;
const fetchedUser = result;
expect(fetchedUser.zip).toBe(ZIP);
expect(fetchedUser.email).toBe(EMAIL);
})
.then(done)
.catch(done.fail);
});
});
@@ -1175,12 +1135,9 @@ describe('Personally Identifiable Information', () => {
userObj.id = user.id;
userObj
.fetch()
.then(
fetchedUser => {
expect(fetchedUser.get('email')).toBe(undefined);
},
e => console.error('error', e)
)
.then(fetchedUser => {
expect(fetchedUser.get('email')).toBe(undefined);
})
.then(done)
.catch(done.fail);
});
8 changes: 7 additions & 1 deletion src/Adapters/Auth/facebook.js
Original file line number Diff line number Diff line change
@@ -7,7 +7,10 @@ function validateAuthData(authData) {
return graphRequest(
'me?fields=id&access_token=' + authData.access_token
).then(data => {
if (data && data.id == authData.id) {
if (
(data && data.id == authData.id) ||
(process.env.TESTING && authData.id === 'test')
) {
return;
}
throw new Parse.Error(
@@ -20,6 +23,9 @@ function validateAuthData(authData) {
// Returns a promise that fulfills iff this app id is valid.
function validateAppId(appIds, authData) {
var access_token = authData.access_token;
if (process.env.TESTING && access_token === 'test') {
return Promise.resolve();
}
if (!appIds.length) {
throw new Parse.Error(
Parse.Error.OBJECT_NOT_FOUND,
22 changes: 18 additions & 4 deletions src/ParseServer.js
Original file line number Diff line number Diff line change
@@ -333,8 +333,6 @@ function addParseCloud() {
}

function injectDefaults(options: ParseServerOptions) {
const hasProtectedFields = !!options.protectedFields;

Object.keys(defaults).forEach(key => {
if (!options.hasOwnProperty(key)) {
options[key] = defaults[key];
@@ -346,7 +344,7 @@ function injectDefaults(options: ParseServerOptions) {
}

// Backwards compatibility
if (!hasProtectedFields && options.userSensitiveFields) {
if (options.userSensitiveFields) {
/* eslint-disable no-console */
!process.env.TESTING &&
console.warn(
@@ -361,7 +359,23 @@ function injectDefaults(options: ParseServerOptions) {
])
);

options.protectedFields = { _User: { '*': userSensitiveFields } };
// If the options.protectedFields is unset,
// it'll be assigned the default above.
// Here, protect against the case where protectedFields
// is set, but doesn't have _User.
if (!('_User' in options.protectedFields)) {
options.protectedFields = Object.assign(
{ _User: [] },
options.protectedFields
);
}

options.protectedFields['_User']['*'] = Array.from(
new Set([
...(options.protectedFields['_User']['*'] || []),
...userSensitiveFields,
])
);
}

// Merge protectedFields options with defaults.