mirror of
https://github.com/strapi/strapi.git
synced 2025-08-05 23:40:04 +00:00
Merge pull request #17464 from strapi/poc/permissions-parametrized-actions
This commit is contained in:
commit
04fc8a0875
@ -0,0 +1,255 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const { mapAsync } = require('@strapi/utils');
|
||||||
|
|
||||||
|
const { createStrapiInstance } = require('api-tests/strapi');
|
||||||
|
const { createAuthRequest, createRequest } = require('api-tests/request');
|
||||||
|
const { createTestBuilder } = require('api-tests/builder');
|
||||||
|
const { describeOnCondition } = require('api-tests/utils');
|
||||||
|
|
||||||
|
const {
|
||||||
|
STAGE_MODEL_UID,
|
||||||
|
WORKFLOW_MODEL_UID,
|
||||||
|
ENTITY_STAGE_ATTRIBUTE,
|
||||||
|
} = require('../../../../packages/core/admin/ee/server/constants/workflows');
|
||||||
|
const { create } = require('lodash');
|
||||||
|
|
||||||
|
const edition = process.env.STRAPI_DISABLE_EE === 'true' ? 'CE' : 'EE';
|
||||||
|
|
||||||
|
const productUID = 'api::product.product';
|
||||||
|
const model = {
|
||||||
|
draftAndPublish: true,
|
||||||
|
pluginOptions: {},
|
||||||
|
singularName: 'product',
|
||||||
|
pluralName: 'products',
|
||||||
|
displayName: 'Product',
|
||||||
|
kind: 'collectionType',
|
||||||
|
attributes: {
|
||||||
|
name: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
reviewWorkflows: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const baseWorkflow = {
|
||||||
|
contentTypes: [productUID],
|
||||||
|
stages: [{ name: 'Stage 1' }, { name: 'Stage 2' }],
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStageTransitionPermissions = (roleIds) => {
|
||||||
|
return roleIds.map((roleId) => ({
|
||||||
|
action: 'admin::review-workflows.stage.transition',
|
||||||
|
role: roleId,
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
describeOnCondition(edition === 'EE')('Review workflows', () => {
|
||||||
|
const builder = createTestBuilder();
|
||||||
|
|
||||||
|
let strapi;
|
||||||
|
let workflow;
|
||||||
|
let rq;
|
||||||
|
let roles;
|
||||||
|
|
||||||
|
const createWorkflow = async (data) => {
|
||||||
|
const name = `workflow-${Math.random().toString(36)}`;
|
||||||
|
const req = await rq.post('/admin/review-workflows/workflows?populate=*', {
|
||||||
|
body: { data: { name, ...data } },
|
||||||
|
});
|
||||||
|
|
||||||
|
const status = req.statusCode;
|
||||||
|
const error = req.body.error;
|
||||||
|
const workflow = req.body.data;
|
||||||
|
|
||||||
|
return { workflow, status, error };
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateWorkflow = async (id, data) => {
|
||||||
|
const req = await rq.put(`/admin/review-workflows/workflows/${id}?populate=stages`, {
|
||||||
|
body: { data },
|
||||||
|
});
|
||||||
|
|
||||||
|
const status = req.statusCode;
|
||||||
|
const error = req.body.error;
|
||||||
|
const workflow = req.body.data;
|
||||||
|
|
||||||
|
return { workflow, status, error };
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteWorkflow = async (id) => {
|
||||||
|
const req = await rq.delete(`/admin/review-workflows/workflows/${id}`);
|
||||||
|
|
||||||
|
const status = req.statusCode;
|
||||||
|
const error = req.body.error;
|
||||||
|
const workflow = req.body.data;
|
||||||
|
|
||||||
|
return { workflow, status, error };
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
await builder.addContentTypes([model]).build();
|
||||||
|
|
||||||
|
strapi = await createStrapiInstance();
|
||||||
|
rq = await createAuthRequest({ strapi });
|
||||||
|
|
||||||
|
workflow = await createWorkflow(baseWorkflow);
|
||||||
|
|
||||||
|
// Get default roles
|
||||||
|
const { body } = await rq.get('/admin/roles');
|
||||||
|
roles = body.data;
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await strapi.destroy();
|
||||||
|
await builder.cleanup();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Assign workflow permissions', () => {
|
||||||
|
// Create stage with permissions
|
||||||
|
test('Can assign new stage permissions', async () => {
|
||||||
|
const { workflow } = await createWorkflow({
|
||||||
|
...baseWorkflow,
|
||||||
|
stages: [
|
||||||
|
{
|
||||||
|
...baseWorkflow.stages[0],
|
||||||
|
permissions: getStageTransitionPermissions([roles[0].id, roles[1].id]),
|
||||||
|
},
|
||||||
|
baseWorkflow.stages[1],
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(workflow.stages[0].permissions).toHaveLength(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Can unassign a role
|
||||||
|
test('Can remove stage permissions', async () => {
|
||||||
|
// Create workflow with permissions to transition to role 0 and 1
|
||||||
|
const { workflow } = await createWorkflow({
|
||||||
|
...baseWorkflow,
|
||||||
|
stages: [
|
||||||
|
{
|
||||||
|
...baseWorkflow.stages[0],
|
||||||
|
permissions: getStageTransitionPermissions([roles[0].id, roles[1].id]),
|
||||||
|
},
|
||||||
|
baseWorkflow.stages[1],
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update workflow to remove role 1
|
||||||
|
const { workflow: updatedWorkflow } = await updateWorkflow(workflow.id, {
|
||||||
|
stages: [
|
||||||
|
{
|
||||||
|
...workflow.stages[0],
|
||||||
|
permissions: getStageTransitionPermissions([roles[0].id]),
|
||||||
|
},
|
||||||
|
workflow.stages[1],
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Validate that permissions have been removed from database
|
||||||
|
const deletedPermission = await strapi.query('admin::permission').findOne({
|
||||||
|
where: {
|
||||||
|
id: workflow.stages[0].permissions[1].id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(updatedWorkflow.stages[0].permissions).toHaveLength(1);
|
||||||
|
expect(deletedPermission).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Deleting stage removes permissions', async () => {
|
||||||
|
const { workflow } = await createWorkflow({
|
||||||
|
...baseWorkflow,
|
||||||
|
stages: [
|
||||||
|
{
|
||||||
|
...baseWorkflow.stages[0],
|
||||||
|
permissions: getStageTransitionPermissions([roles[0].id, roles[1].id]),
|
||||||
|
},
|
||||||
|
baseWorkflow.stages[1],
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const { workflow: updatedWorkflow } = await updateWorkflow(workflow.id, {
|
||||||
|
...workflow,
|
||||||
|
stages: [workflow.stages[1]],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Deleted stage permissions should be removed from database
|
||||||
|
const permissions = await strapi.query('admin::permission').findMany({
|
||||||
|
where: {
|
||||||
|
id: { $in: workflow.stages[0].permissions.map((p) => p.id) },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(permissions).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Deleting workflow removes permissions', async () => {
|
||||||
|
const { workflow } = await createWorkflow({
|
||||||
|
...baseWorkflow,
|
||||||
|
stages: [
|
||||||
|
{
|
||||||
|
...baseWorkflow.stages[0],
|
||||||
|
permissions: getStageTransitionPermissions([roles[0].id, roles[1].id]),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
await deleteWorkflow(workflow.id);
|
||||||
|
|
||||||
|
// Deleted workflow permissions should be removed from database
|
||||||
|
const permissions = await strapi.query('admin::permission').findMany({
|
||||||
|
where: {
|
||||||
|
id: { $in: workflow.stages[0].permissions.map((p) => p.id) },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(permissions).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Fails when using invalid action', async () => {
|
||||||
|
const { status, error } = await createWorkflow({
|
||||||
|
...baseWorkflow,
|
||||||
|
stages: [
|
||||||
|
{
|
||||||
|
...baseWorkflow.stages[0],
|
||||||
|
permissions: [{ action: 'invalid-action', role: roles[0].id }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(status).toBe(400);
|
||||||
|
expect(error.name).toBe('ValidationError');
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO
|
||||||
|
test.skip('Can send permissions as undefined to apply partial update', async () => {
|
||||||
|
// Creates workflow with permissions
|
||||||
|
const { workflow } = await createWorkflow({
|
||||||
|
...baseWorkflow,
|
||||||
|
stages: [
|
||||||
|
{
|
||||||
|
...baseWorkflow.stages[0],
|
||||||
|
permissions: [{ action: 'invalid-action', role: roles[0].id }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const { workflow: updatedWorkflow } = await updateWorkflow(workflow.id, {
|
||||||
|
...workflow,
|
||||||
|
stages: [
|
||||||
|
{
|
||||||
|
...workflow.stages[0],
|
||||||
|
permissions: undefined,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Permissions should be kept
|
||||||
|
expect(updatedWorkflow.stages[0].permissions).toHaveLength(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -1,5 +1,6 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
const { omit } = require('lodash/fp');
|
||||||
const { mapAsync } = require('@strapi/utils');
|
const { mapAsync } = require('@strapi/utils');
|
||||||
|
|
||||||
const { createStrapiInstance } = require('api-tests/strapi');
|
const { createStrapiInstance } = require('api-tests/strapi');
|
||||||
@ -439,11 +440,15 @@ describeOnCondition(edition === 'EE')('Review workflows', () => {
|
|||||||
expect(workflowRes.status).toBe(200);
|
expect(workflowRes.status).toBe(200);
|
||||||
expect(workflowRes.body.data).toBeInstanceOf(Object);
|
expect(workflowRes.body.data).toBeInstanceOf(Object);
|
||||||
expect(workflowRes.body.data.stages).toBeInstanceOf(Array);
|
expect(workflowRes.body.data.stages).toBeInstanceOf(Array);
|
||||||
expect(workflowRes.body.data.stages[0]).toMatchObject(stagesUpdateData[0]);
|
expect(workflowRes.body.data.stages[0]).toMatchObject(
|
||||||
expect(workflowRes.body.data.stages[1]).toMatchObject(stagesUpdateData[1]);
|
omit(['updatedAt'], stagesUpdateData[0])
|
||||||
|
);
|
||||||
|
expect(workflowRes.body.data.stages[1]).toMatchObject(
|
||||||
|
omit(['updatedAt'], stagesUpdateData[1])
|
||||||
|
);
|
||||||
expect(workflowRes.body.data.stages[2]).toMatchObject({
|
expect(workflowRes.body.data.stages[2]).toMatchObject({
|
||||||
id: expect.any(Number),
|
id: expect.any(Number),
|
||||||
...stagesUpdateData[2],
|
...omit(['updatedAt'], stagesUpdateData[2]),
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
expect(workflowRes.status).toBe(404);
|
expect(workflowRes.status).toBe(404);
|
||||||
|
@ -133,6 +133,9 @@ describe('Admin | Settings | Review Workflows | reducer', () => {
|
|||||||
|
|
||||||
expect(reducer(state, action)).toStrictEqual(
|
expect(reducer(state, action)).toStrictEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
|
serverState: expect.objectContaining({
|
||||||
|
workflow: WORKFLOW_FIXTURE,
|
||||||
|
}),
|
||||||
clientState: expect.objectContaining({
|
clientState: expect.objectContaining({
|
||||||
currentWorkflow: expect.objectContaining({
|
currentWorkflow: expect.objectContaining({
|
||||||
data: expect.objectContaining({
|
data: expect.objectContaining({
|
||||||
|
@ -62,5 +62,11 @@ module.exports = {
|
|||||||
category: 'review workflows',
|
category: 'review workflows',
|
||||||
subCategory: 'options',
|
subCategory: 'options',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
uid: 'review-workflows.stage.transition',
|
||||||
|
displayName: 'Change stage',
|
||||||
|
pluginName: 'admin',
|
||||||
|
section: 'internal',
|
||||||
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
WORKFLOW_MODEL_UID: 'admin::workflow',
|
WORKFLOW_MODEL_UID: 'admin::workflow',
|
||||||
STAGE_MODEL_UID: 'admin::workflow-stage',
|
STAGE_MODEL_UID: 'admin::workflow-stage',
|
||||||
|
STAGE_TRANSITION_UID: 'admin::review-workflows.stage.transition',
|
||||||
STAGE_DEFAULT_COLOR: '#4945FF',
|
STAGE_DEFAULT_COLOR: '#4945FF',
|
||||||
ENTITY_STAGE_ATTRIBUTE: 'strapi_stage',
|
ENTITY_STAGE_ATTRIBUTE: 'strapi_stage',
|
||||||
MAX_WORKFLOWS: 200,
|
MAX_WORKFLOWS: 200,
|
||||||
@ -16,4 +17,16 @@ module.exports = {
|
|||||||
'You’ve reached the limit of stages for this workflow in your plan. Try deleting some stages or contact Sales to enable more stages.',
|
'You’ve reached the limit of stages for this workflow in your plan. Try deleting some stages or contact Sales to enable more stages.',
|
||||||
DUPLICATED_STAGE_NAME: 'Stage names must be unique.',
|
DUPLICATED_STAGE_NAME: 'Stage names must be unique.',
|
||||||
},
|
},
|
||||||
|
WORKFLOW_POPULATE: {
|
||||||
|
stages: {
|
||||||
|
populate: {
|
||||||
|
permissions: {
|
||||||
|
fields: ['action', 'actionParameters'],
|
||||||
|
populate: {
|
||||||
|
role: { fields: ['id', 'name'] },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
@ -40,6 +40,12 @@ module.exports = {
|
|||||||
inversedBy: 'stages',
|
inversedBy: 'stages',
|
||||||
configurable: false,
|
configurable: false,
|
||||||
},
|
},
|
||||||
|
permissions: {
|
||||||
|
type: 'relation',
|
||||||
|
target: 'admin::permission',
|
||||||
|
relation: 'manyToMany',
|
||||||
|
configurable: false,
|
||||||
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
const { update, map, property } = require('lodash/fp');
|
||||||
const { mapAsync } = require('@strapi/utils');
|
const { mapAsync } = require('@strapi/utils');
|
||||||
const { getService } = require('../../utils');
|
const { getService } = require('../../utils');
|
||||||
|
|
||||||
@ -7,7 +8,7 @@ const {
|
|||||||
validateWorkflowCreate,
|
validateWorkflowCreate,
|
||||||
validateWorkflowUpdate,
|
validateWorkflowUpdate,
|
||||||
} = require('../../validation/review-workflows');
|
} = require('../../validation/review-workflows');
|
||||||
const { WORKFLOW_MODEL_UID } = require('../../constants/workflows');
|
const { WORKFLOW_MODEL_UID, WORKFLOW_POPULATE } = require('../../constants/workflows');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
@ -22,6 +23,21 @@ function getWorkflowsPermissionChecker({ strapi }, userAbility) {
|
|||||||
.create({ userAbility, model: WORKFLOW_MODEL_UID });
|
.create({ userAbility, model: WORKFLOW_MODEL_UID });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transforms workflow to an admin UI format.
|
||||||
|
* Some attributes (like permissions) are presented in a different format in the admin UI.
|
||||||
|
* @param {Workflow} workflow
|
||||||
|
*/
|
||||||
|
function formatWorkflowToAdmin(workflow) {
|
||||||
|
if (!workflow) return;
|
||||||
|
if (!workflow.stages) return workflow;
|
||||||
|
|
||||||
|
// Transform permissions roles to be the id string instead of an object
|
||||||
|
const transformPermissions = map(update('role', property('id')));
|
||||||
|
const transformStages = map(update('permissions', transformPermissions));
|
||||||
|
return update('stages', transformStages, workflow);
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
/**
|
/**
|
||||||
* Create a new workflow
|
* Create a new workflow
|
||||||
@ -38,10 +54,12 @@ module.exports = {
|
|||||||
const workflowBody = await validateWorkflowCreate(body.data);
|
const workflowBody = await validateWorkflowCreate(body.data);
|
||||||
|
|
||||||
const workflowService = getService('workflows');
|
const workflowService = getService('workflows');
|
||||||
const createdWorkflow = await workflowService.create({
|
const createdWorkflow = await workflowService
|
||||||
data: await sanitizeCreateInput(workflowBody),
|
.create({
|
||||||
populate,
|
data: await sanitizeCreateInput(workflowBody),
|
||||||
});
|
populate,
|
||||||
|
})
|
||||||
|
.then(formatWorkflowToAdmin);
|
||||||
|
|
||||||
ctx.body = {
|
ctx.body = {
|
||||||
data: await sanitizeOutput(createdWorkflow),
|
data: await sanitizeOutput(createdWorkflow),
|
||||||
@ -61,22 +79,27 @@ module.exports = {
|
|||||||
ctx.state.userAbility
|
ctx.state.userAbility
|
||||||
);
|
);
|
||||||
const { populate } = await sanitizedQuery.update(query);
|
const { populate } = await sanitizedQuery.update(query);
|
||||||
|
|
||||||
const workflowBody = await validateWorkflowUpdate(body.data);
|
const workflowBody = await validateWorkflowUpdate(body.data);
|
||||||
|
|
||||||
const workflow = await workflowService.findById(id, { populate: ['stages'] });
|
// Find if workflow exists
|
||||||
|
const workflow = await workflowService.findById(id, { populate: WORKFLOW_POPULATE });
|
||||||
if (!workflow) {
|
if (!workflow) {
|
||||||
return ctx.notFound();
|
return ctx.notFound();
|
||||||
}
|
}
|
||||||
const getPermittedFieldToUpdate = sanitizeUpdateInput(workflow);
|
|
||||||
|
|
||||||
|
// Sanitize input data
|
||||||
|
const getPermittedFieldToUpdate = sanitizeUpdateInput(workflow);
|
||||||
const dataToUpdate = await getPermittedFieldToUpdate(workflowBody);
|
const dataToUpdate = await getPermittedFieldToUpdate(workflowBody);
|
||||||
|
|
||||||
const updatedWorkflow = await workflowService.update(workflow, {
|
// Update workflow
|
||||||
data: dataToUpdate,
|
const updatedWorkflow = await workflowService
|
||||||
populate,
|
.update(workflow, {
|
||||||
});
|
data: dataToUpdate,
|
||||||
|
populate,
|
||||||
|
})
|
||||||
|
.then(formatWorkflowToAdmin);
|
||||||
|
|
||||||
|
// Send sanitized response
|
||||||
ctx.body = {
|
ctx.body = {
|
||||||
data: await sanitizeOutput(updatedWorkflow),
|
data: await sanitizeOutput(updatedWorkflow),
|
||||||
};
|
};
|
||||||
@ -96,12 +119,14 @@ module.exports = {
|
|||||||
);
|
);
|
||||||
const { populate } = await sanitizedQuery.delete(query);
|
const { populate } = await sanitizedQuery.delete(query);
|
||||||
|
|
||||||
const workflow = await workflowService.findById(id, { populate: ['stages'] });
|
const workflow = await workflowService.findById(id, { populate: WORKFLOW_POPULATE });
|
||||||
if (!workflow) {
|
if (!workflow) {
|
||||||
return ctx.notFound("Workflow doesn't exist");
|
return ctx.notFound("Workflow doesn't exist");
|
||||||
}
|
}
|
||||||
|
|
||||||
const deletedWorkflow = await workflowService.delete(workflow, { populate });
|
const deletedWorkflow = await workflowService
|
||||||
|
.delete(workflow, { populate })
|
||||||
|
.then(formatWorkflowToAdmin);
|
||||||
|
|
||||||
ctx.body = {
|
ctx.body = {
|
||||||
data: await sanitizeOutput(deletedWorkflow),
|
data: await sanitizeOutput(deletedWorkflow),
|
||||||
@ -122,7 +147,7 @@ module.exports = {
|
|||||||
const { populate, filters, sort } = await sanitizedQuery.read(query);
|
const { populate, filters, sort } = await sanitizedQuery.read(query);
|
||||||
|
|
||||||
const [workflows, workflowCount] = await Promise.all([
|
const [workflows, workflowCount] = await Promise.all([
|
||||||
workflowService.find({ populate, filters, sort }),
|
workflowService.find({ populate, filters, sort }).then(map(formatWorkflowToAdmin)),
|
||||||
workflowService.count(),
|
workflowService.count(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@ -151,7 +176,7 @@ module.exports = {
|
|||||||
const workflowService = getService('workflows');
|
const workflowService = getService('workflows');
|
||||||
|
|
||||||
const [workflow, workflowCount] = await Promise.all([
|
const [workflow, workflowCount] = await Promise.all([
|
||||||
workflowService.findById(id, { populate }),
|
workflowService.findById(id, { populate }).then(formatWorkflowToAdmin),
|
||||||
workflowService.count(),
|
workflowService.count(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
@ -3,7 +3,11 @@
|
|||||||
const { mapAsync } = require('@strapi/utils');
|
const { mapAsync } = require('@strapi/utils');
|
||||||
const { getService } = require('../../../utils');
|
const { getService } = require('../../../utils');
|
||||||
const { validateUpdateStageOnEntity } = require('../../../validation/review-workflows');
|
const { validateUpdateStageOnEntity } = require('../../../validation/review-workflows');
|
||||||
const { STAGE_MODEL_UID } = require('../../../constants/workflows');
|
const {
|
||||||
|
STAGE_MODEL_UID,
|
||||||
|
ENTITY_STAGE_ATTRIBUTE,
|
||||||
|
STAGE_TRANSITION_UID,
|
||||||
|
} = require('../../../constants/workflows');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
@ -75,18 +79,36 @@ module.exports = {
|
|||||||
*/
|
*/
|
||||||
async updateEntity(ctx) {
|
async updateEntity(ctx) {
|
||||||
const stagesService = getService('stages');
|
const stagesService = getService('stages');
|
||||||
|
const stagePermissions = getService('stage-permissions');
|
||||||
const workflowService = getService('workflows');
|
const workflowService = getService('workflows');
|
||||||
|
|
||||||
const { model_uid: modelUID, id: entityIdString } = ctx.params;
|
const { model_uid: modelUID, id } = ctx.params;
|
||||||
const { body } = ctx.request;
|
const { body } = ctx.request;
|
||||||
|
|
||||||
const entityId = Number(entityIdString);
|
|
||||||
|
|
||||||
const { sanitizeOutput } = strapi
|
const { sanitizeOutput } = strapi
|
||||||
.plugin('content-manager')
|
.plugin('content-manager')
|
||||||
.service('permission-checker')
|
.service('permission-checker')
|
||||||
.create({ userAbility: ctx.state.userAbility, model: modelUID });
|
.create({ userAbility: ctx.state.userAbility, model: modelUID });
|
||||||
|
|
||||||
|
// Load entity
|
||||||
|
const entity = await strapi.entityService.findOne(modelUID, Number(id), {
|
||||||
|
populate: [ENTITY_STAGE_ATTRIBUTE],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!entity) {
|
||||||
|
ctx.throw(404, 'Entity not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate if entity stage can be updated
|
||||||
|
const canTransition = stagePermissions.can(
|
||||||
|
STAGE_TRANSITION_UID,
|
||||||
|
entity[ENTITY_STAGE_ATTRIBUTE]?.id
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!canTransition) {
|
||||||
|
ctx.throw(403, 'Forbidden stage transition');
|
||||||
|
}
|
||||||
|
|
||||||
const { id: stageId } = await validateUpdateStageOnEntity(
|
const { id: stageId } = await validateUpdateStageOnEntity(
|
||||||
{ id: Number(body?.data?.id) },
|
{ id: Number(body?.data?.id) },
|
||||||
'You should pass an id to the body of the put request.'
|
'You should pass an id to the body of the put request.'
|
||||||
@ -95,8 +117,8 @@ module.exports = {
|
|||||||
const workflow = await workflowService.assertContentTypeBelongsToWorkflow(modelUID);
|
const workflow = await workflowService.assertContentTypeBelongsToWorkflow(modelUID);
|
||||||
workflowService.assertStageBelongsToWorkflow(stageId, workflow);
|
workflowService.assertStageBelongsToWorkflow(stageId, workflow);
|
||||||
|
|
||||||
const entity = await stagesService.updateEntity({ id: entityId, modelUID }, stageId);
|
const updatedEntity = await stagesService.updateEntity({ id: entity.id, modelUID }, stageId);
|
||||||
|
|
||||||
ctx.body = { data: await sanitizeOutput(entity) };
|
ctx.body = { data: await sanitizeOutput(updatedEntity) };
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -78,6 +78,12 @@ const servicesMock = {
|
|||||||
validateWorkflowCount: jest.fn().mockResolvedValue(true),
|
validateWorkflowCount: jest.fn().mockResolvedValue(true),
|
||||||
validateWorkflowStages: jest.fn(),
|
validateWorkflowStages: jest.fn(),
|
||||||
},
|
},
|
||||||
|
'admin::stage-permissions': {
|
||||||
|
register: jest.fn(),
|
||||||
|
registerMany: jest.fn(),
|
||||||
|
unregister: jest.fn(),
|
||||||
|
can: jest.fn(() => true),
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const queryUpdateMock = jest.fn(() => Promise.resolve());
|
const queryUpdateMock = jest.fn(() => Promise.resolve());
|
||||||
|
@ -24,7 +24,7 @@ jest.mock('../review-workflows/workflows/content-types', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const workflowsServiceFactory = require('../review-workflows/workflows');
|
const workflowsServiceFactory = require('../review-workflows/workflows');
|
||||||
const { WORKFLOW_MODEL_UID } = require('../../constants/workflows');
|
const { WORKFLOW_MODEL_UID, WORKFLOW_POPULATE } = require('../../constants/workflows');
|
||||||
|
|
||||||
const workflowMock = {
|
const workflowMock = {
|
||||||
id: 1,
|
id: 1,
|
||||||
@ -71,6 +71,12 @@ const servicesMock = {
|
|||||||
replaceStages: jest.fn(async () => stagesMock),
|
replaceStages: jest.fn(async () => stagesMock),
|
||||||
createMany: jest.fn(async () => stagesMock),
|
createMany: jest.fn(async () => stagesMock),
|
||||||
},
|
},
|
||||||
|
'admin::stage-permissions': {
|
||||||
|
register: jest.fn(),
|
||||||
|
registerMany: jest.fn(),
|
||||||
|
unregister: jest.fn(),
|
||||||
|
can: jest.fn(() => true),
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const strapiMock = {
|
const strapiMock = {
|
||||||
@ -145,7 +151,7 @@ describe('Review workflows - Workflows service', () => {
|
|||||||
return stage;
|
return stage;
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
populate: undefined,
|
populate: WORKFLOW_POPULATE,
|
||||||
};
|
};
|
||||||
|
|
||||||
test('Should call entityService with the right model UID', async () => {
|
test('Should call entityService with the right model UID', async () => {
|
||||||
@ -158,7 +164,7 @@ describe('Review workflows - Workflows service', () => {
|
|||||||
name: 'Default',
|
name: 'Default',
|
||||||
stages: workflow.stages.map((stage) => stage.id),
|
stages: workflow.stages.map((stage) => stage.id),
|
||||||
},
|
},
|
||||||
populate: undefined,
|
populate: WORKFLOW_POPULATE,
|
||||||
});
|
});
|
||||||
expect(servicesMock['admin::review-workflows-metrics'].sendDidEditWorkflow).toBeCalled();
|
expect(servicesMock['admin::review-workflows-metrics'].sendDidEditWorkflow).toBeCalled();
|
||||||
});
|
});
|
||||||
@ -177,7 +183,7 @@ describe('Review workflows - Workflows service', () => {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
populate: undefined,
|
populate: WORKFLOW_POPULATE,
|
||||||
};
|
};
|
||||||
|
|
||||||
test('Should call entityService with the right model UID', async () => {
|
test('Should call entityService with the right model UID', async () => {
|
||||||
@ -189,7 +195,7 @@ describe('Review workflows - Workflows service', () => {
|
|||||||
name: 'Workflow',
|
name: 'Workflow',
|
||||||
stages: stagesMock.map((stage) => stage.id),
|
stages: stagesMock.map((stage) => stage.id),
|
||||||
},
|
},
|
||||||
populate: { stages: true },
|
populate: WORKFLOW_POPULATE,
|
||||||
});
|
});
|
||||||
expect(servicesMock['admin::review-workflows-metrics'].sendDidCreateWorkflow).toBeCalled();
|
expect(servicesMock['admin::review-workflows-metrics'].sendDidCreateWorkflow).toBeCalled();
|
||||||
});
|
});
|
||||||
|
@ -8,6 +8,7 @@ module.exports = {
|
|||||||
'seat-enforcement': require('./seat-enforcement'),
|
'seat-enforcement': require('./seat-enforcement'),
|
||||||
workflows: require('./review-workflows/workflows'),
|
workflows: require('./review-workflows/workflows'),
|
||||||
stages: require('./review-workflows/stages'),
|
stages: require('./review-workflows/stages'),
|
||||||
|
'stage-permissions': require('./review-workflows/stage-permissions'),
|
||||||
'review-workflows': require('./review-workflows/review-workflows'),
|
'review-workflows': require('./review-workflows/review-workflows'),
|
||||||
'review-workflows-validation': require('./review-workflows/validation'),
|
'review-workflows-validation': require('./review-workflows/validation'),
|
||||||
'review-workflows-decorator': require('./review-workflows/entity-service-decorator'),
|
'review-workflows-decorator': require('./review-workflows/entity-service-decorator'),
|
||||||
|
@ -0,0 +1,60 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const { prop } = require('lodash/fp');
|
||||||
|
const {
|
||||||
|
mapAsync,
|
||||||
|
errors: { ApplicationError },
|
||||||
|
} = require('@strapi/utils');
|
||||||
|
const { getService } = require('../../utils');
|
||||||
|
const { STAGE_TRANSITION_UID } = require('../../constants/workflows');
|
||||||
|
|
||||||
|
const validActions = [STAGE_TRANSITION_UID];
|
||||||
|
|
||||||
|
module.exports = ({ strapi }) => {
|
||||||
|
const roleService = getService('role');
|
||||||
|
const permissionService = getService('permission');
|
||||||
|
|
||||||
|
return {
|
||||||
|
async register(roleId, action, fromStage) {
|
||||||
|
if (!validActions.includes(action)) {
|
||||||
|
throw new ApplicationError(`Invalid action ${action}`);
|
||||||
|
}
|
||||||
|
const permissions = await roleService.addPermissions(roleId, [
|
||||||
|
{
|
||||||
|
action,
|
||||||
|
actionParameters: {
|
||||||
|
from: fromStage,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
// TODO: Filter response
|
||||||
|
return permissions;
|
||||||
|
},
|
||||||
|
async registerMany(permissions) {
|
||||||
|
return mapAsync(permissions, this.register);
|
||||||
|
},
|
||||||
|
async unregister(permissions) {
|
||||||
|
const permissionIds = permissions.map(prop('id'));
|
||||||
|
await permissionService.deleteByIds(permissionIds);
|
||||||
|
},
|
||||||
|
can(action, fromStage) {
|
||||||
|
const requestState = strapi.requestContext.get()?.state;
|
||||||
|
|
||||||
|
if (!requestState) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Override permissions for super admin
|
||||||
|
const userRoles = requestState.user?.roles;
|
||||||
|
if (userRoles?.some((role) => role.code === 'strapi-super-admin')) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return requestState.userAbility.can({
|
||||||
|
name: action,
|
||||||
|
params: { from: fromStage },
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
@ -2,15 +2,20 @@
|
|||||||
|
|
||||||
const {
|
const {
|
||||||
mapAsync,
|
mapAsync,
|
||||||
|
reduceAsync,
|
||||||
errors: { ApplicationError, ValidationError },
|
errors: { ApplicationError, ValidationError },
|
||||||
} = require('@strapi/utils');
|
} = require('@strapi/utils');
|
||||||
const { map } = require('lodash/fp');
|
const { map, pick, isEqual } = require('lodash/fp');
|
||||||
|
|
||||||
const { STAGE_MODEL_UID, ENTITY_STAGE_ATTRIBUTE, ERRORS } = require('../../constants/workflows');
|
const { STAGE_MODEL_UID, ENTITY_STAGE_ATTRIBUTE, ERRORS } = require('../../constants/workflows');
|
||||||
const { getService } = require('../../utils');
|
const { getService } = require('../../utils');
|
||||||
|
|
||||||
|
const sanitizedStageFields = ['id', 'name', 'workflow'];
|
||||||
|
const sanitizeStageFields = pick(sanitizedStageFields);
|
||||||
|
|
||||||
module.exports = ({ strapi }) => {
|
module.exports = ({ strapi }) => {
|
||||||
const metrics = getService('review-workflows-metrics', { strapi });
|
const metrics = getService('review-workflows-metrics', { strapi });
|
||||||
|
const stagePermissionsService = getService('stage-permissions', { strapi });
|
||||||
const workflowsValidationService = getService('review-workflows-validation', { strapi });
|
const workflowsValidationService = getService('review-workflows-validation', { strapi });
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -32,20 +37,64 @@ module.exports = ({ strapi }) => {
|
|||||||
async createMany(stagesList, { fields } = {}) {
|
async createMany(stagesList, { fields } = {}) {
|
||||||
const params = { select: fields ?? '*' };
|
const params = { select: fields ?? '*' };
|
||||||
|
|
||||||
|
// TODO: pick the fields from the stage
|
||||||
const stages = await Promise.all(
|
const stages = await Promise.all(
|
||||||
stagesList.map((stage) =>
|
stagesList.map((stage) =>
|
||||||
strapi.entityService.create(STAGE_MODEL_UID, { data: stage, ...params })
|
strapi.entityService.create(STAGE_MODEL_UID, {
|
||||||
|
data: sanitizeStageFields(stage),
|
||||||
|
...params,
|
||||||
|
})
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Create stage permissions
|
||||||
|
await reduceAsync(stagesList)(async (_, stage, idx) => {
|
||||||
|
// Ignore stages without permissions
|
||||||
|
if (!stage.permissions || stage.permissions.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const stagePermissions = stage.permissions;
|
||||||
|
const stageId = stages[idx].id;
|
||||||
|
|
||||||
|
const permissions = await mapAsync(
|
||||||
|
stagePermissions,
|
||||||
|
// Register each stage permission
|
||||||
|
(permission) =>
|
||||||
|
stagePermissionsService.register(permission.role, permission.action, stageId)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Update stage with the new permissions
|
||||||
|
await strapi.entityService.update(STAGE_MODEL_UID, stageId, {
|
||||||
|
data: {
|
||||||
|
permissions: permissions.flat().map((p) => p.id),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
metrics.sendDidCreateStage();
|
metrics.sendDidCreateStage();
|
||||||
|
|
||||||
return stages;
|
return stages;
|
||||||
},
|
},
|
||||||
|
|
||||||
async update(stageId, stageData) {
|
async update(srcStage, destStage) {
|
||||||
|
let stagePermissions = [];
|
||||||
|
const stageId = destStage.id;
|
||||||
|
|
||||||
|
await this.deleteStagePermissions([srcStage]);
|
||||||
|
|
||||||
|
if (destStage.permissions) {
|
||||||
|
const permissions = await mapAsync(destStage.permissions, (permission) =>
|
||||||
|
stagePermissionsService.register(permission.role, permission.action, stageId)
|
||||||
|
);
|
||||||
|
stagePermissions = permissions.flat().map((p) => p.id);
|
||||||
|
}
|
||||||
|
|
||||||
const stage = await strapi.entityService.update(STAGE_MODEL_UID, stageId, {
|
const stage = await strapi.entityService.update(STAGE_MODEL_UID, stageId, {
|
||||||
data: stageData,
|
data: {
|
||||||
|
...destStage,
|
||||||
|
permissions: stagePermissions,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
metrics.sendDidEditStage();
|
metrics.sendDidEditStage();
|
||||||
@ -53,20 +102,31 @@ module.exports = ({ strapi }) => {
|
|||||||
return stage;
|
return stage;
|
||||||
},
|
},
|
||||||
|
|
||||||
async delete(stageId) {
|
async delete(stage) {
|
||||||
const stage = await strapi.entityService.delete(STAGE_MODEL_UID, stageId);
|
// Unregister all permissions related to this stage id
|
||||||
|
await this.deleteStagePermissions([stage]);
|
||||||
|
|
||||||
|
const deletedStage = await strapi.entityService.delete(STAGE_MODEL_UID, stage.id);
|
||||||
|
|
||||||
metrics.sendDidDeleteStage();
|
metrics.sendDidDeleteStage();
|
||||||
|
|
||||||
return stage;
|
return deletedStage;
|
||||||
},
|
},
|
||||||
|
|
||||||
async deleteMany(stagesId) {
|
async deleteMany(stages) {
|
||||||
|
await this.deleteStagePermissions(stages);
|
||||||
|
|
||||||
return strapi.entityService.deleteMany(STAGE_MODEL_UID, {
|
return strapi.entityService.deleteMany(STAGE_MODEL_UID, {
|
||||||
filters: { id: { $in: stagesId } },
|
filters: { id: { $in: stages.map((s) => s.id) } },
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async deleteStagePermissions(stages) {
|
||||||
|
// TODO: Find another way to do this for when we use the "to" parameter.
|
||||||
|
const permissions = stages.map((s) => s.permissions || []).flat();
|
||||||
|
await stagePermissionsService.unregister(permissions || []);
|
||||||
|
},
|
||||||
|
|
||||||
count({ workflowId } = {}) {
|
count({ workflowId } = {}) {
|
||||||
const opts = {};
|
const opts = {};
|
||||||
|
|
||||||
@ -91,7 +151,10 @@ module.exports = ({ strapi }) => {
|
|||||||
const createdStagesIds = map('id', createdStages);
|
const createdStagesIds = map('id', createdStages);
|
||||||
|
|
||||||
// Update the workflow stages
|
// Update the workflow stages
|
||||||
await mapAsync(updated, (stage) => this.update(stage.id, stage));
|
await mapAsync(updated, (destStage) => {
|
||||||
|
const srcStage = srcStages.find((s) => s.id === destStage.id);
|
||||||
|
return this.update(srcStage, destStage);
|
||||||
|
});
|
||||||
|
|
||||||
// Delete the stages that are not in the new stages list
|
// Delete the stages that are not in the new stages list
|
||||||
await mapAsync(deleted, async (stage) => {
|
await mapAsync(deleted, async (stage) => {
|
||||||
@ -114,7 +177,7 @@ module.exports = ({ strapi }) => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
return this.delete(stage.id);
|
return this.delete(stage);
|
||||||
});
|
});
|
||||||
|
|
||||||
return destStages.map((stage) => ({ ...stage, id: stage.id ?? createdStagesIds.shift() }));
|
return destStages.map((stage) => ({ ...stage, id: stage.id ?? createdStagesIds.shift() }));
|
||||||
@ -241,12 +304,19 @@ module.exports = ({ strapi }) => {
|
|||||||
*/
|
*/
|
||||||
function getDiffBetweenStages(sourceStages, comparisonStages) {
|
function getDiffBetweenStages(sourceStages, comparisonStages) {
|
||||||
const result = comparisonStages.reduce(
|
const result = comparisonStages.reduce(
|
||||||
|
// ...
|
||||||
|
|
||||||
(acc, stageToCompare) => {
|
(acc, stageToCompare) => {
|
||||||
const srcStage = sourceStages.find((stage) => stage.id === stageToCompare.id);
|
const srcStage = sourceStages.find((stage) => stage.id === stageToCompare.id);
|
||||||
|
|
||||||
if (!srcStage) {
|
if (!srcStage) {
|
||||||
acc.created.push(stageToCompare);
|
acc.created.push(stageToCompare);
|
||||||
} else if (srcStage.name !== stageToCompare.name || srcStage.color !== stageToCompare.color) {
|
} else if (
|
||||||
|
!isEqual(
|
||||||
|
pick(['name', 'color', 'permissions'], srcStage),
|
||||||
|
pick(['name', 'color', 'permissions'], stageToCompare)
|
||||||
|
)
|
||||||
|
) {
|
||||||
acc.updated.push(stageToCompare);
|
acc.updated.push(stageToCompare);
|
||||||
}
|
}
|
||||||
return acc;
|
return acc;
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
const { set, isString, map, get } = require('lodash/fp');
|
const { set, isString, map, get } = require('lodash/fp');
|
||||||
const { ApplicationError } = require('@strapi/utils').errors;
|
const { ApplicationError } = require('@strapi/utils').errors;
|
||||||
const { WORKFLOW_MODEL_UID } = require('../../../constants/workflows');
|
const { WORKFLOW_MODEL_UID, WORKFLOW_POPULATE } = require('../../../constants/workflows');
|
||||||
const { getService } = require('../../../utils');
|
const { getService } = require('../../../utils');
|
||||||
const { getWorkflowContentTypeFilter } = require('../../../utils/review-workflows');
|
const { getWorkflowContentTypeFilter } = require('../../../utils/review-workflows');
|
||||||
const workflowsContentTypesFactory = require('./content-types');
|
const workflowsContentTypesFactory = require('./content-types');
|
||||||
@ -17,6 +17,16 @@ const processFilters = ({ strapi }, filters = {}) => {
|
|||||||
return processedFilters;
|
return processedFilters;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// TODO: How can we improve this? Maybe using traversePopulate?
|
||||||
|
const processPopulate = (populate) => {
|
||||||
|
// If it does not exist or it's not an object (like an array) return the default populate
|
||||||
|
if (!populate) {
|
||||||
|
return populate;
|
||||||
|
}
|
||||||
|
|
||||||
|
return WORKFLOW_POPULATE;
|
||||||
|
};
|
||||||
|
|
||||||
module.exports = ({ strapi }) => {
|
module.exports = ({ strapi }) => {
|
||||||
const workflowsContentTypes = workflowsContentTypesFactory({ strapi });
|
const workflowsContentTypes = workflowsContentTypesFactory({ strapi });
|
||||||
const workflowsValidationService = getService('review-workflows-validation', { strapi });
|
const workflowsValidationService = getService('review-workflows-validation', { strapi });
|
||||||
@ -31,7 +41,9 @@ module.exports = ({ strapi }) => {
|
|||||||
*/
|
*/
|
||||||
async find(opts = {}) {
|
async find(opts = {}) {
|
||||||
const filters = processFilters({ strapi }, opts.filters);
|
const filters = processFilters({ strapi }, opts.filters);
|
||||||
return strapi.entityService.findMany(WORKFLOW_MODEL_UID, { ...opts, filters });
|
const populate = processPopulate(opts.populate);
|
||||||
|
|
||||||
|
return strapi.entityService.findMany(WORKFLOW_MODEL_UID, { ...opts, filters, populate });
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -41,7 +53,8 @@ module.exports = ({ strapi }) => {
|
|||||||
* @returns {Promise<object>} - Workflow object matching the requested ID.
|
* @returns {Promise<object>} - Workflow object matching the requested ID.
|
||||||
*/
|
*/
|
||||||
findById(id, opts) {
|
findById(id, opts) {
|
||||||
return strapi.entityService.findOne(WORKFLOW_MODEL_UID, id, opts);
|
const populate = processPopulate(opts.populate);
|
||||||
|
return strapi.entityService.findOne(WORKFLOW_MODEL_UID, id, { ...opts, populate });
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -51,7 +64,7 @@ module.exports = ({ strapi }) => {
|
|||||||
* @throws {ValidationError} - If the workflow has no stages.
|
* @throws {ValidationError} - If the workflow has no stages.
|
||||||
*/
|
*/
|
||||||
async create(opts) {
|
async create(opts) {
|
||||||
let createOpts = { ...opts, populate: { stages: true } };
|
let createOpts = { ...opts, populate: WORKFLOW_POPULATE };
|
||||||
|
|
||||||
workflowsValidationService.validateWorkflowStages(opts.data.stages);
|
workflowsValidationService.validateWorkflowStages(opts.data.stages);
|
||||||
await workflowsValidationService.validateWorkflowCount(1);
|
await workflowsValidationService.validateWorkflowCount(1);
|
||||||
@ -87,7 +100,7 @@ module.exports = ({ strapi }) => {
|
|||||||
*/
|
*/
|
||||||
async update(workflow, opts) {
|
async update(workflow, opts) {
|
||||||
const stageService = getService('stages', { strapi });
|
const stageService = getService('stages', { strapi });
|
||||||
let updateOpts = { ...opts, populate: { stages: true } };
|
let updateOpts = { ...opts, populate: { ...WORKFLOW_POPULATE } };
|
||||||
let updatedStageIds;
|
let updatedStageIds;
|
||||||
|
|
||||||
await workflowsValidationService.validateWorkflowCount();
|
await workflowsValidationService.validateWorkflowCount();
|
||||||
@ -104,7 +117,7 @@ module.exports = ({ strapi }) => {
|
|||||||
.replaceStages(workflow.stages, opts.data.stages, workflow.contentTypes)
|
.replaceStages(workflow.stages, opts.data.stages, workflow.contentTypes)
|
||||||
.then((stages) => stages.map((stage) => stage.id));
|
.then((stages) => stages.map((stage) => stage.id));
|
||||||
|
|
||||||
updateOpts = set('data.stages', updatedStageIds, opts);
|
updateOpts = set('data.stages', updatedStageIds, updateOpts);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update (un)assigned Content Types
|
// Update (un)assigned Content Types
|
||||||
@ -141,7 +154,7 @@ module.exports = ({ strapi }) => {
|
|||||||
|
|
||||||
return strapi.db.transaction(async () => {
|
return strapi.db.transaction(async () => {
|
||||||
// Delete stages
|
// Delete stages
|
||||||
await stageService.deleteMany(workflow.stages.map((stage) => stage.id));
|
await stageService.deleteMany(workflow.stages);
|
||||||
|
|
||||||
// Unassign all content types, this will migrate the content types to null
|
// Unassign all content types, this will migrate the content types to null
|
||||||
await workflowsContentTypes.migrate({
|
await workflowsContentTypes.migrate({
|
||||||
|
@ -4,11 +4,22 @@
|
|||||||
|
|
||||||
const { yup, validateYupSchema } = require('@strapi/utils');
|
const { yup, validateYupSchema } = require('@strapi/utils');
|
||||||
const { hasStageAttribute } = require('../utils/review-workflows');
|
const { hasStageAttribute } = require('../utils/review-workflows');
|
||||||
|
const { STAGE_TRANSITION_UID } = require('../constants/workflows');
|
||||||
|
|
||||||
const stageObject = yup.object().shape({
|
const stageObject = yup.object().shape({
|
||||||
id: yup.number().integer().min(1),
|
id: yup.number().integer().min(1),
|
||||||
name: yup.string().max(255).required(),
|
name: yup.string().max(255).required(),
|
||||||
color: yup.string().matches(/^#(?:[0-9a-fA-F]{3}){1,2}$/i), // hex color
|
color: yup.string().matches(/^#(?:[0-9a-fA-F]{3}){1,2}$/i), // hex color
|
||||||
|
permissions: yup.array().of(
|
||||||
|
yup.object().shape({
|
||||||
|
role: yup.number().integer().min(1).required(),
|
||||||
|
action: yup.string().oneOf([STAGE_TRANSITION_UID]).required(),
|
||||||
|
actionParameters: yup.object().shape({
|
||||||
|
from: yup.number().integer().min(1).required(),
|
||||||
|
to: yup.number().integer().min(1),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
const validateUpdateStageOnEntity = yup
|
const validateUpdateStageOnEntity = yup
|
||||||
|
@ -29,6 +29,12 @@ module.exports = {
|
|||||||
configurable: false,
|
configurable: false,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
actionParameters: {
|
||||||
|
type: 'json',
|
||||||
|
configurable: false,
|
||||||
|
required: false,
|
||||||
|
default: {},
|
||||||
|
},
|
||||||
subject: {
|
subject: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
minLength: 1,
|
minLength: 1,
|
||||||
|
@ -26,8 +26,16 @@ const {
|
|||||||
* @property {string} subject - The subject on which the permission should applies
|
* @property {string} subject - The subject on which the permission should applies
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const permissionFields = ['id', 'action', 'subject', 'properties', 'conditions', 'role'];
|
const permissionFields = [
|
||||||
const sanitizedPermissionFields = ['id', 'action', 'subject', 'properties', 'conditions'];
|
'id',
|
||||||
|
'action',
|
||||||
|
'actionParameters',
|
||||||
|
'subject',
|
||||||
|
'properties',
|
||||||
|
'conditions',
|
||||||
|
'role',
|
||||||
|
];
|
||||||
|
const sanitizedPermissionFields = ['id', 'action', 'actionParameters', 'subject', 'properties', 'conditions'];
|
||||||
|
|
||||||
const sanitizePermissionFields = pick(sanitizedPermissionFields);
|
const sanitizePermissionFields = pick(sanitizedPermissionFields);
|
||||||
|
|
||||||
@ -36,6 +44,7 @@ const sanitizePermissionFields = pick(sanitizedPermissionFields);
|
|||||||
* @return {Permission}
|
* @return {Permission}
|
||||||
*/
|
*/
|
||||||
const getDefaultPermission = () => ({
|
const getDefaultPermission = () => ({
|
||||||
|
actionParameters: {},
|
||||||
conditions: [],
|
conditions: [],
|
||||||
properties: {},
|
properties: {},
|
||||||
subject: null,
|
subject: null,
|
||||||
|
@ -232,6 +232,7 @@ describe('Content-Type', () => {
|
|||||||
expect(resultLevel1).toEqual([
|
expect(resultLevel1).toEqual([
|
||||||
{
|
{
|
||||||
action: 'action-1',
|
action: 'action-1',
|
||||||
|
actionParameters: {},
|
||||||
subject: 'country',
|
subject: 'country',
|
||||||
properties: { fields: ['name', 'code'] },
|
properties: { fields: ['name', 'code'] },
|
||||||
conditions: [],
|
conditions: [],
|
||||||
@ -253,12 +254,14 @@ describe('Content-Type', () => {
|
|||||||
expect(resultLevel1).toEqual([
|
expect(resultLevel1).toEqual([
|
||||||
{
|
{
|
||||||
action: 'action-1',
|
action: 'action-1',
|
||||||
|
actionParameters: {},
|
||||||
subject: 'country',
|
subject: 'country',
|
||||||
properties: { fields: ['name', 'code'] },
|
properties: { fields: ['name', 'code'] },
|
||||||
conditions: [],
|
conditions: [],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
action: 'action-1',
|
action: 'action-1',
|
||||||
|
actionParameters: {},
|
||||||
subject: 'user',
|
subject: 'user',
|
||||||
properties: { fields: ['firstname', 'restaurant', 'car'] },
|
properties: { fields: ['firstname', 'restaurant', 'car'] },
|
||||||
conditions: [],
|
conditions: [],
|
||||||
@ -280,12 +283,14 @@ describe('Content-Type', () => {
|
|||||||
expect(resultLevel1).toEqual([
|
expect(resultLevel1).toEqual([
|
||||||
{
|
{
|
||||||
action: 'action-1',
|
action: 'action-1',
|
||||||
|
actionParameters: {},
|
||||||
subject: 'country',
|
subject: 'country',
|
||||||
properties: { fields: ['name', 'code'] },
|
properties: { fields: ['name', 'code'] },
|
||||||
conditions: [],
|
conditions: [],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
action: 'action-1',
|
action: 'action-1',
|
||||||
|
actionParameters: {},
|
||||||
subject: 'user',
|
subject: 'user',
|
||||||
properties: {
|
properties: {
|
||||||
fields: [
|
fields: [
|
||||||
@ -312,12 +317,14 @@ describe('Content-Type', () => {
|
|||||||
expect(resultLevel1).toEqual([
|
expect(resultLevel1).toEqual([
|
||||||
{
|
{
|
||||||
action: 'action-1',
|
action: 'action-1',
|
||||||
|
actionParameters: {},
|
||||||
subject: 'country',
|
subject: 'country',
|
||||||
properties: { fields: ['name', 'code'] },
|
properties: { fields: ['name', 'code'] },
|
||||||
conditions: [],
|
conditions: [],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
action: 'action-1',
|
action: 'action-1',
|
||||||
|
actionParameters: {},
|
||||||
subject: 'user',
|
subject: 'user',
|
||||||
properties: {
|
properties: {
|
||||||
fields: [
|
fields: [
|
||||||
@ -387,6 +394,7 @@ describe('Content-Type', () => {
|
|||||||
expect(res).toEqual([
|
expect(res).toEqual([
|
||||||
{
|
{
|
||||||
action: 'foo',
|
action: 'foo',
|
||||||
|
actionParameters: {},
|
||||||
subject: 'user',
|
subject: 'user',
|
||||||
properties: { fields: expectedFields },
|
properties: { fields: expectedFields },
|
||||||
conditions: [],
|
conditions: [],
|
||||||
|
@ -85,34 +85,40 @@ describe('Permission Service', () => {
|
|||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
action: 'action-1',
|
action: 'action-1',
|
||||||
|
actionParameters: {},
|
||||||
properties: { fields: ['name'] },
|
properties: { fields: ['name'] },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 2,
|
id: 2,
|
||||||
action: 'action-2',
|
action: 'action-2',
|
||||||
|
actionParameters: {},
|
||||||
properties: { fields: ['name'] },
|
properties: { fields: ['name'] },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 3,
|
id: 3,
|
||||||
action: 'action-3',
|
action: 'action-3',
|
||||||
|
actionParameters: {},
|
||||||
subject: 'country',
|
subject: 'country',
|
||||||
properties: { fields: ['name'] },
|
properties: { fields: ['name'] },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 4,
|
id: 4,
|
||||||
action: 'action-3',
|
action: 'action-3',
|
||||||
|
actionParameters: {},
|
||||||
subject: 'planet',
|
subject: 'planet',
|
||||||
properties: { fields: ['name'] },
|
properties: { fields: ['name'] },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 5,
|
id: 5,
|
||||||
action: 'action-1',
|
action: 'action-1',
|
||||||
|
actionParameters: {},
|
||||||
subject: 'planet',
|
subject: 'planet',
|
||||||
properties: { fields: ['name', 'description'] },
|
properties: { fields: ['name', 'description'] },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 6,
|
id: 6,
|
||||||
action: 'action-1',
|
action: 'action-1',
|
||||||
|
actionParameters: {},
|
||||||
subject: 'country',
|
subject: 'country',
|
||||||
properties: { fields: null },
|
properties: { fields: null },
|
||||||
},
|
},
|
||||||
|
@ -404,6 +404,7 @@ describe('Role', () => {
|
|||||||
const permissions = [
|
const permissions = [
|
||||||
{
|
{
|
||||||
action: 'action-1',
|
action: 'action-1',
|
||||||
|
actionParameters: {},
|
||||||
subject: 'country',
|
subject: 'country',
|
||||||
properties: { fields: ['name'] },
|
properties: { fields: ['name'] },
|
||||||
conditions: [],
|
conditions: [],
|
||||||
@ -413,36 +414,42 @@ describe('Role', () => {
|
|||||||
const defaultPermissions = [
|
const defaultPermissions = [
|
||||||
{
|
{
|
||||||
action: 'plugin::upload.read',
|
action: 'plugin::upload.read',
|
||||||
|
actionParameters: {},
|
||||||
conditions: ['admin::is-creator'],
|
conditions: ['admin::is-creator'],
|
||||||
properties: {},
|
properties: {},
|
||||||
subject: null,
|
subject: null,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
action: 'plugin::upload.configure-view',
|
action: 'plugin::upload.configure-view',
|
||||||
|
actionParameters: {},
|
||||||
conditions: [],
|
conditions: [],
|
||||||
properties: {},
|
properties: {},
|
||||||
subject: null,
|
subject: null,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
action: 'plugin::upload.assets.create',
|
action: 'plugin::upload.assets.create',
|
||||||
|
actionParameters: {},
|
||||||
conditions: [],
|
conditions: [],
|
||||||
properties: {},
|
properties: {},
|
||||||
subject: null,
|
subject: null,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
action: 'plugin::upload.assets.update',
|
action: 'plugin::upload.assets.update',
|
||||||
|
actionParameters: {},
|
||||||
conditions: ['admin::is-creator'],
|
conditions: ['admin::is-creator'],
|
||||||
properties: {},
|
properties: {},
|
||||||
subject: null,
|
subject: null,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
action: 'plugin::upload.assets.download',
|
action: 'plugin::upload.assets.download',
|
||||||
|
actionParameters: {},
|
||||||
conditions: [],
|
conditions: [],
|
||||||
properties: {},
|
properties: {},
|
||||||
subject: null,
|
subject: null,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
action: 'plugin::upload.assets.copy-link',
|
action: 'plugin::upload.assets.copy-link',
|
||||||
|
actionParameters: {},
|
||||||
conditions: [],
|
conditions: [],
|
||||||
properties: {},
|
properties: {},
|
||||||
subject: null,
|
subject: null,
|
||||||
@ -625,24 +632,28 @@ describe('Role', () => {
|
|||||||
const permissions = [
|
const permissions = [
|
||||||
{
|
{
|
||||||
action: 'action-1',
|
action: 'action-1',
|
||||||
|
actionParameters: {},
|
||||||
subject: 'country',
|
subject: 'country',
|
||||||
properties: { fields: ['name'] },
|
properties: { fields: ['name'] },
|
||||||
conditions: [],
|
conditions: [],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
action: 'action-test2',
|
action: 'action-test2',
|
||||||
|
actionParameters: {},
|
||||||
subject: 'test-subject1',
|
subject: 'test-subject1',
|
||||||
properties: {},
|
properties: {},
|
||||||
conditions: [],
|
conditions: [],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
action: 'action-test2',
|
action: 'action-test2',
|
||||||
|
actionParameters: {},
|
||||||
subject: 'test-subject2',
|
subject: 'test-subject2',
|
||||||
properties: {},
|
properties: {},
|
||||||
conditions: [],
|
conditions: [],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
action: 'action-test3',
|
action: 'action-test3',
|
||||||
|
actionParameters: {},
|
||||||
subject: null,
|
subject: null,
|
||||||
properties: {},
|
properties: {},
|
||||||
conditions: [],
|
conditions: [],
|
||||||
@ -759,11 +770,46 @@ describe('Role', () => {
|
|||||||
|
|
||||||
expect(createMany).toHaveBeenCalledTimes(1);
|
expect(createMany).toHaveBeenCalledTimes(1);
|
||||||
expect(createMany).toHaveBeenCalledWith([
|
expect(createMany).toHaveBeenCalledWith([
|
||||||
{ action: 'action-0', conditions: [], properties: {}, role: 1, subject: null },
|
{
|
||||||
{ action: 'action-1', conditions: [], properties: {}, role: 1, subject: null },
|
action: 'action-0',
|
||||||
{ action: 'action-2', conditions: [], properties: {}, role: 1, subject: null },
|
actionParameters: {},
|
||||||
{ action: 'action-3', conditions: [], properties: {}, role: 1, subject: null },
|
conditions: [],
|
||||||
{ action: 'action-4', conditions: ['cond'], properties: {}, role: 1, subject: null },
|
properties: {},
|
||||||
|
role: 1,
|
||||||
|
subject: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
action: 'action-1',
|
||||||
|
actionParameters: {},
|
||||||
|
conditions: [],
|
||||||
|
properties: {},
|
||||||
|
role: 1,
|
||||||
|
subject: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
action: 'action-2',
|
||||||
|
actionParameters: {},
|
||||||
|
conditions: [],
|
||||||
|
properties: {},
|
||||||
|
role: 1,
|
||||||
|
subject: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
action: 'action-3',
|
||||||
|
actionParameters: {},
|
||||||
|
conditions: [],
|
||||||
|
properties: {},
|
||||||
|
role: 1,
|
||||||
|
subject: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
action: 'action-4',
|
||||||
|
actionParameters: {},
|
||||||
|
conditions: ['cond'],
|
||||||
|
properties: {},
|
||||||
|
role: 1,
|
||||||
|
subject: null,
|
||||||
|
},
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -775,6 +821,7 @@ describe('Role', () => {
|
|||||||
const permissions = [
|
const permissions = [
|
||||||
{
|
{
|
||||||
action: 'someAction',
|
action: 'someAction',
|
||||||
|
actionParameters: {},
|
||||||
conditions: [],
|
conditions: [],
|
||||||
properties: { fields: null },
|
properties: { fields: null },
|
||||||
subject: null,
|
subject: null,
|
||||||
|
@ -24,7 +24,7 @@ const ACTIONS = {
|
|||||||
|
|
||||||
const sanitizeRole = omit(['users', 'permissions']);
|
const sanitizeRole = omit(['users', 'permissions']);
|
||||||
|
|
||||||
const COMPARABLE_FIELDS = ['conditions', 'properties', 'subject', 'action'];
|
const COMPARABLE_FIELDS = ['conditions', 'properties', 'subject', 'action', 'actionParameters'];
|
||||||
const pickComparableFields = pick(COMPARABLE_FIELDS);
|
const pickComparableFields = pick(COMPARABLE_FIELDS);
|
||||||
|
|
||||||
const jsonClean = (data) => JSON.parse(JSON.stringify(data));
|
const jsonClean = (data) => JSON.parse(JSON.stringify(data));
|
||||||
@ -368,17 +368,20 @@ const assignPermissions = async (roleId, permissions = []) => {
|
|||||||
return permissionsToReturn;
|
return permissionsToReturn;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const addPermissions = async (roleId, permissions) => {
|
const addPermissions = async (roleId, permissions) => {
|
||||||
const { conditionProvider, createMany } = getService('permission');
|
const { conditionProvider, createMany } = getService('permission');
|
||||||
const { sanitizeConditions } = permissionDomain;
|
const { sanitizeConditions } = permissionDomain;
|
||||||
|
|
||||||
const permissionsWithRole = permissions
|
const permissionsWithRole = permissions
|
||||||
.map(set('role', roleId))
|
.map(set('role', roleId))
|
||||||
.map(sanitizeConditions(conditionProvider));
|
.map(sanitizeConditions(conditionProvider))
|
||||||
|
.map(permissionDomain.create);
|
||||||
|
|
||||||
return createMany(permissionsWithRole);
|
return createMany(permissionsWithRole);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const isContentTypeAction = (action) => action.section === CONTENT_TYPE_SECTION;
|
const isContentTypeAction = (action) => action.section === CONTENT_TYPE_SECTION;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -17,7 +17,7 @@ const registerProviderActionSchema = yup
|
|||||||
(v) => `${v.path}: The id can only contain lowercase letters, dots and hyphens.`
|
(v) => `${v.path}: The id can only contain lowercase letters, dots and hyphens.`
|
||||||
)
|
)
|
||||||
.required(),
|
.required(),
|
||||||
section: yup.string().oneOf(['contentTypes', 'plugins', 'settings']).required(),
|
section: yup.string().oneOf(['contentTypes', 'plugins', 'settings', 'internal']).required(),
|
||||||
pluginName: yup.mixed().when('section', {
|
pluginName: yup.mixed().when('section', {
|
||||||
is: 'plugins',
|
is: 'plugins',
|
||||||
then: validators.isAPluginName.required(),
|
then: validators.isAPluginName.required(),
|
||||||
|
@ -38,6 +38,7 @@
|
|||||||
"@casl/ability": "5.4.4",
|
"@casl/ability": "5.4.4",
|
||||||
"@strapi/utils": "4.12.1",
|
"@strapi/utils": "4.12.1",
|
||||||
"lodash": "4.17.21",
|
"lodash": "4.17.21",
|
||||||
|
"qs": "6.11.1",
|
||||||
"sift": "16.0.1"
|
"sift": "16.0.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
@ -4,8 +4,14 @@ const PERMISSION_FIELDS = ['action', 'subject', 'properties', 'conditions'] as c
|
|||||||
|
|
||||||
const sanitizePermissionFields = _.pick(PERMISSION_FIELDS);
|
const sanitizePermissionFields = _.pick(PERMISSION_FIELDS);
|
||||||
|
|
||||||
|
export interface ParametrizedAction {
|
||||||
|
name: string;
|
||||||
|
params: Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
export interface Permission {
|
export interface Permission {
|
||||||
action: string;
|
action: string;
|
||||||
|
actionParameters?: Record<string, unknown>;
|
||||||
subject?: string | object | null;
|
subject?: string | object | null;
|
||||||
properties?: object;
|
properties?: object;
|
||||||
conditions?: string[];
|
conditions?: string[];
|
||||||
|
@ -1,9 +1,16 @@
|
|||||||
import * as sift from 'sift';
|
import * as sift from 'sift';
|
||||||
|
import qs from 'qs';
|
||||||
import { AbilityBuilder, Ability, Subject } from '@casl/ability';
|
import { AbilityBuilder, Ability, Subject } from '@casl/ability';
|
||||||
import { pick, isNil, isObject } from 'lodash/fp';
|
import { pick, isNil, isObject } from 'lodash/fp';
|
||||||
|
|
||||||
|
|
||||||
|
export interface ParametrizedAction {
|
||||||
|
name: string;
|
||||||
|
params: Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
export interface PermissionRule {
|
export interface PermissionRule {
|
||||||
action: string;
|
action: string | ParametrizedAction;
|
||||||
subject?: Subject | null;
|
subject?: Subject | null;
|
||||||
properties?: {
|
properties?: {
|
||||||
fields?: string[];
|
fields?: string[];
|
||||||
@ -13,6 +20,7 @@ export interface PermissionRule {
|
|||||||
|
|
||||||
export interface CustomAbilityBuilder {
|
export interface CustomAbilityBuilder {
|
||||||
can(permission: PermissionRule): ReturnType<AbilityBuilder<Ability>['can']>;
|
can(permission: PermissionRule): ReturnType<AbilityBuilder<Ability>['can']>;
|
||||||
|
buildParametrizedAction: (parametrizedAction: ParametrizedAction) => string;
|
||||||
build(): Ability;
|
build(): Ability;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -37,6 +45,10 @@ const conditionsMatcher = (conditions: unknown) => {
|
|||||||
return sift.createQueryTester(conditions, { operations });
|
return sift.createQueryTester(conditions, { operations });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const buildParametrizedAction = ({ name, params }: ParametrizedAction) => {
|
||||||
|
return `${name}?${qs.stringify(params)}`;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Casl Ability Builder.
|
* Casl Ability Builder.
|
||||||
*/
|
*/
|
||||||
@ -48,16 +60,36 @@ export const caslAbilityBuilder = (): CustomAbilityBuilder => {
|
|||||||
const { action, subject, properties = {}, condition } = permission;
|
const { action, subject, properties = {}, condition } = permission;
|
||||||
const { fields } = properties;
|
const { fields } = properties;
|
||||||
|
|
||||||
|
const caslAction = (typeof action === "string") ? action : buildParametrizedAction(action);
|
||||||
|
|
||||||
return can(
|
return can(
|
||||||
action,
|
caslAction,
|
||||||
isNil(subject) ? 'all' : subject,
|
isNil(subject) ? 'all' : subject,
|
||||||
fields,
|
fields,
|
||||||
isObject(condition) ? condition : undefined
|
isObject(condition) ? condition : undefined
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
buildParametrizedAction({ name, params }: ParametrizedAction) {
|
||||||
|
return `${name}?${qs.stringify(params)}`;
|
||||||
|
},
|
||||||
|
|
||||||
build() {
|
build() {
|
||||||
return build({ conditionsMatcher });
|
const ability = build({ conditionsMatcher });
|
||||||
|
|
||||||
|
function decorateCan(originalCan: Ability['can']) {
|
||||||
|
return function (...args: Parameters<Ability['can']>) {
|
||||||
|
const [action, ...rest] = args;
|
||||||
|
const caslAction = (typeof action === "string") ? action : buildParametrizedAction(action);
|
||||||
|
|
||||||
|
// Call the original `can` method
|
||||||
|
return originalCan.apply(ability, [caslAction, ...rest]);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
ability.can = decorateCan(ability.can);
|
||||||
|
return ability;
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
...rest,
|
...rest,
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import _ from 'lodash/fp';
|
import _ from 'lodash/fp';
|
||||||
|
import qs from 'qs';
|
||||||
import { Ability } from '@casl/ability';
|
import { Ability } from '@casl/ability';
|
||||||
import { providerFactory } from '@strapi/utils';
|
import { providerFactory } from '@strapi/utils';
|
||||||
|
|
||||||
@ -93,7 +94,19 @@ const newEngine = (params: EngineParams): Engine => {
|
|||||||
|
|
||||||
await state.hooks['before-evaluate.permission'].call(createBeforeEvaluateContext(permission));
|
await state.hooks['before-evaluate.permission'].call(createBeforeEvaluateContext(permission));
|
||||||
|
|
||||||
const { action, subject, properties, conditions = [] } = permission;
|
const {
|
||||||
|
action: actionName,
|
||||||
|
subject,
|
||||||
|
properties,
|
||||||
|
conditions = [],
|
||||||
|
actionParameters = {},
|
||||||
|
} = permission;
|
||||||
|
|
||||||
|
let action = actionName;
|
||||||
|
|
||||||
|
if (actionParameters && Object.keys(actionParameters).length > 0) {
|
||||||
|
action = `${actionName}?${qs.stringify(actionParameters)}`;
|
||||||
|
}
|
||||||
|
|
||||||
if (conditions.length === 0) {
|
if (conditions.length === 0) {
|
||||||
return register({ action, subject, properties });
|
return register({ action, subject, properties });
|
||||||
|
@ -39,7 +39,10 @@ const TableBody = ({ sortedRoles, canDelete, permissions, setRoleToDelete, onDel
|
|||||||
<Td width="30%">
|
<Td width="30%">
|
||||||
<Typography>
|
<Typography>
|
||||||
{formatMessage(
|
{formatMessage(
|
||||||
{ id: 'Roles.RoleRow.user-count', defaultMessage: '{number, plural, =0 {# user} one {# user} other {# users}}' },
|
{
|
||||||
|
id: 'Roles.RoleRow.user-count',
|
||||||
|
defaultMessage: '{number, plural, =0 {# user} one {# user} other {# users}}',
|
||||||
|
},
|
||||||
{ number: role.nb_users }
|
{ number: role.nb_users }
|
||||||
)}
|
)}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
@ -7903,6 +7903,7 @@ __metadata:
|
|||||||
"@strapi/utils": 4.12.1
|
"@strapi/utils": 4.12.1
|
||||||
eslint-config-custom: 4.12.1
|
eslint-config-custom: 4.12.1
|
||||||
lodash: 4.17.21
|
lodash: 4.17.21
|
||||||
|
qs: 6.11.1
|
||||||
sift: 16.0.1
|
sift: 16.0.1
|
||||||
tsconfig: 4.12.1
|
tsconfig: 4.12.1
|
||||||
languageName: unknown
|
languageName: unknown
|
||||||
|
Loading…
x
Reference in New Issue
Block a user