feat: Private s3 buckets (#20861)

* feat: sign media files

* fix: bootstrap test
This commit is contained in:
Marc Roig 2024-07-23 09:40:57 +02:00 committed by GitHub
parent 2403a57adf
commit ae299ee651
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 52 additions and 146 deletions

View File

@ -56,7 +56,7 @@ describe('Upload plugin bootstrap function', () => {
registerCron() {},
},
extensions: {
contentManager: { entityManager: { addSignedFileUrlsToAdmin: jest.fn() } },
signFileUrlsOnDocumentService: jest.fn(),
},
},
},

View File

@ -41,11 +41,7 @@ export async function bootstrap({ strapi }: { strapi: Core.Strapi }) {
await getService('weeklyMetrics').registerCron();
getService('metrics').sendUploadPluginMetrics();
if (strapi.config.get('plugin::upload.signAdminURLsOnly', false)) {
getService('extensions').contentManager.entityManager.addSignedFileUrlsToAdmin();
} else {
getService('extensions').core.entityService.addSignedFileUrlsToEntityService();
}
getService('extensions').signFileUrlsOnDocumentService();
}
const registerWebhookEvents = async () =>

View File

@ -1,101 +0,0 @@
import { async, traverseEntity } from '@strapi/utils';
import type { Schema, UID } from '@strapi/types';
import { getService } from '../../../utils';
import type { File } from '../../../types';
type SignEntityMediaVisitor = (
args: {
key: string;
value: unknown;
attribute: Schema.Attribute.AnyAttribute;
},
utils: {
set: (key: string, value: unknown) => void;
}
) => Promise<void>;
function isFile(value: unknown, attribute: Schema.Attribute.AnyAttribute): value is File {
if (!value || attribute.type !== 'media') {
return false;
}
return true;
}
const signEntityMediaVisitor: SignEntityMediaVisitor = async (
{ key, value, attribute },
{ set }
) => {
const { signFileUrls } = getService('file');
if (!attribute) {
return;
}
if (attribute.type !== 'media') {
return;
}
if (!isFile(value, attribute)) {
return;
}
// If the attribute is repeatable sign each file
if (attribute.multiple) {
const signedFiles = await async.map(value, signFileUrls);
set(key, signedFiles);
return;
}
// If the attribute is not repeatable only sign a single file
const signedFile = await signFileUrls(value);
set(key, signedFile);
};
/**
*
* Iterate through an entity manager result
* Check which modelAttributes are media and pre sign the image URLs
* if they are from the current upload provider
*
* @param {Object} entity
* @param {Object} modelAttributes
* @returns
*/
const signEntityMedia = async (entity: any, uid: UID.Schema) => {
const model = strapi.getModel(uid);
return traverseEntity(
// @ts-expect-error - FIXME: fix traverseEntity types
signEntityMediaVisitor,
{ schema: model, getModel: strapi.getModel.bind(strapi) },
entity
);
};
const addSignedFileUrlsToAdmin = async () => {
// FIXME: This is not working. Replace with doc service middlewares or wrap the CM somewhow
// const { provider } = strapi.plugins.upload;
// const isPrivate = await provider.isPrivate();
// // We only need to sign the file urls if the provider is private
// if (!isPrivate) {
// return;
// }
// strapi.get('services').extend('plugin::content-manager.entity-manager', (entityManager: any) => {
// /**
// * Map entity manager responses to sign private media URLs
// * @param {Object} entity
// * @param {string} uid
// * @returns
// */
// const mapEntity = async (entity, uid) => {
// const mappedEntity = await entityManager.mapEntity(entity, uid);
// return signEntityMedia(mappedEntity, uid);
// };
// return { ...entityManager, mapEntity };
// });
};
export { addSignedFileUrlsToAdmin, signEntityMedia };

View File

@ -1,3 +0,0 @@
import { addSignedFileUrlsToAdmin } from './entity-manager';
export const entityManager = { addSignedFileUrlsToAdmin };

View File

@ -1,29 +0,0 @@
import { signEntityMedia } from '../utils';
const addSignedFileUrlsToEntityService = async () => {
// FIXME: This is not working. Replace with doc service middlewares
// const { provider } = strapi.plugins.upload;
// const isPrivate = await provider.isPrivate();
// // We only need to sign the file urls if the provider is private
// if (!isPrivate) {
// return;
// }
// const decorator = (service) => ({
// async wrapResult(result, options) {
// const wrappedResult = await service.wrapResult.call(this, result, options);
// // Load returns only the attribute of the entity, not the entity itself,
// if (options.action === 'load') {
// const entity = { [options.field]: result };
// const signedEntity = await signEntityMedia(entity, options.uid);
// return signedEntity[options.field];
// }
// if (Array.isArray(wrappedResult)) {
// return Promise.all(wrappedResult.map((entity) => signEntityMedia(entity, options.uid)));
// }
// return signEntityMedia(wrappedResult, options.uid);
// },
// });
// strapi.entityService.decorate(decorator);
};
export { addSignedFileUrlsToEntityService, signEntityMedia };

View File

@ -1,3 +0,0 @@
import { addSignedFileUrlsToEntityService } from './entity-service';
export const entityService = { addSignedFileUrlsToEntityService };

View File

@ -1,7 +1,53 @@
import * as contentManager from './content-manager';
import * as core from './core';
import { async } from '@strapi/utils';
import { signEntityMedia } from './utils';
const signFileUrlsOnDocumentService = async () => {
const { provider } = strapi.plugins.upload;
const isPrivate = await provider.isPrivate();
// We only need to sign the file urls if the provider is private
if (!isPrivate) {
return;
}
strapi.documents.use(async (ctx, next) => {
const uid = ctx.uid;
const result: any = await next();
if (ctx.action === 'findMany') {
// Shape: [ entry ]
return async.map(result, (entry: any) => signEntityMedia(entry, uid));
}
if (
ctx.action === 'findFirst' ||
ctx.action === 'findOne' ||
ctx.action === 'create' ||
ctx.action === 'update'
) {
// Shape: entry
return signEntityMedia(result, uid);
}
if (
ctx.action === 'delete' ||
ctx.action === 'clone' ||
ctx.action === 'publish' ||
ctx.action === 'unpublish' ||
ctx.action === 'discardDraft'
) {
// Shape: { entries: [ entry ] }
// ...
return {
...result,
entries: await async.map(result.entries, (entry: any) => signEntityMedia(entry, uid)),
};
}
return result;
});
};
export default {
contentManager,
core,
signFileUrlsOnDocumentService,
};