From 7f1eeda007e710a3a4f2548c9f23ba20a9f1b048 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Tue, 19 Nov 2024 20:45:34 +0100 Subject: [PATCH 01/11] feature/Bump OBP-API to Scala version 2.12.20 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 270f475b7..675e43a8d 100644 --- a/pom.xml +++ b/pom.xml @@ -11,7 +11,7 @@ 2011 2.12 - 2.12.12 + 2.12.20 2.5.32 2.0.5 1.1.0 From ed19bfb4cdc169d6b4b0fe49d7304c50f6c02115 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Wed, 20 Nov 2024 10:54:07 +0100 Subject: [PATCH 02/11] feature/Add Embedded Keycloak Testing --- obp-api/pom.xml | 8 +++ .../code/container/EmbeddedKeycloak.scala | 66 +++++++++++++++++++ obp-commons/pom.xml | 1 + pom.xml | 1 + 4 files changed, 76 insertions(+) create mode 100644 obp-api/src/test/scala/code/container/EmbeddedKeycloak.scala diff --git a/obp-api/pom.xml b/obp-api/pom.xml index f5f88cc8a..c79e540c0 100644 --- a/obp-api/pom.xml +++ b/obp-api/pom.xml @@ -515,6 +515,13 @@ test + + com.github.dasniko + testcontainers-keycloak + 3.5.1 + test + + @@ -525,6 +532,7 @@ ${scala.version} true + --add-opens java.base/java.lang=ALL-UNNAMED diff --git a/obp-api/src/test/scala/code/container/EmbeddedKeycloak.scala b/obp-api/src/test/scala/code/container/EmbeddedKeycloak.scala new file mode 100644 index 000000000..40d1f2634 --- /dev/null +++ b/obp-api/src/test/scala/code/container/EmbeddedKeycloak.scala @@ -0,0 +1,66 @@ +package code.container + +import code.api.v5_0_0.V500ServerSetup +import code.setup.DefaultUsers +import dasniko.testcontainers.keycloak.KeycloakContainer +import org.keycloak.TokenVerifier +import org.keycloak.admin.client.Keycloak +import org.keycloak.representations.{AccessToken, AccessTokenResponse} +import org.scalatest.Ignore + +@Ignore +class EmbeddedKeycloak extends V500ServerSetup with DefaultUsers { + + val keycloakContainer = new KeycloakContainer("quay.io/keycloak/keycloak:26.0") + // It registers a shutdown hook, which is a block of code (or function) that runs when the application terminates, + // - either normally(e.g., when the main method completes) + // - or due to an external signal(e.g., Ctrl + C or termination by the operating system). + sys.addShutdownHook { + keycloakContainer.stop() + } + override def beforeAll(): Unit = { + super.beforeAll() + // Start RabbitMQ container + keycloakContainer.start() + } + + override def afterAll(): Unit = { + super.afterAll() + keycloakContainer.stop() + } + + feature(s"test EmbeddedKeycloak") { + scenario("Start and Stop") { + val authServerUrl = keycloakContainer.getAuthServerUrl + println(s"Keycloak server running at: $authServerUrl") + val adminUsername = keycloakContainer.getAdminUsername + println(s"Keycloak admin username: $adminUsername") + val adminPassword = keycloakContainer.getAdminPassword + println(s"Keycloak admin password: $adminPassword") + + + // Assume KEYCLOAK is an instance of Keycloak + val keycloakClient: Keycloak = keycloakContainer.getKeycloakAdminClient() + + // Grant token + keycloakClient.tokenManager().grantToken() + + // Refresh token + keycloakClient.tokenManager().refreshToken() + + // Get access token response + val tokenResponse: AccessTokenResponse = keycloakClient.tokenManager().getAccessToken() + + // Parse the received access token + val verifier = TokenVerifier.create(tokenResponse.getToken, classOf[AccessToken]) + verifier.parse() + + // Retrieve + val accessToken: AccessToken = verifier.getToken + + println(s"Access Token Issuer: ${accessToken.getIssuer}") + + } + } + +} \ No newline at end of file diff --git a/obp-commons/pom.xml b/obp-commons/pom.xml index c7f68bad4..81c407f9b 100644 --- a/obp-commons/pom.xml +++ b/obp-commons/pom.xml @@ -111,6 +111,7 @@ ${scala.version} true + --add-opens java.base/java.lang=ALL-UNNAMED diff --git a/pom.xml b/pom.xml index 675e43a8d..b5d12a924 100644 --- a/pom.xml +++ b/pom.xml @@ -178,6 +178,7 @@ ${scala.version} true + --add-opens java.base/java.lang=ALL-UNNAMED From b07d2ea88ccde09a96da2a9ec62ff0e475c06b43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Thu, 21 Nov 2024 09:17:52 +0100 Subject: [PATCH 03/11] Revert "feature/Add Embedded Keycloak Testing" This reverts commit ed19bfb4cdc169d6b4b0fe49d7304c50f6c02115. --- obp-api/pom.xml | 8 --- .../code/container/EmbeddedKeycloak.scala | 66 ------------------- obp-commons/pom.xml | 1 - pom.xml | 1 - 4 files changed, 76 deletions(-) delete mode 100644 obp-api/src/test/scala/code/container/EmbeddedKeycloak.scala diff --git a/obp-api/pom.xml b/obp-api/pom.xml index c79e540c0..f5f88cc8a 100644 --- a/obp-api/pom.xml +++ b/obp-api/pom.xml @@ -515,13 +515,6 @@ test - - com.github.dasniko - testcontainers-keycloak - 3.5.1 - test - - @@ -532,7 +525,6 @@ ${scala.version} true - --add-opens java.base/java.lang=ALL-UNNAMED diff --git a/obp-api/src/test/scala/code/container/EmbeddedKeycloak.scala b/obp-api/src/test/scala/code/container/EmbeddedKeycloak.scala deleted file mode 100644 index 40d1f2634..000000000 --- a/obp-api/src/test/scala/code/container/EmbeddedKeycloak.scala +++ /dev/null @@ -1,66 +0,0 @@ -package code.container - -import code.api.v5_0_0.V500ServerSetup -import code.setup.DefaultUsers -import dasniko.testcontainers.keycloak.KeycloakContainer -import org.keycloak.TokenVerifier -import org.keycloak.admin.client.Keycloak -import org.keycloak.representations.{AccessToken, AccessTokenResponse} -import org.scalatest.Ignore - -@Ignore -class EmbeddedKeycloak extends V500ServerSetup with DefaultUsers { - - val keycloakContainer = new KeycloakContainer("quay.io/keycloak/keycloak:26.0") - // It registers a shutdown hook, which is a block of code (or function) that runs when the application terminates, - // - either normally(e.g., when the main method completes) - // - or due to an external signal(e.g., Ctrl + C or termination by the operating system). - sys.addShutdownHook { - keycloakContainer.stop() - } - override def beforeAll(): Unit = { - super.beforeAll() - // Start RabbitMQ container - keycloakContainer.start() - } - - override def afterAll(): Unit = { - super.afterAll() - keycloakContainer.stop() - } - - feature(s"test EmbeddedKeycloak") { - scenario("Start and Stop") { - val authServerUrl = keycloakContainer.getAuthServerUrl - println(s"Keycloak server running at: $authServerUrl") - val adminUsername = keycloakContainer.getAdminUsername - println(s"Keycloak admin username: $adminUsername") - val adminPassword = keycloakContainer.getAdminPassword - println(s"Keycloak admin password: $adminPassword") - - - // Assume KEYCLOAK is an instance of Keycloak - val keycloakClient: Keycloak = keycloakContainer.getKeycloakAdminClient() - - // Grant token - keycloakClient.tokenManager().grantToken() - - // Refresh token - keycloakClient.tokenManager().refreshToken() - - // Get access token response - val tokenResponse: AccessTokenResponse = keycloakClient.tokenManager().getAccessToken() - - // Parse the received access token - val verifier = TokenVerifier.create(tokenResponse.getToken, classOf[AccessToken]) - verifier.parse() - - // Retrieve - val accessToken: AccessToken = verifier.getToken - - println(s"Access Token Issuer: ${accessToken.getIssuer}") - - } - } - -} \ No newline at end of file diff --git a/obp-commons/pom.xml b/obp-commons/pom.xml index 81c407f9b..c7f68bad4 100644 --- a/obp-commons/pom.xml +++ b/obp-commons/pom.xml @@ -111,7 +111,6 @@ ${scala.version} true - --add-opens java.base/java.lang=ALL-UNNAMED diff --git a/pom.xml b/pom.xml index b5d12a924..675e43a8d 100644 --- a/pom.xml +++ b/pom.xml @@ -178,7 +178,6 @@ ${scala.version} true - --add-opens java.base/java.lang=ALL-UNNAMED From 3c8f44ebe78cbbd5ada0052e7a0aa1b95f4b3d3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Thu, 21 Nov 2024 13:34:46 +0100 Subject: [PATCH 04/11] Revert "feature/Bump OBP-API to Scala version 2.12.20" This reverts commit 7f1eeda007e710a3a4f2548c9f23ba20a9f1b048. --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 675e43a8d..270f475b7 100644 --- a/pom.xml +++ b/pom.xml @@ -11,7 +11,7 @@ 2011 2.12 - 2.12.20 + 2.12.12 2.5.32 2.0.5 1.1.0 From 791908f3102b897dd6a946003297b04dd947e0a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Thu, 21 Nov 2024 13:57:39 +0100 Subject: [PATCH 05/11] refactor/Remove dependency sun.security.provider.X509Factory --- .../main/scala/code/api/constant/constant.scala | 5 ++++- .../scala/code/api/util/CertificateUtil.scala | 16 ++++++++-------- .../src/main/scala/code/api/util/JwsUtil.scala | 8 +++----- obp-api/src/test/scala/RunMTLSWebApp.scala | 8 +++----- obp-api/src/test/scala/RunTLSWebApp.scala | 14 ++++++-------- 5 files changed, 24 insertions(+), 27 deletions(-) diff --git a/obp-api/src/main/scala/code/api/constant/constant.scala b/obp-api/src/main/scala/code/api/constant/constant.scala index b5f73bdfe..c1dcb2d38 100644 --- a/obp-api/src/main/scala/code/api/constant/constant.scala +++ b/obp-api/src/main/scala/code/api/constant/constant.scala @@ -119,7 +119,10 @@ object Constant extends MdcLoggable { } - +object CertificateConstants { + final val BEGIN_CERT: String = "-----BEGIN CERTIFICATE-----" + final val END_CERT: String = "-----END CERTIFICATE-----" +} object JedisMethod extends Enumeration { type JedisMethod = Value diff --git a/obp-api/src/main/scala/code/api/util/CertificateUtil.scala b/obp-api/src/main/scala/code/api/util/CertificateUtil.scala index cfa38eae2..fae8be114 100644 --- a/obp-api/src/main/scala/code/api/util/CertificateUtil.scala +++ b/obp-api/src/main/scala/code/api/util/CertificateUtil.scala @@ -1,10 +1,6 @@ package code.api.util -import java.io.{FileInputStream, IOException} -import java.security.cert.{Certificate, CertificateException, X509Certificate} -import java.security.interfaces.{RSAPrivateKey, RSAPublicKey} -import java.security.{PublicKey, _} - +import code.api.CertificateConstants import code.api.util.CryptoSystem.CryptoSystem import code.api.util.SelfSignedCertificateUtil.generateSelfSignedCert import code.util.Helper.MdcLoggable @@ -13,7 +9,11 @@ import com.nimbusds.jose.crypto.{MACSigner, RSAEncrypter, RSASSASigner} import com.nimbusds.jose.util.X509CertUtils import com.nimbusds.jwt.{EncryptedJWT, JWTClaimsSet} import net.liftweb.util.Props -import org.bouncycastle.operator.OperatorCreationException + +import java.io.{FileInputStream, IOException} +import java.security.cert.{Certificate, CertificateException, X509Certificate} +import java.security.interfaces.{RSAPrivateKey, RSAPublicKey} +import java.security._ object CryptoSystem extends Enumeration { @@ -227,8 +227,8 @@ object CertificateUtil extends MdcLoggable { // Remove all whitespace characters including spaces, tabs, newlines, and carriage returns def normalizePemX509Certificate(pem: String): String = { - val pemHeader = "-----BEGIN CERTIFICATE-----" - val pemFooter = "-----END CERTIFICATE-----" + val pemHeader = CertificateConstants.BEGIN_CERT + val pemFooter = CertificateConstants.END_CERT def extractContent(pem: String): Option[String] = { val start = pem.indexOf(pemHeader) diff --git a/obp-api/src/main/scala/code/api/util/JwsUtil.scala b/obp-api/src/main/scala/code/api/util/JwsUtil.scala index e07fefe5a..8503ebf9b 100644 --- a/obp-api/src/main/scala/code/api/util/JwsUtil.scala +++ b/obp-api/src/main/scala/code/api/util/JwsUtil.scala @@ -4,8 +4,7 @@ import java.security.interfaces.RSAPublicKey import java.time.format.DateTimeFormatter import java.time.{Duration, ZoneOffset, ZonedDateTime} import java.util - -import code.api.Constant +import code.api.{CertificateConstants, Constant} import code.util.Helper.MdcLoggable import com.nimbusds.jose.crypto.RSASSAVerifier import com.nimbusds.jose.jwk.JWK @@ -16,7 +15,6 @@ import net.liftweb.common.{Box, Failure, Full} import net.liftweb.http.provider.HTTPParam import net.liftweb.json import net.liftweb.util.SecurityHelpers -import sun.security.provider.X509Factory import scala.collection.immutable.{HashMap, List} import scala.jdk.CollectionConverters.seqAsJavaListConverter @@ -164,9 +162,9 @@ object JwsUtil extends MdcLoggable { header.x5c.map(_.headOption.getOrElse("None")).getOrElse("None") case None => "None" } - s"""${X509Factory.BEGIN_CERT} + s"""${CertificateConstants.BEGIN_CERT} |$x5c - |${X509Factory.END_CERT} + |${CertificateConstants.END_CERT} |""".stripMargin } diff --git a/obp-api/src/test/scala/RunMTLSWebApp.scala b/obp-api/src/test/scala/RunMTLSWebApp.scala index 5043518a6..06381729b 100644 --- a/obp-api/src/test/scala/RunMTLSWebApp.scala +++ b/obp-api/src/test/scala/RunMTLSWebApp.scala @@ -27,9 +27,8 @@ TESOBE (http://www.tesobe.com/) import java.lang.reflect.{Proxy => JProxy} import java.security.cert.X509Certificate - import bootstrap.liftweb.Boot -import code.api.{Constant, RequestHeader} +import code.api.{CertificateConstants, Constant, RequestHeader} import code.api.util.APIUtil import code.setup.PropsProgrammatically import net.liftweb.http.LiftRules @@ -38,7 +37,6 @@ import org.apache.commons.codec.binary.Base64 import org.eclipse.jetty.server._ import org.eclipse.jetty.util.ssl.SslContextFactory import org.eclipse.jetty.webapp.WebAppContext -import sun.security.provider.X509Factory object RunMTLSWebApp extends App with PropsProgrammatically { val servletContextPath = "/" @@ -76,9 +74,9 @@ object RunMTLSWebApp extends App with PropsProgrammatically { ) ).trim val certificate = - s"""${X509Factory.BEGIN_CERT} + s"""${CertificateConstants.BEGIN_CERT} |$content - |${X509Factory.END_CERT} + |${CertificateConstants.END_CERT} |""".stripMargin httpFields.add(RequestHeader.`PSD2-CERT`, certificate) } diff --git a/obp-api/src/test/scala/RunTLSWebApp.scala b/obp-api/src/test/scala/RunTLSWebApp.scala index 0b31e39f8..dc4f7afff 100644 --- a/obp-api/src/test/scala/RunTLSWebApp.scala +++ b/obp-api/src/test/scala/RunTLSWebApp.scala @@ -25,12 +25,8 @@ TESOBE (http://www.tesobe.com/) */ -import java.lang.reflect.{Proxy => JProxy} -import java.security.cert.X509Certificate - import bootstrap.liftweb.Boot -import code.api.util.APIUtil -import code.api.{Constant, RequestHeader} +import code.api.{CertificateConstants, Constant, RequestHeader} import code.setup.PropsProgrammatically import net.liftweb.http.LiftRules import net.liftweb.http.provider.HTTPContext @@ -38,7 +34,9 @@ import org.apache.commons.codec.binary.Base64 import org.eclipse.jetty.server._ import org.eclipse.jetty.util.ssl.SslContextFactory import org.eclipse.jetty.webapp.WebAppContext -import sun.security.provider.X509Factory + +import java.lang.reflect.{Proxy => JProxy} +import java.security.cert.X509Certificate object RunTLSWebApp extends App with PropsProgrammatically { val servletContextPath = "/" @@ -76,9 +74,9 @@ object RunTLSWebApp extends App with PropsProgrammatically { ) ).trim val certificate = - s"""${X509Factory.BEGIN_CERT} + s"""${CertificateConstants.BEGIN_CERT} |$content - |${X509Factory.END_CERT} + |${CertificateConstants.END_CERT} |""".stripMargin httpFields.add(RequestHeader.`PSD2-CERT`, certificate) } From 81bdf9ef0a3d0ec68cab58a0722fac1d9984f8d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Sat, 23 Nov 2024 11:56:12 +0100 Subject: [PATCH 06/11] feature/Enable mTLS for Redis --- .../resources/props/sample.props.template | 10 ++++ .../src/main/scala/code/api/cache/Redis.scala | 54 ++++++++++++++++++- 2 files changed, 63 insertions(+), 1 deletion(-) diff --git a/obp-api/src/main/resources/props/sample.props.template b/obp-api/src/main/resources/props/sample.props.template index 051bd4e4b..378d84353 100644 --- a/obp-api/src/main/resources/props/sample.props.template +++ b/obp-api/src/main/resources/props/sample.props.template @@ -166,6 +166,16 @@ jwt.use.ssl=false #truststore.path=/path/to/api.truststore.jks +## Enable mTLS for Redis, if set to true must set paths for the keystore and truststore locations +# redis.use.ssl=false +# Client +# keystore.path.redis = client-keystore.p12 +# keystore.password.redis = keystore-password +# Server +# truststore.path.redis = path/to/ca.jks +# truststore.password.redis = truststore-password + + ## Enable writing API metrics (which APIs are called) to RDBMS write_metrics=true ## Enable writing connector metrics (which methods are called)to RDBMS diff --git a/obp-api/src/main/scala/code/api/cache/Redis.scala b/obp-api/src/main/scala/code/api/cache/Redis.scala index 6e271f387..3ff88914d 100644 --- a/obp-api/src/main/scala/code/api/cache/Redis.scala +++ b/obp-api/src/main/scala/code/api/cache/Redis.scala @@ -10,6 +10,13 @@ import scalacache.{Flags, ScalaCache} import scalacache.redis.RedisCache import scalacache.serialization.Codec +import redis.clients.jedis.{Jedis, JedisPool, JedisPoolConfig} +import java.net.URI +import javax.net.ssl.{KeyManagerFactory, SSLContext, TrustManagerFactory} +import java.io.FileInputStream +import java.security.KeyStore +import com.typesafe.config.{Config, ConfigFactory} + import scala.concurrent.Future import scala.concurrent.duration.Duration import scala.language.postfixOps @@ -18,6 +25,9 @@ object Redis extends MdcLoggable { val url = APIUtil.getPropsValue("cache.redis.url", "127.0.0.1") val port = APIUtil.getPropsAsIntValue("cache.redis.port", 6379) + val timeout = 4000 + val password: String = null // Replace with password if authentication is needed + val useSsl = APIUtil.getPropsAsBoolValue("redis.use.ssl", false) final val poolConfig = new JedisPoolConfig() poolConfig.setMaxTotal(128) @@ -31,8 +41,50 @@ object Redis extends MdcLoggable { poolConfig.setNumTestsPerEvictionRun(3) poolConfig.setBlockWhenExhausted(true) + val jedisPool = + if (useSsl) { + // SSL connection: Use SSLContext with JedisPool + val sslContext = configureSslContext() + new JedisPool(poolConfig, url, port, timeout, password, true, sslContext.getSocketFactory, null, null) + } else { + // Non-SSL connection + new JedisPool(poolConfig, url, port, timeout, password) + } + def jedisPoolDestroy: Unit = jedisPool.destroy() - val jedisPool = new JedisPool(poolConfig,url, port, 4000) + + private def configureSslContext(): SSLContext = { + + // Load the CA certificate + val trustStore = KeyStore.getInstance("JKS") + val trustStorePassword = APIUtil.getPropsValue("keystore.password.redis") + .getOrElse(APIUtil.initPasswd).toCharArray + val truststorePath = APIUtil.getPropsValue("truststore.path.redis").getOrElse("") + val trustStoreStream = new FileInputStream(truststorePath) + trustStore.load(trustStoreStream, trustStorePassword) + trustStoreStream.close() + + // Load the client certificate and private key + val keyStore = KeyStore.getInstance("PKCS12") + val keyStorePassword = APIUtil.getPropsValue("keystore.password.redis") + .getOrElse(APIUtil.initPasswd).toCharArray + val keystorePath = APIUtil.getPropsValue("keystore.path.redis").getOrElse("") + val keyStoreStream = new FileInputStream(keystorePath) + keyStore.load(keyStoreStream, keyStorePassword) + keyStoreStream.close() + + // Initialize KeyManager and TrustManager + val keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm) + keyManagerFactory.init(keyStore, keyStorePassword) + + val trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm) + trustManagerFactory.init(trustStore) + + // Configure and return the SSLContext + val sslContext = SSLContext.getInstance("TLS") + sslContext.init(keyManagerFactory.getKeyManagers, trustManagerFactory.getTrustManagers, null) + sslContext + } /** * this is the help method, which can be used to auto close all the jedisConnection From b79ea1cbbd09864da9582bdd43b42c13356321d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Sun, 24 Nov 2024 16:42:55 +0100 Subject: [PATCH 07/11] refactor/Enable mTLS for Redis, use p12 instead of jks --- obp-api/src/main/resources/props/sample.props.template | 10 ++++++---- obp-api/src/main/scala/code/api/cache/Redis.scala | 2 +- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/obp-api/src/main/resources/props/sample.props.template b/obp-api/src/main/resources/props/sample.props.template index 378d84353..4124ed2c1 100644 --- a/obp-api/src/main/resources/props/sample.props.template +++ b/obp-api/src/main/resources/props/sample.props.template @@ -168,11 +168,13 @@ jwt.use.ssl=false ## Enable mTLS for Redis, if set to true must set paths for the keystore and truststore locations # redis.use.ssl=false -# Client -# keystore.path.redis = client-keystore.p12 +## Client +## PKCS#12 Format: combine private keys and certificates into .p12 files for easier transport +# keystore.path.redis = path/to/client-keystore.p12 # keystore.password.redis = keystore-password -# Server -# truststore.path.redis = path/to/ca.jks +## Trust stores is a list of trusted CA certificates +## Public certificate for the CA (used by clients and servers to validate signatures) +# truststore.path.redis = path/to/ca.p12 # truststore.password.redis = truststore-password diff --git a/obp-api/src/main/scala/code/api/cache/Redis.scala b/obp-api/src/main/scala/code/api/cache/Redis.scala index 3ff88914d..ede6ba533 100644 --- a/obp-api/src/main/scala/code/api/cache/Redis.scala +++ b/obp-api/src/main/scala/code/api/cache/Redis.scala @@ -56,7 +56,7 @@ object Redis extends MdcLoggable { private def configureSslContext(): SSLContext = { // Load the CA certificate - val trustStore = KeyStore.getInstance("JKS") + val trustStore = KeyStore.getInstance("PKCS12") val trustStorePassword = APIUtil.getPropsValue("keystore.password.redis") .getOrElse(APIUtil.initPasswd).toCharArray val truststorePath = APIUtil.getPropsValue("truststore.path.redis").getOrElse("") From 3d8c00842de4b027aa4bbeb1bc6dfa245c4e6b48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Tue, 26 Nov 2024 09:18:46 +0100 Subject: [PATCH 08/11] feature/Add endpoint getAccountsHeldByUserAtBank v5.1.0 --- .../main/scala/code/api/util/ApiRole.scala | 5 ++ .../scala/code/api/v5_1_0/APIMethods510.scala | 49 +++++++++++++++++++ .../scala/code/api/v5_1_0/AccountTest.scala | 26 ++++++++-- 3 files changed, 77 insertions(+), 3 deletions(-) diff --git a/obp-api/src/main/scala/code/api/util/ApiRole.scala b/obp-api/src/main/scala/code/api/util/ApiRole.scala index 99b157654..95be8e467 100644 --- a/obp-api/src/main/scala/code/api/util/ApiRole.scala +++ b/obp-api/src/main/scala/code/api/util/ApiRole.scala @@ -66,6 +66,11 @@ object RoleCombination { object ApiRole extends MdcLoggable{ + case class CanGetAccountsHeldAtOneBank(requiresBankId: Boolean = true) extends ApiRole + lazy val canGetAccountsHeldAtOneBank: CanGetAccountsHeldAtOneBank = CanGetAccountsHeldAtOneBank() + case class CanGetAccountsHeldAtAnyBank(requiresBankId: Boolean = false) extends ApiRole + lazy val canGetAccountsHeldAtAnyBank: CanGetAccountsHeldAtAnyBank = CanGetAccountsHeldAtAnyBank() + case class CanCreateRegulatedEntity(requiresBankId: Boolean = false) extends ApiRole lazy val canCreateRegulatedEntity = CanCreateRegulatedEntity() case class CanDeleteRegulatedEntity(requiresBankId: Boolean = false) extends ApiRole diff --git a/obp-api/src/main/scala/code/api/v5_1_0/APIMethods510.scala b/obp-api/src/main/scala/code/api/v5_1_0/APIMethods510.scala index 070307a93..ea20981cd 100644 --- a/obp-api/src/main/scala/code/api/v5_1_0/APIMethods510.scala +++ b/obp-api/src/main/scala/code/api/v5_1_0/APIMethods510.scala @@ -16,6 +16,7 @@ import code.api.util._ import code.api.util.newstyle.BalanceNewStyle import code.api.util.newstyle.Consumer.createConsumerNewStyle import code.api.util.newstyle.RegulatedEntityNewStyle.{createRegulatedEntityNewStyle, deleteRegulatedEntityNewStyle, getRegulatedEntitiesNewStyle, getRegulatedEntityByEntityIdNewStyle} +import code.api.v2_0_0.AccountsHelper.{accountTypeFilterText, getFilteredCoreAccounts} import code.api.v2_1_0.ConsumerRedirectUrlJSON import code.api.v3_0_0.JSONFactory300 import code.api.v3_0_0.JSONFactory300.createAggregateMetricJson @@ -624,6 +625,54 @@ trait APIMethods510 { } + staticResourceDocs += ResourceDoc( + getAccountsHeldByUserAtBank, + implementedInApiVersion, + nameOf(getAccountsHeldByUserAtBank), + "GET", + "/banks/BANK_ID/users/USER_ID/accounts-held", + "Get Accounts Held By User", + s"""Get Accounts held by the User if even the User has not been assigned the owner View yet. + | + |Can be used to onboard the account to the API - since all other account and transaction endpoints require views to be assigned. + | + |${accountTypeFilterText("/banks/BANK_ID/users/USER_ID/accounts-held")} + | + | + | + """.stripMargin, + EmptyBody, + coreAccountsHeldJsonV300, + List( + $UserNotLoggedIn, + $BankNotFound, + UserNotFoundByUserId, + UnknownError + ), + List(apiTagAccount), + Some(List(canGetAccountsHeldAtOneBank, canGetAccountsHeldAtAnyBank)) + ) + + lazy val getAccountsHeldByUserAtBank: OBPEndpoint = { + case "banks" :: BankId(bankId) :: "users" :: userId :: "accounts-held" :: Nil JsonGet req => { + cc => + implicit val ec = EndpointContext(Some(cc)) + for { + (u, callContext) <- NewStyle.function.getUserByUserId(userId, cc.callContext) + (availableAccounts, callContext) <- NewStyle.function.getAccountsHeld(bankId, u, callContext) + (accounts, callContext) <- NewStyle.function.getBankAccountsHeldFuture(availableAccounts.toList, callContext) + + accountHelds <- getFilteredCoreAccounts(availableAccounts, req, callContext).map { it => + val coreAccountIds: List[String] = it._1.map(_.id) + accounts.filter(accountHeld => coreAccountIds.contains(accountHeld.id)) + } + } yield { + (JSONFactory300.createCoreAccountsByCoreAccountsJSON(accountHelds), HttpCode.`200`(callContext)) + } + } + } + + staticResourceDocs += ResourceDoc( getEntitlementsAndPermissions, diff --git a/obp-api/src/test/scala/code/api/v5_1_0/AccountTest.scala b/obp-api/src/test/scala/code/api/v5_1_0/AccountTest.scala index 59cef9533..5c0022033 100644 --- a/obp-api/src/test/scala/code/api/v5_1_0/AccountTest.scala +++ b/obp-api/src/test/scala/code/api/v5_1_0/AccountTest.scala @@ -1,11 +1,9 @@ package code.api.v5_1_0 import code.api.util.APIUtil.OAuth._ -import code.api.util.ApiRole.CanSeeAccountAccessForAnyUser +import code.api.util.ApiRole.{CanGetAccountsHeldAtAnyBank, CanGetAccountsHeldAtOneBank} import code.api.util.ErrorMessages.{UserHasMissingRoles, UserNotLoggedIn} -import code.api.v4_0_0.AccountsMinimalJson400 import code.api.v5_1_0.OBPAPI5_1_0.Implementations5_1_0 -import code.entitlement.Entitlement import com.github.dwickern.macros.NameOf.nameOf import com.openbankproject.commons.model.ErrorMessage import com.openbankproject.commons.util.ApiVersion @@ -21,6 +19,9 @@ class AccountTest extends V510ServerSetup { */ object VersionOfApi extends Tag(ApiVersion.v5_1_0.toString) object GetCoreAccountByIdThroughView extends Tag(nameOf(Implementations5_1_0.getCoreAccountByIdThroughView)) + object GetAccountsHeldByUser extends Tag(nameOf(Implementations5_1_0.getAccountsHeldByUserAtBank)) + + lazy val bankId = randomBankId feature(s"test ${GetCoreAccountByIdThroughView.name}") { scenario(s"We will test ${GetCoreAccountByIdThroughView.name}", GetCoreAccountByIdThroughView, VersionOfApi) { @@ -34,5 +35,24 @@ class AccountTest extends V510ServerSetup { } } + + feature(s"test ${GetAccountsHeldByUser.name}") { + scenario(s"We will test ${GetAccountsHeldByUser.name}", GetAccountsHeldByUser, VersionOfApi) { + val requestGet = (v5_1_0_Request / "banks" / bankId / "users" / resourceUser2.userId / "accounts-held").GET + // Anonymous call fails + val anonymousResponseGet = makeGetRequest(requestGet) + anonymousResponseGet.code should equal(401) + anonymousResponseGet.body.extract[ErrorMessage].message should equal(UserNotLoggedIn) + } + scenario("We will call the endpoint with user credentials", GetAccountsHeldByUser, VersionOfApi) { + When(s"We make a request $GetAccountsHeldByUser") + val requestGet = (v5_1_0_Request / "banks" / bankId / "users" / resourceUser2.userId / "accounts-held").GET <@(user1) + val response = makeGetRequest(requestGet) + Then("We should get a 403") + response.code should equal(403) + val errorMessage = UserHasMissingRoles + s"${CanGetAccountsHeldAtOneBank} or $CanGetAccountsHeldAtAnyBank" + response.body.extract[ErrorMessage].message contains errorMessage should be(true) + } + } } \ No newline at end of file From 3f5490891c8a4b92e7ec5bbee5858e5c0f5bccb7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Wed, 27 Nov 2024 14:22:26 +0100 Subject: [PATCH 09/11] feature/Tweak endpoint getAccountsHeldByUserAtBank v5.1.0 --- obp-api/src/main/scala/code/api/v5_1_0/APIMethods510.scala | 6 +++--- obp-api/src/test/scala/code/api/v5_1_0/AccountTest.scala | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/obp-api/src/main/scala/code/api/v5_1_0/APIMethods510.scala b/obp-api/src/main/scala/code/api/v5_1_0/APIMethods510.scala index ea20981cd..871a9b1e6 100644 --- a/obp-api/src/main/scala/code/api/v5_1_0/APIMethods510.scala +++ b/obp-api/src/main/scala/code/api/v5_1_0/APIMethods510.scala @@ -630,13 +630,13 @@ trait APIMethods510 { implementedInApiVersion, nameOf(getAccountsHeldByUserAtBank), "GET", - "/banks/BANK_ID/users/USER_ID/accounts-held", + "/users/USER_ID/banks/BANK_ID/accounts-held", "Get Accounts Held By User", s"""Get Accounts held by the User if even the User has not been assigned the owner View yet. | |Can be used to onboard the account to the API - since all other account and transaction endpoints require views to be assigned. | - |${accountTypeFilterText("/banks/BANK_ID/users/USER_ID/accounts-held")} + |${accountTypeFilterText("/users/USER_ID/banks/BANK_ID/accounts-held")} | | | @@ -654,7 +654,7 @@ trait APIMethods510 { ) lazy val getAccountsHeldByUserAtBank: OBPEndpoint = { - case "banks" :: BankId(bankId) :: "users" :: userId :: "accounts-held" :: Nil JsonGet req => { + case "users" :: userId :: "banks" :: BankId(bankId) :: "accounts-held" :: Nil JsonGet req => { cc => implicit val ec = EndpointContext(Some(cc)) for { diff --git a/obp-api/src/test/scala/code/api/v5_1_0/AccountTest.scala b/obp-api/src/test/scala/code/api/v5_1_0/AccountTest.scala index 5c0022033..7367aa308 100644 --- a/obp-api/src/test/scala/code/api/v5_1_0/AccountTest.scala +++ b/obp-api/src/test/scala/code/api/v5_1_0/AccountTest.scala @@ -38,7 +38,7 @@ class AccountTest extends V510ServerSetup { feature(s"test ${GetAccountsHeldByUser.name}") { scenario(s"We will test ${GetAccountsHeldByUser.name}", GetAccountsHeldByUser, VersionOfApi) { - val requestGet = (v5_1_0_Request / "banks" / bankId / "users" / resourceUser2.userId / "accounts-held").GET + val requestGet = (v5_1_0_Request / "users" / resourceUser2.userId / "banks" / bankId / "accounts-held").GET // Anonymous call fails val anonymousResponseGet = makeGetRequest(requestGet) anonymousResponseGet.code should equal(401) @@ -46,7 +46,7 @@ class AccountTest extends V510ServerSetup { } scenario("We will call the endpoint with user credentials", GetAccountsHeldByUser, VersionOfApi) { When(s"We make a request $GetAccountsHeldByUser") - val requestGet = (v5_1_0_Request / "banks" / bankId / "users" / resourceUser2.userId / "accounts-held").GET <@(user1) + val requestGet = (v5_1_0_Request / "users" / resourceUser2.userId / "banks" / bankId / "accounts-held").GET <@(user1) val response = makeGetRequest(requestGet) Then("We should get a 403") response.code should equal(403) From 96393094264a33c9fa85f08b533c7f68caadb119 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Thu, 28 Nov 2024 09:08:13 +0100 Subject: [PATCH 10/11] feature/Add endpoint getAccountsHeldByUser v5.1.0 --- .../main/scala/code/api/util/NewStyle.scala | 5 ++ .../scala/code/api/v5_1_0/APIMethods510.scala | 47 +++++++++++++++++++ .../scala/code/bankconnectors/Connector.scala | 1 + .../bankconnectors/LocalMappedConnector.scala | 5 ++ .../scala/code/api/v5_1_0/AccountTest.scala | 30 ++++++++++-- 5 files changed, 83 insertions(+), 5 deletions(-) diff --git a/obp-api/src/main/scala/code/api/util/NewStyle.scala b/obp-api/src/main/scala/code/api/util/NewStyle.scala index 3b1feb758..4ac213046 100644 --- a/obp-api/src/main/scala/code/api/util/NewStyle.scala +++ b/obp-api/src/main/scala/code/api/util/NewStyle.scala @@ -2537,6 +2537,11 @@ object NewStyle extends MdcLoggable{ i => (unboxFullOrFail(i._1, callContext, s"$InvalidConnectorResponse Cannot ${nameOf(getAccountsHeld(bankId, user, callContext))} in the backend. ", 400), i._2) } } + def getAccountsHeldByUser(user: User, callContext: Option[CallContext]): OBPReturnType[List[BankIdAccountId]] = { + Connector.connector.vend.getAccountsHeldByUser(user, callContext) map { + i => (unboxFullOrFail(i._1, callContext, s"$InvalidConnectorResponse Cannot ${nameOf(getAccountsHeldByUser(user, callContext))} in the backend. ", 400), i._2) + } + } def createOrUpdateKycCheck(bankId: String, customerId: String, diff --git a/obp-api/src/main/scala/code/api/v5_1_0/APIMethods510.scala b/obp-api/src/main/scala/code/api/v5_1_0/APIMethods510.scala index 871a9b1e6..d3de66c21 100644 --- a/obp-api/src/main/scala/code/api/v5_1_0/APIMethods510.scala +++ b/obp-api/src/main/scala/code/api/v5_1_0/APIMethods510.scala @@ -672,6 +672,53 @@ trait APIMethods510 { } } + staticResourceDocs += ResourceDoc( + getAccountsHeldByUser, + implementedInApiVersion, + nameOf(getAccountsHeldByUser), + "GET", + "/users/USER_ID/accounts-held", + "Get Accounts Held By User", + s"""Get Accounts held by the User if even the User has not been assigned the owner View yet. + | + |Can be used to onboard the account to the API - since all other account and transaction endpoints require views to be assigned. + | + |${accountTypeFilterText("/users/USER_ID/accounts-held")} + | + | + | + """.stripMargin, + EmptyBody, + coreAccountsHeldJsonV300, + List( + $UserNotLoggedIn, + $BankNotFound, + UserNotFoundByUserId, + UnknownError + ), + List(apiTagAccount), + Some(List(canGetAccountsHeldAtAnyBank)) + ) + + lazy val getAccountsHeldByUser: OBPEndpoint = { + case "users" :: userId :: "accounts-held" :: Nil JsonGet req => { + cc => + implicit val ec = EndpointContext(Some(cc)) + for { + (u, callContext) <- NewStyle.function.getUserByUserId(userId, cc.callContext) + (availableAccounts, callContext) <- NewStyle.function.getAccountsHeldByUser(u, callContext) + (accounts, callContext) <- NewStyle.function.getBankAccountsHeldFuture(availableAccounts, callContext) + + accountHelds <- getFilteredCoreAccounts(availableAccounts, req, callContext).map { it => + val coreAccountIds: List[String] = it._1.map(_.id) + accounts.filter(accountHeld => coreAccountIds.contains(accountHeld.id)) + } + } yield { + (JSONFactory300.createCoreAccountsByCoreAccountsJSON(accountHelds), HttpCode.`200`(callContext)) + } + } + } + staticResourceDocs += ResourceDoc( diff --git a/obp-api/src/main/scala/code/bankconnectors/Connector.scala b/obp-api/src/main/scala/code/bankconnectors/Connector.scala index 48c6c8bd0..bd272c3bc 100644 --- a/obp-api/src/main/scala/code/bankconnectors/Connector.scala +++ b/obp-api/src/main/scala/code/bankconnectors/Connector.scala @@ -515,6 +515,7 @@ trait Connector extends MdcLoggable { def getBankAccountsHeldLegacy(bankIdAccountIds: List[BankIdAccountId], callContext: Option[CallContext]) : Box[List[AccountHeld]]= Failure(setUnimplementedError(nameOf(getBankAccountsHeldLegacy _))) def getBankAccountsHeld(bankIdAccountIds: List[BankIdAccountId], callContext: Option[CallContext]) : OBPReturnType[Box[List[AccountHeld]]]= Future {(Failure(setUnimplementedError(nameOf(getBankAccountsHeld _))), callContext)} def getAccountsHeld(bankId: BankId, user: User, callContext: Option[CallContext]): OBPReturnType[Box[List[BankIdAccountId]]]= Future {(Failure(setUnimplementedError(nameOf(getAccountsHeld _))), callContext)} + def getAccountsHeldByUser(user: User, callContext: Option[CallContext]): OBPReturnType[Box[List[BankIdAccountId]]]= Future {(Failure(setUnimplementedError(nameOf(getAccountsHeld _))), callContext)} def checkBankAccountExistsLegacy(bankId : BankId, accountId : AccountId, callContext: Option[CallContext] = None) : Box[(BankAccount, Option[CallContext])]= Failure(setUnimplementedError(nameOf(checkBankAccountExistsLegacy _))) def checkBankAccountExists(bankId : BankId, accountId : AccountId, callContext: Option[CallContext] = None) : OBPReturnType[Box[(BankAccount)]] = Future {(Failure(setUnimplementedError(nameOf(checkBankAccountExists _))), callContext)} diff --git a/obp-api/src/main/scala/code/bankconnectors/LocalMappedConnector.scala b/obp-api/src/main/scala/code/bankconnectors/LocalMappedConnector.scala index 086858bb8..762aeb754 100644 --- a/obp-api/src/main/scala/code/bankconnectors/LocalMappedConnector.scala +++ b/obp-api/src/main/scala/code/bankconnectors/LocalMappedConnector.scala @@ -1196,6 +1196,11 @@ object LocalMappedConnector extends Connector with MdcLoggable { (Full(AccountHolders.accountHolders.vend.getAccountsHeld(bankId, user).toList), callContext) } } + override def getAccountsHeldByUser(user: User, callContext: Option[CallContext]): OBPReturnType[Box[List[BankIdAccountId]]] = { + Future { + (Full(AccountHolders.accountHolders.vend.getAccountsHeldByUser(user).toList), callContext) + } + } diff --git a/obp-api/src/test/scala/code/api/v5_1_0/AccountTest.scala b/obp-api/src/test/scala/code/api/v5_1_0/AccountTest.scala index 7367aa308..8aa825e47 100644 --- a/obp-api/src/test/scala/code/api/v5_1_0/AccountTest.scala +++ b/obp-api/src/test/scala/code/api/v5_1_0/AccountTest.scala @@ -19,7 +19,8 @@ class AccountTest extends V510ServerSetup { */ object VersionOfApi extends Tag(ApiVersion.v5_1_0.toString) object GetCoreAccountByIdThroughView extends Tag(nameOf(Implementations5_1_0.getCoreAccountByIdThroughView)) - object GetAccountsHeldByUser extends Tag(nameOf(Implementations5_1_0.getAccountsHeldByUserAtBank)) + object getAccountsHeldByUserAtBank extends Tag(nameOf(Implementations5_1_0.getAccountsHeldByUserAtBank)) + object GetAccountsHeldByUser extends Tag(nameOf(Implementations5_1_0.getAccountsHeldByUser)) lazy val bankId = randomBankId @@ -36,16 +37,16 @@ class AccountTest extends V510ServerSetup { } } - feature(s"test ${GetAccountsHeldByUser.name}") { - scenario(s"We will test ${GetAccountsHeldByUser.name}", GetAccountsHeldByUser, VersionOfApi) { + feature(s"test ${getAccountsHeldByUserAtBank.name}") { + scenario(s"We will test ${getAccountsHeldByUserAtBank.name}", getAccountsHeldByUserAtBank, VersionOfApi) { val requestGet = (v5_1_0_Request / "users" / resourceUser2.userId / "banks" / bankId / "accounts-held").GET // Anonymous call fails val anonymousResponseGet = makeGetRequest(requestGet) anonymousResponseGet.code should equal(401) anonymousResponseGet.body.extract[ErrorMessage].message should equal(UserNotLoggedIn) } - scenario("We will call the endpoint with user credentials", GetAccountsHeldByUser, VersionOfApi) { - When(s"We make a request $GetAccountsHeldByUser") + scenario("We will call the endpoint with user credentials", getAccountsHeldByUserAtBank, VersionOfApi) { + When(s"We make a request $getAccountsHeldByUserAtBank") val requestGet = (v5_1_0_Request / "users" / resourceUser2.userId / "banks" / bankId / "accounts-held").GET <@(user1) val response = makeGetRequest(requestGet) Then("We should get a 403") @@ -54,5 +55,24 @@ class AccountTest extends V510ServerSetup { response.body.extract[ErrorMessage].message contains errorMessage should be(true) } } + + feature(s"test ${GetAccountsHeldByUser.name}") { + scenario(s"We will test ${GetAccountsHeldByUser.name}", GetAccountsHeldByUser, VersionOfApi) { + val requestGet = (v5_1_0_Request / "users" / resourceUser2.userId / "accounts-held").GET + // Anonymous call fails + val anonymousResponseGet = makeGetRequest(requestGet) + anonymousResponseGet.code should equal(401) + anonymousResponseGet.body.extract[ErrorMessage].message should equal(UserNotLoggedIn) + } + scenario("We will call the endpoint with user credentials", GetAccountsHeldByUser, VersionOfApi) { + When(s"We make a request $GetAccountsHeldByUser") + val requestGet = (v5_1_0_Request / "users" / resourceUser2.userId / "accounts-held").GET <@(user1) + val response = makeGetRequest(requestGet) + Then("We should get a 403") + response.code should equal(403) + val errorMessage = UserHasMissingRoles + s"$CanGetAccountsHeldAtAnyBank" + response.body.extract[ErrorMessage].message contains errorMessage should be(true) + } + } } \ No newline at end of file From 513fd7e4e875decbc95c4666056b11a0ea745267 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Fri, 29 Nov 2024 09:55:27 +0100 Subject: [PATCH 11/11] feature/Introduce props oauth2.keycloak.source-of-truth --- .../resources/props/sample.props.template | 2 + obp-api/src/main/scala/code/api/OAuth2.scala | 48 ++++++++++++++++--- 2 files changed, 44 insertions(+), 6 deletions(-) diff --git a/obp-api/src/main/resources/props/sample.props.template b/obp-api/src/main/resources/props/sample.props.template index c0c2f8d67..8f287be25 100644 --- a/obp-api/src/main/resources/props/sample.props.template +++ b/obp-api/src/main/resources/props/sample.props.template @@ -767,6 +767,8 @@ display_internal_errors=false # Keycloak Identity Provider Host # oauth2.keycloak.host=http://localhost:7070 # oauth2.keycloak.well-known=http://localhost:7070/realms/master/.well-known/openid-configuration +# Used to sync IAM of OBP-API and IAM of Keycloak +# oauth2.keycloak.source-of-truth = false # ------------------------------------------------------------------------------ OAuth 2 ------ ## This property is used for documenting at Resource Doc. It may include the port also (but not /obp) diff --git a/obp-api/src/main/scala/code/api/OAuth2.scala b/obp-api/src/main/scala/code/api/OAuth2.scala index 07d7ca281..b776d6c56 100644 --- a/obp-api/src/main/scala/code/api/OAuth2.scala +++ b/obp-api/src/main/scala/code/api/OAuth2.scala @@ -26,28 +26,29 @@ TESOBE (http://www.tesobe.com/) */ package code.api -import java.net.URI -import java.util import code.api.util.ErrorMessages._ -import code.api.util.{APIUtil, CallContext, CertificateUtil, JwtUtil} +import code.api.util._ import code.consumer.Consumers import code.consumer.Consumers.consumers import code.loginattempts.LoginAttempt import code.model.{AppType, Consumer} -import code.util.HydraUtil._ +import code.scope.Scope import code.users.Users import code.util.Helper.MdcLoggable import code.util.HydraUtil +import code.util.HydraUtil._ import com.nimbusds.jwt.JWTClaimsSet import com.nimbusds.openid.connect.sdk.claims.IDTokenClaimsSet import com.openbankproject.commons.ExecutionContext.Implicits.global import com.openbankproject.commons.model.User +import net.liftweb.common.Box.tryo import net.liftweb.common._ import net.liftweb.http.rest.RestHelper import net.liftweb.util.Helpers import org.apache.commons.lang3.StringUtils import sh.ory.hydra.model.OAuth2TokenIntrospection +import java.net.URI import scala.concurrent.Future import scala.jdk.CollectionConverters.mapAsJavaMapConverter @@ -226,7 +227,7 @@ object OAuth2Login extends RestHelper with MdcLoggable { } } - private def getClaim(name: String, idToken: String): Option[String] = { + def getClaim(name: String, idToken: String): Option[String] = { val claim = JwtUtil.getClaim(name = name, jwtToken = idToken) claim match { case null => None @@ -373,6 +374,7 @@ object OAuth2Login extends RestHelper with MdcLoggable { redirectURL = None, createdByUserId = userId.toOption ) + } def applyIdTokenRules(token: String, cc: CallContext): (Box[User], Some[CallContext]) = { @@ -471,10 +473,44 @@ object OAuth2Login extends RestHelper with MdcLoggable { def applyRules(token: String, cc: CallContext): (Box[User], Some[CallContext]) = { JwtUtil.getClaim("typ", token) match { case "ID" => super.applyIdTokenRules(token, cc) - case "Bearer" => super.applyAccessTokenRules(token, cc) + case "Bearer" => + val result = super.applyAccessTokenRules(token, cc) + addScopesToConsumer(token) + result case "" => super.applyAccessTokenRules(token, cc) } } + + private def addScopesToConsumer(token: String): Unit = { + val sourceOfTruth = APIUtil.getPropsAsBoolValue(nameOfProperty = "oauth2.keycloak.source-of-truth", defaultValue = false) + val consumerId = getClaim(name = "azp", idToken = token).getOrElse("") + if(sourceOfTruth) { + logger.debug("Extracting roles from Access Token") + import net.liftweb.json._ + val jsonString = JwtUtil.getSignedPayloadAsJson(token) + val json = parse(jsonString.getOrElse("")) + val openBankRoles: List[String] = { + (json \ "resource_access" \ consumerId \ "roles").extract[List[String]] + .filter(role => tryo(ApiRole.valueOf(role)).isDefined) // Keep only the roles OBP-API can recognise + } + val scopes = Scope.scope.vend.getScopesByConsumerId(consumerId).getOrElse(Nil) + val databaseState = scopes.map(_.roleName) + // Already exist at DB + val existingRoles = openBankRoles.intersect(databaseState) + // Roles to add into DB + val rolesToAdd = openBankRoles.toSet diff databaseState.toSet + rolesToAdd.foreach(roleName => Scope.scope.vend.addScope("", consumerId, roleName)) + // Roles to delete from DB + val rolesToDelete = databaseState.toSet diff openBankRoles.toSet + rolesToDelete.foreach( roleName => + Scope.scope.vend.deleteScope(scopes.find(s => s.roleName == roleName || s.consumerId == consumerId)) + ) + logger.debug(s"Consumer ID: $consumerId # Existing roles: ${existingRoles.mkString} # Added roles: ${rolesToAdd.mkString} # Deleted roles: ${rolesToDelete.mkString}") + } else { + logger.debug(s"Adding scopes omitted due to oauth2.keycloak.source-of-truth = $sourceOfTruth # Consumer ID: $consumerId") + } + } + def applyRulesFuture(value: String, cc: CallContext): Future[(Box[User], Some[CallContext])] = Future { applyRules(value, cc) }