From 0dec4931a0ea1bd9ec5e5dde61d814bfb5d4cac5 Mon Sep 17 00:00:00 2001 From: awgeorge Date: Tue, 29 Jan 2019 08:52:49 +0000 Subject: [PATCH] Add filter sensitive fields logic that apply CLPs\nAdd protectedFields CLP\nAdd defaults for protectedFields CLP\nFix tests --- spec/schemas.spec.js | 7 +++- src/Controllers/DatabaseController.js | 60 ++++++++++++++++++++++++++- src/Controllers/SchemaController.js | 27 ++++++++++-- src/Controllers/types.js | 2 +- src/Options/Definitions.js | 2 +- src/ParseServer.js | 35 ++++++++++++++-- src/RestQuery.js | 2 +- 7 files changed, 122 insertions(+), 13 deletions(-) diff --git a/spec/schemas.spec.js b/spec/schemas.spec.js index a9c9f84d..0e34da9a 100644 --- a/spec/schemas.spec.js +++ b/spec/schemas.spec.js @@ -756,7 +756,12 @@ describe('schemas', () => { newField: { type: 'String' }, ACL: { type: 'ACL' }, }, - classLevelPermissions: defaultClassLevelPermissions, + classLevelPermissions: { + ...defaultClassLevelPermissions, + protectedFields: { + '*': ['email'], + }, + }, }) ).toBeUndefined(); request({ diff --git a/src/Controllers/DatabaseController.js b/src/Controllers/DatabaseController.js index 0fdf55a8..e198bc43 100644 --- a/src/Controllers/DatabaseController.js +++ b/src/Controllers/DatabaseController.js @@ -162,7 +162,15 @@ const validateQuery = (query: any): void => { }; // 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') { return object; } @@ -1141,7 +1149,8 @@ class DatabaseController { distinct, pipeline, readPreference, - }: any = {} + }: any = {}, + auth: any = {} ): Promise { const isMaster = acl === undefined; const aclGroup = acl || []; @@ -1206,6 +1215,7 @@ class DatabaseController { this.reduceInRelation(className, query, schemaController) ) .then(() => { + let protectedFields; if (!isMaster) { query = this.addPointerPermissions( schemaController, @@ -1214,6 +1224,15 @@ class DatabaseController { query, 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 (op === 'get') { @@ -1276,6 +1295,7 @@ class DatabaseController { isMaster, aclGroup, className, + protectedFields, 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 // have a Parse app without it having a _User collection. performInitialization() { diff --git a/src/Controllers/SchemaController.js b/src/Controllers/SchemaController.js index 4e5ab437..4bee7788 100644 --- a/src/Controllers/SchemaController.js +++ b/src/Controllers/SchemaController.js @@ -18,6 +18,8 @@ const Parse = require('parse/node').Parse; import { StorageAdapter } from '../Adapters/Storage/StorageAdapter'; import DatabaseController from './DatabaseController'; +import Config from '../Config'; +import deepcopy from 'deepcopy'; import type { Schema, SchemaFields, @@ -387,16 +389,34 @@ const convertAdapterSchemaToParseSchema = ({ ...schema }) => { class SchemaData { __data: any; - constructor(allSchemas = []) { + __protectedFields: any; + constructor(allSchemas = [], protectedFields = {}) { this.__data = {}; + this.__protectedFields = protectedFields; allSchemas.forEach(schema => { Object.defineProperty(this, schema.className, { get: () => { if (!this.__data[schema.className]) { const data = {}; data.fields = injectDefaultSchema(schema).fields; - data.classLevelPermissions = schema.classLevelPermissions; + data.classLevelPermissions = deepcopy(schema.classLevelPermissions); 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; } return this.__data[schema.className]; @@ -523,6 +543,7 @@ export default class SchemaController { this._dbAdapter = databaseAdapter; this._cache = schemaCache; this.schemaData = new SchemaData(); + this.protectedFields = Config.get(Parse.applicationId).protectedFields; } reloadData(options: LoadSchemaOptions = { clearCache: false }): Promise { @@ -539,7 +560,7 @@ export default class SchemaController { .then(() => { return this.getAllClasses(options).then( allSchemas => { - this.schemaData = new SchemaData(allSchemas); + this.schemaData = new SchemaData(allSchemas, this.protectedFields); delete this.reloadDataPromise; }, err => { diff --git a/src/Controllers/types.js b/src/Controllers/types.js index faf5975a..77a67f6c 100644 --- a/src/Controllers/types.js +++ b/src/Controllers/types.js @@ -26,5 +26,5 @@ export type ClassLevelPermissions = { addField?: { [string]: boolean }, readUserFields?: string[], writeUserFields?: string[], - protectedFields?: { [string]: boolean }, + protectedFields?: { [string]: string[] }, }; diff --git a/src/Options/Definitions.js b/src/Options/Definitions.js index 283f81f0..4ab352d0 100644 --- a/src/Options/Definitions.js +++ b/src/Options/Definitions.js @@ -157,7 +157,7 @@ module.exports.ParseServerOptions = { 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. + default: { _User: { '*': ['email'] } }, }, enableAnonymousUsers: { env: 'PARSE_SERVER_ENABLE_ANON_USERS', diff --git a/src/ParseServer.js b/src/ParseServer.js index e77a39da..7cebb2cd 100644 --- a/src/ParseServer.js +++ b/src/ParseServer.js @@ -333,6 +333,8 @@ function addParseCloud() { } function injectDefaults(options: ParseServerOptions) { + const hasProtectedFields = !!options.protectedFields; + Object.keys(defaults).forEach(key => { if (!options.hasOwnProperty(key)) { options[key] = defaults[key]; @@ -344,15 +346,40 @@ function injectDefaults(options: ParseServerOptions) { } // Backwards compatibility - if (!options.protectedFields && options.userSensitiveFields) { + if (!hasProtectedFields && 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` + !process.env.TESTING && + 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 */ - 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( new Set( options.masterKeyIps.concat(defaults.masterKeyIps, options.masterKeyIps) diff --git a/src/RestQuery.js b/src/RestQuery.js index 20a1b792..a9cceffd 100644 --- a/src/RestQuery.js +++ b/src/RestQuery.js @@ -630,7 +630,7 @@ RestQuery.prototype.runFind = function(options = {}) { findOptions.op = options.op; } return this.config.database - .find(this.className, this.restWhere, findOptions) + .find(this.className, this.restWhere, findOptions, this.auth) .then(results => { if (this.className === '_User') { for (var result of results) {