2022-08-18 12:43:38 +03:00

210 lines
5.6 KiB
JavaScript

'use strict';
const _ = require('lodash/fp');
const abilities = require('./abilities');
const {
createEngineHooks,
createWillRegisterContext,
createBeforeEvaluateContext,
createValidateContext,
} = require('./hooks');
/**
* @typedef {import("../..").PermissionEngine} PermissionEngine
* @typedef {import("../..").ActionProvider} ActionProvider
* @typedef {import("../..").ConditionProvider} ConditionProvider
* @typedef {import("../..").PermissionEngineParams} PermissionEngineParams
* @typedef {import("../..").Permission} Permission
*/
/**
* Create a default state object for the engine
*/
const createEngineState = () => {
const hooks = createEngineHooks();
return { hooks };
};
module.exports = {
abilities,
/**
* Create a new instance of a permission engine
*
* @param {PermissionEngineParams} params
*
* @return {PermissionEngine}
*/
new(params) {
const { providers, abilityBuilderFactory = abilities.caslAbilityBuilder } = params;
const state = createEngineState();
const runValidationHook = async (hook, context) => state.hooks[hook].call(context);
/**
* Evaluate a permission using local and registered behaviors (using hooks).
* Validate, format (add condition, etc...), evaluate (evaluate conditions) and register a permission
*
* @param {object} params
* @param {object} params.options
* @param {Function} params.register
* @param {Permission} params.permission
*/
const evaluate = async (params) => {
const { options, register } = params;
const preFormatValidation = await runValidationHook(
'before-format::validate.permission',
createBeforeEvaluateContext(params.permission)
);
if (preFormatValidation === false) {
return;
}
const permission = await state.hooks['format.permission'].call(params.permission);
const afterFormatValidation = await runValidationHook(
'after-format::validate.permission',
createValidateContext(permission)
);
if (afterFormatValidation === false) {
return;
}
await state.hooks['before-evaluate.permission'].call(createBeforeEvaluateContext(permission));
const { action, subject, properties, conditions = [] } = permission;
if (conditions.length === 0) {
return register({ action, subject, properties });
}
const resolveConditions = _.map(providers.condition.get);
const removeInvalidConditions = _.filter((condition) => _.isFunction(condition.handler));
const evaluateConditions = (conditions) => {
return Promise.all(
conditions.map(async (condition) => ({
condition,
result: await condition.handler(
_.merge(options, { permission: _.cloneDeep(permission) })
),
}))
);
};
const removeInvalidResults = _.filter(
({ result }) => _.isBoolean(result) || _.isObject(result)
);
const evaluatedConditions = await Promise.resolve(conditions)
.then(resolveConditions)
.then(removeInvalidConditions)
.then(evaluateConditions)
.then(removeInvalidResults);
const resultPropEq = _.propEq('result');
const pickResults = _.map(_.prop('result'));
if (evaluatedConditions.every(resultPropEq(false))) {
return;
}
if (_.isEmpty(evaluatedConditions) || evaluatedConditions.some(resultPropEq(true))) {
return register({ action, subject, properties });
}
const results = pickResults(evaluatedConditions).filter(_.isObject);
if (_.isEmpty(results)) {
return register({ action, subject, properties });
}
return register({
action,
subject,
properties,
condition: { $and: [{ $or: results }] },
});
};
return {
get hooks() {
return state.hooks;
},
/**
* Create a register function that wraps a `can` function
* used to register a permission in the ability builder
*
* @param {Function} can
* @param {object} options
*
* @return {Function}
*/
createRegisterFunction(can, options) {
return async (permission) => {
const hookContext = createWillRegisterContext({ options, permission });
await state.hooks['before-register.permission'].call(hookContext);
return can(permission);
};
},
/**
* Register a new handler for a given hook
*
* @param {string} hook
* @param {Function} handler
*
* @return {this}
*/
on(hook, handler) {
const validHooks = Object.keys(state.hooks);
const isValidHook = validHooks.includes(hook);
if (!isValidHook) {
throw new Error(
`Invalid hook supplied when trying to register an handler to the permission engine. Got "${hook}" but expected one of ${validHooks.join(
', '
)}`
);
}
state.hooks[hook].register(handler);
return this;
},
/**
* Generate an ability based on the instance's
* ability builder and the given permissions
*
* @param {Permission[]} permissions
* @param {object} [options]
*
* @return {object}
*/
async generateAbility(permissions, options = {}) {
const { can, build } = abilityBuilderFactory();
for (const permission of permissions) {
const register = this.createRegisterFunction(can, options);
await evaluate({ permission, options, register });
}
return build();
},
};
},
};