mirror of
https://github.com/strapi/strapi.git
synced 2025-11-13 08:38:09 +00:00
* Add a domain layer for the permission, rework the engine handling of the permissions Signed-off-by: Convly <jean-sebastien.herbaux@epitech.eu> * Add permissions-fields-to-properties migration for the admin Signed-off-by: Convly <jean-sebastien.herbaux@epitech.eu> * Removes useless console.log Signed-off-by: Convly <jean-sebastien.herbaux@epitech.eu> * Remove debug logLevel from provider-login.test.e2e.js Signed-off-by: Convly <jean-sebastien.herbaux@epitech.eu> * Adds the new layout for the GET permissions, allow to subscribe to actionRegistered events, adds i18n handlers Signed-off-by: Convly <jean-sebastien.herbaux@epitech.eu> * Fix typo Signed-off-by: Convly <jean-sebastien.herbaux@epitech.eu> * Update permissions validators Signed-off-by: Convly <jean-sebastien.herbaux@epitech.eu> * Update unit tests Signed-off-by: Convly <jean-sebastien.herbaux@epitech.eu> * Update integrations test + fix some validation issues Signed-off-by: Convly <jean-sebastien.herbaux@epitech.eu> * Change plugins & settings section format for the permissions layout * only return locales property to localized subjects for the permission's layout * Do not send the locales property to the permission's layout when there is no locales created * Add the 'locales' property to publish & delete routes * Fix unwanted mutation of the sections builder states on multiple builds * Fix units tests with (new engine) * Fix admin-role e2e test - Add locales property to the update payload * fix e2e testsé * Update e2e snapshots * Fix unit test for i18n bootstrap * Add mocks for i18n/bootstrap test * Fix has-locale condition & updatePermission validator * Avoid mutation in migration, always authorize super admin for has-locales condition * Rework rbac domain objects, add a hook module and a provider factory * Remove old providers * Update the admin services & tests for the new rbac domain & providers * Fix tests, bootstrap functions & services following rbac domain rework * Update migration runner * PR comments Signed-off-by: Convly <jean-sebastien.herbaux@epitech.eu> * Remove useless console.log * Fix sanitizeCondition bug * Section builder rework * Add test for the section-builder section & add jsdoc for the permission domain * pr comments (without the migrations) * fix fields-to-properties migration * Add jsdoc for the sections-builder * Moves createBoundAbstractDomain from permission domain to the engine service * Remove debug logLevel for admin role test (e2e) * Fix core-store * Fix hooks & move business logic from i18n bootstrap to dedicated services * add route get-non-localized-fields * use write and read permission * refacto * add input validator * add route doc * handle ST Co-authored-by: Pierre Noël <petersg83@gmail.com> Co-authored-by: Alexandre BODIN <alexandrebodin@users.noreply.github.com>
245 lines
7.1 KiB
JavaScript
245 lines
7.1 KiB
JavaScript
'use strict';
|
|
|
|
const {
|
|
curry,
|
|
map,
|
|
filter,
|
|
each,
|
|
isFunction,
|
|
isArray,
|
|
isEmpty,
|
|
isObject,
|
|
prop,
|
|
merge,
|
|
pick,
|
|
difference,
|
|
cloneDeep,
|
|
} = require('lodash/fp');
|
|
const { AbilityBuilder, Ability } = require('@casl/ability');
|
|
const sift = require('sift');
|
|
const { hooks } = require('strapi-utils');
|
|
const permissionDomain = require('../../domain/permission/index');
|
|
const { getService } = require('../../utils');
|
|
|
|
const allowedOperations = [
|
|
'$or',
|
|
'$eq',
|
|
'$ne',
|
|
'$in',
|
|
'$nin',
|
|
'$lt',
|
|
'$lte',
|
|
'$gt',
|
|
'$gte',
|
|
'$exists',
|
|
'$elemMatch',
|
|
];
|
|
const operations = pick(allowedOperations, sift);
|
|
|
|
const conditionsMatcher = conditions => {
|
|
return sift.createQueryTester(conditions, { operations });
|
|
};
|
|
|
|
const createBoundAbstractPermissionDomain = permission => ({
|
|
get permission() {
|
|
return cloneDeep(permission);
|
|
},
|
|
|
|
addCondition(condition) {
|
|
Object.assign(permission, permissionDomain.addCondition(condition, permission));
|
|
|
|
return this;
|
|
},
|
|
});
|
|
|
|
module.exports = conditionProvider => {
|
|
const state = {
|
|
hooks: {
|
|
willEvaluatePermission: hooks.createAsyncSeriesHook(),
|
|
},
|
|
};
|
|
|
|
return {
|
|
hooks: state.hooks,
|
|
|
|
/**
|
|
* Generate an ability based on the given user (using associated roles & permissions)
|
|
* @param user
|
|
* @param options
|
|
* @returns {Promise<Ability>}
|
|
*/
|
|
async generateUserAbility(user, options) {
|
|
const permissions = await getService('permission').findUserPermissions(user);
|
|
const abilityCreator = this.generateAbilityCreatorFor(user);
|
|
|
|
return abilityCreator(permissions, options);
|
|
},
|
|
|
|
/**
|
|
* Create an ability factory for a specific user
|
|
* @param user
|
|
* @returns {function(*, *): Promise<Ability>}
|
|
*/
|
|
generateAbilityCreatorFor(user) {
|
|
return async (permissions, options) => {
|
|
const { can, build } = new AbilityBuilder(Ability);
|
|
const registerFn = this.createRegisterFunction(can);
|
|
|
|
for (const permission of permissions) {
|
|
await this.evaluate({ permission, user, options, registerFn });
|
|
}
|
|
|
|
return build({ conditionsMatcher });
|
|
};
|
|
},
|
|
|
|
/**
|
|
* Validate, invalidate and transform the permission attributes
|
|
* @param {Permission} permission
|
|
* @returns {null|Permission}
|
|
*/
|
|
formatPermission(permission) {
|
|
const { actionProvider } = getService('permission');
|
|
|
|
const action = actionProvider.get(permission.action);
|
|
|
|
// If the action isn't registered into the action provider, then ignore the permission
|
|
if (!action) {
|
|
return null;
|
|
}
|
|
|
|
const properties = permission.properties || {};
|
|
|
|
// Only keep the properties allowed by the action (action.applyToProperties)
|
|
const propertiesName = Object.keys(properties);
|
|
const invalidProperties = difference(
|
|
propertiesName,
|
|
action.applyToProperties || propertiesName
|
|
);
|
|
|
|
const permissionWithSanitizedProperties = invalidProperties.reduce(
|
|
property => permissionDomain.deleteProperty(property, permission),
|
|
permission
|
|
);
|
|
|
|
// If the `fields` property is an empty array, then ignore the permission
|
|
const { fields } = properties;
|
|
|
|
if (isArray(fields) && isEmpty(fields)) {
|
|
return null;
|
|
}
|
|
|
|
return permissionWithSanitizedProperties;
|
|
},
|
|
|
|
/**
|
|
* Update the permission components through various processing
|
|
* @param {Permission} permission
|
|
* @returns {Promise<void>}
|
|
*/
|
|
async applyPermissionProcessors(permission) {
|
|
const context = createBoundAbstractPermissionDomain(permission);
|
|
|
|
// 1. Trigger willEvaluatePermission hook and await transformation operated on the permission
|
|
await state.hooks.willEvaluatePermission.call(context);
|
|
},
|
|
|
|
/**
|
|
* Register new rules using `registerFn` based on valid permission's conditions
|
|
* @param options {object}
|
|
* @param options.permission {object}
|
|
* @param options.user {object}
|
|
* @param options.options {object | undefined}
|
|
* @param options.registerFn {Function}
|
|
* @returns {Promise<void>}
|
|
*/
|
|
async evaluate(options) {
|
|
const { user, registerFn, options: conditionOptions } = options;
|
|
|
|
// Assert options.permission validity and format it
|
|
const permission = this.formatPermission(options.permission);
|
|
|
|
// If options.permission is invalid, then ignore the permission
|
|
if (permission === null) {
|
|
return;
|
|
}
|
|
|
|
await this.applyPermissionProcessors(permission);
|
|
|
|
// Extract the up-to-date components from the permission
|
|
const { action, subject = 'all', properties = {}, conditions } = permission;
|
|
|
|
// Register the permission if there is no condition
|
|
if (isEmpty(conditions)) {
|
|
return registerFn({ action, subject, fields: properties.fields, condition: true });
|
|
}
|
|
|
|
/** Set of functions used to resolve + evaluate conditions & register the permission if allowed */
|
|
|
|
// 1. Replace each condition name by its associated value
|
|
const resolveConditions = map(conditionProvider.get);
|
|
|
|
// 2. Only keep the handler of each condition
|
|
const pickHandlers = map(prop('handler'));
|
|
|
|
// 3. Filter conditions, only keep objects and functions
|
|
const filterValidConditions = filter(isObject);
|
|
|
|
// 4. Evaluate the conditions if they're a function, returns the object otherwise
|
|
const evaluateConditions = conditions => {
|
|
return Promise.all(
|
|
conditions.map(cond =>
|
|
isFunction(cond)
|
|
? cond(user, merge(conditionOptions, { permission: cloneDeep(permission) }))
|
|
: cond
|
|
)
|
|
);
|
|
};
|
|
|
|
// 5. Only keeps 'true' booleans or objects as condition's result
|
|
const filterValidResults = filter(result => result === true || isObject(result));
|
|
|
|
// 6. Transform each result into registerFn options
|
|
const transformToRegisterOptions = map(result => ({
|
|
action,
|
|
subject,
|
|
fields: properties.fields,
|
|
condition: result,
|
|
}));
|
|
|
|
// 7. Register each result using the registerFn
|
|
const registerResults = each(registerFn);
|
|
|
|
/**/
|
|
|
|
// Execute all the steps needed to register the permission with its associated conditions
|
|
await Promise.resolve(conditions)
|
|
.then(resolveConditions)
|
|
.then(pickHandlers)
|
|
.then(filterValidConditions)
|
|
.then(evaluateConditions)
|
|
.then(filterValidResults)
|
|
.then(transformToRegisterOptions)
|
|
.then(registerResults);
|
|
},
|
|
|
|
/**
|
|
* Encapsulate a register function with custom params to fit `evaluatePermission`'s syntax
|
|
* @param can
|
|
* @returns {function({action?: *, subject?: *, fields?: *, condition?: *}): *}
|
|
*/
|
|
createRegisterFunction(can) {
|
|
return ({ action, subject, fields, condition }) => {
|
|
return can(action, subject, fields, isObject(condition) ? condition : undefined);
|
|
};
|
|
},
|
|
|
|
/**
|
|
* Check many permissions based on an ability
|
|
*/
|
|
checkMany: curry((ability, permissions) => {
|
|
return permissions.map(({ action, subject, field }) => ability.can(action, subject, field));
|
|
}),
|
|
};
|
|
};
|