From b3f602d2073271c9fb6db8a06e4832e5a68d9ab0 Mon Sep 17 00:00:00 2001 From: Convly Date: Thu, 18 Jun 2020 11:41:12 +0200 Subject: [PATCH] Rework policies resolving (allow policy generators) / Add hasPermissions policy Signed-off-by: Convly --- .../config/policies/hasPermissions.js | 23 +++ .../validation/policies/hasPermissions.js | 22 +++ packages/strapi-utils/lib/policy.js | 182 ++++++++++-------- 3 files changed, 147 insertions(+), 80 deletions(-) create mode 100644 packages/strapi-admin/config/policies/hasPermissions.js create mode 100644 packages/strapi-admin/validation/policies/hasPermissions.js diff --git a/packages/strapi-admin/config/policies/hasPermissions.js b/packages/strapi-admin/config/policies/hasPermissions.js new file mode 100644 index 0000000000..6f6ea90a8e --- /dev/null +++ b/packages/strapi-admin/config/policies/hasPermissions.js @@ -0,0 +1,23 @@ +'use strict'; + +const { validateHasPermissionsInput } = require('../../validation/policies/hasPermissions'); + +module.exports = permissions => { + try { + validateHasPermissionsInput(permissions); + } catch { + throw new Error('Invalid objects submitted to admin::hasPermissions policy.'); + } + + return async (ctx, next) => { + const { userAbility: ability } = ctx.state; + + const isAuthorized = permissions.every(({ action, subject }) => ability.can(action, subject)); + + if (!isAuthorized) { + throw strapi.errors.forbidden(); + } + + return next(); + }; +}; diff --git a/packages/strapi-admin/validation/policies/hasPermissions.js b/packages/strapi-admin/validation/policies/hasPermissions.js new file mode 100644 index 0000000000..d6f06305b8 --- /dev/null +++ b/packages/strapi-admin/validation/policies/hasPermissions.js @@ -0,0 +1,22 @@ +'use strict'; + +const { yup, formatYupErrors } = require('strapi-utils'); + +const hasPermissionsSchema = yup.array().of( + yup.object().shape({ + action: yup.string().required(), + subject: yup.string(), + }) +); + +const validateHasPermissionsInput = data => { + try { + return hasPermissionsSchema.validateSync(data, { strict: true, abortEarly: true }); + } catch (e) { + throw new Error(formatYupErrors(e)); + } +}; + +module.exports = { + validateHasPermissionsInput, +}; diff --git a/packages/strapi-utils/lib/policy.js b/packages/strapi-utils/lib/policy.js index 7d5d74d1e8..6db30749d2 100644 --- a/packages/strapi-utils/lib/policy.js +++ b/packages/strapi-utils/lib/policy.js @@ -5,41 +5,57 @@ const _ = require('lodash'); -const get = (policy, plugin, apiName) => { - if (globalPolicyExists(policy)) { - return parsePolicy(getGlobalPolicy(policy)); +const GLOBAL_PREFIX = 'global::'; +const PLUGIN_PREFIX = 'plugins::'; +const ADMIN_PREFIX = 'admin::'; +const APPLICATION_PREFIX = 'application::'; + +const isPolicyGenerator = _.isArray; + +const getPolicyIn = (container, policy) => + _.get(container, ['config', 'policies', _.toLower(policy)]); + +const policyExistsIn = (container, policy) => !_.isUndefined(getPolicyIn(container, policy)); + +const stripPolicy = (policy, prefix) => policy.replace(prefix, ''); + +const createPolicy = (policyName, args) => ({ policyName, args }); + +const resolveHandler = policy => (_.isFunction(policy) ? policy : policy.handler); + +const parsePolicy = policy => + isPolicyGenerator(policy) ? createPolicy(...policy) : createPolicy(policy); + +const resolvePolicy = policyName => { + for (const policyModel of Object.values(policyModelsProvider)) { + if (policyModel.exists(policyName)) { + return resolveHandler(policyModel.get)(policyName); + } } - if (pluginPolicyExists(policy)) { - return parsePolicy(getPluginPolicy(policy)); - } - - if (adminPolicyExists(policy)) { - return parsePolicy(getAdminPolicy(policy)); - } - - if (APIPolicyExists(policy)) { - return parsePolicy(getAPIPolicy(policy)); - } + return undefined; +}; +const getLegacyPolicy = (policy, plugin, apiName) => { let [absoluteApiName, policyName] = policy.split('.'); let absoluteApi = _.get(strapi.api, absoluteApiName); + if (policyExistsIn(absoluteApi, policyName)) { - return parsePolicy(getPolicyIn(absoluteApi, policyName)); + return resolveHandler(getPolicyIn(absoluteApi, policyName)); } const pluginPolicy = `${PLUGIN_PREFIX}${plugin}.${policy}`; - if (plugin && pluginPolicyExists(pluginPolicy)) { - return parsePolicy(getPluginPolicy(pluginPolicy)); + if (plugin && policyModelsProvider.plugin.exists(pluginPolicy)) { + return resolveHandler(policyModelsProvider.plugin.get(pluginPolicy)); } const api = _.get(strapi.api, apiName); if (api && policyExistsIn(api, policy)) { - return parsePolicy(getPolicyIn(api, policy)); + return resolveHandler(getPolicyIn(api, policy)); } - throw new Error(`Could not find policy "${policy}"`); + return undefined; }; const globalPolicy = ({ method, endpoint, controller, action, plugin }) => { @@ -48,7 +64,7 @@ const globalPolicy = ({ method, endpoint, controller, action, plugin }) => { endpoint: `${method} ${endpoint}`, controller: _.toLower(controller), action: _.toLower(action), - splittedEndpoint: endpoint, + splitEndpoint: endpoint, verb: _.toLower(method), plugin, }; @@ -57,70 +73,76 @@ const globalPolicy = ({ method, endpoint, controller, action, plugin }) => { }; }; -const parsePolicy = policy => { - if (_.isFunction(policy)) { - return policy; +const createPolicyModelsProvider = () => ({ + APIPolicy: { + is(policy) { + return _.startsWith(policy, APPLICATION_PREFIX); + }, + exists(policy) { + return this.is(policy) && !_.isUndefined(this.get(policy)); + }, + get: policy => { + const [, policyWithoutPrefix] = policy.split('::'); + const [api = '', policyName = ''] = policyWithoutPrefix.split('.'); + return getPolicyIn(_.get(strapi, ['api', api]), policyName); + }, + }, + admin: { + is(policy) { + return _.startsWith(policy, ADMIN_PREFIX); + }, + exists(policy) { + return this.is(policy) && !_.isUndefined(this.get(policy)); + }, + get: policy => { + return getPolicyIn(_.get(strapi, 'admin'), stripPolicy(policy, ADMIN_PREFIX)); + }, + }, + plugin: { + is(policy) { + return _.startsWith(policy, PLUGIN_PREFIX); + }, + exists(policy) { + return this.is(policy) && !_.isUndefined(this.get(policy)); + }, + get(policy) { + const [plugin = '', policyName = ''] = stripPolicy(policy, PLUGIN_PREFIX).split('.'); + return getPolicyIn(_.get(strapi, ['plugins', plugin]), policyName); + }, + }, + global: { + is(policy) { + return _.startsWith(policy, GLOBAL_PREFIX); + }, + exists(policy) { + return this.is(policy) && !_.isUndefined(this.get(policy)); + }, + get(policy) { + return getPolicyIn(strapi, stripPolicy(policy, GLOBAL_PREFIX)); + }, + }, +}); + +const policyModelsProvider = createPolicyModelsProvider(); + +const get = (policy, plugin, apiName) => { + const { policyName, args } = parsePolicy(policy); + + const resolvedPolicy = resolvePolicy(policyName); + + if (resolvedPolicy !== undefined) { + return isPolicyGenerator(policy) ? resolvedPolicy(args) : resolvedPolicy; } - return policy.handler; + const legacyPolicy = getLegacyPolicy(policy, plugin, apiName); + + if (legacyPolicy !== undefined) { + return legacyPolicy; + } + + throw new Error(`Could not find policy "${policy}"`); }; -const GLOBAL_PREFIX = 'global::'; -const PLUGIN_PREFIX = 'plugins::'; -const ADMIN_PREFIX = 'admin::'; -const APPLICATION_PREFIX = 'application::'; - -const getPolicyIn = (container, policy) => { - return _.get(container, ['config', 'policies', _.toLower(policy)]); -}; - -const policyExistsIn = (container, policy) => { - return !_.isUndefined(getPolicyIn(container, policy)); -}; - -const isGlobal = policy => _.startsWith(policy, GLOBAL_PREFIX); - -const getGlobalPolicy = policy => { - const strippedPolicy = policy.replace(GLOBAL_PREFIX, ''); - return getPolicyIn(strapi, strippedPolicy); -}; - -const globalPolicyExists = policy => { - return isGlobal(policy) && !_.isUndefined(getGlobalPolicy(policy)); -}; - -const getPluginPolicy = policy => { - const strippedPolicy = policy.replace(PLUGIN_PREFIX, ''); - const [plugin = '', policyName = ''] = strippedPolicy.split('.'); - return getPolicyIn(_.get(strapi, ['plugins', plugin]), policyName); -}; - -const pluginPolicyExists = policy => - isPluginPolicy(policy) && !_.isUndefined(getPluginPolicy(policy)); - -const isPluginPolicy = policy => _.startsWith(policy, PLUGIN_PREFIX); - -const getAdminPolicy = policy => { - const strippedPolicy = policy.replace(ADMIN_PREFIX, ''); - return getPolicyIn(_.get(strapi, 'admin'), strippedPolicy); -}; - -const isAdminPolicy = policy => _.startsWith(policy, ADMIN_PREFIX); - -const adminPolicyExists = policy => isAdminPolicy(policy) && !_.isUndefined(getAdminPolicy(policy)); - -const getAPIPolicy = policy => { - const [, policyWithoutPrefix] = policy.split('::'); - const [api = '', policyName = ''] = policyWithoutPrefix.split('.'); - return getPolicyIn(_.get(strapi, ['api', api]), policyName); -}; - -const APIPolicyExists = policy => { - return isAPIPolicy(policy) && !_.isUndefined(getAPIPolicy(policy)); -}; - -const isAPIPolicy = policy => _.startsWith(policy, APPLICATION_PREFIX); - module.exports = { get, globalPolicy,