mirror of
https://github.com/strapi/strapi.git
synced 2025-11-10 15:19:00 +00:00
* Fix RBAC permissions without subject ignoring conditions * Add unit test for nil subject in the permission engine
276 lines
7.8 KiB
JavaScript
276 lines
7.8 KiB
JavaScript
'use strict';
|
|
|
|
const {
|
|
curry,
|
|
map,
|
|
filter,
|
|
propEq,
|
|
isFunction,
|
|
isBoolean,
|
|
isArray,
|
|
isNil,
|
|
isEmpty,
|
|
isObject,
|
|
prop,
|
|
merge,
|
|
pick,
|
|
difference,
|
|
cloneDeep,
|
|
} = require('lodash/fp');
|
|
const { AbilityBuilder, Ability } = require('@casl/ability');
|
|
const sift = require('sift');
|
|
const permissionDomain = require('../../domain/permission/index');
|
|
const { getService } = require('../../utils');
|
|
const {
|
|
createEngineHooks,
|
|
createWillEvaluateContext,
|
|
createWillRegisterContext,
|
|
} = require('./engine-hooks');
|
|
|
|
const allowedOperations = [
|
|
'$or',
|
|
'$and',
|
|
'$eq',
|
|
'$ne',
|
|
'$in',
|
|
'$nin',
|
|
'$lt',
|
|
'$lte',
|
|
'$gt',
|
|
'$gte',
|
|
'$exists',
|
|
'$elemMatch',
|
|
];
|
|
const operations = pick(allowedOperations, sift);
|
|
|
|
const conditionsMatcher = conditions => {
|
|
return sift.createQueryTester(conditions, { operations });
|
|
};
|
|
|
|
module.exports = conditionProvider => {
|
|
const state = {
|
|
hooks: createEngineHooks(),
|
|
};
|
|
|
|
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);
|
|
|
|
for (const permission of permissions) {
|
|
const registerFn = this.createRegisterFunction(can, permission, user);
|
|
|
|
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 = createWillEvaluateContext(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, properties = {}, conditions } = permission;
|
|
|
|
// Register the permission if there is no condition
|
|
if (isEmpty(conditions)) {
|
|
return registerFn({ action, subject, fields: properties.fields });
|
|
}
|
|
|
|
/** 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. Filter conditions, only keep those whose handler is a function
|
|
const filterValidConditions = filter(condition => isFunction(condition.handler));
|
|
|
|
// 3. Evaluate the conditions handler and returns an object
|
|
// containing both the original condition and its result
|
|
const evaluateConditions = conditions => {
|
|
return Promise.all(
|
|
conditions.map(async condition => ({
|
|
condition,
|
|
result: await condition.handler(
|
|
user,
|
|
merge(conditionOptions, { permission: cloneDeep(permission) })
|
|
),
|
|
}))
|
|
);
|
|
};
|
|
|
|
// 4. Only keeps booleans or objects as condition's result
|
|
const filterValidResults = filter(({ result }) => isBoolean(result) || isObject(result));
|
|
|
|
/**/
|
|
|
|
const evaluatedConditions = await Promise.resolve(conditions)
|
|
.then(resolveConditions)
|
|
.then(filterValidConditions)
|
|
.then(evaluateConditions)
|
|
.then(filterValidResults);
|
|
|
|
// Utils
|
|
const resultPropEq = propEq('result');
|
|
const pickResults = map(prop('result'));
|
|
|
|
if (evaluatedConditions.every(resultPropEq(false))) {
|
|
return;
|
|
}
|
|
|
|
// If there is no condition or if one of them return true, register the permission as is
|
|
if (isEmpty(evaluatedConditions) || evaluatedConditions.some(resultPropEq(true))) {
|
|
return registerFn({ action, subject, fields: properties.fields });
|
|
}
|
|
|
|
const results = pickResults(evaluatedConditions).filter(isObject);
|
|
|
|
if (isEmpty(results)) {
|
|
return registerFn({ action, subject, fields: properties.fields });
|
|
}
|
|
|
|
// Register the permission
|
|
return registerFn({
|
|
action,
|
|
subject,
|
|
fields: properties.fields,
|
|
condition: { $and: [{ $or: results }] },
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Encapsulate a register function with custom params to fit `evaluatePermission`'s syntax
|
|
* @param can
|
|
* @param {Permission} permission
|
|
* @param {object} user
|
|
* @returns {function}
|
|
*/
|
|
createRegisterFunction(can, permission, user) {
|
|
const registerToCasl = caslPermission => {
|
|
const { action, subject, fields, condition } = caslPermission;
|
|
|
|
can(
|
|
action,
|
|
isNil(subject) ? 'all' : subject,
|
|
fields,
|
|
isObject(condition) ? condition : undefined
|
|
);
|
|
};
|
|
|
|
const runWillRegisterHook = async caslPermission => {
|
|
const hookContext = createWillRegisterContext(caslPermission, {
|
|
permission,
|
|
user,
|
|
});
|
|
|
|
await state.hooks.willRegisterPermission.call(hookContext);
|
|
|
|
return caslPermission;
|
|
};
|
|
|
|
return async caslPermission => {
|
|
await runWillRegisterHook(caslPermission);
|
|
registerToCasl(caslPermission);
|
|
};
|
|
},
|
|
|
|
/**
|
|
* Check many permissions based on an ability
|
|
*/
|
|
checkMany: curry((ability, permissions) => {
|
|
return permissions.map(({ action, subject, field }) => ability.can(action, subject, field));
|
|
}),
|
|
};
|
|
};
|