Refactor to check permissions on entities before passing to updateMany

This commit is contained in:
Mark Kaylor 2023-05-04 10:38:09 +02:00
parent 119b88a1b1
commit f2e769cd69
3 changed files with 87 additions and 69 deletions

View File

@ -184,7 +184,7 @@ module.exports = {
async bulkPublish(ctx) { async bulkPublish(ctx) {
const { userAbility } = ctx.state; const { userAbility } = ctx.state;
const { model } = ctx.params; const { model } = ctx.params;
const { query, body } = ctx.request; const { body } = ctx.request;
const { ids } = body; const { ids } = body;
await validateBulkActionInput(body); await validateBulkActionInput(body);
@ -196,24 +196,27 @@ module.exports = {
return ctx.forbidden(); return ctx.forbidden();
} }
const permissionQuery = await permissionChecker.sanitizedQuery.publish(query); const entityPromises = ids.map((id) => entityManager.findOneWithCreatorRoles(id, model));
const entities = await Promise.all(entityPromises);
const idsWhereClause = { id: { $in: ids } }; for (const entity of entities) {
const params = { if (!entity) {
...permissionQuery, return ctx.notFound();
filters: { }
$and: [idsWhereClause].concat(permissionQuery.filters || []),
},
};
const { count } = await entityManager.publishMany(params, model); if (permissionChecker.cannot.publish(entity)) {
return ctx.forbidden();
}
}
const { count } = await entityManager.publishMany(entities, model);
ctx.body = { count }; ctx.body = { count };
}, },
async bulkUnpublish(ctx) { async bulkUnpublish(ctx) {
const { userAbility } = ctx.state; const { userAbility } = ctx.state;
const { model } = ctx.params; const { model } = ctx.params;
const { query, body } = ctx.request; const { body } = ctx.request;
const { ids } = body; const { ids } = body;
await validateBulkActionInput(body); await validateBulkActionInput(body);
@ -225,17 +228,20 @@ module.exports = {
return ctx.forbidden(); return ctx.forbidden();
} }
const permissionQuery = await permissionChecker.sanitizedQuery.unpublish(query); const entityPromises = ids.map((id) => entityManager.findOneWithCreatorRoles(id, model));
const entities = await Promise.all(entityPromises);
const idsWhereClause = { id: { $in: ids } }; for (const entity of entities) {
const params = { if (!entity) {
...permissionQuery, return ctx.notFound();
filters: { }
$and: [idsWhereClause].concat(permissionQuery.filters || []),
},
};
const { count } = await entityManager.unpublishMany(params, model); if (permissionChecker.cannot.publish(entity)) {
return ctx.forbidden();
}
}
const { count } = await entityManager.unpublishMany(entities, model);
ctx.body = { count }; ctx.body = { count };
}, },

View File

