Jean-Sébastien Herbaux 1e5b1c99f6
I18n/ permissions rework (#9535)
* 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>
2021-03-25 14:59:44 +01:00

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