diff --git a/.github/workflows/build_container_non_develop_branch.yml b/.github/workflows/build_container_non_develop_branch.yml index a25f2ded9..7c8a0e695 100644 --- a/.github/workflows/build_container_non_develop_branch.yml +++ b/.github/workflows/build_container_non_develop_branch.yml @@ -53,8 +53,8 @@ jobs: - name: Build the Docker image run: | echo "${{ secrets.DOCKER_HUB_TOKEN }}" | docker login -u "${{ secrets.DOCKER_HUB_USERNAME }}" --password-stdin docker.io - docker build . --file .github/Dockerfile_PreBuild --tag docker.io/${{ env.DOCKER_HUB_ORGANIZATION }}/${{ env.DOCKER_HUB_REPOSITORY }}:$GITHUB_SHA --tag docker.io/${{ env.DOCKER_HUB_ORGANIZATION }}/${{ env.DOCKER_HUB_REPOSITORY }}:latest --tag docker.io/${{ env.DOCKER_HUB_ORGANIZATION }}/${{ env.DOCKER_HUB_REPOSITORY }}:develop - docker build . --file .github/Dockerfile_PreBuild_OC --tag docker.io/${{ env.DOCKER_HUB_ORGANIZATION }}/${{ env.DOCKER_HUB_REPOSITORY }}:$GITHUB_SHA-OC --tag docker.io/${{ env.DOCKER_HUB_ORGANIZATION }}/${{ env.DOCKER_HUB_REPOSITORY }}:latest-OC --tag docker.io/${{ env.DOCKER_HUB_ORGANIZATION }}/${{ env.DOCKER_HUB_REPOSITORY }}:develop-OC --tag docker.io/${{ env.DOCKER_HUB_ORGANIZATION }}/${{ env.DOCKER_HUB_REPOSITORY }}:${GITHUB_REF##*/}-OC + docker build . --file .github/Dockerfile_PreBuild --tag docker.io/${{ env.DOCKER_HUB_ORGANIZATION }}/${{ env.DOCKER_HUB_REPOSITORY }}:$GITHUB_SHA --tag docker.io/${{ env.DOCKER_HUB_ORGANIZATION }}/${{ env.DOCKER_HUB_REPOSITORY }}:${GITHUB_REF##*/} + docker build . --file .github/Dockerfile_PreBuild_OC --tag docker.io/${{ env.DOCKER_HUB_ORGANIZATION }}/${{ env.DOCKER_HUB_REPOSITORY }}:$GITHUB_SHA-OC --tag docker.io/${{ env.DOCKER_HUB_ORGANIZATION }}/${{ env.DOCKER_HUB_REPOSITORY }}:${GITHUB_REF##*/}-OC docker push docker.io/${{ env.DOCKER_HUB_ORGANIZATION }}/${{ env.DOCKER_HUB_REPOSITORY }} --all-tags echo docker done @@ -66,15 +66,11 @@ jobs: - name: Sign container image run: | cosign sign -y --key cosign.key \ - docker.io/${{ env.DOCKER_HUB_ORGANIZATION }}/${{ env.DOCKER_HUB_REPOSITORY }}:develop + docker.io/${{ env.DOCKER_HUB_ORGANIZATION }}/${{ env.DOCKER_HUB_REPOSITORY }}:${GITHUB_REF##*/} cosign sign -y --key cosign.key \ - docker.io/${{ env.DOCKER_HUB_ORGANIZATION }}/${{ env.DOCKER_HUB_REPOSITORY }}:latest + docker.io/${{ env.DOCKER_HUB_ORGANIZATION }}/${{ env.DOCKER_HUB_REPOSITORY }}:${GITHUB_REF##*/}-OC cosign sign -y --key cosign.key \ docker.io/${{ env.DOCKER_HUB_ORGANIZATION }}/${{ env.DOCKER_HUB_REPOSITORY }}:$GITHUB_SHA - cosign sign -y --key cosign.key \ - docker.io/${{ env.DOCKER_HUB_ORGANIZATION }}/${{ env.DOCKER_HUB_REPOSITORY }}:develop-OC - cosign sign -y --key cosign.key \ - docker.io/${{ env.DOCKER_HUB_ORGANIZATION }}/${{ env.DOCKER_HUB_REPOSITORY }}:latest-OC env: COSIGN_PASSWORD: "${{secrets.COSIGN_PASSWORD}}" diff --git a/README.md b/README.md index 008acde83..4e4e8d6c9 100644 --- a/README.md +++ b/README.md @@ -386,6 +386,19 @@ For SSL encryption we use JKS keystores. Note that both the keystore and the tru truststore.path=/path/to/api.truststore.jks ``` +## Using SSL Encryption with RabbitMq + +For SSL encryption we use JKS keystores. Note that both the keystore and the truststore (and all keys within) must have the same password for unlocking, for which the API will stop at boot up and ask for. + +* Edit your props file(s) to contain: + + ``` + rabbitmq.use.ssl=true + keystore.path=/path/to/api.keystore.jks + keystore.password=123456 + truststore.path=/path/to/api.truststore.jks + ``` + ## Using SSL Encryption with props file For SSL encryption we use jks keystores. diff --git a/obp-api/src/main/resources/props/sample.props.template b/obp-api/src/main/resources/props/sample.props.template index 0de450a9c..baf3f958d 100644 --- a/obp-api/src/main/resources/props/sample.props.template +++ b/obp-api/src/main/resources/props/sample.props.template @@ -153,6 +153,9 @@ jwt.use.ssl=false ## Enable SSL for kafka, if set to true must set paths for the keystore locations #kafka.use.ssl=true +## Enable SSL for rabbitmq, if set to true must set paths for the keystore locations +#rabbitmq.use.ssl=false + # Paths to the SSL keystore files - has to be jks #keystore.path=/path/to/api.keystore.jks #keystore password @@ -836,6 +839,10 @@ featured_apis=elasticSearchWarehouseV300 # rabbitmq_connector.port=5672 # rabbitmq_connector.username=obp # rabbitmq_connector.password=obp +# rabbitmq_connector.virtual_host=/ +# -- RabbitMQ Adapter -------------------------------------------- +#rabbitmq.adapter.enabled=false + # -- Scopes ----------------------------------------------------- diff --git a/obp-api/src/main/scala/bootstrap/liftweb/Boot.scala b/obp-api/src/main/scala/bootstrap/liftweb/Boot.scala index 0cb8ca002..6cc758f2a 100644 --- a/obp-api/src/main/scala/bootstrap/liftweb/Boot.scala +++ b/obp-api/src/main/scala/bootstrap/liftweb/Boot.scala @@ -339,6 +339,12 @@ class Boot extends MdcLoggable { } } + // start RabbitMq Adapter(using mapped connector as mockded CBS) + if (APIUtil.getPropsAsBoolValue("rabbitmq.adapter.enabled", false)) { + code.bankconnectors.rabbitmq.Adapter.startRabbitMqAdapter.main(Array("")) + } + + // Database query timeout // APIUtil.getPropsValue("database_query_timeout_in_seconds").map { timeoutInSeconds => // tryo(timeoutInSeconds.toInt).isDefined match { diff --git a/obp-api/src/main/scala/code/api/v2_1_0/APIMethods210.scala b/obp-api/src/main/scala/code/api/v2_1_0/APIMethods210.scala index 49bd34ae4..3213f2391 100644 --- a/obp-api/src/main/scala/code/api/v2_1_0/APIMethods210.scala +++ b/obp-api/src/main/scala/code/api/v2_1_0/APIMethods210.scala @@ -434,16 +434,14 @@ trait APIMethods210 { _ <- NewStyle.function.isEnabledTransactionRequests(callContext) _ <- Helper.booleanToFuture(InvalidAccountIdFormat, cc=callContext) {isValidID(accountId.value)} _ <- Helper.booleanToFuture(InvalidBankIdFormat, cc=callContext) {isValidID(bankId.value)} + _ <- Helper.booleanToFuture(s"${InvalidTransactionRequestType}: '${transactionRequestType.value}'", cc=callContext) { + APIUtil.getPropsValue("transactionRequests_supported_types", "").split(",").contains(transactionRequestType.value) + } (_, callContext) <- NewStyle.function.getBank(bankId, callContext) (fromAccount, callContext) <- NewStyle.function.checkBankAccountExists(bankId, accountId, callContext) account = BankIdAccountId(fromAccount.bankId, fromAccount.accountId) _ <- NewStyle.function.checkAuthorisationToCreateTransactionRequest(viewId, account, u, callContext) - - _ <- Helper.booleanToFuture(s"${InvalidTransactionRequestType}: '${transactionRequestType.value}'", cc=callContext) { - APIUtil.getPropsValue("transactionRequests_supported_types", "").split(",").contains(transactionRequestType.value) - } - // Check the input JSON format, here is just check the common parts of all four types transDetailsJson <- NewStyle.function.tryons(s"$InvalidJsonFormat The Json body should be the $TransactionRequestBodyCommonJSON ", 400, callContext) { json.extract[TransactionRequestBodyCommonJSON] diff --git a/obp-api/src/main/scala/code/bankconnectors/rabbitmq/Adapter/AdapterStubBuilder.scala b/obp-api/src/main/scala/code/bankconnectors/rabbitmq/Adapter/AdapterStubBuilder.scala index 2af366213..5b563549d 100644 --- a/obp-api/src/main/scala/code/bankconnectors/rabbitmq/Adapter/AdapterStubBuilder.scala +++ b/obp-api/src/main/scala/code/bankconnectors/rabbitmq/Adapter/AdapterStubBuilder.scala @@ -59,7 +59,7 @@ object AdapterStubBuilder { println("===================") val path = new File(getClass.getResource("").toURI.toString.replaceFirst("target/.*", "").replace("file:", ""), - "src/main/scala/code/bankconnectors/rabbitmq/Adapter/RPCServer.scala") + "src/main/scala/code/bankconnectors/rabbitmq/Adapter/MockedRabbitMqAdapter.scala") val source = FileUtils.readFileToString(path, "utf-8") val start = "//---------------- dynamic start -------------------please don't modify this line" val end = "//---------------- dynamic end ---------------------please don't modify this line" diff --git a/obp-api/src/main/scala/code/bankconnectors/rabbitmq/Adapter/RPCServer.scala b/obp-api/src/main/scala/code/bankconnectors/rabbitmq/Adapter/MockedRabbitMqAdapter.scala similarity index 98% rename from obp-api/src/main/scala/code/bankconnectors/rabbitmq/Adapter/RPCServer.scala rename to obp-api/src/main/scala/code/bankconnectors/rabbitmq/Adapter/MockedRabbitMqAdapter.scala index 3d2aa85b6..662a1c651 100644 --- a/obp-api/src/main/scala/code/bankconnectors/rabbitmq/Adapter/RPCServer.scala +++ b/obp-api/src/main/scala/code/bankconnectors/rabbitmq/Adapter/MockedRabbitMqAdapter.scala @@ -2,6 +2,7 @@ package code.bankconnectors.rabbitmq.Adapter import bootstrap.liftweb.ToSchemify import code.api.util.APIUtil +import code.bankconnectors.rabbitmq.RabbitMQUtils import com.openbankproject.commons.ExecutionContext.Implicits.global import com.openbankproject.commons.dto._ import com.openbankproject.commons.model._ @@ -14,10 +15,11 @@ import net.liftweb.mapper.Schemifier import scala.concurrent.Future import com.openbankproject.commons.ExecutionContext.Implicits.global - +import code.bankconnectors.rabbitmq.RabbitMQUtils._ import java.util.Date +import code.util.Helper.MdcLoggable -class ServerCallback(val ch: Channel) extends DeliverCallback { +class ServerCallback(val ch: Channel) extends DeliverCallback with MdcLoggable{ private implicit val formats = code.api.util.CustomJsonFormats.nullTolerateFormats @@ -32,7 +34,7 @@ class ServerCallback(val ch: Channel) extends DeliverCallback { .messageId(obpMessageId) .build val message = new String(delivery.getBody, "UTF-8") - println(s"Request: OutBound message from OBP: methodId($obpMessageId) : message is $message ") + logger.debug(s"Request: OutBound message from OBP: methodId($obpMessageId) : message is $message ") try { val responseToOBP = if (obpMessageId.contains("obp_get_banks")) { @@ -3052,10 +3054,10 @@ class ServerCallback(val ch: Channel) extends DeliverCallback { } response = responseToOBP.map(a => write(a)).map("" + _) - response.map(res => println(s"Response: inBound message to OBP: process($obpMessageId) : message is $res ")) + response.map(res => logger.debug(s"Response: inBound message to OBP: process($obpMessageId) : message is $res ")) response } catch { - case e: Throwable => println("Unknown exception: " + e.toString) + case e: Throwable => logger.error("Unknown exception: " + e.toString) } finally { response.map(res => ch.basicPublish("", delivery.getProperties.getReplyTo, replyProps, res.getBytes("UTF-8"))) @@ -3065,14 +3067,14 @@ class ServerCallback(val ch: Channel) extends DeliverCallback { } -object RPCServer extends App { +/** + * This is only for testing, not ready for production. + * use mapped connector as the bank CBS. + * Work in progress + */ +object MockedRabbitMqAdapter extends App with MdcLoggable{ private val RPC_QUEUE_NAME = "obp_rpc_queue" - // lazy initial RabbitMQ connection - val host = APIUtil.getPropsValue("rabbitmq_connector.host").openOrThrowException("mandatory property rabbitmq_connector.host is missing!") - val port = APIUtil.getPropsAsIntValue("rabbitmq_connector.port").openOrThrowException("mandatory property rabbitmq_connector.port is missing!") -// val username = APIUtil.getPropsValue("rabbitmq_connector.username").openOrThrowException("mandatory property rabbitmq_connector.username is missing!") -// val password = APIUtil.getPropsValue("rabbitmq_connector.password").openOrThrowException("mandatory property rabbitmq_connector.password is missing!") - + DB.defineConnectionManager(net.liftweb.util.DefaultConnectionIdentifier, APIUtil.vendor) Schemifier.schemify(true, Schemifier.infoF _, ToSchemify.models: _*) @@ -3083,8 +3085,18 @@ object RPCServer extends App { val factory = new ConnectionFactory() factory.setHost(host) factory.setPort(port) - factory.setUsername("server") - factory.setPassword("server") + factory.setUsername(username) + factory.setPassword(password) + factory.setVirtualHost(virtualHost) + if (APIUtil.getPropsAsBoolValue("rabbitmq.use.ssl", false)){ + factory.useSslProtocol(RabbitMQUtils.createSSLContext( + keystorePath, + keystorePassword, + truststorePath, + truststorePassword + )) + } + connection = factory.newConnection() channel = connection.createChannel() channel.queueDeclare(RPC_QUEUE_NAME, false, false, false, null) @@ -3092,17 +3104,32 @@ object RPCServer extends App { // stop after one consumed message since this is example code val serverCallback = new ServerCallback(channel) channel basicConsume(RPC_QUEUE_NAME, false, serverCallback, _ => {}) - println("Start awaiting OBP Connector Requests:") + logger.info("Start awaiting OBP Connector Requests:") } catch { case e: Exception => e.printStackTrace() } finally { if (connection != null) { try { - // connection.close() + // connection.close() //this is a temporary solution, we keep this connection open to wait for messages } catch { - case e: Exception => println(s"unknown Exception:$e") + case e: Exception => logger.error(s"unknown Exception:$e") } } } } + +/** + * This adapter is only for testing, not ready for the production + */ +object startRabbitMqAdapter { + def main(args: Array[String]): Unit = { + val thread = new Thread(new Runnable { + override def run(): Unit = { + MockedRabbitMqAdapter.main(Array.empty) + } + }) + thread.start() + thread.join() + } +} diff --git a/obp-api/src/main/scala/code/bankconnectors/rabbitmq/RabbitMQConnectionPool.scala b/obp-api/src/main/scala/code/bankconnectors/rabbitmq/RabbitMQConnectionPool.scala index 1ed336e82..489380b2d 100644 --- a/obp-api/src/main/scala/code/bankconnectors/rabbitmq/RabbitMQConnectionPool.scala +++ b/obp-api/src/main/scala/code/bankconnectors/rabbitmq/RabbitMQConnectionPool.scala @@ -8,22 +8,26 @@ import org.apache.commons.pool2.impl.{GenericObjectPool, GenericObjectPoolConfig import org.apache.commons.pool2.BasePooledObjectFactory import org.apache.commons.pool2.PooledObject import org.apache.commons.pool2.impl.DefaultPooledObject +import code.bankconnectors.rabbitmq.RabbitMQUtils._ -// Factory to create RabbitMQ connections class RabbitMQConnectionFactory extends BasePooledObjectFactory[Connection] { - - // lazy initial RabbitMQ connection - val host = APIUtil.getPropsValue("rabbitmq_connector.host").openOrThrowException("mandatory property rabbitmq_connector.host is missing!") - val port = APIUtil.getPropsAsIntValue("rabbitmq_connector.port").openOrThrowException("mandatory property rabbitmq_connector.port is missing!") - val username = APIUtil.getPropsValue("rabbitmq_connector.username").openOrThrowException("mandatory property rabbitmq_connector.username is missing!") - val password = APIUtil.getPropsValue("rabbitmq_connector.password").openOrThrowException("mandatory property rabbitmq_connector.password is missing!") - + private val factory = new ConnectionFactory() factory.setHost(host) factory.setPort(port) factory.setUsername(username) factory.setPassword(password) + factory.setVirtualHost(virtualHost) + if (APIUtil.getPropsAsBoolValue("rabbitmq.use.ssl", false)){ + factory.useSslProtocol(RabbitMQUtils.createSSLContext( + keystorePath, + keystorePassword, + truststorePath, + truststorePassword + )) + } + // Create a new RabbitMQ connection override def create(): Connection = factory.newConnection() diff --git a/obp-api/src/main/scala/code/bankconnectors/rabbitmq/RabbitMQUtils.scala b/obp-api/src/main/scala/code/bankconnectors/rabbitmq/RabbitMQUtils.scala index c6b5e5594..0fa125cb5 100644 --- a/obp-api/src/main/scala/code/bankconnectors/rabbitmq/RabbitMQUtils.scala +++ b/obp-api/src/main/scala/code/bankconnectors/rabbitmq/RabbitMQUtils.scala @@ -3,14 +3,17 @@ package code.bankconnectors.rabbitmq import code.api.util.ErrorMessages.AdapterUnknownError import code.bankconnectors.Connector import code.util.Helper.MdcLoggable +import code.api.util.APIUtil import com.openbankproject.commons.model.TopicTrait import net.liftweb.common.{Box, Empty, Failure, Full} import net.liftweb.json.Serialization.write import com.rabbitmq.client.AMQP.BasicProperties import com.rabbitmq.client._ - import java.util import java.util.UUID +import javax.net.ssl.{KeyManagerFactory, SSLContext, TrustManagerFactory} +import java.io.FileInputStream +import java.security.KeyStore import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.{Future, Promise} @@ -21,9 +24,20 @@ import scala.concurrent.{Future, Promise} */ object RabbitMQUtils extends MdcLoggable{ + val host = APIUtil.getPropsValue("rabbitmq_connector.host").openOrThrowException("mandatory property rabbitmq_connector.host is missing!") + val port = APIUtil.getPropsAsIntValue("rabbitmq_connector.port").openOrThrowException("mandatory property rabbitmq_connector.port is missing!") + val username = APIUtil.getPropsValue("rabbitmq_connector.username").openOrThrowException("mandatory property rabbitmq_connector.username is missing!") + val password = APIUtil.getPropsValue("rabbitmq_connector.password").openOrThrowException("mandatory property rabbitmq_connector.password is missing!") + val virtualHost = APIUtil.getPropsValue("rabbitmq_connector.virtual_host").openOrThrowException("mandatory property rabbitmq_connector.virtual_host is missing!") + + val keystorePath = APIUtil.getPropsValue("keystore.path").getOrElse("") + val keystorePassword = APIUtil.getPropsValue("keystore.password").getOrElse(APIUtil.initPasswd) + val truststorePath = APIUtil.getPropsValue("truststore.path").getOrElse("") + val truststorePassword = APIUtil.getPropsValue("keystore.password").getOrElse(APIUtil.initPasswd) + private implicit val formats = code.api.util.CustomJsonFormats.nullTolerateFormats - val requestQueueName: String = "obp_rpc_queue" + val RPC_QUEUE_NAME: String = "obp_rpc_queue" class ResponseCallback(val rabbitCorrelationId: String, channel: Channel) extends DeliverCallback { @@ -68,6 +82,7 @@ object RabbitMQUtils extends MdcLoggable{ val connection = RabbitMQConnectionPool.borrowConnection() val channel = connection.createChannel() // channel is not thread safe, so we always create new channel for each message. + channel.queueDeclare(RPC_QUEUE_NAME, false, false, false, null) val replyQueueName:String = channel.queueDeclare( "", // Queue name false, // durable: non-persistent @@ -87,7 +102,7 @@ object RabbitMQUtils extends MdcLoggable{ .correlationId(rabbitMQCorrelationId) .replyTo(replyQueueName) .build() - channel.basicPublish("", requestQueueName, rabbitMQProps, rabbitRequestJsonString.getBytes("UTF-8")) + channel.basicPublish("", RPC_QUEUE_NAME, rabbitMQProps, rabbitRequestJsonString.getBytes("UTF-8")) val responseCallback = new ResponseCallback(rabbitMQCorrelationId, channel) channel.basicConsume(replyQueueName, true, responseCallback, cancelCallback) @@ -107,4 +122,36 @@ object RabbitMQUtils extends MdcLoggable{ rabbitResponseJsonFuture.map(rabbitResponseJsonString =>logger.debug(s"${RabbitMQConnector_vOct2024.toString} inBoundJson: $messageId = $rabbitResponseJsonString" )) rabbitResponseJsonFuture.map(rabbitResponseJsonString =>Connector.extractAdapterResponse[T](rabbitResponseJsonString, Empty)) } + + def createSSLContext( + keystorePath: String, + keystorePassword: String, + truststorePath: String, + truststorePassword: String + ): SSLContext = { + // Load client keystore + val keyStore = KeyStore.getInstance("jks") + val keystoreFile = new FileInputStream(keystorePath) + keyStore.load(keystoreFile, keystorePassword.toCharArray) + keystoreFile.close() + // Set up KeyManagerFactory for client certificates + val kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm) + kmf.init(keyStore, keystorePassword.toCharArray) + + // Load truststore for CA certificates + val trustStore = KeyStore.getInstance("jks") + val truststoreFile = new FileInputStream(truststorePath) + trustStore.load(truststoreFile, truststorePassword.toCharArray) + truststoreFile.close() + + // Set up TrustManagerFactory for CA certificates + val tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm) + tmf.init(trustStore) + + // Initialize SSLContext + val sslContext = SSLContext.getInstance("TLSv1.3") + sslContext.init(kmf.getKeyManagers, tmf.getTrustManagers, null) + sslContext + } + } diff --git a/release_notes.md b/release_notes.md index 745511e28..f712348f7 100644 --- a/release_notes.md +++ b/release_notes.md @@ -3,6 +3,12 @@ ### Most recent changes at top of file ``` Date Commit Action +12/11/2024 d2e711b4 Added props rabbitmq_connector.virtual_host, default is /. + If you need to set it, please make sure you already add the virtual_host to the rabbitmq and grant the access to the user: + eg: run `rabbitmqctl add_vhost /obp/` => create the `/obp/` + and run `rabbitmqctl set_permissions -p /obp/ obp ".*" ".*" ".*"` => grant user `obp` the access permissions. +12/11/2024 d2e711b4 Added props rabbitmq.adapter.enabled, default is false +12/11/2024 a5253b4e Added props rabbitmq.use.ssl, default is false 30/10/2024 e69161b6 set V121, V130 and V200 status to DEPRECATED 29/10/2024 c83032f0 added the props for RabbitMq connector: Added props rabbitmq_connector.host=localhost