Skip to content

Commit

Permalink
Merge branch 'develop' of github.com:OpenBankProject/OBP-API into dev…
Browse files Browse the repository at this point in the history
…elop
  • Loading branch information
simonredfern committed Dec 6, 2024
2 parents f88d50a + d074377 commit 5e2370f
Show file tree
Hide file tree
Showing 19 changed files with 319 additions and 44 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/build_contributer_container.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,6 @@ 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 }}/obp-api-${{ env.USER_NAME }}:$GITHUB_SHA --tag docker.io/${{ env.DOCKER_HUB_ORGANIZATION }}/obp-api-${{ env.USER_NAME }}:latest
docker build . --file .github/Dockerfile_PreBuild --tag docker.io/${{ env.DOCKER_HUB_ORGANIZATION }}/obp-api-${{ env.USER_NAME }}:${{ github.event.pull_request.head }} --tag docker.io/${{ env.DOCKER_HUB_ORGANIZATION }}/obp-api-${{ env.USER_NAME }}:latest
docker push docker.io/${{ env.DOCKER_HUB_ORGANIZATION }}/obp-api-${{ env.USER_NAME }} --all-tags
echo docker done
14 changes: 14 additions & 0 deletions obp-api/src/main/resources/props/sample.props.template
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,18 @@ 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
## 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
## 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


## Enable writing API metrics (which APIs are called) to RDBMS
write_metrics=true
## Enable writing connector metrics (which methods are called)to RDBMS
Expand Down Expand Up @@ -755,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)
Expand Down
52 changes: 46 additions & 6 deletions obp-api/src/main/scala/code/api/OAuth2.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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]) = {
Expand Down Expand Up @@ -471,10 +473,48 @@ 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)
result._2.flatMap(_.consumer.map(_.id.get)) match {
case Some(consumerPrimaryKey) =>
addScopesToConsumer(token, consumerPrimaryKey)
case None => // Do nothing
}
result
case "" => super.applyAccessTokenRules(token, cc)
}
}

private def addScopesToConsumer(token: String, consumerPrimaryKey: Long): 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(consumerPrimaryKey.toString).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("", consumerPrimaryKey.toString, 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)
}
Expand Down
54 changes: 53 additions & 1 deletion obp-api/src/main/scala/code/api/cache/Redis.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand All @@ -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("PKCS12")
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
Expand Down
5 changes: 4 additions & 1 deletion obp-api/src/main/scala/code/api/constant/constant.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions obp-api/src/main/scala/code/api/util/ApiRole.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
16 changes: 8 additions & 8 deletions obp-api/src/main/scala/code/api/util/CertificateUtil.scala
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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 {
Expand Down Expand Up @@ -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)
Expand Down
8 changes: 3 additions & 5 deletions obp-api/src/main/scala/code/api/util/JwsUtil.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
}

Expand Down
5 changes: 5 additions & 0 deletions obp-api/src/main/scala/code/api/util/NewStyle.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Loading

0 comments on commit 5e2370f

Please sign in to comment.