Add new definition and update tests to reflect
This commit is contained in:
@@ -269,7 +269,7 @@ describe('Personally Identifiable Information', () => {
|
||||
.then(() => done());
|
||||
});
|
||||
|
||||
describe('with configured sensitive fields', () => {
|
||||
describe('with deprecated configured sensitive fields', () => {
|
||||
beforeEach(done => {
|
||||
reconfigureServer({ userSensitiveFields: ['ssn', 'zip'] }).then(() =>
|
||||
done()
|
||||
@@ -523,7 +523,482 @@ describe('Personally Identifiable Information', () => {
|
||||
});
|
||||
|
||||
// Explict ACL should be able to read sensitive information
|
||||
describe('with privilaged user', () => {
|
||||
describe('with privilaged user no CLP', () => {
|
||||
let adminUser;
|
||||
|
||||
beforeEach(async done => {
|
||||
const adminRole = await new Parse.Role(
|
||||
'Administrator',
|
||||
new Parse.ACL()
|
||||
).save(null, { useMasterKey: true });
|
||||
|
||||
const managementRole = new Parse.Role(
|
||||
'managementOf_user' + user.id,
|
||||
new Parse.ACL(user)
|
||||
);
|
||||
managementRole.getRoles().add(adminRole);
|
||||
await managementRole.save(null, { useMasterKey: true });
|
||||
|
||||
const userACL = new Parse.ACL();
|
||||
userACL.setReadAccess(managementRole, true);
|
||||
await user.setACL(userACL).save(null, { useMasterKey: true });
|
||||
|
||||
adminUser = await Parse.User.signUp('administrator', 'secure');
|
||||
adminUser = await Parse.User.logIn(adminUser.get('username'), 'secure');
|
||||
await adminRole
|
||||
.getUsers()
|
||||
.add(adminUser)
|
||||
.save(null, { useMasterKey: true });
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
it('privilaged 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
|
||||
.fetch()
|
||||
.then(fetchedUser => {
|
||||
expect(fetchedUser.get('email')).toBe(undefined);
|
||||
})
|
||||
.then(done)
|
||||
.catch(done.fail);
|
||||
});
|
||||
|
||||
it('privilaged user should not be able to get user PII via API with Find', done => {
|
||||
new Parse.Query(Parse.User)
|
||||
.equalTo('objectId', user.id)
|
||||
.find()
|
||||
.then(fetchedUser => {
|
||||
fetchedUser = fetchedUser[0];
|
||||
expect(fetchedUser.get('email')).toBe(undefined);
|
||||
expect(fetchedUser.get('zip')).toBe(undefined);
|
||||
expect(fetchedUser.get('ssn')).toBe(undefined);
|
||||
done();
|
||||
})
|
||||
.catch(done.fail);
|
||||
});
|
||||
|
||||
it('privilaged user should not be able to get user PII via API with Get', done => {
|
||||
new Parse.Query(Parse.User)
|
||||
.get(user.id)
|
||||
.then(fetchedUser => {
|
||||
expect(fetchedUser.get('email')).toBe(undefined);
|
||||
expect(fetchedUser.get('zip')).toBe(undefined);
|
||||
expect(fetchedUser.get('ssn')).toBe(undefined);
|
||||
done();
|
||||
})
|
||||
.catch(done.fail);
|
||||
});
|
||||
|
||||
it('privilaged user should not get user PII via REST by ID', done => {
|
||||
request({
|
||||
url: `http://localhost:8378/1/classes/_User/${user.id}`,
|
||||
json: true,
|
||||
headers: {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-Javascript-Key': 'test',
|
||||
'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(() => done())
|
||||
.catch(done.fail);
|
||||
});
|
||||
});
|
||||
|
||||
// Public access ACL should always hide sensitive information
|
||||
describe('with public read ACL', () => {
|
||||
beforeEach(async done => {
|
||||
const userACL = new Parse.ACL();
|
||||
userACL.setPublicReadAccess(true);
|
||||
await user.setACL(userACL).save(null, { useMasterKey: true });
|
||||
done();
|
||||
});
|
||||
|
||||
it('should not be able to get user PII via API with object', done => {
|
||||
Parse.User.logOut().then(() => {
|
||||
const userObj = new (Parse.Object.extend(Parse.User))();
|
||||
userObj.id = user.id;
|
||||
userObj
|
||||
.fetch()
|
||||
.then(fetchedUser => {
|
||||
expect(fetchedUser.get('email')).toBe(undefined);
|
||||
})
|
||||
.then(done)
|
||||
.catch(done.fail);
|
||||
});
|
||||
});
|
||||
|
||||
it('should not be able to get user PII via API with Find', done => {
|
||||
Parse.User.logOut().then(() =>
|
||||
new Parse.Query(Parse.User)
|
||||
.equalTo('objectId', user.id)
|
||||
.find()
|
||||
.then(fetchedUser => {
|
||||
fetchedUser = fetchedUser[0];
|
||||
expect(fetchedUser.get('email')).toBe(undefined);
|
||||
expect(fetchedUser.get('zip')).toBe(undefined);
|
||||
expect(fetchedUser.get('ssn')).toBe(undefined);
|
||||
done();
|
||||
})
|
||||
.catch(done.fail)
|
||||
);
|
||||
});
|
||||
|
||||
it('should not be able to get user PII via API with Get', done => {
|
||||
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(undefined);
|
||||
expect(fetchedUser.get('ssn')).toBe(undefined);
|
||||
done();
|
||||
})
|
||||
.catch(done.fail)
|
||||
);
|
||||
});
|
||||
|
||||
it('should not get user PII via REST by ID', done => {
|
||||
request({
|
||||
url: `http://localhost:8378/1/classes/_User/${user.id}`,
|
||||
json: true,
|
||||
headers: {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-Javascript-Key': 'test',
|
||||
},
|
||||
})
|
||||
.then(response => {
|
||||
const result = response.data;
|
||||
const fetchedUser = result;
|
||||
expect(fetchedUser.zip).toBe(undefined);
|
||||
expect(fetchedUser.email).toBe(undefined);
|
||||
})
|
||||
.then(() => done())
|
||||
.catch(done.fail);
|
||||
});
|
||||
|
||||
// Even with an authenticated user, Public read ACL should never expose sensitive data.
|
||||
describe('with another authenticated user', () => {
|
||||
let anotherUser;
|
||||
|
||||
beforeEach(async done => {
|
||||
return Parse.User.signUp('another', 'abc')
|
||||
.then(loggedInUser => (anotherUser = loggedInUser))
|
||||
.then(() => Parse.User.logIn(anotherUser.get('username'), 'abc'))
|
||||
.then(() => done());
|
||||
});
|
||||
|
||||
it('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
|
||||
.fetch()
|
||||
.then(
|
||||
fetchedUser => {
|
||||
expect(fetchedUser.get('email')).toBe(undefined);
|
||||
},
|
||||
e => console.error('error', e)
|
||||
)
|
||||
.then(done)
|
||||
.catch(done.fail);
|
||||
});
|
||||
|
||||
it('should not be able to get user PII via API with Find', done => {
|
||||
new Parse.Query(Parse.User)
|
||||
.equalTo('objectId', user.id)
|
||||
.find()
|
||||
.then(fetchedUser => {
|
||||
fetchedUser = fetchedUser[0];
|
||||
expect(fetchedUser.get('email')).toBe(undefined);
|
||||
expect(fetchedUser.get('zip')).toBe(undefined);
|
||||
expect(fetchedUser.get('ssn')).toBe(undefined);
|
||||
done();
|
||||
})
|
||||
.catch(done.fail);
|
||||
});
|
||||
|
||||
it('should not be able to get user PII via API with Get', done => {
|
||||
new Parse.Query(Parse.User)
|
||||
.get(user.id)
|
||||
.then(fetchedUser => {
|
||||
expect(fetchedUser.get('email')).toBe(undefined);
|
||||
expect(fetchedUser.get('zip')).toBe(undefined);
|
||||
expect(fetchedUser.get('ssn')).toBe(undefined);
|
||||
done();
|
||||
})
|
||||
.catch(done.fail);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('with configured sensitive fields via CLP', () => {
|
||||
beforeEach(done => {
|
||||
reconfigureServer({
|
||||
protectedFields: {
|
||||
_User: { '*': ['ssn', 'zip'], 'role:Administrator': [] },
|
||||
},
|
||||
}).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 => {
|
||||
expect(fetchedUser.get('email')).toBe(EMAIL);
|
||||
expect(fetchedUser.get('zip')).toBe(ZIP);
|
||||
expect(fetchedUser.get('ssn')).toBe(SSN);
|
||||
done();
|
||||
},
|
||||
e => done.fail(e)
|
||||
);
|
||||
});
|
||||
|
||||
it('should not be able to get PII via API with object', done => {
|
||||
Parse.User.logOut().then(() => {
|
||||
const userObj = new (Parse.Object.extend(Parse.User))();
|
||||
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(done)
|
||||
.catch(done.fail);
|
||||
});
|
||||
});
|
||||
|
||||
it('should be able to get PII via API with object using master key', done => {
|
||||
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);
|
||||
expect(fetchedUser.get('zip')).toBe(ZIP);
|
||||
expect(fetchedUser.get('ssn')).toBe(SSN);
|
||||
}, done.fail)
|
||||
.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 => {
|
||||
expect(fetchedUser.get('email')).toBe(EMAIL);
|
||||
expect(fetchedUser.get('zip')).toBe(ZIP);
|
||||
expect(fetchedUser.get('ssn')).toBe(SSN);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should not get PII via API with Find', done => {
|
||||
Parse.User.logOut().then(() =>
|
||||
new Parse.Query(Parse.User).first().then(fetchedUser => {
|
||||
expect(fetchedUser.get('email')).toBe(undefined);
|
||||
expect(fetchedUser.get('zip')).toBe(undefined);
|
||||
expect(fetchedUser.get('ssn')).toBe(undefined);
|
||||
done();
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should get PII via API with Find using master key', done => {
|
||||
Parse.User.logOut().then(() =>
|
||||
new Parse.Query(Parse.User)
|
||||
.first({ useMasterKey: true })
|
||||
.then(fetchedUser => {
|
||||
expect(fetchedUser.get('email')).toBe(EMAIL);
|
||||
expect(fetchedUser.get('zip')).toBe(ZIP);
|
||||
expect(fetchedUser.get('ssn')).toBe(SSN);
|
||||
done();
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should be able to get own PII via API with Get', done => {
|
||||
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);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should not get PII via API with Get', done => {
|
||||
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(undefined);
|
||||
expect(fetchedUser.get('ssn')).toBe(undefined);
|
||||
done();
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should get PII via API with Get using master key', done => {
|
||||
Parse.User.logOut().then(() =>
|
||||
new Parse.Query(Parse.User)
|
||||
.get(user.id, { useMasterKey: true })
|
||||
.then(fetchedUser => {
|
||||
expect(fetchedUser.get('email')).toBe(EMAIL);
|
||||
expect(fetchedUser.get('zip')).toBe(ZIP);
|
||||
expect(fetchedUser.get('ssn')).toBe(SSN);
|
||||
done();
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should not get PII via REST', done => {
|
||||
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(undefined);
|
||||
expect(fetchedUser.ssn).toBe(undefined);
|
||||
expect(fetchedUser.email).toBe(undefined);
|
||||
}, done.fail)
|
||||
.then(done)
|
||||
.catch(done.fail);
|
||||
});
|
||||
|
||||
it('should get PII via REST with self credentials', done => {
|
||||
request({
|
||||
url: 'http://localhost:8378/1/classes/_User',
|
||||
json: true,
|
||||
headers: {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-Javascript-Key': 'test',
|
||||
'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(done)
|
||||
.catch(done.fail);
|
||||
});
|
||||
|
||||
it('should get PII via REST using master key', done => {
|
||||
request({
|
||||
url: 'http://localhost:8378/1/classes/_User',
|
||||
json: true,
|
||||
headers: {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'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);
|
||||
expect(fetchedUser.ssn).toBe(SSN);
|
||||
},
|
||||
e => done.fail(e.data)
|
||||
)
|
||||
.then(done)
|
||||
.catch(done.fail);
|
||||
});
|
||||
|
||||
it('should not get PII via REST by ID', done => {
|
||||
request({
|
||||
url: `http://localhost:8378/1/classes/_User/${user.id}`,
|
||||
json: true,
|
||||
headers: {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-Javascript-Key': 'test',
|
||||
},
|
||||
})
|
||||
.then(
|
||||
response => {
|
||||
const fetchedUser = response.data;
|
||||
expect(fetchedUser.zip).toBe(undefined);
|
||||
expect(fetchedUser.email).toBe(undefined);
|
||||
},
|
||||
e => done.fail(e.data)
|
||||
)
|
||||
.then(done)
|
||||
.catch(done.fail);
|
||||
});
|
||||
|
||||
it('should get PII via REST by ID with self credentials', done => {
|
||||
request({
|
||||
url: `http://localhost:8378/1/classes/_User/${user.id}`,
|
||||
json: true,
|
||||
headers: {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-Javascript-Key': 'test',
|
||||
'X-Parse-Session-Token': user.getSessionToken(),
|
||||
},
|
||||
})
|
||||
.then(
|
||||
response => {
|
||||
const fetchedUser = response.data;
|
||||
expect(fetchedUser.zip).toBe(ZIP);
|
||||
expect(fetchedUser.email).toBe(EMAIL);
|
||||
},
|
||||
() => {}
|
||||
)
|
||||
.then(done)
|
||||
.catch(done.fail);
|
||||
});
|
||||
|
||||
it('should get PII via REST by ID with master key', done => {
|
||||
request({
|
||||
url: `http://localhost:8378/1/classes/_User/${user.id}`,
|
||||
headers: {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-Javascript-Key': 'test',
|
||||
'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 => done.fail(e.data)
|
||||
)
|
||||
.then(done)
|
||||
.catch(done.fail);
|
||||
});
|
||||
|
||||
// Explict ACL should be able to read sensitive information
|
||||
describe('with privilaged user CLP', () => {
|
||||
let adminUser;
|
||||
|
||||
beforeEach(async done => {
|
||||
|
||||
@@ -26,4 +26,5 @@ export type ClassLevelPermissions = {
|
||||
addField?: { [string]: boolean },
|
||||
readUserFields?: string[],
|
||||
writeUserFields?: string[],
|
||||
protectedFields?: { [string]: boolean },
|
||||
};
|
||||
|
||||
@@ -148,10 +148,17 @@ module.exports.ParseServerOptions = {
|
||||
userSensitiveFields: {
|
||||
env: 'PARSE_SERVER_USER_SENSITIVE_FIELDS',
|
||||
help:
|
||||
'Personally identifiable information fields in the user table the should be removed for non-authorized users.',
|
||||
'Personally identifiable information fields in the user table the should be removed for non-authorized users. **Deprecated** @see protectedFields',
|
||||
action: parsers.arrayParser,
|
||||
default: ['email'],
|
||||
},
|
||||
protectedFields: {
|
||||
env: 'PARSE_SERVER_PROTECTED_FIELDS',
|
||||
help:
|
||||
'Personally identifiable information fields in the user table the should be removed for non-authorized users.',
|
||||
action: parsers.objectParser,
|
||||
//default: {"_User": {"*": ["email"]}} // For backwards compatiability, do not use a default here.
|
||||
},
|
||||
enableAnonymousUsers: {
|
||||
env: 'PARSE_SERVER_ENABLE_ANON_USERS',
|
||||
help: 'Enable (or disable) anon users, defaults to true',
|
||||
|
||||
@@ -81,9 +81,12 @@ export interface ParseServerOptions {
|
||||
:ENV: PARSE_SERVER_PRESERVE_FILE_NAME
|
||||
:DEFAULT: false */
|
||||
preserveFileName: ?boolean;
|
||||
/* Personally identifiable information fields in the user table the should be removed for non-authorized users.
|
||||
/* Personally identifiable information fields in the user table the should be removed for non-authorized users. Deprecated @see protectedFields
|
||||
:DEFAULT: ["email"] */
|
||||
userSensitiveFields: ?(string[]);
|
||||
/* Protected fields that should be treated with extra security when fetching details.
|
||||
:DEFAULT: {"_User": {"*": ["email"]}} */
|
||||
protectedFields: ?any;
|
||||
/* Enable (or disable) anon users, defaults to true
|
||||
:ENV: PARSE_SERVER_ENABLE_ANON_USERS
|
||||
:DEFAULT: true */
|
||||
|
||||
@@ -343,14 +343,15 @@ function injectDefaults(options: ParseServerOptions) {
|
||||
options.serverURL = `http://localhost:${options.port}${options.mountPath}`;
|
||||
}
|
||||
|
||||
options.userSensitiveFields = Array.from(
|
||||
new Set(
|
||||
options.userSensitiveFields.concat(
|
||||
defaults.userSensitiveFields,
|
||||
options.userSensitiveFields
|
||||
)
|
||||
)
|
||||
);
|
||||
// Backwards compatibility
|
||||
if (!options.protectedFields && options.userSensitiveFields) {
|
||||
/* eslint-disable no-console */
|
||||
console.warn(
|
||||
`\nDEPRECATED: userSensitiveFields has been replaced by protectedFields allowing the ability to protect fields in all classes with CLP. \n`
|
||||
);
|
||||
/* eslint-enable no-console */
|
||||
options.protectedFields = { _User: { '*': options.userSensitiveFields } };
|
||||
}
|
||||
|
||||
options.masterKeyIps = Array.from(
|
||||
new Set(
|
||||
|
||||
Reference in New Issue
Block a user