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:
Convly 2020-07-01 13:03:30 +02:00 committed by Alexandre Bodin
parent f2eb3c5726
commit 20f80b2361
8 changed files with 164 additions and 52 deletions

View File

@ -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",

View File

@ -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);
};
},
/**

View File

@ -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,
});
},
});

View File

@ -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',

View File

@ -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;
}
}
});

View File

@ -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;

View File

@ -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;
}, {});

View File

@ -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"