Skip to content

Commit 0dec493

Browse files
awgeorgeacinader
authored andcommitted
Add filter sensitive fields logic that apply CLPs\nAdd protectedFields CLP\nAdd defaults for protectedFields CLP\nFix tests
1 parent b343de0 commit 0dec493

File tree

7 files changed

+122
-13
lines changed

7 files changed

+122
-13
lines changed

spec/schemas.spec.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -756,7 +756,12 @@ describe('schemas', () => {
756756
newField: { type: 'String' },
757757
ACL: { type: 'ACL' },
758758
},
759-
classLevelPermissions: defaultClassLevelPermissions,
759+
classLevelPermissions: {
760+
...defaultClassLevelPermissions,
761+
protectedFields: {
762+
'*': ['email'],
763+
},
764+
},
760765
})
761766
).toBeUndefined();
762767
request({

src/Controllers/DatabaseController.js

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,15 @@ const validateQuery = (query: any): void => {
162162
};
163163

164164
// Filters out any data that shouldn't be on this REST-formatted object.
165-
const filterSensitiveData = (isMaster, aclGroup, className, object) => {
165+
const filterSensitiveData = (
166+
isMaster,
167+
aclGroup,
168+
className,
169+
protectedFields,
170+
object
171+
) => {
172+
protectedFields && protectedFields.forEach(k => delete object[k]);
173+
166174
if (className !== '_User') {
167175
return object;
168176
}
@@ -1141,7 +1149,8 @@ class DatabaseController {
11411149
distinct,
11421150
pipeline,
11431151
readPreference,
1144-
}: any = {}
1152+
}: any = {},
1153+
auth: any = {}
11451154
): Promise<any> {
11461155
const isMaster = acl === undefined;
11471156
const aclGroup = acl || [];
@@ -1206,6 +1215,7 @@ class DatabaseController {
12061215
this.reduceInRelation(className, query, schemaController)
12071216
)
12081217
.then(() => {
1218+
let protectedFields;
12091219
if (!isMaster) {
12101220
query = this.addPointerPermissions(
12111221
schemaController,
@@ -1214,6 +1224,15 @@ class DatabaseController {
12141224
query,
12151225
aclGroup
12161226
);
1227+
// ProtectedFields is generated before executing the query so we
1228+
// can optimize the query using Mongo Projection at a later stage.
1229+
protectedFields = this.addProtectedFields(
1230+
schemaController,
1231+
className,
1232+
query,
1233+
aclGroup,
1234+
auth
1235+
);
12171236
}
12181237
if (!query) {
12191238
if (op === 'get') {
@@ -1276,6 +1295,7 @@ class DatabaseController {
12761295
isMaster,
12771296
aclGroup,
12781297
className,
1298+
protectedFields,
12791299
object
12801300
);
12811301
})
@@ -1390,6 +1410,42 @@ class DatabaseController {
13901410
}
13911411
}
13921412

1413+
addProtectedFields(
1414+
schema: SchemaController.SchemaController,
1415+
className: string,
1416+
query: any = {},
1417+
aclGroup: any[] = [],
1418+
auth: any = {}
1419+
) {
1420+
const perms = schema.getClassLevelPermissions(className);
1421+
if (!perms) return null;
1422+
1423+
const protectedFields = perms.protectedFields;
1424+
if (!protectedFields) return null;
1425+
1426+
if (aclGroup.indexOf(query.objectId) > -1) return null;
1427+
if (
1428+
Object.keys(query).length === 0 &&
1429+
auth &&
1430+
auth.user &&
1431+
aclGroup.indexOf(auth.user.id) > -1
1432+
)
1433+
return null;
1434+
1435+
let protectedKeys;
1436+
[...(auth.userRoles || []), '*'].forEach(role => {
1437+
// If you are in multiple groups assign the role with the least protectedKeys.
1438+
// Technically this could fail if multiple roles protect different fields and produce the same count.
1439+
// But we have no way of knowing the role hierarchy here.
1440+
const fields = protectedFields[role];
1441+
if (fields && (!protectedKeys || fields.length < protectedKeys.length)) {
1442+
protectedKeys = fields;
1443+
}
1444+
});
1445+
1446+
return protectedKeys;
1447+
}
1448+
13931449
// TODO: create indexes on first creation of a _User object. Otherwise it's impossible to
13941450
// have a Parse app without it having a _User collection.
13951451
performInitialization() {

src/Controllers/SchemaController.js

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
const Parse = require('parse/node').Parse;
1919
import { StorageAdapter } from '../Adapters/Storage/StorageAdapter';
2020
import DatabaseController from './DatabaseController';
21+
import Config from '../Config';
22+
import deepcopy from 'deepcopy';
2123
import type {
2224
Schema,
2325
SchemaFields,
@@ -387,16 +389,34 @@ const convertAdapterSchemaToParseSchema = ({ ...schema }) => {
387389

388390
class SchemaData {
389391
__data: any;
390-
constructor(allSchemas = []) {
392+
__protectedFields: any;
393+
constructor(allSchemas = [], protectedFields = {}) {
391394
this.__data = {};
395+
this.__protectedFields = protectedFields;
392396
allSchemas.forEach(schema => {
393397
Object.defineProperty(this, schema.className, {
394398
get: () => {
395399
if (!this.__data[schema.className]) {
396400
const data = {};
397401
data.fields = injectDefaultSchema(schema).fields;
398-
data.classLevelPermissions = schema.classLevelPermissions;
402+
data.classLevelPermissions = deepcopy(schema.classLevelPermissions);
399403
data.indexes = schema.indexes;
404+
405+
const classProtectedFields = this.__protectedFields[
406+
schema.className
407+
];
408+
if (classProtectedFields) {
409+
for (const key in classProtectedFields) {
410+
const unq = new Set([
411+
...(data.classLevelPermissions.protectedFields[key] || []),
412+
...classProtectedFields[key],
413+
]);
414+
data.classLevelPermissions.protectedFields[key] = Array.from(
415+
unq
416+
);
417+
}
418+
}
419+
400420
this.__data[schema.className] = data;
401421
}
402422
return this.__data[schema.className];
@@ -523,6 +543,7 @@ export default class SchemaController {
523543
this._dbAdapter = databaseAdapter;
524544
this._cache = schemaCache;
525545
this.schemaData = new SchemaData();
546+
this.protectedFields = Config.get(Parse.applicationId).protectedFields;
526547
}
527548

528549
reloadData(options: LoadSchemaOptions = { clearCache: false }): Promise<any> {
@@ -539,7 +560,7 @@ export default class SchemaController {
539560
.then(() => {
540561
return this.getAllClasses(options).then(
541562
allSchemas => {
542-
this.schemaData = new SchemaData(allSchemas);
563+
this.schemaData = new SchemaData(allSchemas, this.protectedFields);
543564
delete this.reloadDataPromise;
544565
},
545566
err => {

src/Controllers/types.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,5 +26,5 @@ export type ClassLevelPermissions = {
2626
addField?: { [string]: boolean },
2727
readUserFields?: string[],
2828
writeUserFields?: string[],
29-
protectedFields?: { [string]: boolean },
29+
protectedFields?: { [string]: string[] },
3030
};

src/Options/Definitions.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ module.exports.ParseServerOptions = {
157157
help:
158158
'Personally identifiable information fields in the user table the should be removed for non-authorized users.',
159159
action: parsers.objectParser,
160-
//default: {"_User": {"*": ["email"]}} // For backwards compatiability, do not use a default here.
160+
default: { _User: { '*': ['email'] } },
161161
},
162162
enableAnonymousUsers: {
163163
env: 'PARSE_SERVER_ENABLE_ANON_USERS',

src/ParseServer.js

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -333,6 +333,8 @@ function addParseCloud() {
333333
}
334334

335335
function injectDefaults(options: ParseServerOptions) {
336+
const hasProtectedFields = !!options.protectedFields;
337+
336338
Object.keys(defaults).forEach(key => {
337339
if (!options.hasOwnProperty(key)) {
338340
options[key] = defaults[key];
@@ -344,15 +346,40 @@ function injectDefaults(options: ParseServerOptions) {
344346
}
345347

346348
// Backwards compatibility
347-
if (!options.protectedFields && options.userSensitiveFields) {
349+
if (!hasProtectedFields && options.userSensitiveFields) {
348350
/* eslint-disable no-console */
349-
console.warn(
350-
`\nDEPRECATED: userSensitiveFields has been replaced by protectedFields allowing the ability to protect fields in all classes with CLP. \n`
351+
!process.env.TESTING &&
352+
console.warn(
353+
`\nDEPRECATED: userSensitiveFields has been replaced by protectedFields allowing the ability to protect fields in all classes with CLP. \n`
354+
);
355+
356+
const userSensitiveFields = Array.from(
357+
new Set([
358+
...(defaults.userSensitiveFields || []),
359+
...(options.userSensitiveFields || []),
360+
])
351361
);
362+
352363
/* eslint-enable no-console */
353-
options.protectedFields = { _User: { '*': options.userSensitiveFields } };
364+
options.protectedFields = { _User: { '*': userSensitiveFields } };
354365
}
355366

367+
// Merge protectedFields options with defaults.
368+
Object.keys(defaults.protectedFields).forEach(c => {
369+
const cur = options.protectedFields[c];
370+
if (!cur) {
371+
options.protectedFields[c] = defaults.protectedFields[c];
372+
} else {
373+
Object.keys(defaults.protectedFields[c]).forEach(r => {
374+
const unq = new Set([
375+
...(options.protectedFields[c][r] || []),
376+
...defaults.protectedFields[c][r],
377+
]);
378+
options.protectedFields[c][r] = Array.from(unq);
379+
});
380+
}
381+
});
382+
356383
options.masterKeyIps = Array.from(
357384
new Set(
358385
options.masterKeyIps.concat(defaults.masterKeyIps, options.masterKeyIps)

src/RestQuery.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -630,7 +630,7 @@ RestQuery.prototype.runFind = function(options = {}) {
630630
findOptions.op = options.op;
631631
}
632632
return this.config.database
633-
.find(this.className, this.restWhere, findOptions)
633+
.find(this.className, this.restWhere, findOptions, this.auth)
634634
.then(results => {
635635
if (this.className === '_User') {
636636
for (var result of results) {

0 commit comments

Comments
 (0)