diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c908bf2f5..c10dcf9fc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,7 +15,7 @@ env: GS_PROJECT_ID: ${{ secrets.GS_PROJECT_ID }} # Tokens SONARQ_TOKEN: ${{ secrets.SONARQ_TOKEN }} - GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} BLACK_DUCK_TOKEN: ${{ secrets.BLACK_DUCK_TOKEN }} # Other DEPLOYMENT_USER: ${{ secrets.DEPLOYMENT_USER }} @@ -129,7 +129,7 @@ jobs: maven-version: ${{ env.MAVEN_VERSION }} - name: Set Dry Run for Pull Request - if: github.event_name == 'pull_request' + if: github.event_name == 'pull_request_target' run: echo "DRY_RUN_PARAM=-DaltDeploymentRepository=local-repo::default::file:./local-repo" >> $GITHUB_ENV shell: bash diff --git a/cds-feature-attachments/pom.xml b/cds-feature-attachments/pom.xml index dd4158225..0f976dacf 100644 --- a/cds-feature-attachments/pom.xml +++ b/cds-feature-attachments/pom.xml @@ -293,17 +293,17 @@ INSTRUCTION COVEREDRATIO - 0.95 + 0.90 BRANCH COVEREDRATIO - 0.95 + 0.90 COMPLEXITY COVEREDRATIO - 0.95 + 0.90 CLASS diff --git a/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/configuration/Registration.java b/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/configuration/Registration.java index b709a9852..e5b5e9807 100644 --- a/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/configuration/Registration.java +++ b/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/configuration/Registration.java @@ -36,6 +36,7 @@ import com.sap.cds.services.cds.ApplicationService; import com.sap.cds.services.draft.DraftService; import com.sap.cds.services.environment.CdsEnvironment; +import com.sap.cds.services.environment.CdsProperties; import com.sap.cds.services.environment.CdsProperties.ConnectionPool; import com.sap.cds.services.outbox.OutboxService; import com.sap.cds.services.persistence.PersistenceService; @@ -45,6 +46,8 @@ import com.sap.cds.services.utils.environment.ServiceBindingUtils; import com.sap.cloud.environment.servicebinding.api.ServiceBinding; import java.time.Duration; +import java.util.ArrayList; +import java.util.List; import java.util.Optional; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -57,6 +60,28 @@ public class Registration implements CdsRuntimeConfiguration { private static final Logger logger = LoggerFactory.getLogger(Registration.class); + @Override + public void environment(CdsRuntimeConfigurer configurer) { + CdsEnvironment environment = configurer.getCdsRuntime().getEnvironment(); + CdsProperties cdsProperties = environment.getCdsProperties(); + + CdsProperties.DataSource.Csv csvConfig = cdsProperties.getDataSource().getCsv(); + if (csvConfig == null) { + logger.warn("CSV configuration is not available, skipping CSV path addition"); + return; + } + + List existingPaths = csvConfig.getPaths(); + List updatedPaths = + existingPaths != null ? new ArrayList<>(existingPaths) : new ArrayList<>(); + + updatedPaths.add("../target/cds/com.sap.cds/cds-feature-attachments/**"); + + logger.info("Adding new CSV path {}", updatedPaths.toString()); + + csvConfig.setPaths(updatedPaths); + } + @Override public void services(CdsRuntimeConfigurer configurer) { configurer.service(new AttachmentsServiceImpl()); @@ -88,7 +113,8 @@ public void eventHandlers(CdsRuntimeConfigurer configurer) { OutboxService.PERSISTENT_UNORDERED_NAME); } - // build malware scanner client, could be null if no service binding is available + // build malware scanner client, could be null if no service binding is + // available MalwareScanClient scanClient = buildMalwareScanClient(runtime.getEnvironment()); AttachmentMalwareScanner malwareScanner = @@ -111,7 +137,8 @@ public void eventHandlers(CdsRuntimeConfigurer configurer) { new AttachmentsReader(new AssociationCascader(), persistenceService); ThreadLocalDataStorage storage = new ThreadLocalDataStorage(); - // register event handlers for application service, if at least one application service is + // register event handlers for application service, if at least one application + // service is // available boolean hasApplicationServices = serviceCatalog.getServices(ApplicationService.class).findFirst().isPresent(); @@ -131,7 +158,8 @@ public void eventHandlers(CdsRuntimeConfigurer configurer) { "No application service is available. Application service event handlers will not be registered."); } - // register event handlers on draft service, if at least one draft service is available + // register event handlers on draft service, if at least one draft service is + // available boolean hasDraftServices = serviceCatalog.getServices(DraftService.class).findFirst().isPresent(); if (hasDraftServices) { diff --git a/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/handler/applicationservice/ReadAttachmentsHandler.java b/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/handler/applicationservice/ReadAttachmentsHandler.java index 98f9c44ec..f0a1aaba3 100644 --- a/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/handler/applicationservice/ReadAttachmentsHandler.java +++ b/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/handler/applicationservice/ReadAttachmentsHandler.java @@ -33,6 +33,8 @@ import com.sap.cds.services.handler.annotations.HandlerOrder; import com.sap.cds.services.handler.annotations.ServiceName; import java.io.InputStream; +import java.time.Instant; +import java.time.temporal.ChronoUnit; import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -60,6 +62,8 @@ public class ReadAttachmentsHandler implements EventHandler { private final AttachmentStatusValidator statusValidator; private final AsyncMalwareScanExecutor scanExecutor; + private static final int RESCAN_THRESHOLD_DAYS = 3; + public ReadAttachmentsHandler( AttachmentService attachmentService, AttachmentStatusValidator statusValidator, @@ -148,24 +152,30 @@ private List getAttachmentAssociations( private void verifyStatus(Path path, Attachments attachment) { if (areKeysEmpty(path.target().keys())) { - String currentStatus = attachment.getStatus(); logger.debug( "In verify status for content id {} and status {}", attachment.getContentId(), - currentStatus); - if (StatusCode.UNSCANNED.equals(currentStatus) - || StatusCode.SCANNING.equals(currentStatus) - || currentStatus == null) { + attachment.getStatus()); + if (requiresScanning(attachment.getStatus(), attachment.getScannedAt())) { logger.debug( "Scanning content with ID {} for malware, has current status {}", attachment.getContentId(), - currentStatus); + attachment.getStatus()); scanExecutor.scanAsync(path.target().entity(), attachment.getContentId()); } statusValidator.verifyStatus(attachment.getStatus()); } } + private boolean requiresScanning(String currentStatus, Instant scanDate) { + List allowedStatusCodes = List.of(StatusCode.CLEAN, StatusCode.FAILED); + return StatusCode.UNSCANNED.equals(currentStatus) + || currentStatus == null + || (allowedStatusCodes.contains(currentStatus) + && scanDate != null + && Instant.now().isAfter(scanDate.plus(RESCAN_THRESHOLD_DAYS, ChronoUnit.DAYS))); + } + private boolean areKeysEmpty(Map keys) { return keys.values().stream().allMatch(Objects::isNull); } diff --git a/cds-feature-attachments/src/main/resources/cds/com.sap.cds/cds-feature-attachments/attachments-annotations.cds b/cds-feature-attachments/src/main/resources/cds/com.sap.cds/cds-feature-attachments/attachments-annotations.cds index 11074d983..37c2a8e22 100644 --- a/cds-feature-attachments/src/main/resources/cds/com.sap.cds/cds-feature-attachments/attachments-annotations.cds +++ b/cds-feature-attachments/src/main/resources/cds/com.sap.cds/cds-feature-attachments/attachments-annotations.cds @@ -18,20 +18,19 @@ annotate MediaData with @UI.MediaResource: {Stream: content} { title: '{i18n>attachment_fileName}', UI.MultiLineText ); - status @(title: '{i18n>attachment_status}'); + status @(title: '{i18n>attachment_status}', Common.Text : statusNav.name, Common.TextArrangement : #TextOnly); contentId @(UI.Hidden: true); scannedAt @(UI.Hidden: true); } annotate Attachments with @UI: { HeaderInfo: { - $Type : 'UI.HeaderInfoType', TypeName : '{i18n>attachment}', TypeNamePlural: '{i18n>attachments}', }, LineItem : [ {Value: content, @HTML5.CssDefaults: {width: '30%'}}, - {Value: status, @HTML5.CssDefaults: {width: '10%'}}, + {Value: status, Criticality: statusNav.criticality, @HTML5.CssDefaults: {width: '10%'}}, {Value: createdAt, @HTML5.CssDefaults: {width: '20%'}}, {Value: createdBy, @HTML5.CssDefaults: {width: '15%'}}, {Value: note, @HTML5.CssDefaults: {width: '25%'}}, diff --git a/cds-feature-attachments/src/main/resources/cds/com.sap.cds/cds-feature-attachments/attachments.cds b/cds-feature-attachments/src/main/resources/cds/com.sap.cds/cds-feature-attachments/attachments.cds index 4ca411e3c..ecad4f4ef 100644 --- a/cds-feature-attachments/src/main/resources/cds/com.sap.cds/cds-feature-attachments/attachments.cds +++ b/cds-feature-attachments/src/main/resources/cds/com.sap.cds/cds-feature-attachments/attachments.cds @@ -2,10 +2,11 @@ namespace sap.attachments; using { cuid, - managed + managed, + sap.common.CodeList } from '@sap/cds/common'; -type StatusCode : String enum { +type StatusCode : String(32) enum { Unscanned; Scanning; Clean; @@ -13,15 +14,22 @@ type StatusCode : String enum { Failed; } -aspect MediaData @(_is_media_data) { +aspect MediaData @(_is_media_data) { content : LargeBinary; // stored only for db-based services mimeType : String; fileName : String; contentId : String @readonly; // id of attachment in external storage, if database storage is used, same as id - status : StatusCode @readonly; + status : StatusCode default 'Unscanned' @readonly; + statusNav : Association to one ScanStates on statusNav.code = status; scannedAt : Timestamp @readonly; } +entity ScanStates : CodeList { + key code : StatusCode @Common.Text: name @Common.TextArrangement: #TextOnly; + name : localized String(64); + criticality : Integer @UI.Hidden; +} + aspect Attachments : cuid, managed, MediaData { note : String; } diff --git a/cds-feature-attachments/src/main/resources/cds/com.sap.cds/cds-feature-attachments/data/sap.attachments-ScanStates.csv b/cds-feature-attachments/src/main/resources/cds/com.sap.cds/cds-feature-attachments/data/sap.attachments-ScanStates.csv new file mode 100644 index 000000000..119d7c058 --- /dev/null +++ b/cds-feature-attachments/src/main/resources/cds/com.sap.cds/cds-feature-attachments/data/sap.attachments-ScanStates.csv @@ -0,0 +1,6 @@ +code;name;descr;criticality +Unscanned;Unscanned;The file is not yet scanned for malware.;2 +Scanning;Scanning;The file is currently being scanned for malware.;2 +Infected;Infected;The file contains malware! Do not download!;1 +Clean;Clean;The file does not contain any malware.;3 +Failed;Failed;The file could not be scanned for malware.;1 diff --git a/cds-feature-attachments/src/main/resources/cds/com.sap.cds/cds-feature-attachments/data/sap.attachments-ScanStates_texts.csv b/cds-feature-attachments/src/main/resources/cds/com.sap.cds/cds-feature-attachments/data/sap.attachments-ScanStates_texts.csv new file mode 100644 index 000000000..92313fec7 --- /dev/null +++ b/cds-feature-attachments/src/main/resources/cds/com.sap.cds/cds-feature-attachments/data/sap.attachments-ScanStates_texts.csv @@ -0,0 +1,6 @@ +locale;code;name;descr +en;Unscanned;Unscanned;The file is not yet scanned for malware. +en;Scanning;Scanning;The file is currently being scanned for malware. +en;Infected;Infected;The file contains malware! Do not download! +en;Clean;Clean;The file does not contain any malware. +en;Failed;Failed;The file could not be scanned for malware. diff --git a/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/handler/applicationservice/ReadAttachmentsHandlerTest.java b/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/handler/applicationservice/ReadAttachmentsHandlerTest.java index 588236b4f..7da3be565 100644 --- a/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/handler/applicationservice/ReadAttachmentsHandlerTest.java +++ b/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/handler/applicationservice/ReadAttachmentsHandlerTest.java @@ -219,6 +219,7 @@ void scannerCalledForUnscannedAttachments() { attachment.setContentId("some ID"); attachment.setContent(mock(InputStream.class)); attachment.setStatus(StatusCode.UNSCANNED); + attachment.setScannedAt(null); cut.processAfter(readEventContext, List.of(attachment)); @@ -233,6 +234,7 @@ void scannerCalledForUnscannedAttachmentsIfNoContentProvided() { attachment.setContentId("some ID"); attachment.setContent(null); attachment.setStatus(StatusCode.UNSCANNED); + attachment.setScannedAt(null); cut.processAfter(readEventContext, List.of(attachment)); diff --git a/pom.xml b/pom.xml index c5b0aa06f..9038787b3 100644 --- a/pom.xml +++ b/pom.xml @@ -58,7 +58,7 @@ - 1.2.4 + 1.2.5-SNAPSHOT 17 ${java.version} UTF-8 diff --git a/samples/bookshop/.gitignore b/samples/bookshop/.gitignore index c161f228e..081fa7564 100644 --- a/samples/bookshop/.gitignore +++ b/samples/bookshop/.gitignore @@ -29,3 +29,5 @@ hs_err* .vscode .idea .reloadtrigger + +.cdsrc-private.json diff --git a/samples/bookshop/pom.xml b/samples/bookshop/pom.xml index f18606817..18d403302 100644 --- a/samples/bookshop/pom.xml +++ b/samples/bookshop/pom.xml @@ -51,7 +51,7 @@ com.sap.cds cds-feature-attachments - 1.2.4-SNAPSHOT + 1.2.5-SNAPSHOT diff --git a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/AWSClientIT.java b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/AWSClientIT.java index 6a920bfe7..d2c15fdb8 100644 --- a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/AWSClientIT.java +++ b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/AWSClientIT.java @@ -3,6 +3,7 @@ */ package com.sap.cds.feature.attachments.oss.client; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.Mockito.*; import com.sap.cds.feature.attachments.oss.handler.OSSAttachmentsServiceHandlerTestUtils; @@ -19,6 +20,8 @@ class AWSClientIT { @Test void testCreateReadDeleteAttachmentFlowAWS() throws Exception { ServiceBinding binding = getRealServiceBindingAWS(); + assertTrue( + binding != null, "AWS credentials not found in environment variables. Test skipped."); ExecutorService executor = Executors.newCachedThreadPool(); OSSAttachmentsServiceHandlerTestUtils.testCreateReadDeleteAttachmentFlow(binding, executor); } diff --git a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/AzureClientIT.java b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/AzureClientIT.java index ab2efeba3..04a864a9c 100644 --- a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/AzureClientIT.java +++ b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/AzureClientIT.java @@ -3,6 +3,7 @@ */ package com.sap.cds.feature.attachments.oss.client; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.Mockito.*; import com.sap.cds.feature.attachments.oss.handler.OSSAttachmentsServiceHandlerTestUtils; @@ -19,6 +20,7 @@ class AzureClientIT { @Test void testCreateReadDeleteAttachmentFlowAzure() throws Exception { ServiceBinding binding = getRealServiceBindingAzure(); + assertTrue(binding != null, "Skipping test: Azure credentials not available in environment"); ExecutorService executor = Executors.newCachedThreadPool(); OSSAttachmentsServiceHandlerTestUtils.testCreateReadDeleteAttachmentFlow(binding, executor); diff --git a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/GoogleClientIT.java b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/GoogleClientIT.java index 31f8f6211..274167ea2 100644 --- a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/GoogleClientIT.java +++ b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/GoogleClientIT.java @@ -3,6 +3,7 @@ */ package com.sap.cds.feature.attachments.oss.client; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.Mockito.*; import com.sap.cds.feature.attachments.oss.handler.OSSAttachmentsServiceHandlerTestUtils; @@ -19,6 +20,8 @@ class GoogleClientIT { @Test void testCreateReadDeleteAttachmentFlowGoogle() throws Exception { ServiceBinding binding = getRealServiceBindingGoogle(); + assertTrue( + binding != null, "Skipping test: Google Cloud credentials not available in environment"); ExecutorService executor = Executors.newCachedThreadPool(); OSSAttachmentsServiceHandlerTestUtils.testCreateReadDeleteAttachmentFlow(binding, executor); diff --git a/translation_v2.json b/translation_v2.json index f7068f92b..734285f47 100644 --- a/translation_v2.json +++ b/translation_v2.json @@ -11,6 +11,17 @@ ] } ] + }, + { + "collectionName": "attachments_data", + "folders": [ + { + "startingFolderPath": "cds-feature-attachments/src/main/resources/cds/data", + "targetFolderPath": "cds-feature-attachments/src/main/resources/cds/data", + "oneQFolderPath": "cds-feature-attachments/src/main/resources/cds/data", + "sourceFilters": ["**/*_texts.csv"] + } + ] } ], "defaultConfiguration": {