chore: reintegrate policy logic into core

This commit is contained in:
Alexandre Bodin 2024-03-24 18:01:45 +01:00
parent 29d9bfaedc
commit 78d30d2e60
10 changed files with 171 additions and 255 deletions

View File

@ -0,0 +1,44 @@
import createPoliciesRegistry from '../policies';
describe('Policy util', () => {
const registry = createPoliciesRegistry();
describe('Get policy', () => {
test('Throws on policy not found', () => {
expect(registry.get('undefined')).toBeUndefined();
});
test('Retrieves policy by fullName', () => {
const policyFn = () => {};
registry.set('global::test-policy', policyFn as any);
expect(registry.get('global::test-policy')).toBe(policyFn);
});
test('Retrieves a global plugin policy', () => {
const policyFn = () => {};
registry.set('plugin::test-plugin.test-policy', policyFn as any);
expect(registry.get('test-plugin.test-policy')).toBeUndefined();
expect(registry.get('plugin::test-plugin.test-policy')).toBe(policyFn);
});
test('Retrieves a plugin policy locally', () => {
const policyFn = () => {};
registry.set('plugin::test-plugin.test-policy', policyFn as any);
expect(registry.get('test-policy', { pluginName: 'test-plugin' })).toBe(policyFn);
});
test('Retrieves an api policy locally', () => {
const policyFn = () => {};
registry.set('api::test-api.test-policy', policyFn as any);
expect(registry.get('test-policy', { apiName: 'test-api' })).toBe(policyFn);
});
});
});

View File

