diff --git a/spec/ProtectedFields.spec.js b/spec/ProtectedFields.spec.js new file mode 100644 index 0000000000..92b89a6bae --- /dev/null +++ b/spec/ProtectedFields.spec.js @@ -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(); + }); + }); +}); diff --git a/spec/UserPII.spec.js b/spec/UserPII.spec.js index 001a97c843..84c3c10905 100644 --- a/spec/UserPII.spec.js +++ b/spec/UserPII.spec.js @@ -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); }); diff --git a/src/ParseServer.js b/src/ParseServer.js index a2e0beb7dc..02621ff75d 100644 --- a/src/ParseServer.js +++ b/src/ParseServer.js @@ -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.