Add filter sensitive fields logic that apply CLPs\nAdd protectedFields CLP\nAdd defaults for protectedFields CLP\nFix tests
This commit is contained in:
@@ -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({
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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 => {
|
||||||
|
|||||||
@@ -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[] },
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user