diff --git a/obp-api/src/main/resources/props/sample.props.template b/obp-api/src/main/resources/props/sample.props.template index 340126780..8f287be25 100644 --- a/obp-api/src/main/resources/props/sample.props.template +++ b/obp-api/src/main/resources/props/sample.props.template @@ -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 @@ -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) 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) } 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..ede6ba533 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("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 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/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/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/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 0a76f865f..a6d534779 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,101 @@ trait APIMethods510 { } + staticResourceDocs += ResourceDoc( + getAccountsHeldByUserAtBank, + implementedInApiVersion, + nameOf(getAccountsHeldByUserAtBank), + "GET", + "/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("/users/USER_ID/banks/BANK_ID/accounts-held")} + | + | + | + """.stripMargin, + EmptyBody, + coreAccountsHeldJsonV300, + List( + $UserNotLoggedIn, + $BankNotFound, + UserNotFoundByUserId, + UnknownError + ), + List(apiTagAccount), + Some(List(canGetAccountsHeldAtOneBank, canGetAccountsHeldAtAnyBank)) + ) + + lazy val getAccountsHeldByUserAtBank: OBPEndpoint = { + case "users" :: userId :: "banks" :: BankId(bankId) :: "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( + 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( getEntitlementsAndPermissions, 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/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) } 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..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 @@ -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,10 @@ class AccountTest extends V510ServerSetup { */ object VersionOfApi extends Tag(ApiVersion.v5_1_0.toString) object GetCoreAccountByIdThroughView extends Tag(nameOf(Implementations5_1_0.getCoreAccountByIdThroughView)) + object getAccountsHeldByUserAtBank extends Tag(nameOf(Implementations5_1_0.getAccountsHeldByUserAtBank)) + object GetAccountsHeldByUser extends Tag(nameOf(Implementations5_1_0.getAccountsHeldByUser)) + + lazy val bankId = randomBankId feature(s"test ${GetCoreAccountByIdThroughView.name}") { scenario(s"We will test ${GetCoreAccountByIdThroughView.name}", GetCoreAccountByIdThroughView, VersionOfApi) { @@ -34,5 +36,43 @@ class AccountTest extends V510ServerSetup { } } + + 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", 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") + response.code should equal(403) + val errorMessage = UserHasMissingRoles + s"${CanGetAccountsHeldAtOneBank} or $CanGetAccountsHeldAtAnyBank" + 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