@ -1,48 +1,128 @@
import { pickBy, has } from 'lodash/fp';
import type { Core, UID } from '@strapi/types';
import { pickBy, has, castArray } from 'lodash/fp';
import type { Core } from '@strapi/types';
import { addNamespace, hasNamespace } from './namespace';
type PolicyExtendFn = (policy: Core.Policy) => Core.Policy;
type PolicyMap = Record<string, Core.Policy>;
const PLUGIN_PREFIX = 'plugin::';
const API_PREFIX = 'api::';
interface PolicyInfo {
name: string;
config: unknown;
}
type PolicyConfig = string | PolicyInfo;
interface NamespaceInfo {
pluginName?: string;
apiName?: string;
}
const parsePolicy = (policy: string | PolicyInfo) => {
if (typeof policy === 'string') {
return { policyName: policy, config: {} };
}
const { name, config } = policy;
return { policyName: name, config };
};
// TODO: move instantiation part here instead of in the policy utils
const policiesRegistry = () => {
const policies: PolicyMap = {};
const policies = new Map<string, Core.Policy>();
const find = (name: string, namespaceInfo?: NamespaceInfo) => {
const { pluginName, apiName } = namespaceInfo ?? {};
// try to resolve a full name to avoid extra prefixing
const policy = policies.get(name);
if (policy) {
return policy;
}
if (pluginName) {
return policies.get(`${PLUGIN_PREFIX}${pluginName}.${name}`);
}
if (apiName) {
return policies.get(`${API_PREFIX}${apiName}.${name}`);
}
};
function resolveHandler(policyConfig: PolicyConfig, namespaceInfo?: NamespaceInfo): Core.Policy;
function resolveHandler(
policyConfig: PolicyConfig[],
namespaceInfo?: NamespaceInfo
): Core.Policy[];
function resolveHandler(
policyConfig: PolicyConfig | PolicyConfig[],
namespaceInfo?: NamespaceInfo
): Core.Policy | Core.Policy[] {
if (Array.isArray(policyConfig)) {
return policyConfig.map((config) => {
return resolveHandler(config, namespaceInfo);
});
}
const { policyName, config } = parsePolicy(policyConfig);
const policy = find(policyName, namespaceInfo);
if (!policy) {
throw new Error(`Policy ${policyName} not found.`);
}
if (typeof policy === 'function') {
return policy;
}
if (policy.validator) {
policy.validator(config);
}
return policy.handler;
}
return {
/**
* Returns this list of registered policies uids
*/
keys() {
return Object.keys(policies);
return policies.keys();
},
/**
* Returns the instance of a policy. Instantiate the policy if not already done
*/
get(uid: UID.Policy) {
return policies[uid];
get(name: string, namespaceInfo?: NamespaceInfo) {
return find(name, namespaceInfo);
},
/**
* Checks if a policy is registered
*/
has(name: string, namespaceInfo?: NamespaceInfo) {
const res = find(name, namespaceInfo);
return !!res;
},
/**
* Returns a map with all the policies in a namespace
*/
getAll(namespace: string) {
return pickBy((_, uid) => hasNamespace(uid, namespace))(policies);
return pickBy((_, uid) => hasNamespace(uid, namespace))(Object.fromEntries(policies));
},
/**
* Registers a policy
*/
set(uid: string, policy: Core.Policy) {
policies[uid] = policy;
policies.set(uid, policy);
return this;
},
/**
* Registers a map of policies for a specific namespace
*/
add(namespace: string, newPolicies: PolicyMap) {
add(namespace: string, newPolicies: Record<string, Core.Policy>) {
for (const policyName of Object.keys(newPolicies)) {
const policy = newPolicies[policyName];
const uid = addNamespace(policyName, namespace);
@ -50,26 +130,23 @@ const policiesRegistry = () => {
if (has(uid, policies)) {
throw new Error(`Policy ${uid} has already been registered.`);
}
policies[uid] = policy;
policies.set(uid, policy);
}
},
/**
* Wraps a policy to extend it
* @param {string} uid
* @param {(policy: Policy) => Policy} extendFn
* Resolves a list of policies
*/
extend(uid: UID.Policy, extendFn: PolicyExtendFn) {
const currentPolicy = this.get(uid);
resolve(config: PolicyConfig | PolicyConfig[], namespaceInfo?: NamespaceInfo) {
const { pluginName, apiName } = namespaceInfo ?? {};
if (!currentPolicy) {
throw new Error(`Policy ${uid} doesn't exist`);
}
const newPolicy = extendFn(currentPolicy);
policies[uid] = newPolicy;
return this;
return castArray(config).map((policyConfig) => {
return {
handler: resolveHandler(policyConfig, { pluginName, apiName }),
config: (typeof policyConfig === 'object' && policyConfig.config) || {},
};
});
},
};
};

View File

@ -5,7 +5,7 @@ import Router from '@koa/router';
import compose from 'koa-compose';
import { resolveRouteMiddlewares } from './middleware';
import { resolvePolicies } from './policy';
import { createPolicicesMiddleware } from './policy';
const getMethod = (route: Core.Route) => {
return trim(toLower(route.method)) as Lowercase<Core.Route['method']>;
@ -79,7 +79,6 @@ export default (strapi: Core.Strapi) => {
const path = getPath(route);
const middlewares = resolveRouteMiddlewares(route, strapi);
const policies = resolvePolicies(route);
const action = getAction(route, strapi);
@ -87,7 +86,7 @@ export default (strapi: Core.Strapi) => {
createRouteInfoMiddleware(route),
authenticate,
authorize,
...policies,
createPolicicesMiddleware(route, strapi),
...middlewares,
returnBodyMiddleware,
...castArray(action),

View File

@ -1,9 +1,9 @@
import { policy as policyUtils, errors } from '@strapi/utils';
import type { Core } from '@strapi/types';
const resolvePolicies = (route: Core.Route) => {
const createPolicicesMiddleware = (route: Core.Route, strapi: Core.Strapi) => {
const policiesConfig = route?.config?.policies ?? [];
const resolvedPolicies = policyUtils.resolve(policiesConfig, route.info);
const resolvedPolicies = strapi.get('policies').resolve(policiesConfig, route.info);
const policiesMiddleware: Core.MiddlewareHandler = async (ctx, next) => {
const context = policyUtils.createPolicyContext('koa', ctx);
@ -19,7 +19,7 @@ const resolvePolicies = (route: Core.Route) => {
await next();
};
return [policiesMiddleware];
return policiesMiddleware;
};
export { resolvePolicies };
export { createPolicicesMiddleware };

View File

@ -7,8 +7,16 @@ export type PolicyContext = Omit<ExtendableContext, 'is'> & {
is(name: string): boolean;
};
export type Policy<T = unknown> = (
export type PolicyHandler<TConfig = unknown> = (
ctx: PolicyContext,
cfg: T,
{ strapi }: { strapi: Strapi }
cfg: TConfig,
opts: { strapi: Strapi }
) => boolean | undefined;
export type Policy<TConfig = unknown> =
| {
name: string;
validator?: (config: unknown) => boolean;
handler: PolicyHandler<TConfig>;
}
| PolicyHandler<TConfig>;

View File

@ -1,71 +0,0 @@
import * as policyUtils from '../policy';
describe('Policy util', () => {
describe('Get policy', () => {
test('Throws on policy not found', () => {
expect(() => policyUtils.get('undefined')).toThrow();
});
test('Retrieves global policy', () => {
const policyFn = () => {};
// init global strapi
global.strapi = {
policy(name) {
return this.policies[name];
},
policies: {
'global::test-policy': policyFn,
},
};
expect(policyUtils.get('global::test-policy')).toBe(policyFn);
});
test('Retrieves a global plugin policy', () => {
const policyFn = () => {};
global.strapi = {
policy(name) {
return this.policies[name];
},
policies: {
'plugin::test-plugin.test-policy': policyFn,
},
};
expect(() => policyUtils.get('test-plugin.test-policy')).toThrow();
expect(policyUtils.get('plugin::test-plugin.test-policy')).toBe(policyFn);
});
test('Retrieves a plugin policy locally', () => {
const policyFn = () => {};
global.strapi = {
policy(name) {
return this.policies[name];
},
policies: {
'plugin::test-plugin.test-policy': policyFn,
},
};
expect(policyUtils.get('test-policy', { pluginName: 'test-plugin' })).toBe(policyFn);
});
test('Retrieves an api policy locally', () => {
const policyFn = () => {};
global.strapi = {
policy(name) {
return this.policies[name];
},
policies: {
'api::test-api.test-policy': policyFn,
},
};
expect(policyUtils.get('test-policy', { apiName: 'test-api' })).toBe(policyFn);
});
});
});

View File

@ -1,118 +1,4 @@
/**
* Policies util
*/
import _ from 'lodash';
import { eq } from 'lodash/fp';
import type Koa from 'koa';
const PLUGIN_PREFIX = 'plugin::';
const API_PREFIX = 'api::';
interface PolicyInfo {
name: string;
config: unknown;
}
type PolicyConfig = string | PolicyInfo | (() => PolicyInfo);
interface PolicyContext {
pluginName?: string;
apiName?: string;
}
interface RouteInfo {
method: string;
endpoint: string;
controller: string;
action: string;
plugin: string;
}
const parsePolicy = (policy: string | PolicyInfo) => {
if (typeof policy === 'string') {
return { policyName: policy, config: {} };
}
const { name, config } = policy;
return { policyName: name, config };
};
const searchLocalPolicy = (policyName: string, policyContext: PolicyContext) => {
const { pluginName, apiName } = policyContext ?? {};
if (pluginName) {
return strapi.policy(`${PLUGIN_PREFIX}${pluginName}.${policyName}`);
}
if (apiName) {
return strapi.policy(`${API_PREFIX}${apiName}.${policyName}`);
}
};
const globalPolicy = ({ method, endpoint, controller, action, plugin }: RouteInfo) => {
return async (ctx: Koa.Context, next: () => void) => {
ctx.request.route = {
endpoint: `${method} ${endpoint}`,
controller: _.toLower(controller),
action: _.toLower(action),
verb: _.toLower(method),
plugin,
};
await next();
};
};
const resolvePolicies = (config: PolicyConfig[], policyContext: PolicyContext) => {
const { pluginName, apiName } = policyContext ?? {};
return config.map((policyConfig) => {
return {
handler: getPolicy(policyConfig, { pluginName, apiName }),
config: (typeof policyConfig === 'object' && policyConfig.config) || {},
};
});
};
const findPolicy = (name: string, policyContext: PolicyContext) => {
const { pluginName, apiName } = policyContext ?? {};
const resolvedPolicy = strapi.policy(name);
if (resolvedPolicy !== undefined) {
return resolvedPolicy;
}
const localPolicy = searchLocalPolicy(name, { pluginName, apiName });
if (localPolicy !== undefined) {
return localPolicy;
}
throw new Error(`Could not find policy "${name}"`);
};
const getPolicy = (policyConfig: PolicyConfig, policyContext?: PolicyContext) => {
const { pluginName, apiName } = policyContext ?? {};
if (typeof policyConfig === 'function') {
return policyConfig;
}
const { policyName, config } = parsePolicy(policyConfig);
const policy = findPolicy(policyName, { pluginName, apiName });
if (typeof policy === 'function') {
return policy;
}
if (policy.validator) {
policy.validator(config);
}
return policy.handler;
};
interface Options {
name: string;
@ -152,10 +38,4 @@ const createPolicyContext = (type: string, ctx: object) => {
);
};
export {
getPolicy as get,
resolvePolicies as resolve,
globalPolicy,
createPolicy,
createPolicyContext,
};
export { createPolicy, createPolicyContext };

View File

@ -3,6 +3,6 @@
"compilerOptions": {
"outDir": "dist"
},
"include": ["src", "../core/src/configuration/urls.ts"],
"include": ["src"],
"exclude": ["node_modules", "**/__tests__/**"]
}

View File

@ -9,7 +9,7 @@ const getPoliciesConfig = propOr([], 'policies');
const createPoliciesMiddleware = (resolverConfig: any, { strapi }: { strapi: Core.Strapi }) => {
const resolverPolicies = getPoliciesConfig(resolverConfig);
const policies = policyUtils.resolve(resolverPolicies, {});
const policies = strapi.get('policies').resolve(resolverPolicies, {});
return async (
resolve: GraphQLFieldResolver<any, any>,

View File

@ -12112,31 +12112,10 @@ __metadata:
languageName: node
linkType: hard
"caniuse-lite@npm:^1.0.30001400, caniuse-lite@npm:^1.0.30001517":
version: 1.0.30001522
resolution: "caniuse-lite@npm:1.0.30001522"
checksum: fbb72297c5be7de37fbd51b321b930a5525aeb060dbce706b7c3017de02bc059cd40817274821463fb8230d73009668f8393c941b68b8e36370369580c82b8c8
languageName: node
linkType: hard
"caniuse-lite@npm:^1.0.30001541":
version: 1.0.30001542
resolution: "caniuse-lite@npm:1.0.30001542"
checksum: 07b14b8341d7bf0ea386a5fa5b5edbee41d81dfc072d3d11db22dd1d7a929358f522b16fdf3cbd154c8a5cae84662578cf5c9e490e7d7606ee7d156ccf07c9fa
languageName: node
linkType: hard
"caniuse-lite@npm:^1.0.30001565":
version: 1.0.30001574
resolution: "caniuse-lite@npm:1.0.30001574"
checksum: 159ebd04d9bbef11bd08499f058f70bf795a55641929be5efadf0f6b17216d4b923506778e59bbb939246834304b753b2e88ff1e2430f6a5aef0a86971f98bd3
languageName: node
linkType: hard
"caniuse-lite@npm:^1.0.30001587":
version: 1.0.30001599
resolution: "caniuse-lite@npm:1.0.30001599"
checksum: c9a5ad806fc0d446e4f995d551b840d8fdcbe97958b7f83ff7a255a8ef5e40ca12ca1a508c66b3ab147e19eef932d28772d205c046500dd0740ea9dfb602e2e1
"caniuse-lite@npm:^1.0.30001400, caniuse-lite@npm:^1.0.30001517, caniuse-lite@npm:^1.0.30001541, caniuse-lite@npm:^1.0.30001565, caniuse-lite@npm:^1.0.30001587":
version: 1.0.30001600
resolution: "caniuse-lite@npm:1.0.30001600"
checksum: 4c52f83ed71bc5f6e443bd17923460f1c77915adc2c2aa79ddaedceccc690b5917054b0c41b79e9138cbbd9abcdc0db9e224e79e3e734e581dfec06505f3a2b4
languageName: node
linkType: hard