mirror of
https://github.com/strapi/strapi.git
synced 2025-12-26 14:44:31 +00:00
Better permissions-manager.sanitize behavior
Forbid empty arrays as fields for permissions (on ability creation) Differentiate input from output sanitizing Signed-off-by: Convly <jean-sebastien.herbaux@epitech.eu>
This commit is contained in:
parent
f2eb3c5726
commit
20f80b2361
@ -28,7 +28,7 @@
|
||||
"@buffetjs/icons": "3.1.1-next.13",
|
||||
"@buffetjs/styles": "3.1.1-next.13",
|
||||
"@buffetjs/utils": "3.1.1-next.13",
|
||||
"@casl/ability": "^4.1.3",
|
||||
"@casl/ability": "^4.1.5",
|
||||
"@fortawesome/fontawesome-free": "^5.11.2",
|
||||
"@fortawesome/fontawesome-svg-core": "^1.2.25",
|
||||
"@fortawesome/free-brands-svg-icons": "^5.11.2",
|
||||
@ -95,6 +95,7 @@
|
||||
"shelljs": "^0.7.8",
|
||||
"strapi-helper-plugin": "3.0.5",
|
||||
"strapi-utils": "3.0.5",
|
||||
"sift": "13.1.10",
|
||||
"style-loader": "^0.23.1",
|
||||
"styled-components": "^5.0.0",
|
||||
"terser-webpack-plugin": "^1.2.3",
|
||||
|
||||
@ -47,6 +47,11 @@ module.exports = conditionProvider => ({
|
||||
async evaluatePermission({ permission, user, options, registerFn }) {
|
||||
const { action, subject, fields, conditions } = permission;
|
||||
|
||||
// Permissions with empty fields array should be removed
|
||||
if (Array.isArray(fields) && fields.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Directly registers the permission if there is no condition to check/evaluate
|
||||
if (_.isUndefined(conditions) || _.isEmpty(conditions)) {
|
||||
return registerFn({ action, subject, fields, condition: true });
|
||||
@ -95,8 +100,9 @@ module.exports = conditionProvider => ({
|
||||
* @returns {function({action?: *, subject?: *, fields?: *, condition?: *}): *}
|
||||
*/
|
||||
createRegisterFunction(can) {
|
||||
return ({ action, subject, fields, condition }) =>
|
||||
can(action, subject, fields, _.isObject(condition) ? condition : undefined);
|
||||
return ({ action, subject, fields, condition }) => {
|
||||
return can(action, subject, fields, _.isObject(condition) ? condition : undefined);
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||
@ -14,33 +14,132 @@ 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 });
|
||||
},
|
||||
|
||||
sanitize(data, options = {}) {
|
||||
const { subject = this.toSubject(data), action: actionOverride = action } = options;
|
||||
const {
|
||||
subject = this.toSubject(data),
|
||||
action: actionOverride = action,
|
||||
withPrivate = true,
|
||||
isInput = false,
|
||||
} = options;
|
||||
|
||||
if (_.isArray(data)) {
|
||||
return data.map(this.sanitize.bind(this));
|
||||
}
|
||||
|
||||
const permittedFields = permittedFieldsOf(ability, actionOverride, subject);
|
||||
|
||||
const dedicatedRules = ability.rules.filter(
|
||||
r => r.action === actionOverride && r.subject === model
|
||||
);
|
||||
const hasFieldRestrictions = _.some(
|
||||
dedicatedRules.map(_.property('fields')),
|
||||
_.negate(_.isUndefined)
|
||||
);
|
||||
const sanitizeDeep = (
|
||||
data,
|
||||
{ modelName, modelPlugin, withPrivate, fields, isInput = false }
|
||||
) => {
|
||||
if (typeof data !== 'object' || _.isNil(data)) return data;
|
||||
|
||||
// permittedFields can be an empty array for multiple reasons:
|
||||
// 1 - No permission for this action/subject
|
||||
// 2 - A permission without fields restriction (eg: can(action, subject))
|
||||
// 3 - A permission with strict fields restriction (eg: can(action, subject, []))
|
||||
// We need to check either we have to filter the properties (1, 3) or not (2)
|
||||
if (_.isEmpty(permittedFields) && !_.isEmpty(dedicatedRules) && !hasFieldRestrictions) {
|
||||
return data;
|
||||
}
|
||||
const plainData = typeof data.toJSON === 'function' ? data.toJSON() : data;
|
||||
if (typeof plainData !== 'object') return plainData;
|
||||
|
||||
return _.pick(data, permittedFields);
|
||||
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,
|
||||
fields: _.isEmpty(permittedFields) ? null : permittedFields,
|
||||
isInput,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@ -115,7 +115,7 @@ module.exports = ({ models, target }, ctx) => {
|
||||
GLOBALS,
|
||||
});
|
||||
|
||||
if (!definition.uid.startsWith('strapi::')) {
|
||||
if (!definition.uid.startsWith('strapi::') && definition.modelType !== 'component') {
|
||||
definition.attributes['created_by'] = {
|
||||
model: 'user',
|
||||
plugin: 'admin',
|
||||
|
||||
@ -27,7 +27,7 @@ module.exports = ({ models, target }, ctx) => {
|
||||
primaryKeyType: 'string',
|
||||
});
|
||||
|
||||
if (!definition.uid.startsWith('strapi::')) {
|
||||
if (!definition.uid.startsWith('strapi::') && definition.modelType !== 'component') {
|
||||
definition.attributes['created_by'] = {
|
||||
autoPopulate: false,
|
||||
model: 'user',
|
||||
@ -226,15 +226,13 @@ module.exports = ({ models, target }, ctx) => {
|
||||
}
|
||||
|
||||
if (type === 'dynamiczone') {
|
||||
if(returned[name]){
|
||||
const components = returned[name].map(el => {
|
||||
if (returned[name]) {
|
||||
returned[name] = returned[name].map(el => {
|
||||
return {
|
||||
__component: findComponentByGlobalId(el.kind).uid,
|
||||
...el.ref,
|
||||
};
|
||||
});
|
||||
|
||||
returned[name] = components;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@ -72,6 +72,7 @@ module.exports = {
|
||||
const {
|
||||
state: { userAbility },
|
||||
params: { model },
|
||||
request,
|
||||
} = ctx;
|
||||
const contentManagerService = strapi.plugins['content-manager'].services.contentmanager;
|
||||
const pm = strapi.admin.services.permission.createPermissionsManager(
|
||||
@ -81,10 +82,11 @@ module.exports = {
|
||||
);
|
||||
|
||||
let entities = [];
|
||||
|
||||
if (_.has(ctx.request.query, '_q')) {
|
||||
entities = await contentManagerService.search(model, ctx.request.query, pm.query);
|
||||
entities = await contentManagerService.search(model, request.query, pm.query);
|
||||
} else {
|
||||
entities = await contentManagerService.fetchAll(model, ctx.request.query, pm.query);
|
||||
entities = await contentManagerService.fetchAll(model, request.query, pm.query);
|
||||
}
|
||||
|
||||
if (!entities) {
|
||||
@ -161,16 +163,18 @@ module.exports = {
|
||||
throw strapi.errors.forbidden();
|
||||
}
|
||||
|
||||
const sanitize = e => pm.sanitize(e, { subject: model });
|
||||
const sanitize = e => pm.pickPermittedFieldsOf(e, { subject: model });
|
||||
|
||||
const userId = user.id;
|
||||
const { data, files } = ctx.is('multipart') ? parseMultipartBody(ctx) : { data: body };
|
||||
|
||||
try {
|
||||
data.created_by = userId;
|
||||
data.updated_by = userId;
|
||||
|
||||
const result = await contentManagerService.create({ data: sanitize(data), files }, { model });
|
||||
const result = await contentManagerService.create(
|
||||
{
|
||||
data: { ...sanitize(data), created_by: user.id, updated_by: user.id },
|
||||
files,
|
||||
},
|
||||
{ model }
|
||||
);
|
||||
|
||||
ctx.body = pm.sanitize(result, { action: ACTIONS.read });
|
||||
|
||||
@ -205,17 +209,14 @@ module.exports = {
|
||||
id
|
||||
);
|
||||
|
||||
const sanitize = e => pm.sanitize(e, { subject: pm.toSubject(entity) });
|
||||
const sanitize = e => pm.pickPermittedFieldsOf(e, { subject: pm.toSubject(entity) });
|
||||
|
||||
const userId = user.id;
|
||||
const { data, files } = ctx.is('multipart') ? parseMultipartBody(ctx) : { data: body };
|
||||
|
||||
try {
|
||||
data.updated_by = userId;
|
||||
|
||||
const result = await contentManagerService.edit(
|
||||
{ id },
|
||||
{ data: sanitize(data), files },
|
||||
{ data: { ...sanitize(data), updated_by: user.id }, files },
|
||||
{ model }
|
||||
);
|
||||
|
||||
@ -253,7 +254,7 @@ module.exports = {
|
||||
*/
|
||||
async deleteMany(ctx) {
|
||||
const {
|
||||
state: userAbility,
|
||||
state: { userAbility },
|
||||
params: { model },
|
||||
} = ctx;
|
||||
const contentManagerService = strapi.plugins['content-manager'].services.contentmanager;
|
||||
|
||||
@ -14,14 +14,8 @@ module.exports = function sanitizeEntity(data, { model, withPrivate = false }) {
|
||||
return acc;
|
||||
}
|
||||
|
||||
if (
|
||||
attribute &&
|
||||
(attribute.model ||
|
||||
attribute.collection ||
|
||||
attribute.type === 'component')
|
||||
) {
|
||||
const targetName =
|
||||
attribute.model || attribute.collection || attribute.component;
|
||||
if (attribute && (attribute.model || attribute.collection || attribute.type === 'component')) {
|
||||
const targetName = attribute.model || attribute.collection || attribute.component;
|
||||
|
||||
const targetModel = strapi.getModel(targetName, attribute.plugin);
|
||||
|
||||
@ -36,6 +30,14 @@ module.exports = function sanitizeEntity(data, { model, withPrivate = false }) {
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
acc[key] = plainData[key];
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
13
yarn.lock
13
yarn.lock
@ -1155,10 +1155,10 @@
|
||||
dependencies:
|
||||
yup "^0.27.0"
|
||||
|
||||
"@casl/ability@^4.1.3":
|
||||
version "4.1.3"
|
||||
resolved "https://registry.yarnpkg.com/@casl/ability/-/ability-4.1.3.tgz#cd94392e1efaec8812335273b9cea0cbe9fe1021"
|
||||
integrity sha512-kcoH01WpOvSC4ExYKKAvGLChQ32aM/kE1J9lRTLt0tIq8EJtv4FLNyLH4BQ+V6XlkHUV0SnjRxn0P0wK8/UzBw==
|
||||
"@casl/ability@^4.1.5":
|
||||
version "4.1.5"
|
||||
resolved "https://registry.yarnpkg.com/@casl/ability/-/ability-4.1.5.tgz#c34fedc1fd7e631f9f70309eecd5c8a2a7550ead"
|
||||
integrity sha512-9nY2ear3CfCi0ckRZ4V6dRiy+f0wxj/aUPkelmGrZuEJusXfhROKahRHFGxXf5xLi3oA10SS/vbSLK4RAy7ttA==
|
||||
dependencies:
|
||||
sift "^13.0.0"
|
||||
|
||||
@ -16580,6 +16580,11 @@ side-channel@^1.0.2:
|
||||
es-abstract "^1.17.0-next.1"
|
||||
object-inspect "^1.7.0"
|
||||
|
||||
sift@13.1.10:
|
||||
version "13.1.10"
|
||||
resolved "https://registry.yarnpkg.com/sift/-/sift-13.1.10.tgz#53cd1052c0214dc5d28a931e154191cce5cba942"
|
||||
integrity sha512-Z+7ZMTbnmbuVCwER+8jNerXpuJNYsxFSZf1er8VUqF/qYdgTrG5o5TQ7C6nWDycQY/TA1pczVCj58y5RvrUtrA==
|
||||
|
||||
sift@7.0.1:
|
||||
version "7.0.1"
|
||||
resolved "https://registry.yarnpkg.com/sift/-/sift-7.0.1.tgz#47d62c50b159d316f1372f8b53f9c10cd21a4b08"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user