@ -17,11 +17,11 @@ describe('Content-Manager', () => {
beforeEach(() => { beforeEach(() => {
global.strapi = { global.strapi = {
entityService: { entityService: {
findMany: jest.fn().mockResolvedValue([{ id: 1 }, { id: 2 }]),
update: jest.fn().mockReturnValue({ id: 1, publishedAt: new Date() }), update: jest.fn().mockReturnValue({ id: 1, publishedAt: new Date() }),
}, },
db: { db: {
query: jest.fn(() => ({ query: jest.fn(() => ({
findMany: jest.fn().mockResolvedValue([{ id: 1 }, { id: 2 }]),
updateMany: queryUpdateMock, updateMany: queryUpdateMock,
})), })),
}, },
@ -55,13 +55,16 @@ describe('Content-Manager', () => {
test('Publish many content-types', async () => { test('Publish many content-types', async () => {
const uid = 'api::test.test'; const uid = 'api::test.test';
const params = { filters: { $and: [1, 2] } }; const entities = [
{ id: 1, publishedAt: null },
{ id: 2, publishedAt: null },
];
await entityManager.publishMany(params, uid); await entityManager.publishMany(entities, uid);
expect(strapi.db.query().updateMany).toBeCalledWith({ expect(strapi.db.query().updateMany).toHaveBeenCalledWith({
where: { where: {
$and: [1, 2], id: { $in: [1, 2] },
}, },
data: { publishedAt: expect.any(Date) }, data: { publishedAt: expect.any(Date) },
}); });
@ -74,11 +77,11 @@ describe('Content-Manager', () => {
global.strapi = { global.strapi = {
db: { db: {
query: jest.fn(() => ({ query: jest.fn(() => ({
findMany: jest.fn().mockResolvedValue([{ id: 1 }, { id: 2 }]),
updateMany: queryUpdateMock, updateMany: queryUpdateMock,
})), })),
}, },
entityService: { entityService: {
findMany: jest.fn().mockResolvedValue([{ id: 1 }, { id: 2 }]),
update: jest.fn().mockReturnValue({ id: 1, publishedAt: null }), update: jest.fn().mockReturnValue({ id: 1, publishedAt: null }),
}, },
eventHub: { emit: jest.fn(), sanitizeEntity: (entity) => entity }, eventHub: { emit: jest.fn(), sanitizeEntity: (entity) => entity },
@ -107,13 +110,16 @@ describe('Content-Manager', () => {
test('Unpublish many content-types', async () => { test('Unpublish many content-types', async () => {
const uid = 'api::test.test'; const uid = 'api::test.test';
const params = { filters: { $and: [1, 2] } }; const entities = [
{ id: 1, publishedAt: new Date() },
{ id: 2, publishedAt: new Date() },
];
await entityManager.unpublishMany(params, uid); await entityManager.unpublishMany(entities, uid);
expect(strapi.db.query().updateMany).toBeCalledWith({ expect(strapi.db.query().updateMany).toHaveBeenCalledWith({
where: { where: {
$and: [1, 2], id: { $in: [1, 2] },
}, },
data: { publishedAt: null }, data: { publishedAt: null },
}); });

View File

@ -3,7 +3,6 @@
const { assoc, has, prop, omit } = require('lodash/fp'); const { assoc, has, prop, omit } = require('lodash/fp');
const strapiUtils = require('@strapi/utils'); const strapiUtils = require('@strapi/utils');
const { mapAsync } = require('@strapi/utils'); const { mapAsync } = require('@strapi/utils');
const { transformParamsToQuery } = require('@strapi/utils').convertQueryParams;
const { ApplicationError } = require('@strapi/utils').errors; const { ApplicationError } = require('@strapi/utils').errors;
const { getDeepPopulate, getDeepPopulateDraftCount } = require('./utils/populate'); const { getDeepPopulate, getDeepPopulateDraftCount } = require('./utils/populate');
const { getDeepRelationsCount } = require('./utils/count'); const { getDeepRelationsCount } = require('./utils/count');
@ -267,41 +266,44 @@ module.exports = ({ strapi }) => ({
return mappedEntity; return mappedEntity;
}, },
async publishMany(opts, uid) { async publishMany(entities, uid) {
const params = { if (!entities.length) {
...opts,
data: {
[PUBLISHED_AT_ATTRIBUTE]: new Date(),
},
};
const query = transformParamsToQuery(uid, params);
const entitiesToUpdate = await strapi.db.query(uid).findMany(query);
// No entities to update, return early
if (!entitiesToUpdate.length) {
return null; return null;
} }
// Validate entities before publishing, throw if invalid // Validate entities before publishing, throw if invalid
await Promise.all( await Promise.all(
entitiesToUpdate.map((entityToUpdate) => entities.map((entityToUpdate) => {
strapi.entityValidator.validateEntityCreation( if (entityToUpdate[PUBLISHED_AT_ATTRIBUTE]) {
throw new ApplicationError('already.published');
}
return strapi.entityValidator.validateEntityCreation(
strapi.getModel(uid), strapi.getModel(uid),
entityToUpdate, entityToUpdate,
{ {
isDraft: true, isDraft: true,
}, },
entityToUpdate entityToUpdate
) );
) })
); );
// Everything is valid, publish
const publishedEntitiesCount = await strapi.db
.query(uid)
.updateMany({ ...query, data: params.data });
const where = { id: { $in: entities.map((entity) => entity.id) } };
const data = {
[PUBLISHED_AT_ATTRIBUTE]: new Date(),
};
const populate = isRelationsPopulateEnabled(uid)
? getDeepPopulate(uid, {})
: getDeepPopulate(uid, { countMany: true, countOne: true });
// Everything is valid, publish
const publishedEntitiesCount = await strapi.db.query(uid).updateMany({
where,
data,
});
// Get the updated entities since updateMany only returns the count // Get the updated entities since updateMany only returns the count
const publishedEntities = await strapi.db.query(uid).findMany(query); const publishedEntities = await strapi.entityService.findMany(uid, { where, populate });
// Emit the publish event for all updated entities // Emit the publish event for all updated entities
await Promise.all(publishedEntities.map((entity) => emitEvent(ENTRY_PUBLISH, entity, uid))); await Promise.all(publishedEntities.map((entity) => emitEvent(ENTRY_PUBLISH, entity, uid)));
@ -309,28 +311,32 @@ module.exports = ({ strapi }) => ({
return publishedEntitiesCount; return publishedEntitiesCount;
}, },
async unpublishMany(opts, uid) { async unpublishMany(entities, uid) {
const params = { if (!entities.length) {
...opts,
data: {
[PUBLISHED_AT_ATTRIBUTE]: null,
},
};
const query = transformParamsToQuery(uid, params);
const entitiesToUpdate = await strapi.db.query(uid).findMany(query);
// No entities to update, return early
if (!entitiesToUpdate.length) {
return null; return null;
} }
// No need to validate, unpublish entities.forEach((entity) => {
const unpublishedEntitiesCount = await strapi.db if (!entity[PUBLISHED_AT_ATTRIBUTE]) {
.query(uid) throw new ApplicationError('already.draft');
.updateMany({ ...query, data: params.data }); }
});
const where = { id: { $in: entities.map((entity) => entity.id) } };
const data = {
[PUBLISHED_AT_ATTRIBUTE]: null,
};
const populate = isRelationsPopulateEnabled(uid)
? getDeepPopulate(uid, {})
: getDeepPopulate(uid, { countMany: true, countOne: true });
// No need to validate, unpublish
const unpublishedEntitiesCount = await strapi.db.query(uid).updateMany({
where,
data,
});
// Get the updated entities since updateMany only returns the count // Get the updated entities since updateMany only returns the count
const unpublishedEntities = await strapi.db.query(uid).findMany(query); const unpublishedEntities = await strapi.entityService.findMany(uid, { where, populate });
// Emit the unpublish event for all updated entities // Emit the unpublish event for all updated entities
await Promise.all(unpublishedEntities.map((entity) => emitEvent(ENTRY_UNPUBLISH, entity, uid))); await Promise.all(unpublishedEntities.map((entity) => emitEvent(ENTRY_UNPUBLISH, entity, uid)));