From 4a929c584942689229d5e985ac87c240440b36fe Mon Sep 17 00:00:00 2001 From: serdimic <123578532+serdimic@users.noreply.github.com> Date: Mon, 23 Dec 2024 15:21:27 +0100 Subject: [PATCH] feat: P4ADEV-1781 add API to get decrypted api-key of broker (#8) Co-authored-by: antonioT90 <34568575+antonioT90@users.noreply.github.com> --- .gitignore | 1 + build.gradle.kts | 31 ++++ helm/values.yaml | 1 + openapi/generated.openapi.json | 131 +++++++++++--- openapi/p4pa-organization.openapi.json | 86 ++++++++++ .../controller/BrokerController.java | 26 +++ .../exception/ControllerExceptionHandler.java | 38 ++++ .../pagopa/pu/organization/model/Broker.java | 2 + .../broker/BrokerEncryptionService.java | 44 +++++ .../service/broker/BrokerService.java | 29 ++++ .../pagopa/pu/organization/util/AESUtils.java | 162 ++++++++++++++++++ src/main/resources/application.yml | 47 ++--- .../controller/BrokerControllerTest.java | 42 +++++ .../ControllerExceptionHandlerTest.java | 44 +++++ .../broker/BrokerEncryptionServiceTest.java | 73 ++++++++ .../service/broker/BrokerServiceTest.java | 79 +++++++++ .../pu/organization/util/AESUtilsTest.java | 61 +++++++ 17 files changed, 855 insertions(+), 42 deletions(-) create mode 100644 openapi/p4pa-organization.openapi.json create mode 100644 src/main/java/it/gov/pagopa/pu/organization/controller/BrokerController.java create mode 100644 src/main/java/it/gov/pagopa/pu/organization/exception/ControllerExceptionHandler.java create mode 100644 src/main/java/it/gov/pagopa/pu/organization/service/broker/BrokerEncryptionService.java create mode 100644 src/main/java/it/gov/pagopa/pu/organization/service/broker/BrokerService.java create mode 100644 src/main/java/it/gov/pagopa/pu/organization/util/AESUtils.java create mode 100644 src/test/java/it/gov/pagopa/pu/organization/controller/BrokerControllerTest.java create mode 100644 src/test/java/it/gov/pagopa/pu/organization/exception/ControllerExceptionHandlerTest.java create mode 100644 src/test/java/it/gov/pagopa/pu/organization/service/broker/BrokerEncryptionServiceTest.java create mode 100644 src/test/java/it/gov/pagopa/pu/organization/service/broker/BrokerServiceTest.java create mode 100644 src/test/java/it/gov/pagopa/pu/organization/util/AESUtilsTest.java diff --git a/.gitignore b/.gitignore index e1a6165..28df53d 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ HELP.md !**/src/test/**/target/ #**/src/main/resources/application-local*.properties +/src/main/resources/local-*.env ### STS ### .apt_generated diff --git a/build.gradle.kts b/build.gradle.kts index ce1bad5..4fcac3b 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -94,6 +94,37 @@ openApi { outputFileName.set("generated.openapi.json") } +configure { + named("main") { + java.srcDir("$projectDir/build/generated/src/main/java") + } +} + +tasks.compileJava { + dependsOn("openApiGenerateOrganization") +} + springBoot { mainClass.value("it.gov.pagopa.pu.organization.OrganizationApplication") } + +tasks.register("openApiGenerateOrganization") { + group = "openapi" + description = "description" + + generatorName.set("spring") + inputSpec.set("$rootDir/openapi/p4pa-organization.openapi.json") + outputDir.set("$projectDir/build/generated") + apiPackage.set("it.gov.pagopa.pu.organization.controller.generated") + modelPackage.set("it.gov.pagopa.pu.organization.dto.generated") + configOptions.set(mapOf( + "dateLibrary" to "java8", + "requestMappingMode" to "api_interface", + "useSpringBoot3" to "true", + "interfaceOnly" to "true", + "useTags" to "true", + "generateConstructorWithAllArgs" to "false", + "generatedConstructorWithRequiredArgs" to "false", + "additionalModelTypeAnnotations" to "@lombok.Data @lombok.Builder @lombok.AllArgsConstructor" + )) +} diff --git a/helm/values.yaml b/helm/values.yaml index afd7a60..4c8ca4e 100644 --- a/helm/values.yaml +++ b/helm/values.yaml @@ -67,6 +67,7 @@ microservice-chart: ORGANIZATION_DB_HOST: db-host ORGANIZATION_DB_USER: db-mypay-login-username ORGANIZATION_DB_PASSWORD: db-mypay-login-password + BROKER_ENCRYPT_PASSWORD: broker-encrypt-password # nodeSelector: {} diff --git a/openapi/generated.openapi.json b/openapi/generated.openapi.json index 45dbd55..c674279 100644 --- a/openapi/generated.openapi.json +++ b/openapi/generated.openapi.json @@ -11,6 +11,12 @@ "description": "Generated server url" } ], + "tags": [ + { + "name": "Broker", + "description": "the Broker API" + } + ], "paths": { "/brokers": { "get": { @@ -653,6 +659,73 @@ } } } + }, + "/brokers/apiKey/{brokerId}": { + "get": { + "tags": [ + "Broker" + ], + "summary": "Retrieve decrypted API keys for a broker", + "operationId": "getBrokerApiKeys", + "parameters": [ + { + "name": "brokerId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BrokerApiKeys" + } + }, + "application/hal+json": { + "schema": { + "$ref": "#/components/schemas/BrokerApiKeys" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BrokerApiKeys" + } + }, + "application/hal+json": { + "schema": { + "$ref": "#/components/schemas/BrokerApiKeys" + } + } + } + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BrokerApiKeys" + } + }, + "application/hal+json": { + "schema": { + "$ref": "#/components/schemas/BrokerApiKeys" + } + } + } + } + } + } } }, "components": { @@ -946,6 +1019,28 @@ } } }, + "PagedModelEntityModelBroker": { + "type": "object", + "properties": { + "_embedded": { + "type": "object", + "properties": { + "broker": { + "type": "array", + "items": { + "$ref": "#/components/schemas/EntityModelBroker" + } + } + } + }, + "_links": { + "$ref": "#/components/schemas/Links" + }, + "page": { + "$ref": "#/components/schemas/PageMetadata" + } + } + }, "PersonalisationFe": { "type": "object", "properties": { @@ -972,28 +1067,6 @@ } } }, - "PagedModelEntityModelBroker": { - "type": "object", - "properties": { - "_embedded": { - "type": "object", - "properties": { - "broker": { - "type": "array", - "items": { - "$ref": "#/components/schemas/EntityModelBroker" - } - } - } - }, - "_links": { - "$ref": "#/components/schemas/Links" - }, - "page": { - "$ref": "#/components/schemas/PageMetadata" - } - } - }, "BrokerRequestBody": { "type": "object", "properties": { @@ -1154,6 +1227,20 @@ } } }, + "BrokerApiKeys": { + "type": "object", + "properties": { + "syncKey": { + "type": "string" + }, + "acaKey": { + "type": "string" + }, + "gpdKey": { + "type": "string" + } + } + }, "Link": { "type": "object", "properties": { diff --git a/openapi/p4pa-organization.openapi.json b/openapi/p4pa-organization.openapi.json new file mode 100644 index 0000000..5052b88 --- /dev/null +++ b/openapi/p4pa-organization.openapi.json @@ -0,0 +1,86 @@ +{ + "openapi": "3.0.1", + "info": { + "title": "p4pa-organization", + "description": "Api and Models", + "version": "0.0.1" + }, + "servers": [ + { + "url": "http://localhost:8080", + "description": "Generated server url" + } + ], + "paths": { + "/brokers/apiKey/{brokerId}": { + "get": { + "tags": [ + "Broker" + ], + "summary": "Retrieve decrypted API keys for a broker", + "operationId": "getBrokerApiKeys", + "parameters": [ + { + "name": "brokerId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BrokerApiKeys" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/hal+json": { + "schema": { + "$ref": "#/components/schemas/BrokerApiKeys" + } + } + } + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/hal+json": { + "schema": { + "$ref": "#/components/schemas/BrokerApiKeys" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "BrokerApiKeys": { + "type": "object", + "properties": { + "syncKey": { + "type": "string" + }, + "acaKey": { + "type": "string" + }, + "gpdKey": { + "type": "string" + } + } + } + } + } +} diff --git a/src/main/java/it/gov/pagopa/pu/organization/controller/BrokerController.java b/src/main/java/it/gov/pagopa/pu/organization/controller/BrokerController.java new file mode 100644 index 0000000..4d9edee --- /dev/null +++ b/src/main/java/it/gov/pagopa/pu/organization/controller/BrokerController.java @@ -0,0 +1,26 @@ +package it.gov.pagopa.pu.organization.controller; + +import it.gov.pagopa.pu.organization.controller.generated.BrokerApi; +import it.gov.pagopa.pu.organization.dto.generated.BrokerApiKeys; +import it.gov.pagopa.pu.organization.service.broker.BrokerService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@Slf4j +public class BrokerController implements BrokerApi { + + private final BrokerService brokerService; + + public BrokerController(BrokerService brokerService){ + this.brokerService = brokerService; + } + + @Override + public ResponseEntity getBrokerApiKeys(Long brokerId) { + log.info("invoking getBrokerApiKeys, brokerId[{}]", brokerId); + return ResponseEntity.ofNullable(brokerService.getBrokerApiKeys(brokerId)); + } + +} diff --git a/src/main/java/it/gov/pagopa/pu/organization/exception/ControllerExceptionHandler.java b/src/main/java/it/gov/pagopa/pu/organization/exception/ControllerExceptionHandler.java new file mode 100644 index 0000000..bcc7c2f --- /dev/null +++ b/src/main/java/it/gov/pagopa/pu/organization/exception/ControllerExceptionHandler.java @@ -0,0 +1,38 @@ +package it.gov.pagopa.pu.organization.exception; + +import jakarta.servlet.http.HttpServletRequest; +import lombok.extern.slf4j.Slf4j; +import org.slf4j.event.Level; +import org.springdoc.api.ErrorMessage; +import org.springframework.data.rest.webmvc.ResourceNotFoundException; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +@RestControllerAdvice +@Slf4j +public class ControllerExceptionHandler { + + @ExceptionHandler(ResourceNotFoundException.class) + public ResponseEntity resourceNotFoundException(ResourceNotFoundException ex, HttpServletRequest request) { + HttpStatus returnStatus = HttpStatus.NOT_FOUND; + logException(ex, request, returnStatus, Level.INFO, false); + return ResponseEntity.status(returnStatus) + .body(new ErrorMessage("resource not found: %s".formatted(ex.getMessage()))); + } + + private void logException(Exception ex, HttpServletRequest request, HttpStatus httpStatus, Level level, boolean printStackTrace) { + printStackTrace = printStackTrace || log.isTraceEnabled(); + log.atLevel(level) + .setCause(printStackTrace ? ex : null) + .log("A {} occurred handling request {} {} - HttpStatus {} - {}", + ex.getClass().getSimpleName(), + request.getMethod(), + request.getRequestURI(), + httpStatus.value(), + ex.getMessage() + ); + } + +} diff --git a/src/main/java/it/gov/pagopa/pu/organization/model/Broker.java b/src/main/java/it/gov/pagopa/pu/organization/model/Broker.java index e748e56..62b2a89 100644 --- a/src/main/java/it/gov/pagopa/pu/organization/model/Broker.java +++ b/src/main/java/it/gov/pagopa/pu/organization/model/Broker.java @@ -11,6 +11,7 @@ import jakarta.persistence.SequenceGenerator; import java.io.Serializable; import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; import org.hibernate.annotations.JdbcTypeCode; @@ -19,6 +20,7 @@ @Entity(name = "broker") @AllArgsConstructor @NoArgsConstructor +@Builder @Data public class Broker implements Serializable { @Id diff --git a/src/main/java/it/gov/pagopa/pu/organization/service/broker/BrokerEncryptionService.java b/src/main/java/it/gov/pagopa/pu/organization/service/broker/BrokerEncryptionService.java new file mode 100644 index 0000000..dee16e3 --- /dev/null +++ b/src/main/java/it/gov/pagopa/pu/organization/service/broker/BrokerEncryptionService.java @@ -0,0 +1,44 @@ +package it.gov.pagopa.pu.organization.service.broker; + +import it.gov.pagopa.pu.organization.dto.generated.BrokerApiKeys; +import it.gov.pagopa.pu.organization.model.Broker; +import it.gov.pagopa.pu.organization.util.AESUtils; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +@Service +@Slf4j +public class BrokerEncryptionService { + + private final String brokerEncryptPassword; + + public BrokerEncryptionService( + @Value("${app.brokerEncryptPassword}") String brokerEncryptPassword) { + this.brokerEncryptPassword = brokerEncryptPassword; + } + + private final Map apiKeyDecryptMap = new ConcurrentHashMap<>(); + + public BrokerApiKeys getBrokerDecryptedApiKeys(Broker broker){ + return BrokerApiKeys.builder() + .syncKey(decryptKey(broker.getSyncKey(),"SYNC", broker.getBrokerId())) + .acaKey(decryptKey(broker.getAcaKey(),"ACA", broker.getBrokerId())) + .gpdKey(decryptKey(broker.getGpdKey(),"GPD", broker.getBrokerId())) + .build(); + } + + private String decryptKey(byte[] encryptedKey, String type, Long brokerId){ + if(encryptedKey==null || encryptedKey.length==0) { + log.debug("null or empty api-key"); + return null; + } + return apiKeyDecryptMap.computeIfAbsent(encryptedKey, c -> { + log.debug("invoking AESUtils to decrypt api-key[{}] for broker[{}]", type, brokerId); + return AESUtils.decrypt(brokerEncryptPassword,c); + }); + } +} diff --git a/src/main/java/it/gov/pagopa/pu/organization/service/broker/BrokerService.java b/src/main/java/it/gov/pagopa/pu/organization/service/broker/BrokerService.java new file mode 100644 index 0000000..d112d6f --- /dev/null +++ b/src/main/java/it/gov/pagopa/pu/organization/service/broker/BrokerService.java @@ -0,0 +1,29 @@ +package it.gov.pagopa.pu.organization.service.broker; + +import it.gov.pagopa.pu.organization.dto.generated.BrokerApiKeys; +import it.gov.pagopa.pu.organization.model.Broker; +import it.gov.pagopa.pu.organization.repository.BrokerRepository; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.rest.webmvc.ResourceNotFoundException; +import org.springframework.stereotype.Service; + +@Service +@Slf4j +public class BrokerService { + + private final BrokerRepository brokerRepository; + private final BrokerEncryptionService brokerEncryptionService; + + public BrokerService( + BrokerRepository brokerRepository, + BrokerEncryptionService brokerEncryptionService) { + this.brokerEncryptionService = brokerEncryptionService; + this.brokerRepository = brokerRepository; + } + + public BrokerApiKeys getBrokerApiKeys(Long brokerId){ + Broker broker = brokerRepository.findById(brokerId).orElseThrow(() -> new ResourceNotFoundException("broker [%s]".formatted(brokerId))); + return brokerEncryptionService.getBrokerDecryptedApiKeys(broker); + } + +} diff --git a/src/main/java/it/gov/pagopa/pu/organization/util/AESUtils.java b/src/main/java/it/gov/pagopa/pu/organization/util/AESUtils.java new file mode 100644 index 0000000..ab9b48a --- /dev/null +++ b/src/main/java/it/gov/pagopa/pu/organization/util/AESUtils.java @@ -0,0 +1,162 @@ +package it.gov.pagopa.pu.organization.util; + +import javax.crypto.*; +import javax.crypto.spec.GCMParameterSpec; +import javax.crypto.spec.PBEKeySpec; +import javax.crypto.spec.SecretKeySpec; +import java.io.*; +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.StandardCopyOption; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.KeySpec; + +public class AESUtils { + private AESUtils() { + } + + private static final String ALGORITHM = "AES/GCM/NoPadding"; + private static final String FACTORY_INSTANCE = "PBKDF2WithHmacSHA256"; + private static final int TAG_LENGTH_BIT = 128; + private static final int IV_LENGTH_BYTE = 12; + private static final int SALT_LENGTH_BYTE = 16; + private static final String ALGORITHM_TYPE = "AES"; + private static final int KEY_LENGTH = 256; + private static final int ITERATION_COUNT = 65536; + private static final Charset UTF_8 = StandardCharsets.UTF_8; + + public static final String CIPHER_EXTENSION = ".cipher"; + + public static byte[] getRandomNonce(int length) { + byte[] nonce = new byte[length]; + new SecureRandom().nextBytes(nonce); + return nonce; + } + + public static SecretKey getSecretKey(String password, byte[] salt) { + KeySpec spec = new PBEKeySpec(password.toCharArray(), salt, ITERATION_COUNT, KEY_LENGTH); + + try { + SecretKeyFactory factory = SecretKeyFactory.getInstance(FACTORY_INSTANCE); + return new SecretKeySpec(factory.generateSecret(spec).getEncoded(), ALGORITHM_TYPE); + } catch (NoSuchAlgorithmException | InvalidKeySpecException e) { + throw new IllegalStateException("Cannot initialize cryptographic data", e); + } + } + + public static byte[] encrypt(String password, String plainMessage) { + byte[] salt = getRandomNonce(SALT_LENGTH_BYTE); + SecretKey secretKey = getSecretKey(password, salt); + + // GCM recommends 12 bytes iv + byte[] iv = getRandomNonce(IV_LENGTH_BYTE); + Cipher cipher = initCipher(Cipher.ENCRYPT_MODE, secretKey, iv); + + byte[] encryptedMessageByte = executeCipherOp(cipher, plainMessage.getBytes(UTF_8)); + + // prefix IV and Salt to cipher text + return ByteBuffer.allocate(iv.length + salt.length + encryptedMessageByte.length) + .put(iv) + .put(salt) + .put(encryptedMessageByte) + .array(); + } + + public static InputStream encrypt(String password, InputStream plainStream) { + byte[] salt = getRandomNonce(SALT_LENGTH_BYTE); + SecretKey secretKey = getSecretKey(password, salt); + + // GCM recommends 12 bytes iv + byte[] iv = getRandomNonce(IV_LENGTH_BYTE); + Cipher cipher = initCipher(Cipher.ENCRYPT_MODE, secretKey, iv); + + // prefix IV and Salt to cipher text + byte[] prefix = ByteBuffer.allocate(iv.length + salt.length) + .put(iv) + .put(salt) + .array(); + + return new SequenceInputStream( + new ByteArrayInputStream(prefix), + new CipherInputStream(new BufferedInputStream(plainStream), cipher)); + } + + public static File encrypt(String password, File plainFile) { + File cipherFile = new File(plainFile.getAbsolutePath() + CIPHER_EXTENSION); + try(FileInputStream fis = new FileInputStream(plainFile); + InputStream cipherStream = encrypt(password, fis)){ + Files.copy(cipherStream, cipherFile.toPath(), StandardCopyOption.REPLACE_EXISTING); + } catch (IOException e) { + throw new IllegalStateException("Something went wrong when ciphering input file " + plainFile.getAbsolutePath(), e); + } + return cipherFile; + } + + public static String decrypt(String password, byte[] cipherMessage) { + ByteBuffer byteBuffer = ByteBuffer.wrap(cipherMessage); + + byte[] iv = new byte[IV_LENGTH_BYTE]; + byteBuffer.get(iv); + + byte[] salt = new byte[SALT_LENGTH_BYTE]; + byteBuffer.get(salt); + + byte[] encryptedByte = new byte[byteBuffer.remaining()]; + byteBuffer.get(encryptedByte); + + SecretKey secretKey = getSecretKey(password, salt); + Cipher cipher = initCipher(Cipher.DECRYPT_MODE, secretKey, iv); + + byte[] decryptedMessageByte = executeCipherOp(cipher, encryptedByte); + return new String(decryptedMessageByte, UTF_8); + } + + public static InputStream decrypt(String password, InputStream cipherStream) { + try { + byte[] iv = cipherStream.readNBytes(IV_LENGTH_BYTE); + byte[] salt = cipherStream.readNBytes(SALT_LENGTH_BYTE); + + SecretKey secretKey = getSecretKey(password, salt); + Cipher cipher = initCipher(Cipher.DECRYPT_MODE, secretKey, iv); + + return new CipherInputStream(new BufferedInputStream(cipherStream), cipher); + } catch (IOException e) { + throw new IllegalStateException("Cannot read AES prefix data", e); + } + } + + public static void decrypt(String password, File cipherFile, File outputPlainFile) { + try(FileInputStream fis = new FileInputStream(cipherFile); + InputStream plainStream = decrypt(password, fis)){ + Files.copy(plainStream, outputPlainFile.toPath(), StandardCopyOption.REPLACE_EXISTING); + } catch (IOException e) { + throw new IllegalStateException("Something went wrong when deciphering input file " + cipherFile.getAbsolutePath(), e); + } + } + + private static byte[] executeCipherOp(Cipher cipher, byte[] encryptedByte) { + try { + return cipher.doFinal(encryptedByte); + } catch (IllegalBlockSizeException | BadPaddingException e) { + throw new IllegalStateException("Cannot execute cipher op", e); + } + } + + private static Cipher initCipher(int mode, SecretKey secretKey, byte[] iv) { + try { + Cipher cipher = Cipher.getInstance(ALGORITHM); + cipher.init(mode, secretKey, new GCMParameterSpec(TAG_LENGTH_BIT, iv)); + return cipher; + } catch (NoSuchPaddingException | NoSuchAlgorithmException | InvalidKeyException + | InvalidAlgorithmParameterException e) { + throw new IllegalStateException("Cannot initialize cipher data", e); + } + } + +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index ab2e363..0cc8f0d 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -1,23 +1,30 @@ spring: - application: - name: ${artifactId} - version: ${version} - jmx.enabled: true - datasource: - url: \${ORGANIZATION_DB_URL:jdbc:postgresql://\${ORGANIZATION_DB_HOST:localhost}:\${ORGANIZATION_DB_PORT:5432}/\${ORGANIZATION_DB_NAME:mypay}} - username: \${ORGANIZATION_DB_USER} - password: \${ORGANIZATION_DB_PASSWORD} - driverClassName: org.postgresql.Driver + application: + name: ${artifactId} + version: ${version} + jmx.enabled: true + datasource: + url: \${ORGANIZATION_DB_URL:jdbc:postgresql://\${ORGANIZATION_DB_HOST:localhost}:\${ORGANIZATION_DB_PORT:5432}/\${ORGANIZATION_DB_NAME:mypay}} + username: \${ORGANIZATION_DB_USER} + password: \${ORGANIZATION_DB_PASSWORD} + driverClassName: org.postgresql.Driver management: - endpoint: - health: - probes.enabled: true - group: - readiness.include: "*" - liveness.include: livenessState,diskSpace,ping - endpoints: - jmx: - exposure.include: "*" - web: - exposure.include: info, health + endpoint: + health: + probes.enabled: true + group: + readiness.include: "*" + liveness.include: livenessState,diskSpace,ping + endpoints: + jmx: + exposure.include: "*" + web: + exposure.include: info, health + +logging: + level: + it.gov.pagopa.pu.organization.exception.ControllerExceptionHandler: "\${LOGGING_LEVEL_CONTROLLER_EXCEPTION:INFO}" + +app: + brokerEncryptPassword: \${BROKER_ENCRYPT_PASSWORD} diff --git a/src/test/java/it/gov/pagopa/pu/organization/controller/BrokerControllerTest.java b/src/test/java/it/gov/pagopa/pu/organization/controller/BrokerControllerTest.java new file mode 100644 index 0000000..41cd302 --- /dev/null +++ b/src/test/java/it/gov/pagopa/pu/organization/controller/BrokerControllerTest.java @@ -0,0 +1,42 @@ +package it.gov.pagopa.pu.organization.controller; + +import it.gov.pagopa.pu.organization.dto.generated.BrokerApiKeys; +import it.gov.pagopa.pu.organization.service.broker.BrokerService; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.ResponseEntity; + +@ExtendWith(MockitoExtension.class) +class BrokerControllerTest { + + @Mock + private BrokerService brokerServiceMock; + + @InjectMocks + private BrokerController brokerController; + + private static final Long VALID_BROKER_ID = 1L; + + private static final BrokerApiKeys VALID_BROKER_API_KEYS = BrokerApiKeys.builder() + .syncKey("sync") + .acaKey("aca") + .gpdKey("gpd") + .build(); + + @Test + void givenValidBrokerWhenGetBrokerApiKeysThenOk(){ + //given + Mockito.when(brokerServiceMock.getBrokerApiKeys(VALID_BROKER_ID)).thenReturn(VALID_BROKER_API_KEYS); + //when + ResponseEntity response = brokerController.getBrokerApiKeys(VALID_BROKER_ID); + //verify + Assertions.assertNotNull(response); + Assertions.assertEquals(VALID_BROKER_API_KEYS, response.getBody()); + Mockito.verify(brokerServiceMock, Mockito.times(1)).getBrokerApiKeys(VALID_BROKER_ID); + } +} diff --git a/src/test/java/it/gov/pagopa/pu/organization/exception/ControllerExceptionHandlerTest.java b/src/test/java/it/gov/pagopa/pu/organization/exception/ControllerExceptionHandlerTest.java new file mode 100644 index 0000000..934ed5f --- /dev/null +++ b/src/test/java/it/gov/pagopa/pu/organization/exception/ControllerExceptionHandlerTest.java @@ -0,0 +1,44 @@ +package it.gov.pagopa.pu.organization.exception; + +import it.gov.pagopa.pu.organization.controller.BrokerController; +import it.gov.pagopa.pu.organization.service.broker.BrokerService; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; +import org.springframework.data.rest.webmvc.ResourceNotFoundException; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.test.web.servlet.result.MockMvcResultMatchers; + +@WebMvcTest +@Import({BrokerController.class}) +class ControllerExceptionHandlerTest { + + @MockBean + private BrokerService brokerServiceMock; + + @Autowired + private MockMvc mockMvc; + + @Test + void test() throws Exception{ + //given + Long invalidBrokerId = 1L; + String errorMessage = "broker [%s]".formatted(invalidBrokerId); + Mockito.when(brokerServiceMock.getBrokerApiKeys(invalidBrokerId)).thenThrow(new ResourceNotFoundException(errorMessage)); + //when + mockMvc.perform( + MockMvcRequestBuilders.get("/brokers/apiKey/{brokerId}", invalidBrokerId) ) + //verify + .andExpect(MockMvcResultMatchers.status().isNotFound()) + .andExpect(MockMvcResultMatchers.jsonPath("$.message") + .value("resource not found: %s".formatted(errorMessage))) + .andReturn(); + + Mockito.verify(brokerServiceMock, Mockito.times(1)).getBrokerApiKeys(invalidBrokerId); + } + +} diff --git a/src/test/java/it/gov/pagopa/pu/organization/service/broker/BrokerEncryptionServiceTest.java b/src/test/java/it/gov/pagopa/pu/organization/service/broker/BrokerEncryptionServiceTest.java new file mode 100644 index 0000000..92a60cc --- /dev/null +++ b/src/test/java/it/gov/pagopa/pu/organization/service/broker/BrokerEncryptionServiceTest.java @@ -0,0 +1,73 @@ +package it.gov.pagopa.pu.organization.service.broker; + +import it.gov.pagopa.pu.organization.dto.generated.BrokerApiKeys; +import it.gov.pagopa.pu.organization.model.Broker; +import it.gov.pagopa.pu.organization.util.AESUtils; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.MockedStatic; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.List; +import java.util.stream.Stream; + +@ExtendWith(MockitoExtension.class) +class BrokerEncryptionServiceTest { + + private static final String VALID_BROKER_ENCRYPT_PASSWORD = "VALID_PASSWORD"; + private static final byte[] VALID_ENCRYPTED_SYNC_PASSWORD = new byte[]{1, 2, 3}; + private static final byte[] VALID_ENCRYPTED_ACA_PASSWORD = new byte[]{4, 5, 6}; + private static final byte[] VALID_ENCRYPTED_GPD_PASSWORD = new byte[]{7, 8, 9}; + private static final Long VALID_BROKER_ID = 1L; + private static final Broker VALID_BROKER = Broker.builder() + .brokerId(VALID_BROKER_ID) + .syncKey(VALID_ENCRYPTED_SYNC_PASSWORD) + .acaKey(VALID_ENCRYPTED_ACA_PASSWORD) + .gpdKey(VALID_ENCRYPTED_GPD_PASSWORD) + .build(); + + private BrokerEncryptionService brokerEncryptionService; + + @BeforeEach + void setUp() { + brokerEncryptionService = new BrokerEncryptionService(VALID_BROKER_ENCRYPT_PASSWORD); + } + + @Test + void givenValidBrokerWhenGetBrokerDecryptedApiKeysThenOk(){ + //given + try (MockedStatic aesUtilsMock = Mockito.mockStatic(AESUtils.class)) { + Stream.of(VALID_ENCRYPTED_SYNC_PASSWORD, VALID_ENCRYPTED_ACA_PASSWORD, VALID_ENCRYPTED_GPD_PASSWORD).forEach( p -> + aesUtilsMock.when(() -> AESUtils.decrypt(VALID_BROKER_ENCRYPT_PASSWORD, p)) + .thenReturn(List.of(p).toString()) + ); + + //when + BrokerApiKeys response = brokerEncryptionService.getBrokerDecryptedApiKeys(VALID_BROKER); + + //verify + Assertions.assertEquals(List.of(VALID_ENCRYPTED_SYNC_PASSWORD).toString(),response.getSyncKey()); + Assertions.assertEquals(List.of(VALID_ENCRYPTED_ACA_PASSWORD).toString(),response.getAcaKey()); + Assertions.assertEquals(List.of(VALID_ENCRYPTED_GPD_PASSWORD).toString(),response.getGpdKey()); + aesUtilsMock.verify(() -> AESUtils.decrypt(Mockito.eq(VALID_BROKER_ENCRYPT_PASSWORD), Mockito.any(byte[].class)), Mockito.times(3)); + } + } + + @Test + void givenValidBrokerIdWithoutKeyWhenGetBrokerDecryptedApiKeysThenNullKey(){ + //given + Broker broker = Broker.builder().build(); + + //when + BrokerApiKeys response = brokerEncryptionService.getBrokerDecryptedApiKeys(broker); + + //verify + Assertions.assertNull(response.getSyncKey()); + Assertions.assertNull(response.getAcaKey()); + Assertions.assertNull(response.getGpdKey()); + } + +} diff --git a/src/test/java/it/gov/pagopa/pu/organization/service/broker/BrokerServiceTest.java b/src/test/java/it/gov/pagopa/pu/organization/service/broker/BrokerServiceTest.java new file mode 100644 index 0000000..11ffe55 --- /dev/null +++ b/src/test/java/it/gov/pagopa/pu/organization/service/broker/BrokerServiceTest.java @@ -0,0 +1,79 @@ +package it.gov.pagopa.pu.organization.service.broker; + +import it.gov.pagopa.pu.organization.dto.generated.BrokerApiKeys; +import it.gov.pagopa.pu.organization.model.Broker; +import it.gov.pagopa.pu.organization.repository.BrokerRepository; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.data.rest.webmvc.ResourceNotFoundException; + +import java.util.List; +import java.util.Optional; + +@ExtendWith(MockitoExtension.class) +class BrokerServiceTest { + + private static final byte[] VALID_ENCRYPTED_SYNC_PASSWORD = new byte[]{1, 2, 3}; + private static final byte[] VALID_ENCRYPTED_ACA_PASSWORD = new byte[]{4, 5, 6}; + private static final byte[] VALID_ENCRYPTED_GPD_PASSWORD = new byte[]{7, 8, 9}; + private static final Long VALID_BROKER_ID = 1L; + private static final Broker VALID_BROKER = Broker.builder() + .brokerId(VALID_BROKER_ID) + .syncKey(VALID_ENCRYPTED_SYNC_PASSWORD) + .acaKey(VALID_ENCRYPTED_ACA_PASSWORD) + .gpdKey(VALID_ENCRYPTED_GPD_PASSWORD) + .build(); + private static final BrokerApiKeys VALID_BROKER_API_KEYS = BrokerApiKeys.builder() + .syncKey(List.of(VALID_ENCRYPTED_SYNC_PASSWORD).toString()) + .acaKey(List.of(VALID_ENCRYPTED_ACA_PASSWORD).toString()) + .gpdKey(List.of(VALID_ENCRYPTED_GPD_PASSWORD).toString()) + .build(); + + @Mock + private BrokerRepository brokerRepositoryMock; + + @Mock + private BrokerEncryptionService brokerEncryptionService; + + private BrokerService brokerService; + + @BeforeEach + void setUp() { + brokerService = new BrokerService(brokerRepositoryMock, brokerEncryptionService); + } + + @Test + void givenValidBrokerIdWhenGetBrokerApiKeysThenOk(){ + //given + Mockito.when(brokerRepositoryMock.findById(VALID_BROKER_ID)).thenReturn(Optional.of(VALID_BROKER)); + Mockito.when(brokerEncryptionService.getBrokerDecryptedApiKeys(VALID_BROKER)).thenReturn(VALID_BROKER_API_KEYS); + + //when + BrokerApiKeys response = brokerService.getBrokerApiKeys(VALID_BROKER_ID); + + //verify + Assertions.assertEquals(List.of(VALID_ENCRYPTED_SYNC_PASSWORD).toString(),response.getSyncKey()); + Assertions.assertEquals(List.of(VALID_ENCRYPTED_ACA_PASSWORD).toString(),response.getAcaKey()); + Assertions.assertEquals(List.of(VALID_ENCRYPTED_GPD_PASSWORD).toString(),response.getGpdKey()); + Mockito.verify(brokerRepositoryMock, Mockito.times(1)).findById(VALID_BROKER_ID); + Mockito.verify(brokerEncryptionService, Mockito.times(1)).getBrokerDecryptedApiKeys(VALID_BROKER); + } + + @Test + void givenNotFoundBrokerIdWhenGetBrokerApiKeysThenException(){ + //given + String errorMessage = "broker [%s]".formatted(VALID_BROKER_ID); + Mockito.when(brokerRepositoryMock.findById(VALID_BROKER_ID)).thenThrow(new ResourceNotFoundException(errorMessage)); + + //when + ResourceNotFoundException exception = Assertions.assertThrows(ResourceNotFoundException.class, () -> brokerService.getBrokerApiKeys(VALID_BROKER_ID)); + + //verify + Assertions.assertEquals(errorMessage, exception.getMessage()); + } +} diff --git a/src/test/java/it/gov/pagopa/pu/organization/util/AESUtilsTest.java b/src/test/java/it/gov/pagopa/pu/organization/util/AESUtilsTest.java new file mode 100644 index 0000000..1477d5a --- /dev/null +++ b/src/test/java/it/gov/pagopa/pu/organization/util/AESUtilsTest.java @@ -0,0 +1,61 @@ +package it.gov.pagopa.pu.organization.util; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; + +class AESUtilsTest { + + @Test + void test() { + // Given + String plain = "PLAINTEXT"; + String psw = "PSW"; + + // When + byte[] cipher = AESUtils.encrypt(psw, plain); + String result = AESUtils.decrypt(psw, cipher); + + // Then + Assertions.assertEquals(plain, result); + } + + @Test + void testStream() throws IOException { + // Given + String plain = "PLAINTEXT"; + String psw = "PSW"; + + // When + InputStream cipherStream = AESUtils.encrypt(psw, new ByteArrayInputStream(plain.getBytes(StandardCharsets.UTF_8))); + InputStream resultStream = AESUtils.decrypt(psw, cipherStream); + + // Then + Assertions.assertEquals(plain, new String(resultStream.readAllBytes(), StandardCharsets.UTF_8)); + } + + @Test + void testFile() throws IOException { + // Given + String plain = "PLAINTEXT"; + Path plainFile = Path.of("build", "tmp", "plainFile.txt"); + Files.writeString(plainFile, plain); + String psw = "PSW"; + Path decryptedFile = plainFile.getParent().resolve("decryptedFile.txt"); + + // When + File cipherFile = AESUtils.encrypt(psw, plainFile.toFile()); + AESUtils.decrypt(psw, cipherFile, decryptedFile.toFile()); + + // Then + Assertions.assertEquals(Files.readAllLines(decryptedFile), List.of(plain)); + } +}