mirror of
https://github.com/strapi/strapi.git
synced 2025-11-03 03:17:11 +00:00
chore: reintegrate policy logic into core
This commit is contained in:
parent
29d9bfaedc
commit
78d30d2e60
44
packages/core/core/src/registries/__tests__/policies.test.ts
Normal file
44
packages/core/core/src/registries/__tests__/policies.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -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) || {},
|
||||
};
|
||||
});
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
@ -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),
|
||||
|
||||
@ -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 };
|
||||
|
||||
@ -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>;
|
||||
|
||||
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -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 };
|
||||
|
||||
@ -3,6 +3,6 @@
|
||||
"compilerOptions": {
|
||||
"outDir": "dist"
|
||||
},
|
||||
"include": ["src", "../core/src/configuration/urls.ts"],
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules", "**/__tests__/**"]
|
||||
}
|
||||
|
||||
@ -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>,
|
||||
|
||||
29
yarn.lock
29
yarn.lock
@ -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
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user