Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -156,8 +156,7 @@ public void eventHandlers(CdsRuntimeConfigurer configurer) {
boolean hasApplicationServices =
serviceCatalog.getServices(ApplicationService.class).findFirst().isPresent();
if (hasApplicationServices) {
configurer.eventHandler(
new CreateAttachmentsHandler(eventFactory, storage, defaultMaxSize, runtime));
configurer.eventHandler(new CreateAttachmentsHandler(eventFactory, storage, defaultMaxSize));
configurer.eventHandler(
new UpdateAttachmentsHandler(
eventFactory, attachmentsReader, outboxedAttachmentService, storage, defaultMaxSize));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@
import com.sap.cds.services.handler.annotations.HandlerOrder;
import com.sap.cds.services.handler.annotations.On;
import com.sap.cds.services.handler.annotations.ServiceName;
import com.sap.cds.services.runtime.CdsRuntime;
import com.sap.cds.services.utils.OrderConstants;
import java.util.ArrayList;
import java.util.List;
Expand All @@ -42,17 +41,14 @@ public class CreateAttachmentsHandler implements EventHandler {
private final ModifyAttachmentEventFactory eventFactory;
private final ThreadDataStorageReader storageReader;
private final String defaultMaxSize;
private final CdsRuntime cdsRuntime;

public CreateAttachmentsHandler(
ModifyAttachmentEventFactory eventFactory,
ThreadDataStorageReader storageReader,
String defaultMaxSize,
CdsRuntime cdsRuntime) {
String defaultMaxSize) {
this.eventFactory = eventFactory;
this.storageReader = storageReader;
this.defaultMaxSize = defaultMaxSize;
this.cdsRuntime = cdsRuntime;
}

@Before
Expand All @@ -69,7 +65,7 @@ void processBeforeForDraft(CdsCreateEventContext context, List<CdsData> data) {
@HandlerOrder(HandlerOrder.BEFORE)
void processBeforeForMetadata(EventContext context, List<CdsData> data) {
CdsEntity target = context.getTarget();
AttachmentValidationHelper.validateMediaAttachments(target, data, cdsRuntime);
AttachmentValidationHelper.validateMediaAttachments(target, data, context.getModel());
}

@Before
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import com.sap.cds.feature.attachments.handler.applicationservice.helper.ModifyApplicationHandlerHelper;
import com.sap.cds.feature.attachments.handler.applicationservice.helper.ReadonlyDataContextEnhancer;
import com.sap.cds.feature.attachments.handler.applicationservice.helper.ThreadDataStorageReader;
import com.sap.cds.feature.attachments.handler.applicationservice.helper.mimeTypeValidation.AttachmentValidationHelper;
import com.sap.cds.feature.attachments.handler.applicationservice.modifyevents.ModifyAttachmentEventFactory;
import com.sap.cds.feature.attachments.handler.common.ApplicationHandlerHelper;
import com.sap.cds.feature.attachments.handler.common.AttachmentsReader;
Expand Down Expand Up @@ -68,6 +69,16 @@ void processBeforeForDraft(CdsUpdateEventContext context, List<CdsData> data) {
context.getTarget(), data, storageReader.get());
}

@Before
@HandlerOrder(HandlerOrder.BEFORE)
void processBeforeForMetadata(CdsUpdateEventContext context, List<CdsData> data) {
// Enforce @Core.AcceptableMediaTypes on updates as well, so replacement content or a
// metadata-only change (e.g. mimeType/fileName) cannot bypass the media type policy that is
// applied on create.
AttachmentValidationHelper.validateMediaAttachments(
context.getTarget(), data, context.getModel());
}

@Before
@HandlerOrder(HandlerOrder.LATE)
void processBefore(CdsUpdateEventContext context, List<CdsData> data) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
import com.sap.cds.reflect.CdsModel;
import com.sap.cds.services.ErrorStatuses;
import com.sap.cds.services.ServiceException;
import com.sap.cds.services.runtime.CdsRuntime;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
Expand All @@ -29,14 +28,14 @@ static void setCascader(AssociationCascader testCascader) {
*
* @param entity the {@link CdsEntity entity} type of the given data
* @param data the list of {@link CdsData} to process
* @param cdsModel the (tenant-specific) {@link CdsModel} to resolve media entities against
* @throws ServiceException if the media type of the attachment is not acceptable
*/
public static void validateMediaAttachments(
CdsEntity entity, List<CdsData> data, CdsRuntime cdsRuntime) {
CdsEntity entity, List<? extends CdsData> data, CdsModel cdsModel) {
if (entity == null) {
return;
}
CdsModel cdsModel = cdsRuntime.getCdsModel();

List<String> mediaEntityNames =
ApplicationHandlerHelper.isMediaEntity(entity)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import com.sap.cds.Result;
import com.sap.cds.feature.attachments.generated.cds4j.sap.attachments.Attachments;
import com.sap.cds.feature.attachments.handler.applicationservice.helper.ModifyApplicationHandlerHelper;
import com.sap.cds.feature.attachments.handler.applicationservice.helper.mimeTypeValidation.AttachmentValidationHelper;
import com.sap.cds.feature.attachments.handler.applicationservice.modifyevents.ModifyAttachmentEventFactory;
import com.sap.cds.feature.attachments.handler.common.ApplicationHandlerHelper;
import com.sap.cds.ql.Select;
Expand Down Expand Up @@ -55,6 +56,11 @@ void processBeforeDraftPatch(DraftPatchEventContext context, List<? extends CdsD
logger.debug(
"Processing before {} event for entity {}", context.getEvent(), context.getTarget());

// Enforce @Core.AcceptableMediaTypes on draft patches as well, so replacement content or
// metadata cannot bypass the media type policy that is applied on create/activate.
AttachmentValidationHelper.validateMediaAttachments(
context.getTarget(), data, context.getModel());

Converter converter =
(path, element, value) -> {
CdsEntity draftEntity = DraftUtils.getDraftEntity(path.target().entity());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
import com.sap.cds.feature.attachments.handler.applicationservice.readhelper.CountingInputStream;
import com.sap.cds.feature.attachments.handler.helper.RuntimeHelper;
import com.sap.cds.reflect.CdsEntity;
import com.sap.cds.reflect.CdsModel;
import com.sap.cds.services.ErrorStatuses;
import com.sap.cds.services.EventContext;
import com.sap.cds.services.ServiceException;
Expand Down Expand Up @@ -75,10 +76,7 @@ void setup() {
storageReader = mock(ThreadDataStorageReader.class);
cut =
new CreateAttachmentsHandler(
eventFactory,
storageReader,
ModifyApplicationHandlerHelper.DEFAULT_SIZE_WITH_SCANNER,
runtime);
eventFactory, storageReader, ModifyApplicationHandlerHelper.DEFAULT_SIZE_WITH_SCANNER);

createContext = mock(CdsCreateEventContext.class);
event = mock(ModifyAttachmentEvent.class);
Expand Down Expand Up @@ -355,19 +353,20 @@ void processBeforeForMetadata_executesValidation() {
EventContext context = mock(EventContext.class);
CdsEntity entity = mock(CdsEntity.class);
List<CdsData> data = List.of(mock(CdsData.class));
CdsModel model = runtime.getCdsModel();
when(context.getTarget()).thenReturn(entity);
when(context.getModel()).thenReturn(model);

try (MockedStatic<AttachmentValidationHelper> helper =
mockStatic(AttachmentValidationHelper.class)) {
helper
.when(() -> AttachmentValidationHelper.validateMediaAttachments(entity, data, runtime))
.when(() -> AttachmentValidationHelper.validateMediaAttachments(entity, data, model))
.thenAnswer(invocation -> null);
// when
new CreateAttachmentsHandler(eventFactory, storageReader, "400MB", runtime)
new CreateAttachmentsHandler(eventFactory, storageReader, "400MB")
.processBeforeForMetadata(context, data);
// then
helper.verify(
() -> AttachmentValidationHelper.validateMediaAttachments(entity, data, runtime));
helper.verify(() -> AttachmentValidationHelper.validateMediaAttachments(entity, data, model));
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
import com.sap.cds.reflect.CdsEntity;
import com.sap.cds.reflect.CdsModel;
import com.sap.cds.services.ServiceException;
import com.sap.cds.services.runtime.CdsRuntime;
import java.util.List;
import java.util.Map;
import java.util.Optional;
Expand Down Expand Up @@ -47,23 +46,21 @@ void doesNothing_whenEntityNotFoundInModel() {
CdsModel model = mock(CdsModel.class);
when(model.findEntity("Entity")).thenReturn(Optional.empty());

CdsRuntime runtime = mockRuntime(model);

try (MockedStatic<ApplicationHandlerHelper> helper =
mockStatic(ApplicationHandlerHelper.class)) {
helper.when(() -> ApplicationHandlerHelper.isMediaEntity(entity)).thenReturn(false);

setupMockCascader(entity, model, false);

assertDoesNotThrow(
() -> AttachmentValidationHelper.validateMediaAttachments(entity, List.of(), runtime));
() -> AttachmentValidationHelper.validateMediaAttachments(entity, List.of(), model));
}
}

@Test
void doesNothing_whenNoEntityHasAcceptableMediaTypesAnnotation() {
CdsEntity entity = mockEntity("Entity");
CdsRuntime runtime = mockRuntime(entity);
CdsModel model = mockModel(entity);

try (MockedStatic<ApplicationHandlerHelper> helper =
mockStatic(ApplicationHandlerHelper.class);
Expand All @@ -83,7 +80,7 @@ void doesNothing_whenNoEntityHasAcceptableMediaTypesAnnotation() {

// Verify that AttachmentDataExtractor is never called (early return)
assertDoesNotThrow(
() -> AttachmentValidationHelper.validateMediaAttachments(entity, List.of(), runtime));
() -> AttachmentValidationHelper.validateMediaAttachments(entity, List.of(), model));

extractor.verifyNoInteractions();
}
Expand All @@ -100,7 +97,7 @@ void doesNotThrow_whenNoFiles() {
MockedStatic<MediaTypeResolver> resolver = mockStatic(MediaTypeResolver.class);
MockedStatic<AttachmentDataExtractor> extractor =
mockStatic(AttachmentDataExtractor.class)) {
CdsRuntime runtime = mockRuntime(entity);
CdsModel model = mockModel(entity);
helper.when(() -> ApplicationHandlerHelper.isMediaEntity(entity)).thenReturn(true);

resolver
Expand All @@ -116,7 +113,7 @@ void doesNotThrow_whenNoFiles() {
.thenReturn(null);

assertDoesNotThrow(
() -> AttachmentValidationHelper.validateMediaAttachments(entity, List.of(), runtime));
() -> AttachmentValidationHelper.validateMediaAttachments(entity, List.of(), model));
}
}

Expand All @@ -125,7 +122,7 @@ void doesNotThrow_whenNoFiles() {
void doesNotThrow_whenFilesAreValid(boolean isMediaEntity) {

CdsEntity entity = mockEntity("Entity");
CdsRuntime runtime = mockRuntime(entity);
CdsModel model = mockModel(entity);

Map<String, List<String>> allowed = Map.of(ATTACHMENTS_ENTITY, List.of("image/png"));
Map<String, Set<String>> files = Map.of(ATTACHMENTS_ENTITY, Set.of("file.png"));
Expand All @@ -137,7 +134,7 @@ void doesNotThrow_whenFilesAreValid(boolean isMediaEntity) {
mockStatic(AttachmentDataExtractor.class)) {

helper.when(() -> ApplicationHandlerHelper.isMediaEntity(entity)).thenReturn(isMediaEntity);
setupMockCascader(entity, runtime.getCdsModel(), !isMediaEntity);
setupMockCascader(entity, model, !isMediaEntity);

resolver
.when(
Expand All @@ -152,7 +149,7 @@ void doesNotThrow_whenFilesAreValid(boolean isMediaEntity) {
.thenReturn(files);

assertDoesNotThrow(
() -> AttachmentValidationHelper.validateMediaAttachments(entity, List.of(), runtime));
() -> AttachmentValidationHelper.validateMediaAttachments(entity, List.of(), model));
}
}

Expand All @@ -168,7 +165,7 @@ private static Stream<Arguments> validFileScenarios() {
void throwsException_whenFilesAreInvalid(boolean isMediaEntity) {

CdsEntity entity = mockEntity("Entity");
CdsRuntime runtime = mockRuntime(entity);
CdsModel model = mockModel(entity);

Map<String, List<String>> allowed = Map.of(ATTACHMENTS_ENTITY, List.of("image/png"));
Map<String, Set<String>> files = Map.of(ATTACHMENTS_ENTITY, Set.of("file.txt"));
Expand All @@ -180,7 +177,7 @@ void throwsException_whenFilesAreInvalid(boolean isMediaEntity) {
mockStatic(AttachmentDataExtractor.class)) {

helper.when(() -> ApplicationHandlerHelper.isMediaEntity(entity)).thenReturn(isMediaEntity);
setupMockCascader(entity, runtime.getCdsModel(), !isMediaEntity);
setupMockCascader(entity, model, !isMediaEntity);

resolver
.when(
Expand All @@ -197,8 +194,7 @@ void throwsException_whenFilesAreInvalid(boolean isMediaEntity) {
ServiceException ex =
assertThrows(
ServiceException.class,
() ->
AttachmentValidationHelper.validateMediaAttachments(entity, List.of(), runtime));
() -> AttachmentValidationHelper.validateMediaAttachments(entity, List.of(), model));

assertTrue(ex.getMessage().contains("Unsupported file types detected"));
}
Expand All @@ -215,20 +211,10 @@ private void setupMockCascader(CdsEntity entity, CdsModel model, boolean hasAtta
AttachmentValidationHelper.setCascader(cascader);
}

private CdsRuntime mockRuntime(CdsEntity entity) {
private CdsModel mockModel(CdsEntity entity) {
CdsModel model = mock(CdsModel.class);
when(model.findEntity(entity.getQualifiedName())).thenReturn(Optional.of(entity));

CdsRuntime runtime = mock(CdsRuntime.class);
when(runtime.getCdsModel()).thenReturn(model);

return runtime;
}

private CdsRuntime mockRuntime(CdsModel model) {
CdsRuntime runtime = mock(CdsRuntime.class);
when(runtime.getCdsModel()).thenReturn(model);
return runtime;
return model;
}

private CdsEntity mockEntity(String name) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -192,5 +192,6 @@ private void getEntityAndMockContext(String cdsName) {

private void mockTargetInUpdateContext(CdsEntity serviceEntity) {
when(eventContext.getTarget()).thenReturn(serviceEntity);
when(eventContext.getModel()).thenReturn(runtime.getCdsModel());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import org.junit.jupiter.params.provider.CsvSource;
import org.springframework.http.MediaType;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.web.servlet.MvcResult;

@ActiveProfiles(Profiles.TEST_HANDLER_DISABLED)
public class MediaValidatedAttachmentsDraftTest extends DraftOdataRequestValidationBase {
Expand Down Expand Up @@ -72,6 +73,30 @@ void shouldPass_whenFileNameMissing_inDraft() throws Exception {
buildDraftAttachmentCreationUrl(rootId), metadata, status().isCreated());
}

@Test
void shouldValidateMediaType_whenPatchingDraftAttachment() throws Exception {
String rootId = createDraftRootAndReturnId();
CdsData created =
requestHelper.executePostWithODataResponseAndAssertStatusCreated(
buildDraftAttachmentCreationUrl(rootId),
objectMapper.writeValueAsString(Map.of("fileName", "image.jpg")));
String attachmentId = (String) created.get("ID");

String attachmentUrl =
BASE_URL
+ "DraftRoots_mediaValidatedAttachments(up__ID="
+ rootId
+ ",ID="
+ attachmentId
+ ",IsActiveEntity=false)";

MvcResult result =
requestHelper.executePatch(
attachmentUrl, objectMapper.writeValueAsString(Map.of("fileName", "notes.txt")));

assertThat(result.getResponse().getStatus()).isEqualTo(415);
}

// Helper methods
private String createDraftRootAndReturnId() throws Exception {
CdsData response =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,14 @@
*/
package com.sap.cds.feature.attachments.integrationtests.nondraftservice;

import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.sap.cds.CdsData;
import com.sap.cds.Result;
import com.sap.cds.Struct;
import com.sap.cds.feature.attachments.generated.integration.test.cds4j.sap.attachments.Attachments;
import com.sap.cds.feature.attachments.generated.integration.test.cds4j.testservice.AttachmentEntity;
import com.sap.cds.feature.attachments.generated.integration.test.cds4j.testservice.Roots;
Expand All @@ -26,6 +29,7 @@
import org.junit.jupiter.params.provider.CsvSource;
import org.springframework.http.MediaType;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.web.servlet.MvcResult;

@ActiveProfiles(Profiles.TEST_HANDLER_DISABLED)
class MediaValidatedAttachmentsNonDraftTest extends OdataRequestValidationBase {
Expand Down Expand Up @@ -82,6 +86,29 @@ void shouldRejectAttachment_whenFileNameIsEmpty() throws Exception {
status().isBadRequest());
}

@Test
void shouldValidateMediaType_whenUpdatingNonDraftAttachment() throws Exception {
String rootId = createRootAndReturnId();
CdsData created =
requestHelper.executePostWithODataResponseAndAssertStatusCreated(
createUrl(rootId, MEDIA_VALIDATED_ATTACHMENTS),
objectMapper.writeValueAsString(Map.of("fileName", "image.jpg")));
Attachments attachment = Struct.access(created).as(Attachments.class);
String attachmentUrl =
createUrl(rootId, MEDIA_VALIDATED_ATTACHMENTS)
+ "(ID="
+ attachment.getId()
+ ",up__ID="
+ rootId
+ ")";
Comment thread
Schmarvinius marked this conversation as resolved.

MvcResult result =
requestHelper.executePatch(
attachmentUrl, objectMapper.writeValueAsString(Map.of("fileName", "notes.txt")));

assertThat(result.getResponse().getStatus()).isEqualTo(415);
}

@Test
void shouldAcceptUppercaseExtension_whenMimeTypeIsAllowed() throws Exception {
String rootId = createRootAndReturnId();
Expand Down
Loading