Merge pull request #2453 from constantine2nd/develop

Accounts held by user, keycloak to scope roles, Redis SSL
This commit is contained in:
Simon Redfern 2024-12-02 10:23:26 +01:00 committed by GitHub
commit 0691767549
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 288 additions and 37 deletions

View File

@ -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)

View File

@ -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)
}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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
}

View File

@ -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,

View File

@ -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,

View File

@ -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)}

View File

@ -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)
}
}

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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)
}
}
}