mirror of
https://github.com/strapi/strapi.git
synced 2025-09-25 08:19:07 +00:00
Add new sanitize-entity.js
Signed-off-by: Convly <jean-sebastien.herbaux@epitech.eu>
This commit is contained in:
parent
46f0c87b7d
commit
36facdfd00
@ -3,7 +3,7 @@
|
||||
const _ = require('lodash');
|
||||
const { subject: asSubject } = require('@casl/ability');
|
||||
const { permittedFieldsOf, rulesToQuery } = require('@casl/ability/extra');
|
||||
const { VALID_REST_OPERATORS } = require('strapi-utils');
|
||||
const { VALID_REST_OPERATORS, sanitizeEntity } = require('strapi-utils');
|
||||
|
||||
const ops = {
|
||||
common: VALID_REST_OPERATORS,
|
||||
@ -20,16 +20,12 @@ module.exports = (ability, action, model) => ({
|
||||
return buildStrapiQuery(buildCaslQuery(ability, action, model));
|
||||
},
|
||||
|
||||
queryFrom(otherQuery = {}) {
|
||||
return { ...otherQuery, _where: { ...otherQuery._where, ...this.query } };
|
||||
},
|
||||
|
||||
toSubject(target, subjectType = model) {
|
||||
return asSubject(subjectType, target);
|
||||
},
|
||||
|
||||
pickPermittedFieldsOf(data, options = {}) {
|
||||
return this.sanitize(data, { ...options, isInput: true });
|
||||
return this.sanitize(data, { ...options, isOutput: false });
|
||||
},
|
||||
|
||||
sanitize(data, options = {}) {
|
||||
@ -37,7 +33,7 @@ module.exports = (ability, action, model) => ({
|
||||
subject = this.toSubject(data),
|
||||
action: actionOverride = action,
|
||||
withPrivate = true,
|
||||
isInput = false,
|
||||
isOutput = true,
|
||||
} = options;
|
||||
|
||||
if (_.isArray(data)) {
|
||||
@ -46,105 +42,11 @@ module.exports = (ability, action, model) => ({
|
||||
|
||||
const permittedFields = permittedFieldsOf(ability, actionOverride, subject);
|
||||
|
||||
const sanitizeDeep = (
|
||||
data,
|
||||
{ modelName, modelPlugin, withPrivate, fields, isInput = false }
|
||||
) => {
|
||||
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,
|
||||
return sanitizeEntity(data, {
|
||||
model: strapi.getModel(model),
|
||||
includeFields: _.isEmpty(permittedFields) ? null : permittedFields,
|
||||
withPrivate,
|
||||
fields: _.isEmpty(permittedFields) ? null : permittedFields,
|
||||
isInput,
|
||||
isOutput,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
@ -1,44 +1,124 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = function sanitizeEntity(data, { model, withPrivate = false }) {
|
||||
if (typeof data !== 'object' || data == null) return data;
|
||||
const _ = require('lodash');
|
||||
|
||||
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;
|
||||
return Object.keys(plainData).reduce((acc, key) => {
|
||||
const data = parseOriginalData(dataSource);
|
||||
|
||||
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];
|
||||
if (attribute && attribute.private === true && withPrivate !== true) {
|
||||
const allowedFieldsHasKey = allowedFields.includes(key);
|
||||
|
||||
if (shouldRemoveAttribute(attribute, { withPrivate, isOutput })) {
|
||||
return acc;
|
||||
}
|
||||
|
||||
if (attribute && (attribute.model || attribute.collection || attribute.type === 'component')) {
|
||||
const targetName = attribute.model || attribute.collection || attribute.component;
|
||||
|
||||
const targetModel = strapi.getModel(targetName, attribute.plugin);
|
||||
|
||||
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 });
|
||||
// Relations
|
||||
const relation = attribute && (attribute.model || attribute.collection || attribute.component);
|
||||
if (relation && value !== null) {
|
||||
const [nextFields, isAllowed] = getNextFields(allowedFields, key, { allowedFieldsHasKey });
|
||||
|
||||
if (!isAllowed) {
|
||||
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) {
|
||||
acc[key] = plainData[key].map(data => {
|
||||
const model = strapi.getModel(data.__component);
|
||||
return model ? sanitizeEntity(data, { model, withPrivate }) : null;
|
||||
});
|
||||
return acc;
|
||||
// Dynamic zones
|
||||
if (attribute && attribute.components && value !== null && allowedFieldsHasKey) {
|
||||
const nextVal = value.map(elem =>
|
||||
sanitizeEntity(elem, {
|
||||
model: strapi.getModel(elem.__component),
|
||||
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 _.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;
|
||||
|
Loading…
x
Reference in New Issue
Block a user