-
Notifications
You must be signed in to change notification settings - Fork 9
Enforce rescan-on-download for all content read shapes #868
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| /* | ||
| * © 2026 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. | ||
| */ | ||
| package com.sap.cds.feature.attachments.handler.applicationservice.readhelper; | ||
|
|
||
| /** | ||
| * Guard invoked by {@link LazyProxyInputStream} right before attachment content bytes are served. | ||
| * | ||
| * <p>It enforces the malware scan status and freshness policy (rescan-on-download) for the content | ||
| * that is about to be read, regardless of the read shape (content-only {@code $value} reads as well | ||
| * as keyed reads that stream the content). Implementations throw an {@link | ||
| * AttachmentStatusException} to block the download when the content is not clean or must be | ||
| * rescanned first. | ||
| */ | ||
| @FunctionalInterface | ||
| public interface ContentReadGuard { | ||
|
|
||
| /** | ||
| * Enforces the status and freshness policy before content is served. | ||
| * | ||
| * @throws AttachmentStatusException if the content must not be served (e.g. not clean or a rescan | ||
| * has been triggered) | ||
| */ | ||
| void verify(); | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -12,23 +12,20 @@ | |||||||||||||||||||
| /** | ||||||||||||||||||||
| * The class {@link LazyProxyInputStream} is a lazy proxy for an {@link InputStream}. The class is | ||||||||||||||||||||
| * used to create a proxy for an {@link InputStream} that is not yet available. Before the {@link | ||||||||||||||||||||
| * InputStream} is accessed, the status of the attachment is validated. | ||||||||||||||||||||
| * InputStream} is accessed, the {@link ContentReadGuard} enforces the attachment status and | ||||||||||||||||||||
| * freshness policy. | ||||||||||||||||||||
| */ | ||||||||||||||||||||
| public class LazyProxyInputStream extends InputStream { | ||||||||||||||||||||
| private static final Logger logger = LoggerFactory.getLogger(LazyProxyInputStream.class); | ||||||||||||||||||||
|
|
||||||||||||||||||||
| private final Supplier<InputStream> inputStreamSupplier; | ||||||||||||||||||||
| private final AttachmentStatusValidator attachmentStatusValidator; | ||||||||||||||||||||
| private final String status; | ||||||||||||||||||||
| private final ContentReadGuard contentReadGuard; | ||||||||||||||||||||
| private InputStream delegate; | ||||||||||||||||||||
|
|
||||||||||||||||||||
| public LazyProxyInputStream( | ||||||||||||||||||||
| Supplier<InputStream> inputStreamSupplier, | ||||||||||||||||||||
| AttachmentStatusValidator attachmentStatusValidator, | ||||||||||||||||||||
| String status) { | ||||||||||||||||||||
| Supplier<InputStream> inputStreamSupplier, ContentReadGuard contentReadGuard) { | ||||||||||||||||||||
| this.inputStreamSupplier = inputStreamSupplier; | ||||||||||||||||||||
| this.attachmentStatusValidator = attachmentStatusValidator; | ||||||||||||||||||||
| this.status = status; | ||||||||||||||||||||
| this.contentReadGuard = contentReadGuard; | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| @Override | ||||||||||||||||||||
|
|
@@ -58,7 +55,7 @@ public void close() throws IOException { | |||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| private InputStream getDelegate() { | ||||||||||||||||||||
| attachmentStatusValidator.verifyStatus(status); | ||||||||||||||||||||
| contentReadGuard.verify(); | ||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Bug:
Suggested change
Double-check suggestion before committing. Edit this comment for amendments. Please provide feedback on the review comment by checking the appropriate box:
|
||||||||||||||||||||
|
|
||||||||||||||||||||
| if (delegate == null) { | ||||||||||||||||||||
| logger.debug("Creating delegate input stream"); | ||||||||||||||||||||
|
|
||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -14,40 +14,34 @@ | |
| import static org.mockito.Mockito.verifyNoInteractions; | ||
| import static org.mockito.Mockito.when; | ||
|
|
||
| import com.sap.cds.feature.attachments.generated.cds4j.sap.attachments.StatusCode; | ||
| import com.sap.cds.feature.attachments.service.AttachmentService; | ||
| import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; | ||
| import java.io.IOException; | ||
| import java.io.InputStream; | ||
| import java.nio.charset.StandardCharsets; | ||
| import org.junit.jupiter.api.BeforeEach; | ||
| import org.junit.jupiter.api.Test; | ||
| import org.junit.jupiter.params.ParameterizedTest; | ||
| import org.junit.jupiter.params.provider.ValueSource; | ||
|
|
||
| class LazyProxyInputStreamTest { | ||
|
|
||
| private LazyProxyInputStream cut; | ||
| private InputStream inputStream; | ||
| private AttachmentService attachmentService; | ||
| private AttachmentStatusValidator attachmentStatusValidator; | ||
| private ContentReadGuard contentReadGuard; | ||
|
|
||
| @BeforeEach | ||
| void setup() { | ||
| inputStream = mock(InputStream.class); | ||
| attachmentService = mock(AttachmentService.class); | ||
| attachmentStatusValidator = mock(AttachmentStatusValidator.class); | ||
| contentReadGuard = mock(ContentReadGuard.class); | ||
| when(attachmentService.readAttachment(any())).thenReturn(inputStream); | ||
| cut = | ||
| new LazyProxyInputStream( | ||
| () -> attachmentService.readAttachment(any()), | ||
| attachmentStatusValidator, | ||
| StatusCode.CLEAN); | ||
| cut = new LazyProxyInputStream(() -> attachmentService.readAttachment(any()), contentReadGuard); | ||
| } | ||
|
|
||
| @Test | ||
| void noMethodCallNoStreamAccess() { | ||
| verifyNoInteractions(attachmentService); | ||
| verifyNoInteractions(contentReadGuard); | ||
| } | ||
|
|
||
| @Test | ||
|
|
@@ -115,26 +109,24 @@ void closeCallsInputStream() throws IOException { | |
| verify(attachmentService).readAttachment(any()); | ||
| } | ||
|
|
||
| @ParameterizedTest | ||
| @ValueSource(strings = {StatusCode.UNSCANNED, StatusCode.INFECTED}) | ||
| void exceptionIfWrongStatus(String status) { | ||
| doThrow(AttachmentStatusException.class).when(attachmentStatusValidator).verifyStatus(status); | ||
| @Test | ||
| void guardIsVerifiedBeforeContentIsServed() throws IOException { | ||
| cut.read(); | ||
|
|
||
| verify(contentReadGuard).verify(); | ||
| verify(attachmentService).readAttachment(any()); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Bug: Please provide feedback on the review comment by checking the appropriate box:
|
||
| } | ||
|
|
||
| cut = | ||
| new LazyProxyInputStream( | ||
| () -> attachmentService.readAttachment(any()), attachmentStatusValidator, status); | ||
| @Test | ||
| void exceptionIfGuardBlocksRead() { | ||
| doThrow(AttachmentStatusException.class).when(contentReadGuard).verify(); | ||
|
|
||
| assertThrows(AttachmentStatusException.class, () -> cut.read()); | ||
| verifyNoInteractions(attachmentService); | ||
| } | ||
|
|
||
| @Test | ||
| void noExceptionIfCorrectStatus() { | ||
| cut = | ||
| new LazyProxyInputStream( | ||
| () -> attachmentService.readAttachment(any()), | ||
| attachmentStatusValidator, | ||
| StatusCode.CLEAN); | ||
|
|
||
| void noExceptionIfGuardPasses() { | ||
| assertDoesNotThrow(() -> cut.read()); | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Best Practice: The Javadoc
@throwstag referencesAttachmentStatusExceptionwithout importing or qualifying it, so the cross-reference is broken in generated Javadoc. Use a fully-qualified name or add the import to make the link resolvable.Double-check suggestion before committing. Edit this comment for amendments.
Please provide feedback on the review comment by checking the appropriate box: