Add filter sensitive fields logic that apply CLPs\nAdd protectedFields CLP\nAdd defaults for protectedFields CLP\nFix tests

This commit is contained in:
awgeorge
2019-01-29 08:52:49 +00:00
committed by Arthur Cinader
parent b343de0c70
commit 0dec4931a0
7 changed files with 122 additions and 13 deletions

View File

@@ -756,7 +756,12 @@ describe('schemas', () => {
newField: { type: 'String' }, newField: { type: 'String' },
ACL: { type: 'ACL' }, ACL: { type: 'ACL' },
}, },
classLevelPermissions: defaultClassLevelPermissions, classLevelPermissions: {
...defaultClassLevelPermissions,
protectedFields: {
'*': ['email'],
},
},
}) })
).toBeUndefined(); ).toBeUndefined();
request({ request({

View File

@@ -162,7 +162,15 @@ const validateQuery = (query: any): void => {
}; };
// Filters out any data that shouldn't be on this REST-formatted object. // Filters out any data that shouldn't be on this REST-formatted object.
const filterSensitiveData = (isMaster, aclGroup, className, object) => { const filterSensitiveData = (
isMaster,
aclGroup,
className,
protectedFields,
object
) => {
protectedFields && protectedFields.forEach(k => delete object[k]);
if (className !== '_User') { if (className !== '_User') {
return object; return object;
} }
@@ -1141,7 +1149,8 @@ class DatabaseController {
distinct, distinct,
pipeline, pipeline,
readPreference, readPreference,
}: any = {} }: any = {},
auth: any = {}
): Promise<any> { ): Promise<any> {
const isMaster = acl === undefined; const isMaster = acl === undefined;
const aclGroup = acl || []; const aclGroup = acl || [];
@@ -1206,6 +1215,7 @@ class DatabaseController {
this.reduceInRelation(className, query, schemaController) this.reduceInRelation(className, query, schemaController)
) )
.then(() => { .then(() => {
let protectedFields;
if (!isMaster) { if (!isMaster) {
query = this.addPointerPermissions( query = this.addPointerPermissions(
schemaController, schemaController,
@@ -1214,6 +1224,15 @@ class DatabaseController {
query, query,
aclGroup aclGroup
); );
// ProtectedFields is generated before executing the query so we
// can optimize the query using Mongo Projection at a later stage.
protectedFields = this.addProtectedFields(
schemaController,
className,
query,
aclGroup,
auth
);
} }
if (!query) { if (!query) {
if (op === 'get') { if (op === 'get') {
@@ -1276,6 +1295,7 @@ class DatabaseController {
isMaster, isMaster,
aclGroup, aclGroup,
className, className,
protectedFields,
object object
); );
}) })
@@ -1390,6 +1410,42 @@ class DatabaseController {
} }
} }
addProtectedFields(
schema: SchemaController.SchemaController,
className: string,
query: any = {},
aclGroup: any[] = [],
auth: any = {}
) {
const perms = schema.getClassLevelPermissions(className);
if (!perms) return null;
const protectedFields = perms.protectedFields;
if (!protectedFields) return null;
if (aclGroup.indexOf(query.objectId) > -1) return null;
if (
Object.keys(query).length === 0 &&
auth &&
auth.user &&
aclGroup.indexOf(auth.user.id) > -1
)
return null;
let protectedKeys;
[...(auth.userRoles || []), '*'].forEach(role => {
// If you are in multiple groups assign the role with the least protectedKeys.
// Technically this could fail if multiple roles protect different fields and produce the same count.
// But we have no way of knowing the role hierarchy here.
const fields = protectedFields[role];
if (fields && (!protectedKeys || fields.length < protectedKeys.length)) {
protectedKeys = fields;
}
});
return protectedKeys;
}
// TODO: create indexes on first creation of a _User object. Otherwise it's impossible to // TODO: create indexes on first creation of a _User object. Otherwise it's impossible to
// have a Parse app without it having a _User collection. // have a Parse app without it having a _User collection.
performInitialization() { performInitialization() {

View File

@@ -18,6 +18,8 @@
const Parse = require('parse/node').Parse; const Parse = require('parse/node').Parse;
import { StorageAdapter } from '../Adapters/Storage/StorageAdapter'; import { StorageAdapter } from '../Adapters/Storage/StorageAdapter';
import DatabaseController from './DatabaseController'; import DatabaseController from './DatabaseController';
import Config from '../Config';
import deepcopy from 'deepcopy';
import type { import type {
Schema, Schema,
SchemaFields, SchemaFields,
@@ -387,16 +389,34 @@ const convertAdapterSchemaToParseSchema = ({ ...schema }) => {
class SchemaData { class SchemaData {
__data: any; __data: any;
constructor(allSchemas = []) { __protectedFields: any;
constructor(allSchemas = [], protectedFields = {}) {
this.__data = {}; this.__data = {};
this.__protectedFields = protectedFields;
allSchemas.forEach(schema => { allSchemas.forEach(schema => {
Object.defineProperty(this, schema.className, { Object.defineProperty(this, schema.className, {
get: () => { get: () => {
if (!this.__data[schema.className]) { if (!this.__data[schema.className]) {
const data = {}; const data = {};
data.fields = injectDefaultSchema(schema).fields; data.fields = injectDefaultSchema(schema).fields;
data.classLevelPermissions = schema.classLevelPermissions; data.classLevelPermissions = deepcopy(schema.classLevelPermissions);
data.indexes = schema.indexes; data.indexes = schema.indexes;
const classProtectedFields = this.__protectedFields[
schema.className
];
if (classProtectedFields) {
for (const key in classProtectedFields) {
const unq = new Set([
...(data.classLevelPermissions.protectedFields[key] || []),
...classProtectedFields[key],
]);
data.classLevelPermissions.protectedFields[key] = Array.from(
unq
);
}
}
this.__data[schema.className] = data; this.__data[schema.className] = data;
} }
return this.__data[schema.className]; return this.__data[schema.className];
@@ -523,6 +543,7 @@ export default class SchemaController {
this._dbAdapter = databaseAdapter; this._dbAdapter = databaseAdapter;
this._cache = schemaCache; this._cache = schemaCache;
this.schemaData = new SchemaData(); this.schemaData = new SchemaData();
this.protectedFields = Config.get(Parse.applicationId).protectedFields;
} }
reloadData(options: LoadSchemaOptions = { clearCache: false }): Promise<any> { reloadData(options: LoadSchemaOptions = { clearCache: false }): Promise<any> {
@@ -539,7 +560,7 @@ export default class SchemaController {
.then(() => { .then(() => {
return this.getAllClasses(options).then( return this.getAllClasses(options).then(
allSchemas => { allSchemas => {
this.schemaData = new SchemaData(allSchemas); this.schemaData = new SchemaData(allSchemas, this.protectedFields);
delete this.reloadDataPromise; delete this.reloadDataPromise;
}, },
err => { err => {

View File

@@ -26,5 +26,5 @@ export type ClassLevelPermissions = {
addField?: { [string]: boolean }, addField?: { [string]: boolean },
readUserFields?: string[], readUserFields?: string[],
writeUserFields?: string[], writeUserFields?: string[],
protectedFields?: { [string]: boolean }, protectedFields?: { [string]: string[] },
}; };

View File

@@ -157,7 +157,7 @@ module.exports.ParseServerOptions = {
help: 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.',
action: parsers.objectParser, action: parsers.objectParser,
//default: {"_User": {"*": ["email"]}} // For backwards compatiability, do not use a default here. default: { _User: { '*': ['email'] } },
}, },
enableAnonymousUsers: { enableAnonymousUsers: {
env: 'PARSE_SERVER_ENABLE_ANON_USERS', env: 'PARSE_SERVER_ENABLE_ANON_USERS',

View File

@@ -333,6 +333,8 @@ function addParseCloud() {
} }
function injectDefaults(options: ParseServerOptions) { function injectDefaults(options: ParseServerOptions) {
const hasProtectedFields = !!options.protectedFields;
Object.keys(defaults).forEach(key => { Object.keys(defaults).forEach(key => {
if (!options.hasOwnProperty(key)) { if (!options.hasOwnProperty(key)) {
options[key] = defaults[key]; options[key] = defaults[key];
@@ -344,15 +346,40 @@ function injectDefaults(options: ParseServerOptions) {
} }
// Backwards compatibility // Backwards compatibility
if (!options.protectedFields && options.userSensitiveFields) { if (!hasProtectedFields && options.userSensitiveFields) {
/* eslint-disable no-console */ /* eslint-disable no-console */
console.warn( !process.env.TESTING &&
`\nDEPRECATED: userSensitiveFields has been replaced by protectedFields allowing the ability to protect fields in all classes with CLP. \n` console.warn(
`\nDEPRECATED: userSensitiveFields has been replaced by protectedFields allowing the ability to protect fields in all classes with CLP. \n`
);
const userSensitiveFields = Array.from(
new Set([
...(defaults.userSensitiveFields || []),
...(options.userSensitiveFields || []),
])
); );
/* eslint-enable no-console */ /* eslint-enable no-console */
options.protectedFields = { _User: { '*': options.userSensitiveFields } }; options.protectedFields = { _User: { '*': userSensitiveFields } };
} }
// Merge protectedFields options with defaults.
Object.keys(defaults.protectedFields).forEach(c => {
const cur = options.protectedFields[c];
if (!cur) {
options.protectedFields[c] = defaults.protectedFields[c];
} else {
Object.keys(defaults.protectedFields[c]).forEach(r => {
const unq = new Set([
...(options.protectedFields[c][r] || []),
...defaults.protectedFields[c][r],
]);
options.protectedFields[c][r] = Array.from(unq);
});
}
});
options.masterKeyIps = Array.from( options.masterKeyIps = Array.from(
new Set( new Set(
options.masterKeyIps.concat(defaults.masterKeyIps, options.masterKeyIps) options.masterKeyIps.concat(defaults.masterKeyIps, options.masterKeyIps)

View File

@@ -630,7 +630,7 @@ RestQuery.prototype.runFind = function(options = {}) {
findOptions.op = options.op; findOptions.op = options.op;
} }
return this.config.database return this.config.database
.find(this.className, this.restWhere, findOptions) .find(this.className, this.restWhere, findOptions, this.auth)
.then(results => { .then(results => {
if (this.className === '_User') { if (this.className === '_User') {
for (var result of results) { for (var result of results) {