Add new sanitize-entity.js

Signed-off-by: Convly <jean-sebastien.herbaux@epitech.eu>
This commit is contained in:
Convly 2020-07-01 18:08:21 +02:00 committed by Alexandre Bodin
parent 46f0c87b7d
commit 36facdfd00
2 changed files with 113 additions and 131 deletions

View File

@ -3,7 +3,7 @@
const _ = require('lodash'); const _ = require('lodash');
const { subject: asSubject } = require('@casl/ability'); const { subject: asSubject } = require('@casl/ability');
const { permittedFieldsOf, rulesToQuery } = require('@casl/ability/extra'); const { permittedFieldsOf, rulesToQuery } = require('@casl/ability/extra');
const { VALID_REST_OPERATORS } = require('strapi-utils'); const { VALID_REST_OPERATORS, sanitizeEntity } = require('strapi-utils');
const ops = { const ops = {
common: VALID_REST_OPERATORS, common: VALID_REST_OPERATORS,
@ -20,16 +20,12 @@ module.exports = (ability, action, model) => ({
return buildStrapiQuery(buildCaslQuery(ability, action, model)); return buildStrapiQuery(buildCaslQuery(ability, action, model));
}, },
queryFrom(otherQuery = {}) {
return { ...otherQuery, _where: { ...otherQuery._where, ...this.query } };
},
toSubject(target, subjectType = model) { toSubject(target, subjectType = model) {
return asSubject(subjectType, target); return asSubject(subjectType, target);
}, },
pickPermittedFieldsOf(data, options = {}) { pickPermittedFieldsOf(data, options = {}) {
return this.sanitize(data, { ...options, isInput: true }); return this.sanitize(data, { ...options, isOutput: false });
}, },
sanitize(data, options = {}) { sanitize(data, options = {}) {
@ -37,7 +33,7 @@ module.exports = (ability, action, model) => ({
subject = this.toSubject(data), subject = this.toSubject(data),
action: actionOverride = action, action: actionOverride = action,
withPrivate = true, withPrivate = true,
isInput = false, isOutput = true,
} = options; } = options;
if (_.isArray(data)) { if (_.isArray(data)) {
@ -46,105 +42,11 @@ module.exports = (ability, action, model) => ({
const permittedFields = permittedFieldsOf(ability, actionOverride, subject); const permittedFields = permittedFieldsOf(ability, actionOverride, subject);
const sanitizeDeep = ( return sanitizeEntity(data, {
data, model: strapi.getModel(model),
{ modelName, modelPlugin, withPrivate, fields, isInput = false } includeFields: _.isEmpty(permittedFields) ? null : permittedFields,
) => {
if (typeof data !== 'object' || _.isNil(data)) return data;
const plainData = typeof data.toJSON === 'function' ? data.toJSON() : data;
if (typeof plainData !== 'object') return plainData;
const modelDef = strapi.getModel(modelName, modelPlugin);
if (!modelDef) return null;
const { attributes, options, primaryKey } = modelDef;
const timestamps = options.timestamps || [];
const creatorFields = ['created_by', 'updated_by'];
const componentFields = ['__component'];
const inputFields = [primaryKey, componentFields];
const outputFields = [primaryKey, timestamps, creatorFields, componentFields];
const allowedFields = _.concat(fields, ...(isInput ? inputFields : outputFields));
const filterFields = (fields, key) =>
fields
.filter(field => field.startsWith(`${key}.`))
.map(field => field.replace(`${key}.`, ''));
return _.reduce(
plainData,
(acc, value, key) => {
const attribute = attributes[key];
const isAllowedField = !fields || allowedFields.includes(key);
// Always remove password fields from entities in output mode
if (attribute && attribute.type === 'password' && !isInput) {
return acc;
}
// Removes private fields if needed
if (attribute && attribute.private === true && !withPrivate && !isInput) {
return acc;
}
const relation =
attribute && (attribute.model || attribute.collection || attribute.component);
// Attribute is a relation
if (relation && value !== null) {
const filteredFields = filterFields(allowedFields, key);
const isAllowed = allowedFields.includes(key) || filteredFields.length > 0;
if (!isAllowed) {
return acc;
}
const nextFields = allowedFields.includes(key) ? null : filteredFields;
const opts = {
modelName: relation,
modelPlugin: attribute.plugin,
withPrivate,
fields: nextFields,
isInput,
};
const val = Array.isArray(value)
? value.map(entity => sanitizeDeep(entity, opts))
: sanitizeDeep(value, opts);
return { ...acc, [key]: val };
}
// Attribute is a dynamic zone
if (attribute && attribute.components && value !== null && allowedFields.includes(key)) {
return {
...acc,
[key]: value.map(data =>
sanitizeDeep(data, { modelName: data.__component, withPrivate, isInput })
),
};
}
// Add the key & value if we have the permission
if (isAllowedField) {
return { ...acc, [key]: value };
}
return acc;
},
{}
);
};
return sanitizeDeep(data, {
modelName: model,
withPrivate, withPrivate,
fields: _.isEmpty(permittedFields) ? null : permittedFields, isOutput,
isInput,
}); });
}, },
}); });

View File

@ -1,44 +1,124 @@
'use strict'; 'use strict';
module.exports = function sanitizeEntity(data, { model, withPrivate = false }) { const _ = require('lodash');
if (typeof data !== 'object' || data == null) return data;
let plainData = typeof data.toJSON === 'function' ? data.toJSON() : data; const sanitizeEntity = (dataSource, options) => {
const { model, withPrivate = false, isOutput = true, includeFields = null } = options;
if (typeof plainData !== 'object') return plainData; if (typeof dataSource !== 'object' || _.isNil(dataSource)) {
return dataSource;
}
const attributes = model.attributes; const data = parseOriginalData(dataSource);
return Object.keys(plainData).reduce((acc, key) => {
if (typeof data !== 'object') {
return data;
}
if (_.isNil(model)) {
return null;
}
const { attributes } = model;
const allowedFields = getAllowedFields({ includeFields, model, isOutput });
const reducerFn = (acc, value, key) => {
const attribute = attributes[key]; const attribute = attributes[key];
if (attribute && attribute.private === true && withPrivate !== true) { const allowedFieldsHasKey = allowedFields.includes(key);
if (shouldRemoveAttribute(attribute, { withPrivate, isOutput })) {
return acc; return acc;
} }
if (attribute && (attribute.model || attribute.collection || attribute.type === 'component')) { // Relations
const targetName = attribute.model || attribute.collection || attribute.component; const relation = attribute && (attribute.model || attribute.collection || attribute.component);
if (relation && value !== null) {
const targetModel = strapi.getModel(targetName, attribute.plugin); const [nextFields, isAllowed] = getNextFields(allowedFields, key, { allowedFieldsHasKey });
if (targetModel && plainData[key] !== null) {
acc[key] = Array.isArray(plainData[key])
? plainData[key].map(entity =>
sanitizeEntity(entity, { model: targetModel, withPrivate })
)
: sanitizeEntity(plainData[key], { model: targetModel, withPrivate });
if (!isAllowed) {
return acc; return acc;
} }
const nextOptions = {
model: strapi.getModel(relation, attribute.plugin),
withPrivate,
isOutput,
includeFields: nextFields,
};
const nextVal = Array.isArray(value)
? value.map(elem => sanitizeEntity(elem, nextOptions))
: sanitizeEntity(value, nextOptions);
return { ...acc, [key]: nextVal };
} }
if (attribute && attribute.components && plainData[key] !== null) { // Dynamic zones
acc[key] = plainData[key].map(data => { if (attribute && attribute.components && value !== null && allowedFieldsHasKey) {
const model = strapi.getModel(data.__component); const nextVal = value.map(elem =>
return model ? sanitizeEntity(data, { model, withPrivate }) : null; sanitizeEntity(elem, {
}); model: strapi.getModel(elem.__component),
return acc; withPrivate,
isOutput,
})
);
return { ...acc, [key]: nextVal };
}
// Other fields
const isAllowedField = !includeFields || allowedFieldsHasKey;
if (isAllowedField) {
return { ...acc, [key]: value };
} }
acc[key] = plainData[key];
return acc; return acc;
}, {}); };
return _.reduce(data, reducerFn, {});
}; };
const parseOriginalData = data => (_.isFunction(data.toJSON) ? data.toJSON() : data);
const getAllowedFields = ({ includeFields, model, isOutput }) => {
const { options, primaryKey } = model;
const timestamps = options.timestamps || [];
const creatorFields = ['created_by', 'updated_by'];
const componentFields = ['__component'];
return _.concat(
includeFields || [],
...(isOutput
? [primaryKey, componentFields, timestamps, creatorFields]
: [primaryKey, componentFields])
);
};
const getNextFields = (fields, key, { allowedFieldsHasKey }) => {
const searchStr = `${key}.`;
const transformedFields = (fields || [])
.filter(field => field.startsWith(searchStr))
.map(field => field.replace(searchStr, ''));
const isAllowed = allowedFieldsHasKey || transformedFields.length > 0;
const nextFields = allowedFieldsHasKey ? null : transformedFields;
return [nextFields, isAllowed];
};
const shouldRemoveAttribute = (attribute, { withPrivate, isOutput }) => {
if (_.isNil(attribute)) {
return false;
}
const isPassword = attribute.type === 'password';
const isPrivate = attribute.private === true;
const shouldRemovePassword = isOutput;
const shouldRemovePrivate = !withPrivate && isOutput;
return !!((isPassword && shouldRemovePassword) || (isPrivate && shouldRemovePrivate));
};
module.exports = sanitizeEntity;