From d79f07655600de795b86576ceccf6dcbc49654ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Fri, 13 Dec 2024 16:54:05 +0100 Subject: [PATCH] feature/Add endpoint updateConsentPayloadByConsentId v5.1.0 --- .../main/scala/code/api/util/ApiRole.scala | 4 + .../scala/code/api/util/ConsentUtil.scala | 52 +++++++++++- .../scala/code/api/v5_1_0/APIMethods510.scala | 81 ++++++++++++++++++- .../code/api/v5_1_0/JSONFactory5.1.0.scala | 3 + .../scala/code/api/v5_1_0/ConsentsTest.scala | 36 ++++++++- 5 files changed, 172 insertions(+), 4 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 51f6e78fa..3945a7aa2 100644 --- a/obp-api/src/main/scala/code/api/util/ApiRole.scala +++ b/obp-api/src/main/scala/code/api/util/ApiRole.scala @@ -992,6 +992,10 @@ object ApiRole extends MdcLoggable{ lazy val canUpdateConsentStatusAtOneBank = CanUpdateConsentStatusAtOneBank() case class CanUpdateConsentStatusAtAnyBank(requiresBankId: Boolean = false) extends ApiRole lazy val canUpdateConsentStatusAtAnyBank = CanUpdateConsentStatusAtAnyBank() + case class CanUpdateConsentPayloadAtOneBank(requiresBankId: Boolean = true) extends ApiRole + lazy val canUpdateConsentPayloadAtOneBank = CanUpdateConsentPayloadAtOneBank() + case class CanUpdateConsentPayloadAtAnyBank(requiresBankId: Boolean = false) extends ApiRole + lazy val canUpdateConsentPayloadAtAnyBank = CanUpdateConsentPayloadAtAnyBank() case class CanRevokeConsentAtBank(requiresBankId: Boolean = true) extends ApiRole lazy val canRevokeConsentAtBank = CanRevokeConsentAtBank() case class CanGetConsentsAtOneBank(requiresBankId: Boolean = true) extends ApiRole diff --git a/obp-api/src/main/scala/code/api/util/ConsentUtil.scala b/obp-api/src/main/scala/code/api/util/ConsentUtil.scala index 3b89e9da7..9006275a2 100644 --- a/obp-api/src/main/scala/code/api/util/ConsentUtil.scala +++ b/obp-api/src/main/scala/code/api/util/ConsentUtil.scala @@ -2,7 +2,6 @@ package code.api.util import java.text.SimpleDateFormat import java.util.{Date, UUID} - import code.api.berlin.group.v1_3.JSONFactory_BERLIN_GROUP_1_3.{ConsentAccessJson, PostConsentJson} import code.api.util.ApiRole.{canCreateEntitlementAtAnyBank, canCreateEntitlementAtOneBank} import code.api.v3_1_0.{PostConsentBodyCommonJson, PostConsentEntitlementJsonV310, PostConsentViewJsonV310} @@ -741,6 +740,57 @@ object Consent extends MdcLoggable { } } } + def updateBerlinGroupConsentJWT(access: ConsentAccessJson, + consent: MappedConsent, + callContext: Option[CallContext]): Future[Box[String]] = { + implicit val dateFormats = CustomJsonFormats.formats + val payloadToUpdate: Box[ConsentJWT] = JwtUtil.getSignedPayloadAsJson(consent.jsonWebToken) // Payload as JSON string + .map(net.liftweb.json.parse(_).extract[ConsentJWT]) // Extract case class + + + // 1. Add access + val accounts: List[Future[ConsentView]] = access.accounts.getOrElse(Nil) map { account => + Connector.connector.vend.getBankAccountByIban(account.iban.getOrElse(""), callContext) map { bankAccount => + logger.debug(s"createBerlinGroupConsentJWT.accounts.bankAccount: $bankAccount") + ConsentView( + bank_id = bankAccount._1.map(_.bankId.value).getOrElse(""), + account_id = bankAccount._1.map(_.accountId.value).getOrElse(""), + view_id = Constant.SYSTEM_READ_ACCOUNTS_BERLIN_GROUP_VIEW_ID + ) + } + } + val balances: List[Future[ConsentView]] = access.balances.getOrElse(Nil) map { account => + Connector.connector.vend.getBankAccountByIban(account.iban.getOrElse(""), callContext) map { bankAccount => + logger.debug(s"createBerlinGroupConsentJWT.balances.bankAccount: $bankAccount") + ConsentView( + bank_id = bankAccount._1.map(_.bankId.value).getOrElse(""), + account_id = bankAccount._1.map(_.accountId.value).getOrElse(""), + view_id = Constant.SYSTEM_READ_BALANCES_BERLIN_GROUP_VIEW_ID + ) + } + } + val transactions: List[Future[ConsentView]] = access.transactions.getOrElse(Nil) map { account => + Connector.connector.vend.getBankAccountByIban(account.iban.getOrElse(""), callContext) map { bankAccount => + logger.debug(s"createBerlinGroupConsentJWT.transactions.bankAccount: $bankAccount") + ConsentView( + bank_id = bankAccount._1.map(_.bankId.value).getOrElse(""), + account_id = bankAccount._1.map(_.accountId.value).getOrElse(""), + view_id = Constant.SYSTEM_READ_TRANSACTIONS_BERLIN_GROUP_VIEW_ID + ) + } + } + + Future.sequence(accounts ::: balances ::: transactions) map { views => + if(views.isEmpty) { + Empty + } else { + val updatedPayload = payloadToUpdate.map(i => i.copy(views = views)) + val jwtPayloadAsJson = compactRender(Extraction.decompose(updatedPayload)) + val jwtClaims: JWTClaimsSet = JWTClaimsSet.parse(jwtPayloadAsJson) + Full(CertificateUtil.jwtWithHmacProtection(jwtClaims, consent.secret)) + } + } + } def createUKConsentJWT( user: Option[User], 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 d5b24b561..d7769a6d5 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 @@ -3,6 +3,7 @@ package code.api.v5_1_0 import code.api.Constant import code.api.ResourceDocs1_4_0.SwaggerDefinitionsJSON._ +import code.api.berlin.group.v1_3.JSONFactory_BERLIN_GROUP_1_3.{ConsentAccessAccountsJson, ConsentAccessJson} import code.api.util.APIUtil._ import code.api.util.ApiRole._ import code.api.util.ApiTag._ @@ -28,7 +29,7 @@ import code.api.v5_0_0.JSONFactory500 import code.api.v5_1_0.JSONFactory510.{createConsentsInfoJsonV510, createConsentsJsonV510, createRegulatedEntitiesJson, createRegulatedEntityJson} import code.atmattribute.AtmAttribute import code.bankconnectors.Connector -import code.consent.{ConsentRequests, ConsentStatus, Consents} +import code.consent.{ConsentRequests, ConsentStatus, Consents, MappedConsent} import code.consumer.Consumers import code.loginattempts.LoginAttempt import code.metrics.APIMetrics @@ -55,6 +56,7 @@ import net.liftweb.mapper.By import net.liftweb.util.Helpers.tryo import net.liftweb.util.{Helpers, StringHelpers} +import java.text.SimpleDateFormat import java.time.{LocalDate, ZoneId} import java.util.Date import scala.collection.immutable.{List, Nil} @@ -1347,6 +1349,83 @@ trait APIMethods510 { } } + staticResourceDocs += ResourceDoc( + updateConsentPayloadByConsentId, + implementedInApiVersion, + nameOf(updateConsentPayloadByConsentId), + "PUT", + "/management/banks/BANK_ID/consents/CONSENT_ID/payload", + "Update Consent Payload by CONSENT_ID", + s""" + | + |This endpoint is used to update the Payload of Consent. + | + |${authenticationRequiredMessage(true)} + | + |""", + PutConsentPayloadJsonV510( + access = ConsentAccessJson( + accounts = Option(List(ConsentAccessAccountsJson( + iban = Some(ExampleValue.ibanExample.value), + bban = None, + pan = None, + maskedPan = None, + msisdn = None, + currency = None, + ))) + ) + ), + ConsentChallengeJsonV310( + consent_id = "9d429899-24f5-42c8-8565-943ffa6a7945", + jwt = "eyJhbGciOiJIUzI1NiJ9.eyJlbnRpdGxlbWVudHMiOltdLCJjcmVhdGVkQnlVc2VySWQiOiJhYjY1MzlhOS1iMTA1LTQ0ODktYTg4My0wYWQ4ZDZjNjE2NTciLCJzdWIiOiIyMWUxYzhjYy1mOTE4LTRlYWMtYjhlMy01ZTVlZWM2YjNiNGIiLCJhdWQiOiJlanpuazUwNWQxMzJyeW9tbmhieDFxbXRvaHVyYnNiYjBraWphanNrIiwibmJmIjoxNTUzNTU0ODk5LCJpc3MiOiJodHRwczpcL1wvd3d3Lm9wZW5iYW5rcHJvamVjdC5jb20iLCJleHAiOjE1NTM1NTg0OTksImlhdCI6MTU1MzU1NDg5OSwianRpIjoiMDlmODhkNWYtZWNlNi00Mzk4LThlOTktNjYxMWZhMWNkYmQ1Iiwidmlld3MiOlt7ImFjY291bnRfaWQiOiJtYXJrb19wcml2aXRlXzAxIiwiYmFua19pZCI6ImdoLjI5LnVrLngiLCJ2aWV3X2lkIjoib3duZXIifSx7ImFjY291bnRfaWQiOiJtYXJrb19wcml2aXRlXzAyIiwiYmFua19pZCI6ImdoLjI5LnVrLngiLCJ2aWV3X2lkIjoib3duZXIifV19.8cc7cBEf2NyQvJoukBCmDLT7LXYcuzTcSYLqSpbxLp4", + status = "AUTHORISED" + ), + List( + $UserNotLoggedIn, + $BankNotFound, + InvalidJsonFormat, + ConsentNotFound, + InvalidConnectorResponse, + UnknownError + ), + apiTagConsent :: apiTagPSD2AIS :: Nil, + Some(List(canUpdateConsentPayloadAtOneBank, canUpdateConsentPayloadAtAnyBank)) + ) + + lazy val updateConsentPayloadByConsentId: OBPEndpoint = { + case "management" :: "banks" :: BankId(bankId) :: "consents" :: consentId :: "payload" :: Nil JsonPut json -> _ => { + cc => + implicit val ec = EndpointContext(Some(cc)) + for { + consentJson <- NewStyle.function.tryons(s"$InvalidJsonFormat The Json body should be the $PutConsentPayloadJsonV510 ", 400, cc.callContext) { + json.extract[PutConsentPayloadJsonV510] + } + consent: MappedConsent <- Future(Consents.consentProvider.vend.getConsentByConsentId(consentId)) map { + unboxFullOrFail(_, cc.callContext, s"$ConsentNotFound ($consentId)", 404) + } + consentJWT <- Consent.updateBerlinGroupConsentJWT( + consentJson.access, + consent, + cc.callContext + ) map { + i => connectorEmptyResponse(i, cc.callContext) + } + updatedConsent <- Future(Consents.consentProvider.vend.setJsonWebToken(consent.consentId, consentJWT)) map { + i => connectorEmptyResponse(i, cc.callContext) + } + } yield { + ( + ConsentJsonV310( + updatedConsent.consentId, + updatedConsent.jsonWebToken, + updatedConsent.status + ), + HttpCode.`200`(cc.callContext) + ) + } + } + } + staticResourceDocs += ResourceDoc( getMyConsents, diff --git a/obp-api/src/main/scala/code/api/v5_1_0/JSONFactory5.1.0.scala b/obp-api/src/main/scala/code/api/v5_1_0/JSONFactory5.1.0.scala index 1b9f1f6f4..d9fff3dff 100644 --- a/obp-api/src/main/scala/code/api/v5_1_0/JSONFactory5.1.0.scala +++ b/obp-api/src/main/scala/code/api/v5_1_0/JSONFactory5.1.0.scala @@ -27,6 +27,7 @@ package code.api.v5_1_0 import code.api.Constant +import code.api.berlin.group.v1_3.JSONFactory_BERLIN_GROUP_1_3.ConsentAccessJson import code.api.util.APIUtil.{DateWithDay, DateWithSeconds, gitCommit, stringOrNull} import code.api.util._ import code.api.v1_2_1.BankRoutingJsonV121 @@ -134,6 +135,8 @@ case class ConsentInfoJsonV510(consent_id: String, ) case class ConsentsInfoJsonV510(consents: List[ConsentInfoJsonV510]) +case class PutConsentPayloadJsonV510(access: ConsentAccessJson) + case class AllConsentJsonV510(consent_reference_id: String, consumer_id: String, created_by_user_id: String, diff --git a/obp-api/src/test/scala/code/api/v5_1_0/ConsentsTest.scala b/obp-api/src/test/scala/code/api/v5_1_0/ConsentsTest.scala index 014957511..940909e61 100644 --- a/obp-api/src/test/scala/code/api/v5_1_0/ConsentsTest.scala +++ b/obp-api/src/test/scala/code/api/v5_1_0/ConsentsTest.scala @@ -28,7 +28,7 @@ package code.api.v5_1_0 import code.api.Constant import code.api.ResourceDocs1_4_0.SwaggerDefinitionsJSON import code.api.util.APIUtil.OAuth._ -import code.api.util.ApiRole.{canUpdateConsentStatusAtOneBank, _} +import code.api.util.ApiRole._ import code.api.util.Consent import code.api.util.ErrorMessages._ import code.api.v3_1_0.{PostConsentChallengeJsonV310, PostConsentEntitlementJsonV310} @@ -68,6 +68,7 @@ class ConsentsTest extends V510ServerSetup with PropsReset{ object ApiEndpoint8 extends Tag(nameOf(Implementations5_1_0.getMyConsents)) object ApiEndpoint9 extends Tag(nameOf(Implementations5_1_0.getConsentsAtBank)) object UpdateConsentStatusByUser extends Tag(nameOf(Implementations5_1_0.updateConsentStatusByUser)) + object UpdateConsentPayloadByConsentId extends Tag(nameOf(Implementations5_1_0.updateConsentPayloadByConsentId)) lazy val entitlements = List(PostConsentEntitlementJsonV310("", CanGetAnyUser.toString())) lazy val bankId = testBankId1.value @@ -93,6 +94,7 @@ class ConsentsTest extends V510ServerSetup with PropsReset{ def getMyConsents(consentId: String) = (v5_1_0_Request / "banks" / bankId / "my" / "consents").GET def getConsentsAtBAnk(consentId: String) = (v5_1_0_Request / "management"/ "consents" / "banks" / bankId).GET def updateConsentStatusByUser(consentId: String) = (v5_1_0_Request / "management" / "banks" / bankId / "consents" / consentId).PUT + def updateConsentPayloadByConsent(consentId: String) = (v5_1_0_Request / "management" / "banks" / bankId / "consents" / consentId / "payload").PUT feature(s"test $ApiEndpoint6 version $VersionOfApi - Unauthorized access") { scenario("We will call the endpoint without user credentials", ApiEndpoint6, VersionOfApi) { @@ -155,7 +157,7 @@ class ConsentsTest extends V510ServerSetup with PropsReset{ feature(s"test $UpdateConsentStatusByUser version $VersionOfApi - Unauthenticated access") { scenario("We will call the endpoint without user credentials", UpdateConsentStatusByUser, VersionOfApi) { When(s"We make a request $UpdateConsentStatusByUser") - val response510 = makeGetRequest(getConsentsAtBAnk("whatever")) + val response510 = makePutRequest(updateConsentStatusByUser("whatever"), write(consentStatus)) Then("We should get a 401") response510.code should equal(401) response510.body.extract[ErrorMessage].message should equal(UserNotLoggedIn) @@ -180,6 +182,36 @@ class ConsentsTest extends V510ServerSetup with PropsReset{ response510.body.extract[ErrorMessage].message should startWith(ConsentNotFound) } } + + + feature(s"test $UpdateConsentPayloadByConsentId version $VersionOfApi - Unauthenticated access") { + scenario("We will call the endpoint without user credentials", UpdateConsentPayloadByConsentId, VersionOfApi) { + When(s"We make a request $UpdateConsentPayloadByConsentId") + val response510 = makePutRequest(updateConsentPayloadByConsent("whatever"), write(consentStatus)) + Then("We should get a 401") + response510.code should equal(401) + response510.body.extract[ErrorMessage].message should equal(UserNotLoggedIn) + } + } + feature(s"test $UpdateConsentPayloadByConsentId version $VersionOfApi - Authenticated access") { + scenario("We will call the endpoint with user credentials", UpdateConsentPayloadByConsentId, VersionOfApi) { + When(s"We make a request $UpdateConsentPayloadByConsentId") + val response510 = makePutRequest(updateConsentPayloadByConsent("whatever") <@ user1, write(consentStatus)) + Then("We should get a 403") + response510.code should equal(403) + response510.body.extract[ErrorMessage].message contains (UserHasMissingRoles + s"$CanUpdateConsentPayloadAtOneBank or $CanUpdateConsentPayloadAtAnyBank") should be(true) + } + } + feature(s"test $UpdateConsentPayloadByConsentId version $VersionOfApi - Authenticated access with Role $CanUpdateConsentStatusAtAnyBank") { + scenario("We will call the endpoint with user credentials", UpdateConsentPayloadByConsentId, VersionOfApi) { + When(s"We make a request $UpdateConsentPayloadByConsentId") + Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, CanUpdateConsentPayloadAtAnyBank.toString) + val response510 = makePutRequest(updateConsentPayloadByConsent("whatever") <@ user1, write(consentStatus)) + Then("We should get a 404") + response510.code should equal(404) + response510.body.extract[ErrorMessage].message should startWith(ConsentNotFound) + } + } feature(s"Create/Use/Revoke Consent $VersionOfApi") { scenario("We will call the Create, Get and Delete endpoints with user credentials ", ApiEndpoint1, ApiEndpoint2, ApiEndpoint3, ApiEndpoint4, ApiEndpoint5, ApiEndpoint6, ApiEndpoint7, VersionOfApi) {