From dadc36b6b4fca56eb086b751079abed127ad3925 Mon Sep 17 00:00:00 2001 From: Naphlin Akena Date: Wed, 23 Jul 2025 11:46:04 +0300 Subject: [PATCH] FINERACT-2118: Use S3 service accounts Allow usage of S3 service accounts and support any S3 compatible storage endpoints --- .../core/config/FineractProperties.java | 3 + .../core/config/ContentS3Config.java | 26 ++- .../src/main/resources/application.properties | 3 + .../core/config/ContentS3ConfigTest.java | 207 ++++++++++++++++++ 4 files changed, 236 insertions(+), 3 deletions(-) create mode 100644 fineract-provider/src/test/java/org/apache/fineract/infrastructure/core/config/ContentS3ConfigTest.java diff --git a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/config/FineractProperties.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/config/FineractProperties.java index b308ced99..3b8b594a5 100644 --- a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/config/FineractProperties.java +++ b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/config/FineractProperties.java @@ -372,6 +372,9 @@ public class FineractProperties { private String bucketName; private String accessKey; private String secretKey; + private String region; + private String endpoint; + private Boolean pathStyleAddressingEnabled; } @Getter diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/config/ContentS3Config.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/config/ContentS3Config.java index 924e0dba5..e2fa7514c 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/config/ContentS3Config.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/config/ContentS3Config.java @@ -19,13 +19,19 @@ package org.apache.fineract.infrastructure.core.config; +import com.google.common.base.Strings; +import java.net.URI; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; +import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; +import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider; import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; +import software.amazon.awssdk.regions.Region; import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.S3ClientBuilder; @Slf4j @Configuration @@ -34,8 +40,22 @@ public class ContentS3Config { @Bean @ConditionalOnProperty("fineract.content.s3.enabled") public S3Client contentS3Client(FineractProperties fineractProperties) { - return S3Client.builder().credentialsProvider(StaticCredentialsProvider.create(AwsBasicCredentials - .create(fineractProperties.getContent().getS3().getAccessKey(), fineractProperties.getContent().getS3().getSecretKey()))) - .build(); + S3ClientBuilder builder = S3Client.builder().credentialsProvider(getCredentialProvider(fineractProperties.getContent().getS3())) + .region(Region.of(fineractProperties.getContent().getS3().getRegion())); + + if (!Strings.isNullOrEmpty(fineractProperties.getContent().getS3().getEndpoint())) { + builder.endpointOverride(URI.create(fineractProperties.getContent().getS3().getEndpoint())) + .forcePathStyle(fineractProperties.getContent().getS3().getPathStyleAddressingEnabled()); + } + + return builder.build(); + } + + private AwsCredentialsProvider getCredentialProvider(FineractProperties.FineractContentS3Properties s3Properties) { + if (Strings.isNullOrEmpty(s3Properties.getAccessKey()) || Strings.isNullOrEmpty(s3Properties.getSecretKey())) { + return DefaultCredentialsProvider.create(); + } + + return StaticCredentialsProvider.create(AwsBasicCredentials.create(s3Properties.getAccessKey(), s3Properties.getSecretKey())); } } diff --git a/fineract-provider/src/main/resources/application.properties b/fineract-provider/src/main/resources/application.properties index 33642742e..be00ca5c9 100644 --- a/fineract-provider/src/main/resources/application.properties +++ b/fineract-provider/src/main/resources/application.properties @@ -165,6 +165,9 @@ fineract.content.s3.enabled=${FINERACT_CONTENT_S3_ENABLED:false} fineract.content.s3.bucketName=${FINERACT_CONTENT_S3_BUCKET_NAME:} fineract.content.s3.accessKey=${FINERACT_CONTENT_S3_ACCESS_KEY:} fineract.content.s3.secretKey=${FINERACT_CONTENT_S3_SECRET_KEY:} +fineract.content.s3.region=${FINERACT_CONTENT_S3_REGION:us-east-1} +fineract.content.s3.endpoint=${FINERACT_CONTENT_S3_ENDPOINT:} +fineract.content.s3.path-style-addressing-enabled=${FINERACT_CONTENT_S3_PATH_STYLE_ADDRESSING_ENABLED:false} fineract.template.regex-whitelist-enabled=${FINERACT_TEMPLATE_REGEX_WHITELIST_ENABLED:true} fineract.template.regex-whitelist=${FINERACT_TEMPLATE_REGEX_WHITELIST:} diff --git a/fineract-provider/src/test/java/org/apache/fineract/infrastructure/core/config/ContentS3ConfigTest.java b/fineract-provider/src/test/java/org/apache/fineract/infrastructure/core/config/ContentS3ConfigTest.java new file mode 100644 index 000000000..4880699f6 --- /dev/null +++ b/fineract-provider/src/test/java/org/apache/fineract/infrastructure/core/config/ContentS3ConfigTest.java @@ -0,0 +1,207 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.fineract.infrastructure.core.config; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import software.amazon.awssdk.services.s3.S3Client; + +@ExtendWith(MockitoExtension.class) +class ContentS3ConfigTest { + + @InjectMocks + private ContentS3Config contentS3Config; + + @Mock + private FineractProperties fineractProperties; + + @Mock + private FineractProperties.FineractContentProperties contentProperties; + + @Mock + private FineractProperties.FineractContentS3Properties s3Properties; + + @BeforeEach + void setUp() { + when(fineractProperties.getContent()).thenReturn(contentProperties); + when(contentProperties.getS3()).thenReturn(s3Properties); + } + + @Test + void testContentS3Client_WithCredentialsAndNoEndpoint() { + + String accessKey = "test-access-key"; + String secretKey = "test-secret-key"; + String region = "us-east-1"; + + when(s3Properties.getAccessKey()).thenReturn(accessKey); + when(s3Properties.getSecretKey()).thenReturn(secretKey); + when(s3Properties.getRegion()).thenReturn(region); + when(s3Properties.getEndpoint()).thenReturn(null); + + S3Client s3Client = contentS3Config.contentS3Client(fineractProperties); + + assertNotNull(s3Client); + + verify(s3Properties, times(2)).getAccessKey(); + verify(s3Properties, times(2)).getSecretKey(); + verify(s3Properties).getRegion(); + verify(s3Properties).getEndpoint(); + } + + @Test + void testContentS3Client_WithNoCredentials() { + + String region = "us-west-2"; + + when(s3Properties.getAccessKey()).thenReturn(null); + when(s3Properties.getRegion()).thenReturn(region); + when(s3Properties.getEndpoint()).thenReturn(null); + + S3Client s3Client = contentS3Config.contentS3Client(fineractProperties); + + assertNotNull(s3Client); + + verify(s3Properties, times(1)).getAccessKey(); + verify(s3Properties, times(0)).getSecretKey(); + verify(s3Properties).getRegion(); + verify(s3Properties).getEndpoint(); + } + + @Test + void testContentS3Client_WithCredentialsAndEndpoint() { + + String accessKey = "test-access-key"; + String secretKey = "test-secret-key"; + String region = "eu-west-1"; + String endpoint = "http://localhost:4566"; + boolean pathStyleAddressing = true; + + when(s3Properties.getAccessKey()).thenReturn(accessKey); + when(s3Properties.getSecretKey()).thenReturn(secretKey); + when(s3Properties.getRegion()).thenReturn(region); + when(s3Properties.getEndpoint()).thenReturn(endpoint); + when(s3Properties.getPathStyleAddressingEnabled()).thenReturn(pathStyleAddressing); + + S3Client s3Client = contentS3Config.contentS3Client(fineractProperties); + + assertNotNull(s3Client); + + verify(s3Properties, times(2)).getAccessKey(); + verify(s3Properties, times(2)).getSecretKey(); + verify(s3Properties).getRegion(); + verify(s3Properties, times(2)).getEndpoint(); + verify(s3Properties).getPathStyleAddressingEnabled(); + } + + @Test + void testContentS3Client_WithEmptyCredentials() { + + String accessKey = ""; + String region = "ap-southeast-1"; + + when(s3Properties.getAccessKey()).thenReturn(accessKey); + when(s3Properties.getEndpoint()).thenReturn(null); + when(s3Properties.getRegion()).thenReturn(region); + + S3Client s3Client = contentS3Config.contentS3Client(fineractProperties); + + assertNotNull(s3Client); + + verify(s3Properties, times(1)).getAccessKey(); + verify(s3Properties, times(0)).getSecretKey(); + verify(s3Properties).getRegion(); + verify(s3Properties).getEndpoint(); + } + + @Test + void testContentS3Client_WithEmptyEndpoint() { + + String accessKey = "test-access-key"; + String secretKey = "test-secret-key"; + String region = "ca-central-1"; + String endpoint = ""; + + when(s3Properties.getAccessKey()).thenReturn(accessKey); + when(s3Properties.getSecretKey()).thenReturn(secretKey); + when(s3Properties.getRegion()).thenReturn(region); + when(s3Properties.getEndpoint()).thenReturn(endpoint); + + S3Client s3Client = contentS3Config.contentS3Client(fineractProperties); + + assertNotNull(s3Client); + + verify(s3Properties, times(2)).getAccessKey(); + verify(s3Properties, times(2)).getSecretKey(); + verify(s3Properties).getRegion(); + verify(s3Properties).getEndpoint(); + + verify(s3Properties, never()).getPathStyleAddressingEnabled(); + } + + @Test + void testContentS3Client_WithOnlyAccessKey() { + String accessKey = "test-access-key"; + String region = "sa-east-1"; + + when(s3Properties.getAccessKey()).thenReturn(accessKey); + when(s3Properties.getSecretKey()).thenReturn(null); + when(s3Properties.getRegion()).thenReturn(region); + when(s3Properties.getEndpoint()).thenReturn(null); + + S3Client s3Client = contentS3Config.contentS3Client(fineractProperties); + + assertNotNull(s3Client); + + verify(s3Properties, times(1)).getAccessKey(); + verify(s3Properties, times(1)).getSecretKey(); + verify(s3Properties).getRegion(); + verify(s3Properties).getEndpoint(); + } + + @Test + void testContentS3Client_WithOnlySecretKey() { + + String region = "eu-central-1"; + + when(s3Properties.getAccessKey()).thenReturn(null); + when(s3Properties.getEndpoint()).thenReturn(null); + when(s3Properties.getRegion()).thenReturn(region); + + S3Client s3Client = contentS3Config.contentS3Client(fineractProperties); + + assertNotNull(s3Client); + + verify(s3Properties, times(1)).getAccessKey(); + verify(s3Properties, times(0)).getSecretKey(); + verify(s3Properties).getRegion(); + verify(s3Properties).getEndpoint(); + } +}