diff --git a/obp-api/src/main/resources/props/sample.props.template b/obp-api/src/main/resources/props/sample.props.template index 191d70757..bb781c6a5 100644 --- a/obp-api/src/main/resources/props/sample.props.template +++ b/obp-api/src/main/resources/props/sample.props.template @@ -165,8 +165,12 @@ jwt.use.ssl=false # Bypass TPP signature validation # bypass_tpp_signature_validation = false -## Reject Berlin Group Consents in status "received" after defined time -# berlin_group_outdated_consents_interval = 5 +## Reject Berlin Group consents with status "received" after a defined time (in seconds) +# berlin_group_outdated_consents_time_in_seconds = 300 +# berlin_group_outdated_consents_interval_in_seconds = + +## Expire Berlin Group consents with status "valid" +# berlin_group_expired_consents_interval_in_seconds = ## Enable writing API metrics (which APIs are called) to RDBMS @@ -1124,7 +1128,7 @@ default_auth_context_update_request_key=CUSTOMER_NUMBER #berlin_group_error_message_show_path = true # Check presence of the mandatory headers -#berlin_group_mandatory_headers = X-Request-ID,PSU-IP-Address,PSU-Device-ID,PSU-Device-Name +#berlin_group_mandatory_headers = Content-Type,Date,Digest,PSU-Device-ID,PSU-Device-Name,PSU-IP-Address,Signature,TPP-Signature-Certificate,X-Request-ID #berlin_group_mandatory_header_consent = TPP-Redirect-URL ## Berlin Group Create Consent Frequency per Day Upper Limit diff --git a/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala b/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala index b4a96a6d4..c3dd9bd1b 100644 --- a/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala +++ b/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala @@ -5508,6 +5508,9 @@ object SwaggerDefinitionsJSON { val consumerLogoUrlJson = ConsumerLogoUrlJson( "http://localhost:8888" ) + val consumerCertificateJson = ConsumerCertificateJson( + "QmFnIEF0dHJpYnV0ZXMNCiAgICBsb2NhbEtleUlEOiBFMSA3RiBCMyBCOCBEQiA4QyA2NCBGNiA4QyA1NSAzNCA3QSAyNiBCRSBEMCBCNCBENCBBMyBGRCA2NiANCnN1YmplY3Q9QyA9IE1ELCBPID0gTUFJQiwgQ04gPSBNQUlCIFByaXNhY2FydSBTZXJnaXUgKFRlc3QpDQoNCmlzc3Vlcj1DID0gTUQsIE8gPSBCTk0sIE9VID0gRFRJLCBDTiA9IEJOTSBDQSAodGVzdCksIGVtYWlsQWRkcmVzcyA9IGFkbWluQGJubS5tZA0KDQotLS0tLUJFR0lOIENFUlRJRklDQVRFLS0tLS0NCk1JSUdoVENDQkcyZ0F3SUJBZ0lDQkRvd0RRWUpLb1pJaHZjTkFRRUZCUUF3WGpFTE1Ba0dBMVVFQmhNQ1RVUXgNCkREQUtCZ05WQkFvTUEwSk9UVEVNTUFvR0ExVUVDd3dEUkZSSk1SWXdGQVlEVlFRRERBMUNUazBnUTBFZ0tIUmwNCmMzUXBNUnN3R1FZSktvWklodmNOQVFrQkZneGhaRzFwYmtCaWJtMHViV1F3SGhjTk1qUXdOREU0TVRFME5qUXgNCldoY05Nall3TkRFNE1URTBOalF4V2pCRE1Rc3dDUVlEVlFRR0V3Sk5SREVOTUFzR0ExVUVDZ3dFVFVGSlFqRWwNCk1DTUdBMVVFQXd3Y1RVRkpRaUJRY21sellXTmhjblVnVTJWeVoybDFJQ2hVWlhOMEtUQ0NBU0l3RFFZSktvWkkNCmh2Y05BUUVCQlFBRGdnRVBBRENDQVFvQ2dnRUJBTFdYMzlFSmZLNEg5MDZKSVpMbHRxTU56amxDd3NyMm0rZjMNCjVYdHZ4SVY1akEvUWlZSDdDVjBQK0E1U2grKytaNldUb1NnQStQemYwdTdWYWRVbWtyWEZBV0lzOXlPemduUjQNCmZ5TVVSNXR4UWJYdmZYcXVJUS9XQ0ZnRHBIU3I4eWN0UHlsOGdsUjFidVF0UmlTdEdMT0RnalhsTmhTMlhTYTMNCmFwVGhUVHAya3o1dEoyWjBXRnlxa1ZVM1FJNkdNVGU5eWhDdnVZQkI1QWJuUUU4bXVPb2NNaEJkRFREY2ZGdW0NCk5paUozelhLMXZzKzEzNW5sZEMxOXozWnBuaVBSeER2WGthR00wc0xiNnk5T1NIOUdmYTZHcXJnendTTmpubEkNCnZCeWFlK1dtbG16TzlBZXVKNVRaUFhPdzNwcFdpTWdTOVlZOWp1UUtFQUFBQUFBQTQ4c0NBd0VBQWFPQ0FtWXcNCmdnSmlNQkVHQ1dDR1NBR0crRUlCQVFRRUF3SUZvREFwQmdOVkhTVUVJakFnQmdnckJnRUZCUWNEQWdZSUt3WUINCkJRVUhBd1FHQ2lzR0FRUUJnamNVQWdJd0hRWURWUjBPQkJZRUZGR2ptcXM4OXUyMXcvZmNHVlgrb0pNZSsvWTYNCk1JR1FCZ05WSFNNRWdZZ3dnWVdBRkh1ckdvcWhWYVFUVkJwRVlObmNnRUl5Vkd3dG9XS2tZREJlTVFzd0NRWUQNClZRUUdFd0pOUkRFTU1Bb0dBMVVFQ2d3RFFrNU5NUXd3Q2dZRFZRUUxEQU5FVkVreEZqQVVCZ05WQkFNTURVSk8NClRTQkRRU0FvZEdWemRDa3hHekFaQmdrcWhraUc5dzBCQ1FFV0RHRmtiV2x1UUdKdWJTNXRaSUlKQUpuU0UxdVoNCkU1MU5NQlFHQTFVZEVnUU5NQXVCQ1VOQlFHSnViUzV0WkRBMkJnbGdoa2dCaHZoQ0FRUUVLUlluYUhSMGNEb3YNCkwzQnJhUzVpYm0wdWJXUXZjR3RwTDNCMVlpOWpjbXd2WTJGamNtd3VZM0pzTURZR0NXQ0dTQUdHK0VJQkF3UXANCkZpZG9kSFJ3T2k4dmNHdHBMbUp1YlM1dFpDOXdhMmt2Y0hWaUwyTnliQzlqWVdOeWJDNWpjbXd3T0FZRFZSMGYNCkJERXdMekF0b0N1Z0tZWW5hSFIwY0RvdkwzQnJhUzVpYm0wdWJXUXZjR3RwTDNCMVlpOWpjbXd2WTJGamNtd3UNClkzSnNNRU1HQ0NzR0FRVUZCd0VCQkRjd05UQXpCZ2dyQmdFRkJRY3dBb1luYUhSMGNEb3ZMM2QzZHk1aWJtMHUNCmJXUXZjSFZpTDJOaFkyVnlkQzlqWVdObGNuUXVZM0owTUZBR0NXQ0dTQUdHK0VJQkRRUkRGa0ZMWlhrZ1VHRnANCmNpQkhaVzVsY21GMFpXUWdZbmtnVlc1cFEzSjVjSFFnZGk0d0xqWXVPQzQySUdadmNpQlFTME5USXpFeUxXWnANCmJHVWdjM1J2Y21GblpUQUpCZ05WSFJNRUFqQUFNQTRHQTFVZER3RUIvd1FFQXdJR1FEQU5CZ2txaGtpRzl3MEINCkFRVUZBQU9DQWdFQUpTU0ZhRWZOOWVna2wyYVFEc3QvVEtWWmxSbFdWZWkrVmZwMnM1ZXpWNG9ibnZRUXI5QkcNCmZrNklqaU8zbGZHTjQyTkVZSTV6SGh3SDl2WTRiMjM2ZkdMZWltbmZDc2lGb0FyTEtGUDR6Y0dvS0ZJR2ZBNDINCnQzSmxIcENvbmNpMmxqUzg4MzN2c1k5M2xGSzFTa2NvUjBMT0s0NzdaNlBWMjVtdjVjdmhCN1ZkNWs4SWpLU3MNCllwWkpaSi9STWZNT3dPQUtqeDFhWDNxQUhhNVhTOUNINEJaMEl4SnBYcWZpMm5GUFVNRy8yU0JmSTN4dDhsM1UNClJtVy9qZVRoRG5tL0Vsb05sb3pObzdRS3AvbysyUVBFZDBUWkFBdUljQWFiM09waUptOWlrUlh3c21mNkFmS0INCnIwQmtHcTFiTi9RQk1DMDM4RHA4S1pKZmdmaTYxYnBiVUNFdDRsVWY0R252TW9FdjZnbTh1czE2VTI1d0Y0SUwNCnd5cmFBZHJUVHhaWEVydGY2c3pWY2JBRUY0QmdFM0hCVmF2V2FxbDZac1FFRFJoTGVtWVJwMHhleUtwYXI4d3INClhqN1oycmJteWpFci9ES1hMdHF2UlFIQVVrVDBEQXRST2R4NmpsNUtGSFVvbTM2QUZmeU5UcjJ6a0p2MkZWTlENCmc0TnJMRnk0WldidE84ZDc2M2NoMEpjaWYzZUdadnFmQnVETUs3Q25jUWluamxVcTg1cFpzeGlFUW56VTJOdGgNClRFUzBqZjZ6ZS9ibHpVaUsrRXlyeWpEeWNaYlk3RHlwWWVlTlJJbk9zVUVjZmtFT3BVL3dFTG83dnpNaGY1b2MNCmdjcUFKSzdOQWlEQzVHR0Iyb296ZzNSTTJBbGdPT1ZpRFZwRzRMaUxPenpqVStqaXlyclY3OGs9DQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tDQo=" + ) val consumerNameJson = ConsumerNameJson( "App name" ) diff --git a/obp-api/src/main/scala/code/api/berlin/group/v1_3/AccountInformationServiceAISApi.scala b/obp-api/src/main/scala/code/api/berlin/group/v1_3/AccountInformationServiceAISApi.scala index 84245e841..cc2a81b24 100644 --- a/obp-api/src/main/scala/code/api/berlin/group/v1_3/AccountInformationServiceAISApi.scala +++ b/obp-api/src/main/scala/code/api/berlin/group/v1_3/AccountInformationServiceAISApi.scala @@ -242,10 +242,10 @@ recurringIndicator: for { (Full(user), callContext) <- authenticatedAccess(cc) _ <- passesPsd2Aisp(callContext) - consent <- Future(Consents.consentProvider.vend.getConsentByConsentId(consentId)) map { + _ <- Future(Consents.consentProvider.vend.getConsentByConsentId(consentId)) map { unboxFullOrFail(_, callContext, ConsentNotFound) } - consent <- Future(Consents.consentProvider.vend.revoke(consentId)) map { + _ <- Future(Consents.consentProvider.vend.revokeBerlinGroupConsent(consentId)) map { i => connectorEmptyResponse(i, callContext) } } yield { @@ -694,8 +694,10 @@ where the consent was directly managed between ASPSP and PSU e.g. in a re-direct consent <- Future(Consents.consentProvider.vend.getConsentByConsentId(consentId)) map { unboxFullOrFail(_, callContext, s"$ConsentNotFound ($consentId)") } - _ <- Helper.booleanToFuture(failMsg = s"${consent.mConsumerId.get} != ${cc.consumer.map(_.consumerId.get).getOrElse("None")}", failCode = 404, cc = cc.callContext) { - consent.mConsumerId.get == callContext.map(_.consumer.map(_.consumerId.get).getOrElse("None")).getOrElse("None") + consumerIdFromConsent = consent.mConsumerId.get + consumerIdFromCurrentCall = callContext.map(_.consumer.map(_.consumerId.get).getOrElse("None")).getOrElse("None") + _ <- Helper.booleanToFuture(failMsg = s"$ConsentNotFound $consumerIdFromConsent != $consumerIdFromCurrentCall", failCode = 403, cc = cc.callContext) { + consumerIdFromConsent == consumerIdFromCurrentCall } } yield { (createGetConsentResponseJson(consent), HttpCode.`200`(callContext)) @@ -767,8 +769,7 @@ This method returns the SCA status of a consent initiation's authorisation sub-r unboxFullOrFail(_, callContext, ConsentNotFound) } } yield { - val status = consent.status.toLowerCase() - .replace(ConsentStatus.REVOKED.toString.toLowerCase(), "revokedByPsu") + val status = consent.status (JSONFactory_BERLIN_GROUP_1_3.ConsentStatusJsonV13(status), HttpCode.`200`(callContext)) } diff --git a/obp-api/src/main/scala/code/api/berlin/group/v1_3/BgSpecValidation.scala b/obp-api/src/main/scala/code/api/berlin/group/v1_3/BgSpecValidation.scala index 056fe73bc..363cbce1c 100644 --- a/obp-api/src/main/scala/code/api/berlin/group/v1_3/BgSpecValidation.scala +++ b/obp-api/src/main/scala/code/api/berlin/group/v1_3/BgSpecValidation.scala @@ -48,19 +48,19 @@ object BgSpecValidation { // Example usage def main(args: Array[String]): Unit = { val testDates = Seq( - "2025-05-10", // ❌ More than 180 days ahead - "9999-12-31", // ❌ Exceeds max allowed - "2015-01-01", // ❌ In the past - "invalid-date", // ❌ Invalid format - LocalDate.now().plusDays(90).toString, // ✅ Valid (within 180 days) - LocalDate.now().plusDays(180).toString, // ✅ Valid (exactly 180 days) - LocalDate.now().plusDays(181).toString // ❌ More than 180 days + "2025-05-10", // More than 180 days ahead + "9999-12-31", // Exceeds max allowed + "2015-01-01", // In the past + "invalid-date", // Invalid format + LocalDate.now().plusDays(90).toString, // Valid (within 180 days) + LocalDate.now().plusDays(180).toString, // Valid (exactly 180 days) + LocalDate.now().plusDays(181).toString // More than 180 days ) testDates.foreach { date => validateValidUntil(date) match { - case Right(validDate) => println(s"✅ Valid date: $validDate") - case Left(error) => println(s"❌ Error: $error") + case Right(validDate) => println(s"Valid date: $validDate") + case Left(error) => println(s"Error: $error") } } } diff --git a/obp-api/src/main/scala/code/api/util/APIUtil.scala b/obp-api/src/main/scala/code/api/util/APIUtil.scala index d917d93c0..8460be9ec 100644 --- a/obp-api/src/main/scala/code/api/util/APIUtil.scala +++ b/obp-api/src/main/scala/code/api/util/APIUtil.scala @@ -254,16 +254,6 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{ case _ => None } } - /** - * Purpose of this helper function is to get the PSD2-CERT value from a Request Headers. - * @return the PSD2-CERT value from a Request Header as a String - */ - def getTppSignatureCertificate(requestHeaders: List[HTTPParam]): Option[String] = { - requestHeaders.toSet.filter(_.name == RequestHeader.`TPP-Signature-Certificate`).toList match { - case x :: Nil => Some(x.values.mkString(", ")) - case _ => None - } - } def getRequestHeader(name: String, requestHeaders: List[HTTPParam]): String = { requestHeaders.toSet.filter(_.name.toLowerCase == name.toLowerCase).toList match { @@ -527,8 +517,16 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{ * */ def getRequestHeadersToMirror(callContext: Option[CallContextLight]): CustomResponseHeaders = { + val mirrorByProperties = getPropsValue("mirror_request_headers_to_response", "").split(",").toList.map(_.trim) + val mirrorRequestHeadersToResponse: List[String] = - getPropsValue("mirror_request_headers_to_response", "").split(",").toList.map(_.trim) + if (callContext.exists(_.url.contains(ApiVersion.berlinGroupV13.urlPrefix))) { + // Berlin Group Specification + RequestHeader.`X-Request-ID` :: mirrorByProperties + } else { + mirrorByProperties + } + callContext match { case Some(cc) => cc.requestHeaders match { @@ -536,13 +534,14 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{ case _ => val headers = cc.requestHeaders .filter(item => mirrorRequestHeadersToResponse.contains(item.name)) - .map(item => (item.name, item.values.head)) + .map(item => (item.name, item.values.headOption.getOrElse(""))) // Safe extraction CustomResponseHeaders(headers) } case None => CustomResponseHeaders(Nil) } } + /** * */ @@ -2993,15 +2992,23 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{ val remoteIpAddress = getRemoteIpAddress() val authHeaders = AuthorisationUtil.getAuthorisationHeaders(reqHeaders) + val authHeadersWithEmptyValues = RequestHeadersUtil.checkEmptyRequestHeaderValues(reqHeaders) + val authHeadersWithEmptyNames = RequestHeadersUtil.checkEmptyRequestHeaderNames(reqHeaders) // Identify consumer via certificate - val consumerByCertificate = Consent.getCurrentConsumerViaMtls(callContext = cc) + val consumerByCertificate = Consent.getCurrentConsumerViaTppSignatureCertOrMtls(callContext = cc) val res = - if (authHeaders.size > 1) { // Check Authorization Headers ambiguity + if (authHeadersWithEmptyValues.nonEmpty) { // Check Authorization Headers Empty Values + val message = ErrorMessages.EmptyRequestHeaders + s"Header names: ${authHeadersWithEmptyValues.mkString(", ")}" + Future { (fullBoxOrException(Empty ~> APIFailureNewStyle(message, 400, Some(cc.toLight))), None) } + } else if (authHeadersWithEmptyNames.nonEmpty) { // Check Authorization Headers Empty Names + val message = ErrorMessages.EmptyRequestHeaders + s"Header values: ${authHeadersWithEmptyNames.mkString(", ")}" + Future { (fullBoxOrException(Empty ~> APIFailureNewStyle(message, 400, Some(cc.toLight))), None) } + } else if (authHeaders.size > 1) { // Check Authorization Headers ambiguity Future { (Failure(ErrorMessages.AuthorizationHeaderAmbiguity + s"${authHeaders}"), None) } } else if (APIUtil.`hasConsent-ID`(reqHeaders)) { // Berlin Group's Consent - Consent.applyBerlinGroupRules(APIUtil.`getConsent-ID`(reqHeaders), cc) + Consent.applyBerlinGroupRules(APIUtil.`getConsent-ID`(reqHeaders), cc.copy(consumer = consumerByCertificate)) } else if (APIUtil.hasConsentJWT(reqHeaders)) { // Open Bank Project's Consent val consentValue = APIUtil.getConsentJWT(reqHeaders) Consent.getConsentJwtValueByConsentId(consentValue.getOrElse("")) match { @@ -3011,12 +3018,12 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{ // Note: At this point we are getting the Consumer from the Consumer in the Consent. // This may later be cross checked via the value in consumer_validation_method_for_consent. // Get the source of truth for Consumer (e.g. CONSUMER_CERTIFICATE) as early as possible. - cc.copy(consumer = Consent.getCurrentConsumerViaMtls(callContext = cc)) + cc.copy(consumer = consumerByCertificate) ) case _ => JwtUtil.checkIfStringIsJWTValue(consentValue.getOrElse("")).isDefined match { case true => // It's JWT obtained via "Consent-JWT" request header - Consent.applyRules(APIUtil.getConsentJWT(reqHeaders), cc) + Consent.applyRules(APIUtil.getConsentJWT(reqHeaders), cc.copy(consumer = consumerByCertificate)) case false => // Unrecognised consent value Future { (Failure(ErrorMessages.ConsentHeaderValueInvalid), None) } } @@ -3115,8 +3122,7 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{ } else if(Option(cc).flatMap(_.user).isDefined) { Future{(cc.user, Some(cc))} - } - else { + } else { if(hasAuthorizationHeader(reqHeaders)) { // We want to throw error in case of wrong or unsupported header. For instance: // - Authorization: mF_9.B5f-4.1JqM 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 1547f0bda..63f27b4a3 100644 --- a/obp-api/src/main/scala/code/api/util/ApiRole.scala +++ b/obp-api/src/main/scala/code/api/util/ApiRole.scala @@ -243,6 +243,8 @@ object ApiRole extends MdcLoggable{ case class CanUpdateConsumerLogoUrl(requiresBankId: Boolean = false) extends ApiRole lazy val canUpdateConsumerLogoUrl = CanUpdateConsumerLogoUrl() + case class CanUpdateConsumerCertificate(requiresBankId: Boolean = false) extends ApiRole + lazy val canUpdateConsumerCertificate = CanUpdateConsumerCertificate() case class CanUpdateConsumerName(requiresBankId: Boolean = false) extends ApiRole lazy val canUpdateConsumerName = CanUpdateConsumerName() diff --git a/obp-api/src/main/scala/code/api/util/BerlinGroupCheck.scala b/obp-api/src/main/scala/code/api/util/BerlinGroupCheck.scala index 7f68af487..d6d0ca462 100644 --- a/obp-api/src/main/scala/code/api/util/BerlinGroupCheck.scala +++ b/obp-api/src/main/scala/code/api/util/BerlinGroupCheck.scala @@ -9,8 +9,10 @@ import net.liftweb.http.provider.HTTPParam object BerlinGroupCheck { + + private val defaultMandatoryHeaders = "Content-Type,Date,Digest,PSU-Device-ID,PSU-Device-Name,PSU-IP-Address,Signature,TPP-Signature-Certificate,X-Request-ID" // Parse mandatory headers from a comma-separated string - private val berlinGroupMandatoryHeaders: List[String] = APIUtil.getPropsValue("berlin_group_mandatory_headers", defaultValue = "X-Request-ID,PSU-IP-Address,PSU-Device-ID,PSU-Device-Name") + private val berlinGroupMandatoryHeaders: List[String] = APIUtil.getPropsValue("berlin_group_mandatory_headers", defaultValue = defaultMandatoryHeaders) .split(",") .map(_.trim.toLowerCase) .toList.filterNot(_.isEmpty) diff --git a/obp-api/src/main/scala/code/api/util/BerlinGroupError.scala b/obp-api/src/main/scala/code/api/util/BerlinGroupError.scala index 382885fe2..269a6d84d 100644 --- a/obp-api/src/main/scala/code/api/util/BerlinGroupError.scala +++ b/obp-api/src/main/scala/code/api/util/BerlinGroupError.scala @@ -56,6 +56,9 @@ object BerlinGroupError { case "401" if message.contains("OBP-20207") => "PSU_CREDENTIALS_INVALID" case "401" if message.contains("OBP-20204") => "TOKEN_EXPIRED" + case "401" if message.contains("OBP-20215") => "TOKEN_INVALID" + case "401" if message.contains("OBP-20205") => "TOKEN_INVALID" + case "401" if message.contains("OBP-20204") => "TOKEN_INVALID" case "401" if message.contains("OBP-35003") => "CONSENT_EXPIRED" @@ -66,6 +69,11 @@ object BerlinGroupError { case "401" if message.contains("OBP-35018") => "CONSENT_INVALID" case "401" if message.contains("OBP-35005") => "CONSENT_INVALID" + case "403" if message.contains("OBP-35001") => "CONSENT_UNKNOWN" + + case "401" if message.contains("OBP-20312") => "CERTIFICATE_INVALID" + case "401" if message.contains("OBP-20310") => "SIGNATURE_INVALID" + case "401" if message.contains("OBP-20060") => "ROLE_INVALID" case "400" if message.contains("OBP-35018") => "CONSENT_UNKNOWN" @@ -78,6 +86,8 @@ object BerlinGroupError { case "400" if message.contains("OBP-10001") => "FORMAT_ERROR" case "400" if message.contains("OBP-20062") => "FORMAT_ERROR" case "400" if message.contains("OBP-20063") => "FORMAT_ERROR" + case "400" if message.contains("OBP-20252") => "FORMAT_ERROR" + case "400" if message.contains("OBP-20251") => "FORMAT_ERROR" case "429" if message.contains("OBP-10018") => "ACCESS_EXCEEDED" case _ => code diff --git a/obp-api/src/main/scala/code/api/util/BerlinGroupSigning.scala b/obp-api/src/main/scala/code/api/util/BerlinGroupSigning.scala index fa22c4923..05fc9b9d9 100644 --- a/obp-api/src/main/scala/code/api/util/BerlinGroupSigning.scala +++ b/obp-api/src/main/scala/code/api/util/BerlinGroupSigning.scala @@ -156,7 +156,8 @@ object BerlinGroupSigning extends MdcLoggable { } def getHeaderValue(name: String, requestHeaders: List[HTTPParam]): String = { - requestHeaders.find(_.name.toLowerCase() == name.toLowerCase()).map(_.values.mkString).getOrElse("None") + requestHeaders.find(_.name.toLowerCase() == name.toLowerCase()).map(_.values.mkString) + .getOrElse(SecureRandomUtil.csprng.nextLong().toString) } private def getPem(requestHeaders: List[HTTPParam]): String = { val certificate = getHeaderValue(RequestHeader.`TPP-Signature-Certificate`, requestHeaders) 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 21cc5d542..b5d87a934 100644 --- a/obp-api/src/main/scala/code/api/util/ConsentUtil.scala +++ b/obp-api/src/main/scala/code/api/util/ConsentUtil.scala @@ -4,6 +4,7 @@ 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.util.BerlinGroupSigning.getHeaderValue import code.api.util.ErrorMessages.{CouldNotAssignAccountAccess, InvalidConnectorResponse, NoViewReadAccountsBerlinGroup} import code.api.v3_1_0.{PostConsentBodyCommonJson, PostConsentEntitlementJsonV310, PostConsentViewJsonV310} import code.api.v5_0_0.HelperInfoJson @@ -29,6 +30,7 @@ import net.liftweb.json.JsonParser.ParseException import net.liftweb.json.{Extraction, MappingException, compactRender, parse} import net.liftweb.mapper.By import net.liftweb.util.{ControlHelpers, Props} +import org.apache.commons.lang3.StringUtils import sh.ory.hydra.model.OAuth2TokenIntrospection import scala.collection.immutable.{List, Nil} @@ -126,24 +128,46 @@ object Consent extends MdcLoggable { case _ => None } } - /** - * Purpose of this helper function is to get the Consumer via MTLS info i.e. PEM certificate. - * @return the boxed Consumer - */ - def getCurrentConsumerViaMtls(callContext: CallContext): Box[Consumer] = { - val clientCert: String = APIUtil.`getPSD2-CERT`(callContext.requestHeaders) // MTLS certificate QWAC (Qualified Website Authentication Certificate) - .orElse(BerlinGroupSigning.getTppSignatureCertificate(callContext.requestHeaders)) // Signature certificate QSealC (Qualified Electronic Seal Certificate) - .getOrElse(SecureRandomUtil.csprng.nextLong().toString) // Force to fail - { // 1st search is via the original value - logger.debug(s"getConsumerByPemCertificate ${clientCert}") - Consumers.consumers.vend.getConsumerByPemCertificate(clientCert) - }.or { // 2nd search is via the original value we normalize - logger.debug(s"getConsumerByPemCertificate ${CertificateUtil.normalizePemX509Certificate(clientCert)}") - Consumers.consumers.vend.getConsumerByPemCertificate(CertificateUtil.normalizePemX509Certificate(clientCert)) + /** + * Retrieves the current Consumer using either the MTLS (QWAC) certificate or the TPP signature certificate (QSealC). + * This method checks the request headers for the relevant PEM certificates and searches for the corresponding Consumer. + * + * @param callContext The request context containing headers. + * @return A Box containing the Consumer if found, otherwise Empty. + */ + def getCurrentConsumerViaTppSignatureCertOrMtls(callContext: CallContext): Box[Consumer] = { + { // Attempt to get the Consumer via the TPP-Signature-Certificate (Qualified Electronic Seal Certificate - QSealC) + val tppSignatureCert: String = APIUtil.getRequestHeader(RequestHeader.`TPP-Signature-Certificate`, callContext.requestHeaders) + if (tppSignatureCert.isEmpty) { + logger.debug(s"| No `TPP-Signature-Certificate` header found |") + Empty // No `TPP-Signature-Certificate` header found, continue to MTLS check + } else { + logger.debug(s"Get Consumer By RequestHeader.`TPP-Signature-Certificate`: $tppSignatureCert") + Consumers.consumers.vend.getConsumerByPemCertificate(tppSignatureCert) + } + }.or { // If TPP certificate is not available, try to get Consumer via MTLS (Qualified Website Authentication Certificate - QWAC) + val psd2Cert: String = APIUtil.getRequestHeader(RequestHeader.`PSD2-CERT`, callContext.requestHeaders) + if (psd2Cert.isEmpty) { + logger.debug(s"| No `PSD2-CERT` header found |") + Empty // No `PSD2-CERT` header found + } else { + val consumerByPsd2Cert: Box[Consumer] = { + // First, try to find the Consumer using the original certificate value + logger.debug(s"Get Consumer By RequestHeader.`PSD2-CERT`: $psd2Cert") + Consumers.consumers.vend.getConsumerByPemCertificate(psd2Cert) + }.or { + // If the original value lookup fails, normalize the certificate and try again + val normalizedCert = CertificateUtil.normalizePemX509Certificate(psd2Cert) + logger.debug(s"Get Consumer By RequestHeader.`PSD2-CERT` (normalized): $normalizedCert") + Consumers.consumers.vend.getConsumerByPemCertificate(normalizedCert) + } + consumerByPsd2Cert + } } } - + + private def verifyHmacSignedJwt(jwtToken: String, c: MappedConsent): Boolean = { logger.debug(s"code.api.util.Consent.verifyHmacSignedJwt beginning:: jwtToken($jwtToken), MappedConsent($c)") val result = JwtUtil.verifyHmacSignedJwt(jwtToken, c.secret) @@ -151,16 +175,19 @@ object Consent extends MdcLoggable { result } + private def removeBreakLines(input: String) = input + .replace("\n", "") + .replace("\r", "") private def checkConsumerIsActiveAndMatched(consent: ConsentJWT, callContext: CallContext): Box[Boolean] = { val consumerBox = Consumers.consumers.vend.getConsumerByConsumerId(consent.aud) logger.debug(s"code.api.util.Consent.checkConsumerIsActiveAndMatched.getConsumerByConsumerId consumerBox:: consumerBox($consumerBox)") consumerBox match { case Full(consumerFromConsent) if consumerFromConsent.isActive.get == true => // Consumer is active - val validationMetod = APIUtil.getPropsValue(nameOfProperty = "consumer_validation_method_for_consent", defaultValue = "CONSUMER_CERTIFICATE") - if(validationMetod != "CONSUMER_CERTIFICATE" && Props.mode == Props.RunModes.Production) { - logger.warn(s"consumer_validation_method_for_consent is not set to CONSUMER_CERTIFICATE! The current value is: ${validationMetod}") + val validationMethod = APIUtil.getPropsValue(nameOfProperty = "consumer_validation_method_for_consent", defaultValue = "CONSUMER_CERTIFICATE") + if(validationMethod != "CONSUMER_CERTIFICATE" && Props.mode == Props.RunModes.Production) { + logger.warn(s"consumer_validation_method_for_consent is not set to CONSUMER_CERTIFICATE! The current value is: ${validationMethod}") } - validationMetod match { + validationMethod match { case "CONSUMER_KEY_VALUE" => val requestHeaderConsumerKey = getConsumerKey(callContext.requestHeaders) logger.debug(s"code.api.util.Consent.checkConsumerIsActiveAndMatched.consumerBox.requestHeaderConsumerKey:: requestHeaderConsumerKey($requestHeaderConsumerKey)") @@ -169,23 +196,29 @@ object Consent extends MdcLoggable { if (reqHeaderConsumerKey == consumerFromConsent.key.get) Full(true) // This consent can be used by current application else // This consent can NOT be used by current application - Failure(ErrorMessages.ConsentDoesNotMatchConsumer) + Failure(s"${ErrorMessages.ConsentDoesNotMatchConsumer} CONSUMER_KEY_VALUE") case None => Failure(ErrorMessages.ConsumerKeyHeaderMissing) // There is no header `Consumer-Key` in request headers } case "CONSUMER_CERTIFICATE" => val clientCert: String = APIUtil.`getPSD2-CERT`(callContext.requestHeaders).getOrElse(SecureRandomUtil.csprng.nextLong().toString) - logger.debug(s"code.api.util.Consent.checkConsumerIsActiveAndMatched.consumerBox clientCert:: clientCert($clientCert)") - def removeBreakLines(input: String) = input - .replace("\n", "") - .replace("\r", "") - val certificate = consumerFromConsent.clientCertificate - logger.debug(s"code.api.util.Consent.checkConsumerIsActiveAndMatched.consumer.certificate:: certificate($certificate)") - logger.debug(s"code.api.util.Consent.checkConsumerIsActiveAndMatched.consumer.certificate.dbNotNull_?(${certificate.dbNotNull_?})") - if (certificate.dbNotNull_? && removeBreakLines(clientCert) == removeBreakLines(certificate.get)) { - logger.debug(s"certificate.dbNotNull_? && removeBreakLines(clientCert) == removeBreakLines(consumerFromConsent.clientCertificate.get) result == true") + logger.debug(s"| Consent.checkConsumerIsActiveAndMatched | clientCert | $clientCert |") + logger.debug(s"| Consent.checkConsumerIsActiveAndMatched | consumerFromConsent.clientCertificate | ${consumerFromConsent.clientCertificate} |") + if (removeBreakLines(clientCert) == removeBreakLines(consumerFromConsent.clientCertificate.get)) { + logger.debug(s"| removeBreakLines(clientCert) == removeBreakLines(consumerFromConsent.clientCertificate.get | true |") Full(true) // This consent can be used by current application - } else // This consent can NOT be used by current application - Failure(ErrorMessages.ConsentDoesNotMatchConsumer) + } else { // This consent can NOT be used by current application + Failure(s"${ErrorMessages.ConsentDoesNotMatchConsumer} CONSUMER_CERTIFICATE") + } + case "TPP_SIGNATURE_CERTIFICATE" => + val tppSignatureCertificate = getHeaderValue(RequestHeader.`TPP-Signature-Certificate`, callContext.requestHeaders) + logger.debug(s"| Consent.checkConsumerIsActiveAndMatched | tppSignatureCertificate | $tppSignatureCertificate |") + logger.debug(s"| Consent.checkConsumerIsActiveAndMatched | consumerFromConsent.clientCertificate | ${consumerFromConsent.clientCertificate} |") + if (removeBreakLines(tppSignatureCertificate) == removeBreakLines(consumerFromConsent.clientCertificate.get)) { + logger.debug(s"""| removeBreakLines(tppSignatureCertificate) == removeBreakLines(consumerFromConsent.clientCertificate.get | true |""") + Full(true) // This consent can be used by current application + } else { // This consent can NOT be used by current application + Failure(s"${ErrorMessages.ConsentDoesNotMatchConsumer} TPP_SIGNATURE_CERTIFICATE") + } case "NONE" => // This instance does not require validation method Full(true) case _ => // This instance does not specify validation method @@ -225,7 +258,7 @@ object Consent extends MdcLoggable { Failure(s"${ErrorMessages.ConsentStatusIssue}${ConsentStatus.valid.toString}.") case Full(c) if c.mStatus.toString().toUpperCase() != ConsentStatus.ACCEPTED.toString => Failure(s"${ErrorMessages.ConsentStatusIssue}${ConsentStatus.ACCEPTED.toString}.") - case _ => + case _ => Failure(ErrorMessages.ConsentNotFound) } logger.debug(s"code.api.util.Consent.checkConsent.consentBox.result: result($result)") @@ -268,7 +301,7 @@ object Consent extends MdcLoggable { val bankId = if (role.requiresBankId) entitlement.bank_id else "" Entitlement.entitlement.vend.addEntitlement(bankId, user.userId, entitlement.role_name) match { case Full(_) => (entitlement, "AddedOrExisted") - case _ => + case _ => (entitlement, "Cannot add the entitlement: " + entitlement) } case true => @@ -291,7 +324,7 @@ object Consent extends MdcLoggable { val failedToAdd: List[(Role, String)] = triedToAdd.filter(_._2 != "AddedOrExisted") failedToAdd match { case Nil => Full(user) - case _ => + case _ => Failure("The entitlements cannot be added. " + failedToAdd.map(i => (i._1, i._2)).mkString(", ")) } case _ => @@ -315,7 +348,7 @@ object Consent extends MdcLoggable { Views.views.vend.systemView(ViewId(view.view_id)) match { case Full(systemView) => Views.views.vend.grantAccessToSystemView(BankId(view.bank_id), AccountId(view.account_id), systemView, user) - case _ => + case _ => // It's not system view Views.views.vend.grantAccessToCustomView(bankIdAccountIdViewId, user) } @@ -327,7 +360,7 @@ object Consent extends MdcLoggable { } if (errorMessages.isEmpty) Full(user) else Failure(CouldNotAssignAccountAccess + errorMessages.mkString(", ")) } - + private def applyConsentRulesCommonOldStyle(consentIdAsJwt: String, calContext: CallContext): Box[User] = { implicit val dateFormats = CustomJsonFormats.formats @@ -372,8 +405,8 @@ object Consent extends MdcLoggable { case _ => Failure("Cannot extract data from: " + consentIdAsJwt) } - } - + } + private def applyConsentRulesCommon(consentAsJwt: String, callContext: CallContext): Future[(Box[User], Option[CallContext])] = { implicit val dateFormats = CustomJsonFormats.formats @@ -411,16 +444,13 @@ object Consent extends MdcLoggable { logger.debug(s"applyConsentRulesCommon.Start of net.liftweb.json.parse(jsonAsString).extract[ConsentJWT]: $jsonAsString") val consent = net.liftweb.json.parse(jsonAsString).extract[ConsentJWT] logger.debug(s"applyConsentRulesCommon.End of net.liftweb.json.parse(jsonAsString).extract[ConsentJWT]: $consent") - // Set Consumer into Call Context - val consumer = getCurrentConsumerViaMtls(callContext) - val updatedCallContext = callContext.copy(consumer = consumer) - checkConsent(consent, consentAsJwt, updatedCallContext) match { // Check is it Consent-JWT expired + checkConsent(consent, consentAsJwt, callContext) match { // Check is it Consent-JWT expired case (Full(true)) => // OK applyConsentRules(consent) case failure@Failure(_, _, _) => // Handled errors - Future(failure, Some(updatedCallContext)) + Future(failure, Some(callContext)) case _ => // Unexpected errors - Future(Failure(ErrorMessages.ConsentCheckExpiredIssue), Some(updatedCallContext)) + Future(Failure(ErrorMessages.ConsentCheckExpiredIssue), Some(callContext)) } } catch { // Possible exceptions case e: ParseException => Future(Failure("ParseException: " + e.getMessage), Some(callContext)) @@ -433,7 +463,7 @@ object Consent extends MdcLoggable { Future(Failure("Cannot extract data from: " + consentAsJwt), Some(callContext)) } } - + def applyRules(consentJwt: Option[String], callContext: CallContext): Future[(Box[User], Option[CallContext])] = { val allowed = APIUtil.getPropsAsBoolValue(nameOfProperty="consents.allowed", defaultValue=false) (consentJwt, allowed) match { @@ -442,12 +472,12 @@ object Consent extends MdcLoggable { case (None, _) => Future((Failure(ErrorMessages.ConsentHeaderNotFound), Some(callContext))) } } - + def getConsentJwtValueByConsentId(consentId: String): Option[MappedConsent] = { APIUtil.checkIfStringIsUUID(consentId) match { case true => // String is a UUID Consents.consentProvider.vend.getConsentByConsentId(consentId) match { - case Full(consent) => Some(consent) + case Full(consent) => Some(consent) case _ => None // It's not valid UUID value } case false => None // It's not UUID at all @@ -493,7 +523,7 @@ object Consent extends MdcLoggable { (Failure("Cannot create or get the user based on: " + consentId), Some(cc)) } } - + def checkFrequencyPerDay(storedConsent: consent.ConsentTrait) = { def isSameDay(date1: Date, date2: Date): Boolean = { val fmt = new SimpleDateFormat("yyyyMMdd") @@ -504,7 +534,7 @@ object Consent extends MdcLoggable { case false => // The consent is for one access to the account data if(usesSoFarTodayCounter == 0) // Maximum value is "1". (true, 0) // All good - else + else (false, 1) // Exceeded rate limit case true => // The consent is for recurring access to the account data if(!isSameDay(storedConsent.usesSoFarTodayCounterUpdatedAt, new Date())) { @@ -520,11 +550,9 @@ object Consent extends MdcLoggable { // 1st we need to find a Consent via the field MappedConsent.consentId Consents.consentProvider.vend.getConsentByConsentId(consentId) match { case Full(storedConsent) => - // Set Consumer into Call Context - val consumer = getCurrentConsumerViaMtls(callContext) val user = Users.users.vend.getUserByUserId(storedConsent.userId) logger.debug(s"applyBerlinGroupConsentRulesCommon.storedConsent.user : $user") - val updatedCallContext = callContext.copy(consumer = consumer).copy(consenter = user) + val updatedCallContext = callContext.copy(consenter = user) // This function MUST be called only once per call. I.e. it's date dependent val (canBeUsed, currentCounterState) = checkFrequencyPerDay(storedConsent) if(canBeUsed) { diff --git a/obp-api/src/main/scala/code/api/util/ErrorMessages.scala b/obp-api/src/main/scala/code/api/util/ErrorMessages.scala index a253d6185..8bf52372f 100644 --- a/obp-api/src/main/scala/code/api/util/ErrorMessages.scala +++ b/obp-api/src/main/scala/code/api/util/ErrorMessages.scala @@ -267,6 +267,7 @@ object ErrorMessages { val AuthorizationHeaderAmbiguity = "OBP-20250: Request headers used for authorization are ambiguous. " val MissingMandatoryBerlinGroupHeaders= "OBP-20251: Missing mandatory request headers. " + val EmptyRequestHeaders = "OBP-20252: Empty or null headers are not allowed. " // X.509 val X509GeneralError = "OBP-20300: PEM Encoded Certificate issue." 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 84d31cccb..7bd755fd9 100644 --- a/obp-api/src/main/scala/code/api/util/NewStyle.scala +++ b/obp-api/src/main/scala/code/api/util/NewStyle.scala @@ -740,8 +740,9 @@ object NewStyle extends MdcLoggable{ redirectURL: Option[String] = None, createdByUserId: Option[String] = None, logoURL: Option[String] = None, + certificate: Option[String] = None, callContext: Option[CallContext]): Future[Consumer] = { - Future(Consumers.consumers.vend.updateConsumer(id, key, secret, isActive, name, appType, description, developerEmail, redirectURL, createdByUserId, logoURL)) map { + Future(Consumers.consumers.vend.updateConsumer(id, key, secret, isActive, name, appType, description, developerEmail, redirectURL, createdByUserId, logoURL, certificate)) map { unboxFullOrFail(_, callContext, UpdateConsumerError, 404) } } diff --git a/obp-api/src/main/scala/code/api/util/RequestHeadersUtil.scala b/obp-api/src/main/scala/code/api/util/RequestHeadersUtil.scala new file mode 100644 index 000000000..294f7cc06 --- /dev/null +++ b/obp-api/src/main/scala/code/api/util/RequestHeadersUtil.scala @@ -0,0 +1,22 @@ +package code.api.util + +import code.api.RequestHeader._ +import net.liftweb.http.provider.HTTPParam + +object RequestHeadersUtil { + def checkEmptyRequestHeaderValues(requestHeaders: List[HTTPParam]): List[String] = { + val emptyValues = requestHeaders + .filter(header => header != null && (header.values == null || header.values.isEmpty || header.values.exists(_.trim.isEmpty))) + .map(_.name) // Extract header names with empty values + + emptyValues + } + def checkEmptyRequestHeaderNames(requestHeaders: List[HTTPParam]): List[String] = { + val emptyNames = requestHeaders + .filter(header => header == null || header.name == null || header.name.trim.isEmpty) + .map(_.values.mkString("'")) // List values without names + + emptyNames + } + +} diff --git a/obp-api/src/main/scala/code/api/v2_1_0/APIMethods210.scala b/obp-api/src/main/scala/code/api/v2_1_0/APIMethods210.scala index 317d941e4..b8d8803e1 100644 --- a/obp-api/src/main/scala/code/api/v2_1_0/APIMethods210.scala +++ b/obp-api/src/main/scala/code/api/v2_1_0/APIMethods210.scala @@ -954,7 +954,7 @@ trait APIMethods210 { case false => NewStyle.function.ownEntitlement("", u.userId, ApiRole.canDisableConsumers, cc.callContext) } consumer <- Consumers.consumers.vend.getConsumerByPrimaryId(consumerId.toLong) - updatedConsumer <- Consumers.consumers.vend.updateConsumer(consumer.id.get, None, None, Some(putData.enabled), None, None, None, None, None, None, None) ?~! "Cannot update Consumer" + updatedConsumer <- Consumers.consumers.vend.updateConsumer(consumer.id.get, None, None, Some(putData.enabled), None, None, None, None, None, None, None, None) ?~! "Cannot update Consumer" } yield { // Format the data as json val json = PutEnabledJSON(updatedConsumer.isActive.get) diff --git a/obp-api/src/main/scala/code/api/v3_1_0/APIMethods310.scala b/obp-api/src/main/scala/code/api/v3_1_0/APIMethods310.scala index 2436afd8c..019bf75dc 100644 --- a/obp-api/src/main/scala/code/api/v3_1_0/APIMethods310.scala +++ b/obp-api/src/main/scala/code/api/v3_1_0/APIMethods310.scala @@ -5997,7 +5997,7 @@ trait APIMethods310 { } consumer <- NewStyle.function.getConsumerByConsumerId(consumerId, callContext) updatedConsumer <- Future { - Consumers.consumers.vend.updateConsumer(consumer.id.get, None, None, Some(putData.enabled), None, None, None, None, None,None, None) ?~! "Cannot update Consumer" + Consumers.consumers.vend.updateConsumer(consumer.id.get, None, None, Some(putData.enabled), None, None, None, None, None,None, None, None) ?~! "Cannot update Consumer" } } yield { // Format the data as json 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 d96342769..c8b05ec29 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 @@ -1773,7 +1773,8 @@ trait APIMethods510 { consent <- Future { Consents.consentProvider.vend.getConsentByConsentId(consentId)} map { unboxFullOrFail(_, cc.callContext, ConsentNotFound, 404) } - _ <- Helper.booleanToFuture(failMsg = s"${consent.mConsumerId.get} != ${cc.consumer.map(_.consumerId.get).getOrElse("None")}", failCode = 404, cc = cc.callContext) { + errorMessage = s" ${consent.mConsumerId.get} != ${cc.consumer.map(_.consumerId.get).getOrElse("None")}" + _ <- Helper.booleanToFuture(failMsg = ConsentNotFound + errorMessage, failCode = 404, cc = cc.callContext) { consent.mConsumerId.get == cc.consumer.map(_.consumerId.get).getOrElse("None") } } yield { @@ -3197,6 +3198,53 @@ trait APIMethods510 { } } } + staticResourceDocs += ResourceDoc( + updateConsumerCertificate, + implementedInApiVersion, + nameOf(updateConsumerCertificate), + "PUT", + "/management/consumers/CONSUMER_ID/consumer/certificate", + "Update Consumer Certificate", + s"""Update a Certificate for a Consumer specified by CONSUMER_ID. + | + | ${consumerDisabledText()} + | + | CONSUMER_ID can be obtained after you register the application. + | + | Or use the endpoint 'Get Consumers' to get it + | + """.stripMargin, + consumerCertificateJson, + consumerJsonV510, + List( + $UserNotLoggedIn, + UserHasMissingRoles, + UnknownError + ), + List(apiTagConsumer), + Some(List(canUpdateConsumerCertificate)) + ) + + lazy val updateConsumerCertificate: OBPEndpoint = { + case "management" :: "consumers" :: consumerId :: "consumer" :: "certificate" :: Nil JsonPut json -> _ => { + cc => + implicit val ec = EndpointContext(Some(cc)) + for { + (Full(u), callContext) <- authenticatedAccess(cc) + postJson <- NewStyle.function.tryons(InvalidJsonFormat, 400, callContext) { + json.extract[ConsumerCertificateJson] + } + consumer <- NewStyle.function.getConsumerByConsumerId(consumerId, callContext) + updatedConsumer <- NewStyle.function.updateConsumer( + id = consumer.id.get, + certificate = Some(postJson.certificate), + callContext = callContext + ) + } yield { + (JSONFactory510.createConsumerJSON(updatedConsumer), HttpCode.`200`(callContext)) + } + } + } staticResourceDocs += ResourceDoc( updateConsumerName, 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 861ca2aac..d5295b740 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 @@ -559,6 +559,9 @@ case class APITags( case class ConsumerLogoUrlJson( logo_url: String ) +case class ConsumerCertificateJson( + certificate: String +) case class ConsumerNameJson(app_name: String) case class TransactionRequestJsonV510( diff --git a/obp-api/src/main/scala/code/consent/ConsentProvider.scala b/obp-api/src/main/scala/code/consent/ConsentProvider.scala index 1b89c54b9..158cd1056 100644 --- a/obp-api/src/main/scala/code/consent/ConsentProvider.scala +++ b/obp-api/src/main/scala/code/consent/ConsentProvider.scala @@ -26,6 +26,7 @@ trait ConsentProvider { def createObpConsent(user: User, challengeAnswer: String, consentRequestId:Option[String], consumer: Option[Consumer] = None): Box[MappedConsent] def setJsonWebToken(consentId: String, jwt: String): Box[MappedConsent] def revoke(consentId: String): Box[MappedConsent] + def revokeBerlinGroupConsent(consentId: String): Box[MappedConsent] def checkAnswer(consentId: String, challenge: String): Box[MappedConsent] def createBerlinGroupConsent( user: Option[User], @@ -191,7 +192,7 @@ object ConsentStatus extends Enumeration { type ConsentStatus = Value val INITIATED, ACCEPTED, REJECTED, rejected, REVOKED, // The following one only exist in case of BerlinGroup - received, valid, REVOKEDBYPSU, revokedByPsu, EXPIRED, expired, TERMINATEDBYTPP, terminatedByTpp, + received, valid, revokedByPsu, expired, terminatedByTpp, //these added for UK Open Banking AUTHORISED, AWAITINGAUTHORISATION = Value } diff --git a/obp-api/src/main/scala/code/consent/MappedConsent.scala b/obp-api/src/main/scala/code/consent/MappedConsent.scala index c9fdca8e0..5a9ca2dae 100644 --- a/obp-api/src/main/scala/code/consent/MappedConsent.scala +++ b/obp-api/src/main/scala/code/consent/MappedConsent.scala @@ -230,7 +230,24 @@ object MappedConsentProvider extends ConsentProvider { case _ => Failure(ErrorMessages.UnknownError) } - } + } + override def revokeBerlinGroupConsent(consentId: String): Box[MappedConsent] = { + MappedConsent.find(By(MappedConsent.mConsentId, consentId)) match { + case Full(consent) if consent.status == ConsentStatus.terminatedByTpp.toString => + Failure(ErrorMessages.ConsentAlreadyRevoked) + case Full(consent) => + tryo(consent + .mStatus(ConsentStatus.terminatedByTpp.toString) + .mLastActionDate(now) + .saveMe()) + case Empty => + Empty ?~! ErrorMessages.ConsentNotFound + case Failure(msg, _, _) => + Failure(msg) + case _ => + Failure(ErrorMessages.UnknownError) + } + } override def checkAnswer(consentId: String, challengeAnswer: String): Box[MappedConsent] = { def isAnswerCorrect(expectedAnswerHashed: String, answer: String, salt: String) = { val challengeAnswerHashed = BCrypt.hashpw(answer, salt).substring(0, 44) diff --git a/obp-api/src/main/scala/code/consumer/ConsumerProvider.scala b/obp-api/src/main/scala/code/consumer/ConsumerProvider.scala index 4a318870a..0e0fac5cb 100644 --- a/obp-api/src/main/scala/code/consumer/ConsumerProvider.scala +++ b/obp-api/src/main/scala/code/consumer/ConsumerProvider.scala @@ -48,6 +48,7 @@ trait ConsumersProvider { redirectURL: Option[String], createdByUserId: Option[String], LogoURL: Option[String], + certificate: Option[String], ): Box[Consumer] def updateConsumerCallLimits(id: Long, perSecond: Option[String], perMinute: Option[String], perHour: Option[String], perDay: Option[String], perWeek: Option[String], perMonth: Option[String]): Future[Box[Consumer]] def getOrCreateConsumer(consumerId: Option[String], diff --git a/obp-api/src/main/scala/code/model/OAuth.scala b/obp-api/src/main/scala/code/model/OAuth.scala index 778eb7859..e694468db 100644 --- a/obp-api/src/main/scala/code/model/OAuth.scala +++ b/obp-api/src/main/scala/code/model/OAuth.scala @@ -238,7 +238,8 @@ object MappedConsumersProvider extends ConsumersProvider with MdcLoggable { developerEmail: Option[String], redirectURL: Option[String], createdByUserId: Option[String], - logoURL: Option[String] + logoURL: Option[String], + certificate: Option[String], ): Box[Consumer] = { val consumer = Consumer.find(By(Consumer.id, id)) consumer match { @@ -260,6 +261,10 @@ object MappedConsumersProvider extends ConsumersProvider with MdcLoggable { case Some(v) => c.name(v) case None => } + certificate match { + case Some(v) => c.clientCertificate(v) + case None => + } appType match { case Some(v) => v match { case Confidential => c.appType(Confidential.toString) diff --git a/obp-api/src/main/scala/code/scheduler/ConsentScheduler.scala b/obp-api/src/main/scala/code/scheduler/ConsentScheduler.scala index be03a364d..f3079ca0c 100644 --- a/obp-api/src/main/scala/code/scheduler/ConsentScheduler.scala +++ b/obp-api/src/main/scala/code/scheduler/ConsentScheduler.scala @@ -5,6 +5,7 @@ import code.api.util.APIUtil import code.consent.{ConsentStatus, MappedConsent} import code.util.Helper.MdcLoggable import com.openbankproject.commons.util.ApiVersion +import net.liftweb.common.Full import net.liftweb.mapper.{By, By_<} import java.util.concurrent.TimeUnit @@ -21,8 +22,22 @@ object ConsentScheduler extends MdcLoggable { // Starts multiple scheduled tasks with different intervals def startAll(): Unit = { - startTask(interval = 60, () => unfinishedBerlinGroupConsents()) // Runs every 60 sec - startTask(interval = 60, () => expiredBerlinGroupConsents(), 10) // Start 10 seconds after previous job + APIUtil.getPropsAsIntValue("berlin_group_outdated_consents_interval_in_seconds") match { + case Full(interval) if interval > 0 => + val time = APIUtil.getPropsAsIntValue("berlin_group_outdated_consents_time_in_seconds", 300) + startTask(interval = interval, () => unfinishedBerlinGroupConsents(time)) // Runs periodically + case _ => + logger.warn("|---> Skipping unfinishedBerlinGroupConsents task: berlin_group_outdated_consents_interval_in_seconds not set or invalid") + } + + APIUtil.getPropsAsIntValue("berlin_group_expired_consents_interval_in_seconds") match { + case Full(interval) if interval > 0 => + startTask(interval = interval, () => expiredBerlinGroupConsents(), 10) // Delay for 10 seconds + case _ => + logger.warn("|---> Skipping expiredBerlinGroupConsents task: berlin_group_expired_consents_interval_in_seconds not set or invalid") + } + + } // Generic method to schedule a task @@ -37,21 +52,20 @@ object ConsentScheduler extends MdcLoggable { } // Calculate the timestamp 5 minutes ago - private val someMinutesAgo: Date = { - val minutes = APIUtil.getPropsAsIntValue("berlin_group_outdated_consents_interval", 5) + private def someSecondsAgo(seconds: Int): Date = { val cal = Calendar.getInstance() - cal.add(Calendar.MINUTE, -minutes) + cal.add(Calendar.SECOND, -seconds) cal.getTime } - private def unfinishedBerlinGroupConsents(): Unit = { + private def unfinishedBerlinGroupConsents(seconds: Int): Unit = { Try { logger.debug("|---> Checking for outdated Berlin Group consents...") val outdatedConsents = MappedConsent.findAll( By(MappedConsent.mStatus, ConsentStatus.received.toString), By(MappedConsent.mApiStandard, ApiVersion.berlinGroupV13.apiStandard), - By_<(MappedConsent.updatedAt, someMinutesAgo) + By_<(MappedConsent.updatedAt, someSecondsAgo(seconds)) ) logger.debug(s"|---> Found ${outdatedConsents.size} outdated consents") diff --git a/obp-api/src/main/scala/code/snippet/BerlinGroupConsent.scala b/obp-api/src/main/scala/code/snippet/BerlinGroupConsent.scala index 757c1be3d..0379fc0bd 100644 --- a/obp-api/src/main/scala/code/snippet/BerlinGroupConsent.scala +++ b/obp-api/src/main/scala/code/snippet/BerlinGroupConsent.scala @@ -75,8 +75,8 @@ class BerlinGroupConsent extends MdcLoggable with RestHelper with APIMethods510 } } private object accessAccountsDefinedVar extends SessionVar(true) - private object accessBalancesDefinedVar extends SessionVar(true) - private object accessTransactionsDefinedVar extends SessionVar(true) + private object accessBalancesDefinedVar extends SessionVar(false) + private object accessTransactionsDefinedVar extends SessionVar(false) /** * Creates a ConsentAccessJson object from lists of IBANs for accounts, balances, and transactions. * @@ -167,9 +167,9 @@ class BerlinGroupConsent extends MdcLoggable with RestHelper with APIMethods510 // Determine which IBANs the user can access for accounts, balances, and transactions val canReadAccountsIbans: List[String] = json.access.accounts match { - case Some(accounts) if accounts.isEmpty => // Access is requested + case Some(accounts) if accounts.isEmpty => // Access is requested via "accounts": [] updateConsentPayloadValue.set(true) - accessAccountsDefinedVar.set(true) + accessAccountsDefinedVar.set(true) // only account details access will be provided List() case Some(accounts) if accounts.flatMap(_.iban).toSet.subsetOf(userIbans) => // Access is requested for specific IBANs accessAccountsDefinedVar.set(true) @@ -183,8 +183,10 @@ class BerlinGroupConsent extends MdcLoggable with RestHelper with APIMethods510 List() } val canReadBalancesIbans: List[String] = json.access.balances match { - case Some(balances) if balances.isEmpty => // Access is requested + case Some(balances) if balances.isEmpty => // Access is requested via "balances": [] updateConsentPayloadValue.set(true) + // access to account details and balances will be provided + accessAccountsDefinedVar.set(true) accessBalancesDefinedVar.set(true) List() case Some(balances) if balances.flatMap(_.iban).toSet.subsetOf(userIbans) => // Access is requested for specific IBANs @@ -199,8 +201,10 @@ class BerlinGroupConsent extends MdcLoggable with RestHelper with APIMethods510 List() } val canReadTransactionsIbans: List[String] = json.access.transactions match { - case Some(transactions) if transactions.isEmpty => // Access is requested + case Some(transactions) if transactions.isEmpty => // Access is requested via "transactions": [] updateConsentPayloadValue.set(true) + // access to account details and transactions will be provided + accessAccountsDefinedVar.set(true) accessTransactionsDefinedVar.set(true) List() case Some(transactions) if transactions.flatMap(_.iban).toSet.subsetOf(userIbans) => // Access is requested for specific IBANs diff --git a/obp-api/src/test/scala/code/api/berlin/group/v1_3/AccountInformationServiceAISApiTest.scala b/obp-api/src/test/scala/code/api/berlin/group/v1_3/AccountInformationServiceAISApiTest.scala index d25e4da8b..9e993fc2a 100644 --- a/obp-api/src/test/scala/code/api/berlin/group/v1_3/AccountInformationServiceAISApiTest.scala +++ b/obp-api/src/test/scala/code/api/berlin/group/v1_3/AccountInformationServiceAISApiTest.scala @@ -8,6 +8,7 @@ import code.api.util.APIUtil import code.api.util.APIUtil.OAuth._ import code.api.util.ErrorMessages._ import code.api.v4_0_0.PostViewJsonV400 +import code.consent.ConsentStatus import code.model.dataAccess.{BankAccountRouting, MappedBankAccount} import code.setup.{APIResponse, DefaultUsers} import com.github.dwickern.macros.NameOf.nameOf @@ -262,7 +263,7 @@ class AccountInformationServiceAISApiTest extends BerlinGroupServerSetupV1_3 wit Then("We should get a 201 ") response.code should equal(201) response.body.extract[PostConsentResponseJson].consentId should not be (empty) - response.body.extract[PostConsentResponseJson].consentStatus should be ("received") + response.body.extract[PostConsentResponseJson].consentStatus should be (ConsentStatus.received.toString) } } @@ -302,10 +303,16 @@ class AccountInformationServiceAISApiTest extends BerlinGroupServerSetupV1_3 wit val consentId =response.body.extract[PostConsentResponseJson].consentId Then("We test the delete consent ") - val requestDelete = (V1_3_BG / "consents"/consentId ).DELETE <@ (user1) + val requestDelete = (V1_3_BG / "consents"/ consentId ).DELETE <@ (user1) val responseDelete = makeDeleteRequest(requestDelete) responseDelete.code should be (204) + Then(s"We test the $getConsentStatus") + val requestGetStatus = (V1_3_BG / "consents" / consentId / "status").GET <@ (user1) + val responseGetStatus = makeGetRequest(requestGetStatus) + responseGetStatus.code should be(200) + responseGetStatus.body.extract[ConsentStatusJsonV13].consentStatus should be(ConsentStatus.terminatedByTpp.toString) + //TODO We can not delete one consent two time, will fix it later. // val responseDeleteSecondTime = makeDeleteRequest(requestDelete) // responseDeleteSecondTime.code should be (400) @@ -350,13 +357,13 @@ class AccountInformationServiceAISApiTest extends BerlinGroupServerSetupV1_3 wit val requestGet = (V1_3_BG / "consents"/consentId ).GET <@ (user1) val responseGet = makeGetRequest(requestGet) responseGet.code should be (200) - responseGet.body.extract[GetConsentResponseJson].consentStatus should be ("received") + responseGet.body.extract[GetConsentResponseJson].consentStatus should be (ConsentStatus.received.toString) Then(s"We test the $getConsentStatus") val requestGetStatus = (V1_3_BG / "consents"/consentId /"status" ).GET <@ (user1) val responseGetStatus = makeGetRequest(requestGetStatus) responseGetStatus.code should be (200) - responseGetStatus.body.extract[ConsentStatusJsonV13].consentStatus should be ("received") + responseGetStatus.body.extract[ConsentStatusJsonV13].consentStatus should be (ConsentStatus.received.toString) } } @@ -398,7 +405,7 @@ class AccountInformationServiceAISApiTest extends BerlinGroupServerSetupV1_3 wit val requestStartConsentAuthorisation = (V1_3_BG / "consents"/consentId /"authorisations" ).POST <@ (user1) val responseStartConsentAuthorisation = makePostRequest(requestStartConsentAuthorisation, """{"scaAuthenticationData":""}""") responseStartConsentAuthorisation.code should be (201) - responseStartConsentAuthorisation.body.extract[StartConsentAuthorisationJson].scaStatus should be ("received") + responseStartConsentAuthorisation.body.extract[StartConsentAuthorisationJson].scaStatus should be (ConsentStatus.received.toString) } } @@ -456,7 +463,7 @@ class AccountInformationServiceAISApiTest extends BerlinGroupServerSetupV1_3 wit val requestStartConsentAuthorisation = (V1_3_BG / "consents"/consentId /"authorisations" ).POST <@ (user1) val responseStartConsentAuthorisation = makePostRequest(requestStartConsentAuthorisation, """{"scaAuthenticationData":""}""") responseStartConsentAuthorisation.code should be (201) - responseStartConsentAuthorisation.body.extract[StartConsentAuthorisationJson].scaStatus should be ("received") + responseStartConsentAuthorisation.body.extract[StartConsentAuthorisationJson].scaStatus should be (ConsentStatus.received.toString) Then(s"We test the $getConsentAuthorisation") val requestGetConsentAuthorisation = (V1_3_BG / "consents"/consentId /"authorisations" ).GET<@ (user1) @@ -469,7 +476,7 @@ class AccountInformationServiceAISApiTest extends BerlinGroupServerSetupV1_3 wit val requestGetConsentScaStatus = (V1_3_BG / "consents"/consentId /"authorisations"/authorisationId ).GET <@ (user1) val responseGetConsentScaStatus = makeGetRequest(requestGetConsentScaStatus) responseGetConsentScaStatus.code should be (200) - responseGetConsentScaStatus.body.extract[ScaStatusJsonV13].scaStatus should be ("received") + responseGetConsentScaStatus.body.extract[ScaStatusJsonV13].scaStatus should be (ConsentStatus.received.toString) } } diff --git a/obp-api/src/test/scala/code/api/v5_1_0/ConsumerTest.scala b/obp-api/src/test/scala/code/api/v5_1_0/ConsumerTest.scala index 4fa77c795..6bc95ef20 100644 --- a/obp-api/src/test/scala/code/api/v5_1_0/ConsumerTest.scala +++ b/obp-api/src/test/scala/code/api/v5_1_0/ConsumerTest.scala @@ -27,7 +27,7 @@ package code.api.v5_1_0 import code.api.ResourceDocs1_4_0.SwaggerDefinitionsJSON import code.api.util.APIUtil.OAuth._ -import code.api.util.ApiRole.{canCreateConsumer, canGetConsumers, canUpdateConsumerLogoUrl, canUpdateConsumerName, canUpdateConsumerRedirectUrl} +import code.api.util.ApiRole.{canCreateConsumer, canGetConsumers, canUpdateConsumerCertificate, canUpdateConsumerLogoUrl, canUpdateConsumerName, canUpdateConsumerRedirectUrl} import code.api.util.ErrorMessages.{InvalidJsonFormat, UserNotLoggedIn} import code.api.v3_1_0.ConsumerJsonV310 import code.api.v5_1_0.OBPAPI5_1_0.Implementations5_1_0 @@ -52,10 +52,11 @@ class ConsumerTest extends V510ServerSetup { object ApiEndpoint3 extends Tag(nameOf(Implementations5_1_0.updateConsumerRedirectURL)) object ApiEndpoint4 extends Tag(nameOf(Implementations5_1_0.updateConsumerLogoURL)) object UpdateConsumerName extends Tag(nameOf(Implementations5_1_0.updateConsumerName)) + object UpdateConsumerCertificate extends Tag(nameOf(Implementations5_1_0.updateConsumerCertificate)) object GetConsumer extends Tag(nameOf(Implementations5_1_0.getConsumer)) feature("Test all error cases ") { - scenario("We test the authentication errors", UpdateConsumerName, GetConsumer, ApiEndpoint1, ApiEndpoint2, ApiEndpoint3, ApiEndpoint4, VersionOfApi) { + scenario("We test the authentication errors", UpdateConsumerName, GetConsumer, ApiEndpoint1, ApiEndpoint2, ApiEndpoint3, ApiEndpoint4, UpdateConsumerCertificate, VersionOfApi) { When("We make a request v5.1.0") lazy val postApiCollectionJson = SwaggerDefinitionsJSON.postApiCollectionJson400 val requestApiEndpoint1 = (v5_1_0_Request / "management" / "consumers").POST @@ -73,6 +74,9 @@ class ConsumerTest extends V510ServerSetup { val requestApiUpdateConsumerName = (v5_1_0_Request /"management" / "consumers" / "CONSUMER_ID" / "consumer" / "name").PUT val responseApiUpdateConsumerName = makePutRequest(requestApiUpdateConsumerName, write(postApiCollectionJson)) + val requestApiUpdateConsumerCertificate = (v5_1_0_Request /"management" / "consumers" / "CONSUMER_ID" / "consumer" / "certificate").PUT + val responseApiUpdateConsumerCertificate = makePutRequest(requestApiUpdateConsumerCertificate, write(postApiCollectionJson)) + Then(s"we should get the error messages") responseApiEndpoint1.code should equal(401) responseApiEndpoint2.code should equal(401) @@ -86,6 +90,9 @@ class ConsumerTest extends V510ServerSetup { responseApiUpdateConsumerName.code should equal(401) responseApiUpdateConsumerName.body.toString contains(s"$UserNotLoggedIn") should be (true) + responseApiUpdateConsumerCertificate.code should equal(401) + responseApiUpdateConsumerCertificate.body.toString contains(s"$UserNotLoggedIn") should be (true) + // Endpoint GetConsumer val requestApiEndpoint5 = (v5_1_0_Request / "management" / "consumers" / "whatever").GET val responseApiEndpoint5 = makeGetRequest(requestApiEndpoint5) @@ -93,7 +100,7 @@ class ConsumerTest extends V510ServerSetup { responseApiEndpoint5.body.toString contains(s"$UserNotLoggedIn") should be (true) } - scenario("We test the missing roles errors", UpdateConsumerName, GetConsumer, ApiEndpoint1, ApiEndpoint2, ApiEndpoint3, ApiEndpoint4, VersionOfApi) { + scenario("We test the missing roles errors", UpdateConsumerName, GetConsumer, ApiEndpoint1, ApiEndpoint2, ApiEndpoint3, ApiEndpoint4, UpdateConsumerCertificate, VersionOfApi) { When("We make a request v5.1.0") lazy val wrongJsonForTesting = SwaggerDefinitionsJSON.routing @@ -111,7 +118,10 @@ class ConsumerTest extends V510ServerSetup { val requestApiUpdateConsumerName = (v5_1_0_Request /"management" / "consumers" / "CONSUMER_ID" / "consumer" / "name").PUT<@ (user1) val responseApiUpdateConsumerName = makePutRequest(requestApiUpdateConsumerName, write(wrongJsonForTesting)) - + + val requestApiUpdateConsumerCertificate = (v5_1_0_Request / "management" / "consumers" / "CONSUMER_ID" / "consumer" / "certificate").PUT <@ (user1) + val responseApiUpdateConsumerCertificate = makePutRequest(requestApiUpdateConsumerCertificate, write(wrongJsonForTesting)) + Then(s"we should get the error messages") responseApiEndpoint1.code should equal(403) responseApiEndpoint1.body.toString contains(s"$canCreateConsumer") should be (true) @@ -124,6 +134,9 @@ class ConsumerTest extends V510ServerSetup { responseApiUpdateConsumerName.code should equal(403) responseApiUpdateConsumerName.body.toString contains(s"$canUpdateConsumerName") should be (true) + responseApiUpdateConsumerCertificate.code should equal(403) + responseApiUpdateConsumerCertificate.body.toString contains(s"$canUpdateConsumerCertificate") should be (true) + // Endpoint GetConsumer val requestApiEndpoint5 = (v5_1_0_Request / "management" / "consumers" / "whatever").GET <@ user1 val responseApiEndpoint5 = makeGetRequest(requestApiEndpoint5) @@ -137,6 +150,7 @@ class ConsumerTest extends V510ServerSetup { Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, canUpdateConsumerLogoUrl.toString) Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, canUpdateConsumerRedirectUrl.toString) Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, canUpdateConsumerName.toString) + Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, canUpdateConsumerCertificate.toString) When("We make a request v5.1.0") lazy val wrongJsonForTesting = SwaggerDefinitionsJSON.postApiCollectionJson400 @@ -152,6 +166,9 @@ class ConsumerTest extends V510ServerSetup { val requestApiUpdateConsumerName = (v5_1_0_Request / "management" / "consumers" / "CONSUMER_ID" / "consumer" / "name").PUT <@ (user1) val responseApiUpdateConsumerName = makePutRequest(requestApiUpdateConsumerName, write(wrongJsonForTesting)) + val requestApiUpdateConsumerCertificate = (v5_1_0_Request / "management" / "consumers" / "CONSUMER_ID" / "consumer" / "certificate").PUT <@ (user1) + val responseApiUpdateConsumerCertificate = makePutRequest(requestApiUpdateConsumerCertificate, write(wrongJsonForTesting)) + Then(s"we should get the error messages") responseApiEndpoint1.code should equal(400) responseApiEndpoint1.body.toString contains(s"$InvalidJsonFormat") should be (true) @@ -161,6 +178,9 @@ class ConsumerTest extends V510ServerSetup { responseApiEndpoint4.body.toString contains(s"$InvalidJsonFormat") should be (true) responseApiUpdateConsumerName.code should equal(400) responseApiUpdateConsumerName.body.toString contains(s"$InvalidJsonFormat") should be (true) + + responseApiUpdateConsumerCertificate.code should equal(400) + responseApiUpdateConsumerCertificate.body.toString contains(s"$InvalidJsonFormat") should be (true) } } @@ -173,11 +193,13 @@ class ConsumerTest extends V510ServerSetup { Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, canUpdateConsumerLogoUrl.toString) Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, canUpdateConsumerRedirectUrl.toString) Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, canUpdateConsumerName.toString) + Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, canUpdateConsumerCertificate.toString) lazy val createConsumerRequestJsonV510 = SwaggerDefinitionsJSON.createConsumerRequestJsonV510 lazy val consumerRedirectUrlJSON = SwaggerDefinitionsJSON.consumerRedirectUrlJSON lazy val consumerLogoUrlJson = SwaggerDefinitionsJSON.consumerLogoUrlJson lazy val consumerNameJson = SwaggerDefinitionsJSON.consumerNameJson + lazy val consumerCertificateJson = SwaggerDefinitionsJSON.consumerCertificateJson val requestApiEndpoint1 = (v5_1_0_Request / "management" / "consumers").POST<@ (user1) val responseApiEndpoint1 = makePostRequest(requestApiEndpoint1, write(createConsumerRequestJsonV510)) val consumerId = responseApiEndpoint1.body.extract[ConsumerJsonV510].consumer_id @@ -200,6 +222,11 @@ class ConsumerTest extends V510ServerSetup { val responseApiUpdateConsumerName = makePutRequest(requestApiUpdateConsumerName, write(consumerNameJson)) val name = responseApiUpdateConsumerName.body.extract[ConsumerJsonV510].app_name name shouldBe(consumerNameJson.app_name) + + val requestApiUpdateConsumerCertificate = (v5_1_0_Request / "management" / "consumers" / consumerId / "consumer" / "certificate").PUT <@ (user1) + val responseApiUpdateConsumerCertificate = makePutRequest(requestApiUpdateConsumerCertificate, write(consumerCertificateJson)) + val certificatePem = responseApiUpdateConsumerCertificate.body.extract[ConsumerJsonV510].certificate_pem + certificatePem shouldBe(consumerCertificateJson.certificate) Then(s"we should get the error messages") responseApiEndpoint1.code should equal(201)