mirror of
https://github.com/strapi/strapi.git
synced 2025-09-27 01:09:49 +00:00
Merge pull request #14164 from strapi/api-token-v2/fix-syncActions
Api token v2/fix sync actions
This commit is contained in:
commit
c0bb3a6014
@ -454,7 +454,7 @@ class Strapi {
|
|||||||
await this.runLifecyclesFunctions(LIFECYCLES.BOOTSTRAP);
|
await this.runLifecyclesFunctions(LIFECYCLES.BOOTSTRAP);
|
||||||
|
|
||||||
// TODO: is this the best place for this?
|
// TODO: is this the best place for this?
|
||||||
await this.contentAPI.permissions.syncActions();
|
await this.contentAPI.permissions.registerActions();
|
||||||
|
|
||||||
this.cron.start();
|
this.cron.start();
|
||||||
|
|
||||||
|
@ -0,0 +1,291 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const createContentAPI = require('../content-api');
|
||||||
|
|
||||||
|
describe('Content API - Permissions', () => {
|
||||||
|
const bindToContentAPI = (action) => {
|
||||||
|
Object.assign(action, { [Symbol.for('__type__')]: ['content-api'] });
|
||||||
|
return action;
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('Get Actions Map', () => {
|
||||||
|
test('When no API are defined, it should return an empty object', () => {
|
||||||
|
global.strapi = {};
|
||||||
|
|
||||||
|
const contentAPI = createContentAPI(global.strapi);
|
||||||
|
const actionsMap = contentAPI.permissions.getActionsMap();
|
||||||
|
|
||||||
|
expect(actionsMap).toEqual({});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('When no controller are defined for an API, it should ignore the API', () => {
|
||||||
|
global.strapi = {
|
||||||
|
api: {
|
||||||
|
foo: {},
|
||||||
|
bar: {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const contentAPI = createContentAPI(global.strapi);
|
||||||
|
const actionsMap = contentAPI.permissions.getActionsMap();
|
||||||
|
|
||||||
|
expect(actionsMap).toEqual({});
|
||||||
|
});
|
||||||
|
|
||||||
|
test(`Do not register controller if they're not bound to the content API`, () => {
|
||||||
|
const actionC = () => {};
|
||||||
|
Object.assign(actionC, { [Symbol.for('__type__')]: ['admin-api'] });
|
||||||
|
|
||||||
|
global.strapi = {
|
||||||
|
api: {
|
||||||
|
foo: {
|
||||||
|
controllers: {
|
||||||
|
controllerA: {
|
||||||
|
actionA: bindToContentAPI(() => {}),
|
||||||
|
actionB() {},
|
||||||
|
actionC,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const contentAPI = createContentAPI(global.strapi);
|
||||||
|
const actionsMap = contentAPI.permissions.getActionsMap();
|
||||||
|
|
||||||
|
expect(actionsMap).toEqual({
|
||||||
|
'api::foo': { controllers: { controllerA: ['actionA'] } },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Creates and populate a map of actions from APIs and plugins', () => {
|
||||||
|
global.strapi = {
|
||||||
|
api: {
|
||||||
|
foo: {
|
||||||
|
controllers: {
|
||||||
|
controllerA: {
|
||||||
|
actionA: bindToContentAPI(() => {}),
|
||||||
|
actionB: bindToContentAPI(() => {}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
bar: {
|
||||||
|
controllers: {
|
||||||
|
controllerA: {
|
||||||
|
actionA: bindToContentAPI(() => {}),
|
||||||
|
actionB: bindToContentAPI(() => {}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
foobar: {
|
||||||
|
controllers: {
|
||||||
|
controllerA: {
|
||||||
|
actionA: bindToContentAPI(() => {}),
|
||||||
|
actionB: bindToContentAPI(() => {}),
|
||||||
|
},
|
||||||
|
controllerB: {
|
||||||
|
actionC: bindToContentAPI(() => {}),
|
||||||
|
actionD: bindToContentAPI(() => {}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
foo: {
|
||||||
|
controllers: {
|
||||||
|
controllerA: {
|
||||||
|
actionA: bindToContentAPI(() => {}),
|
||||||
|
actionB: bindToContentAPI(() => {}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
bar: {
|
||||||
|
controllers: {
|
||||||
|
controllerA: {
|
||||||
|
actionA: bindToContentAPI(() => {}),
|
||||||
|
actionB: bindToContentAPI(() => {}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const contentAPI = createContentAPI(global.strapi);
|
||||||
|
const actionsMap = contentAPI.permissions.getActionsMap();
|
||||||
|
|
||||||
|
expect(actionsMap).toEqual({
|
||||||
|
'api::foo': { controllers: { controllerA: ['actionA', 'actionB'] } },
|
||||||
|
'api::bar': { controllers: { controllerA: ['actionA', 'actionB'] } },
|
||||||
|
'api::foobar': {
|
||||||
|
controllers: {
|
||||||
|
controllerA: ['actionA', 'actionB'],
|
||||||
|
controllerB: ['actionC', 'actionD'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'plugin::foo': { controllers: { controllerA: ['actionA', 'actionB'] } },
|
||||||
|
'plugin::bar': { controllers: { controllerA: ['actionA', 'actionB'] } },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Register Actions', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
global.strapi = {
|
||||||
|
api: {
|
||||||
|
foo: {
|
||||||
|
controllers: {
|
||||||
|
controllerA: {
|
||||||
|
actionA: bindToContentAPI(() => {}),
|
||||||
|
actionB: bindToContentAPI(() => {}),
|
||||||
|
},
|
||||||
|
controllerB: {
|
||||||
|
actionC: bindToContentAPI(() => {}),
|
||||||
|
actionD: bindToContentAPI(() => {}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
foo: {
|
||||||
|
controllers: {
|
||||||
|
controllerA: {
|
||||||
|
actionA: bindToContentAPI(() => {}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
test('The action provider should holds every action from APIs and plugins', async () => {
|
||||||
|
const contentAPI = createContentAPI(global.strapi);
|
||||||
|
|
||||||
|
await contentAPI.permissions.registerActions();
|
||||||
|
|
||||||
|
const values = contentAPI.permissions.providers.action.values();
|
||||||
|
|
||||||
|
expect(values).toEqual([
|
||||||
|
{
|
||||||
|
uid: 'api::foo.controllerA.actionA',
|
||||||
|
api: 'api::foo',
|
||||||
|
controller: 'controllerA',
|
||||||
|
action: 'actionA',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
uid: 'api::foo.controllerA.actionB',
|
||||||
|
api: 'api::foo',
|
||||||
|
controller: 'controllerA',
|
||||||
|
action: 'actionB',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
uid: 'api::foo.controllerB.actionC',
|
||||||
|
api: 'api::foo',
|
||||||
|
controller: 'controllerB',
|
||||||
|
action: 'actionC',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
uid: 'api::foo.controllerB.actionD',
|
||||||
|
api: 'api::foo',
|
||||||
|
controller: 'controllerB',
|
||||||
|
action: 'actionD',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
uid: 'plugin::foo.controllerA.actionA',
|
||||||
|
api: 'plugin::foo',
|
||||||
|
controller: 'controllerA',
|
||||||
|
action: 'actionA',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Call registerActions twice should throw a duplicate error', async () => {
|
||||||
|
const contentAPI = createContentAPI(global.strapi);
|
||||||
|
|
||||||
|
await contentAPI.permissions.registerActions();
|
||||||
|
|
||||||
|
expect(() => contentAPI.permissions.registerActions()).rejects.toThrowError(
|
||||||
|
'Duplicated item key: api::foo.controllerA.actionA'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Providers', () => {
|
||||||
|
test('You should not be able to register action once strapi is loaded', () => {
|
||||||
|
global.strapi.isLoaded = true;
|
||||||
|
|
||||||
|
const contentAPI = createContentAPI(global.strapi);
|
||||||
|
|
||||||
|
// Actions
|
||||||
|
expect(() =>
|
||||||
|
contentAPI.permissions.providers.action.register('foo', {})
|
||||||
|
).rejects.toThrowError(`You can't register new actions outside the bootstrap function.`);
|
||||||
|
|
||||||
|
// Conditions
|
||||||
|
expect(() =>
|
||||||
|
contentAPI.permissions.providers.condition.register({ name: 'myCondition' })
|
||||||
|
).rejects.toThrowError(`You can't register new conditions outside the bootstrap function.`);
|
||||||
|
|
||||||
|
// Register Actions
|
||||||
|
expect(() => contentAPI.permissions.registerActions()).rejects.toThrowError(
|
||||||
|
`You can't register new actions outside the bootstrap function.`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Engine', () => {
|
||||||
|
test('Engine warns when registering an unknown action', async () => {
|
||||||
|
global.strapi = {
|
||||||
|
log: {
|
||||||
|
debug: jest.fn(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const contentAPI = createContentAPI();
|
||||||
|
|
||||||
|
const ability = await contentAPI.permissions.engine.generateAbility([{ action: 'foo' }]);
|
||||||
|
|
||||||
|
expect(ability.rules).toHaveLength(0);
|
||||||
|
expect(global.strapi.log.debug).toHaveBeenCalledWith(
|
||||||
|
`Unknown action "foo" supplied when registering a new permission`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Engine filter out invalid action when generating an ability', async () => {
|
||||||
|
global.strapi = {
|
||||||
|
log: {
|
||||||
|
debug: jest.fn(),
|
||||||
|
},
|
||||||
|
|
||||||
|
api: {
|
||||||
|
foo: {
|
||||||
|
controllers: {
|
||||||
|
bar: { foobar: bindToContentAPI(() => {}) },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const contentAPI = createContentAPI(global.strapi);
|
||||||
|
|
||||||
|
await contentAPI.permissions.registerActions();
|
||||||
|
|
||||||
|
const ability = await contentAPI.permissions.engine.generateAbility([
|
||||||
|
{ action: 'foo' },
|
||||||
|
{ action: 'api::foo.bar.foobar' },
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(ability.rules).toHaveLength(1);
|
||||||
|
expect(ability.rules).toEqual([
|
||||||
|
{
|
||||||
|
action: 'api::foo.bar.foobar',
|
||||||
|
subject: 'all',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(global.strapi.log.debug).toHaveBeenCalledTimes(1);
|
||||||
|
expect(global.strapi.log.debug).toHaveBeenCalledWith(
|
||||||
|
`Unknown action "foo" supplied when registering a new permission`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -1,65 +1,7 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const { uniq } = require('lodash');
|
const _ = require('lodash');
|
||||||
const permissions = require('./permissions');
|
const permissions = require('./permissions');
|
||||||
/**
|
|
||||||
* Create a content API container that holds logic, tools and utils. (eg: permissions, ...)
|
|
||||||
*/
|
|
||||||
const createContentAPI = (/* strapi */) => {
|
|
||||||
const syncActions = async () => {
|
|
||||||
/**
|
|
||||||
* NOTE: For some reason, this doesn't seem to be necessary because all the routes exist
|
|
||||||
* createActionProvider uses a providerFactory, which seems to already include everything, and when we try
|
|
||||||
* to register our actions we get an error that the keys already exist
|
|
||||||
* Could providerFactory not be providing a new provider, and instead sharing the registry with everything that uses it?
|
|
||||||
*
|
|
||||||
* If this isn't an issue to fix and is expected, we don't need the route registration code below and it should be removed
|
|
||||||
* */
|
|
||||||
// Start of route registration
|
|
||||||
const apiRoutesName = Object.values(strapi.api)
|
|
||||||
.map((api) => api.routes)
|
|
||||||
.reduce((acc, routesMap) => {
|
|
||||||
const routes = Object.values(routesMap)
|
|
||||||
// Only content api routes
|
|
||||||
.filter((p) => p.type === 'content-api')
|
|
||||||
// Resolve every handler name for each route
|
|
||||||
.reduce((a, p) => a.concat(p.routes.map((i) => i.handler)), []);
|
|
||||||
return acc.concat(routes);
|
|
||||||
}, []);
|
|
||||||
const pluginsRoutesname = Object.values(strapi.plugins)
|
|
||||||
.map((plugin) => plugin.routes['content-api'] || {})
|
|
||||||
.map((p) => (p.routes || []).map((i) => i.handler))
|
|
||||||
.flat();
|
|
||||||
const actions = apiRoutesName.concat(pluginsRoutesname);
|
|
||||||
Promise.all(
|
|
||||||
uniq(actions).map((action) =>
|
|
||||||
providers.action.register(action).catch(() => {
|
|
||||||
// console.log('Key already exists', action);
|
|
||||||
})
|
|
||||||
)
|
|
||||||
);
|
|
||||||
};
|
|
||||||
// End of route registration
|
|
||||||
|
|
||||||
// Add providers
|
|
||||||
const providers = {
|
|
||||||
action: permissions.providers.createActionProvider(),
|
|
||||||
condition: permissions.providers.createConditionProvider(),
|
|
||||||
};
|
|
||||||
|
|
||||||
// create permission engine
|
|
||||||
const engine = permissions
|
|
||||||
.createPermissionEngine({ providers })
|
|
||||||
.on('before-format::validate.permission', createValidatePermissionHandler(providers.action));
|
|
||||||
|
|
||||||
return {
|
|
||||||
permissions: {
|
|
||||||
engine,
|
|
||||||
providers,
|
|
||||||
syncActions,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates an handler which check that the permission's action exists in the action registry
|
* Creates an handler which check that the permission's action exists in the action registry
|
||||||
@ -78,4 +20,96 @@ const createValidatePermissionHandler =
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a content API container that holds logic, tools and utils. (eg: permissions, ...)
|
||||||
|
*/
|
||||||
|
const createContentAPI = (strapi) => {
|
||||||
|
// Add providers
|
||||||
|
const providers = {
|
||||||
|
action: permissions.providers.createActionProvider(),
|
||||||
|
condition: permissions.providers.createConditionProvider(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const getActionsMap = () => {
|
||||||
|
const actionMap = {};
|
||||||
|
|
||||||
|
const isContentApi = (action) => {
|
||||||
|
if (!_.has(action, Symbol.for('__type__'))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return action[Symbol.for('__type__')].includes('content-api');
|
||||||
|
};
|
||||||
|
|
||||||
|
const registerAPIsActions = (apis, source) => {
|
||||||
|
_.forEach(apis, (api, apiName) => {
|
||||||
|
const controllers = _.reduce(
|
||||||
|
api.controllers,
|
||||||
|
(acc, controller, controllerName) => {
|
||||||
|
const contentApiActions = _.pickBy(controller, isContentApi);
|
||||||
|
|
||||||
|
if (_.isEmpty(contentApiActions)) {
|
||||||
|
return acc;
|
||||||
|
}
|
||||||
|
|
||||||
|
acc[controllerName] = Object.keys(contentApiActions);
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!_.isEmpty(controllers)) {
|
||||||
|
actionMap[`${source}::${apiName}`] = { controllers };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
registerAPIsActions(strapi.api, 'api');
|
||||||
|
registerAPIsActions(strapi.plugins, 'plugin');
|
||||||
|
|
||||||
|
return actionMap;
|
||||||
|
};
|
||||||
|
|
||||||
|
const registerActions = async () => {
|
||||||
|
const actionsMap = getActionsMap();
|
||||||
|
|
||||||
|
// For each API
|
||||||
|
for (const [api, value] of Object.entries(actionsMap)) {
|
||||||
|
const { controllers } = value;
|
||||||
|
|
||||||
|
// Register controllers methods as actions
|
||||||
|
for (const [controller, actions] of Object.entries(controllers)) {
|
||||||
|
// Register each action individually
|
||||||
|
await Promise.all(
|
||||||
|
actions.map((action) => {
|
||||||
|
const actionUID = `${api}.${controller}.${action}`;
|
||||||
|
|
||||||
|
return providers.action.register(actionUID, {
|
||||||
|
api,
|
||||||
|
controller,
|
||||||
|
action,
|
||||||
|
uid: actionUID,
|
||||||
|
});
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// create permission engine
|
||||||
|
const engine = permissions
|
||||||
|
.createPermissionEngine({ providers })
|
||||||
|
.on('before-format::validate.permission', createValidatePermissionHandler(providers.action));
|
||||||
|
|
||||||
|
return {
|
||||||
|
permissions: {
|
||||||
|
engine,
|
||||||
|
providers,
|
||||||
|
registerActions,
|
||||||
|
getActionsMap,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
module.exports = createContentAPI;
|
module.exports = createContentAPI;
|
||||||
|
@ -8,12 +8,12 @@ module.exports = (options = {}) => {
|
|||||||
return {
|
return {
|
||||||
...provider,
|
...provider,
|
||||||
|
|
||||||
async register(action) {
|
async register(action, payload) {
|
||||||
if (strapi.isLoaded) {
|
if (strapi.isLoaded) {
|
||||||
throw new Error(`You can't register new actions outside the bootstrap function.`);
|
throw new Error(`You can't register new actions outside the bootstrap function.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return provider.register(action, { name: action });
|
return provider.register(action, payload);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -171,10 +171,6 @@ module.exports = ({ strapi }) => ({
|
|||||||
|
|
||||||
const toDelete = _.difference(permissionsFoundInDB, allActions);
|
const toDelete = _.difference(permissionsFoundInDB, allActions);
|
||||||
|
|
||||||
// Register actions into the content API action provider
|
|
||||||
// TODO: do this in the content API bootstrap phase instead
|
|
||||||
allActions.forEach((action) => strapi.contentAPI.permissions.providers.action.register(action));
|
|
||||||
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
toDelete.map((action) => {
|
toDelete.map((action) => {
|
||||||
return strapi.query('plugin::users-permissions.permission').delete({ where: { action } });
|
return strapi.query('plugin::users-permissions.permission').delete({ where: { action } });
|
||||||
|
Loading…
x
Reference in New Issue
Block a user