From 2ae4bc7f97c04c6fc89effcf701fba25f71c5ae5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Mon, 10 Oct 2022 08:15:43 +0200 Subject: [PATCH 1/4] feature/Show which fields are optional / required in API Explorer --- .../code/api/v1_4_0/JSONFactory1_4_0.scala | 29 ++++++++++++++----- .../api/v1_4_0/JSONFactory1_4_0Test.scala | 2 +- .../commons/util/JsonUtils.scala | 9 ++++-- 3 files changed, 29 insertions(+), 11 deletions(-) diff --git a/obp-api/src/main/scala/code/api/v1_4_0/JSONFactory1_4_0.scala b/obp-api/src/main/scala/code/api/v1_4_0/JSONFactory1_4_0.scala index b9c284e7a..bf6b74580 100644 --- a/obp-api/src/main/scala/code/api/v1_4_0/JSONFactory1_4_0.scala +++ b/obp-api/src/main/scala/code/api/v1_4_0/JSONFactory1_4_0.scala @@ -408,7 +408,7 @@ object JSONFactory1_4_0 extends MdcLoggable{ if(findMatches.nonEmpty) { val urlParameters: List[String] = findMatches.toList.sorted - val parametersDescription: List[String] = urlParameters.map(prepareDescription) + val parametersDescription: List[String] = urlParameters.map(i => prepareDescription(i, Nil)) parametersDescription.mkString("\n\n\n**URL Parameters:**", "", "\n") } else { "" @@ -420,31 +420,44 @@ object JSONFactory1_4_0 extends MdcLoggable{ * @param parameter BANK_ID * @return [BANK_ID](/glossary#Bank.bank_id):gh.29.uk */ - def prepareDescription(parameter: String): String = { + def prepareDescription(parameter: String, types: List[(String, Boolean)]): String = { val glossaryItemTitle = getGlossaryItemTitle(parameter) val exampleFieldValue = getExampleFieldValue(parameter) + def boldIfMandatory() = { + types.exists(i => i._1 == parameter && i._2 == false) match { + case true => + s"**$parameter**" + case false => + s"$parameter" + } + } if(exampleFieldValue.contains(ExampleValue.NoExampleProvided)){ "" } else { s""" | - |* [${parameter}](/glossary#$glossaryItemTitle): $exampleFieldValue + |* [${boldIfMandatory()}](/glossary#$glossaryItemTitle): $exampleFieldValue | |""".stripMargin } } def prepareJsonFieldDescription(jsonBody: scala.Product, jsonType: String): String = { - - val jsonBodyJValue = jsonBody match { + jsonBody.productIterator + val (jsonBodyJValue: json.JValue, types) = jsonBody match { case JvalueCaseClass(jValue) => - jValue - case _ => decompose(jsonBody) + val types = Nil + (jValue, types) + case _ => + val types = jsonBody.getClass() + .getDeclaredFields().toList + .map(f => (f.getName(), f.getType().getCanonicalName().contains("Option"))) + (decompose(jsonBody), types) } val jsonBodyFields =JsonUtils.collectFieldNames(jsonBodyJValue).keySet.toList.sorted - val jsonFieldsDescription = jsonBodyFields.map(prepareDescription) + val jsonFieldsDescription = jsonBodyFields.map(i => prepareDescription(i, types)) val jsonTitleType = if (jsonType.contains("request")) "\n\n\n**JSON request body fields:**\n\n" else "\n\n\n**JSON response body fields:**\n\n" diff --git a/obp-api/src/test/scala/code/api/v1_4_0/JSONFactory1_4_0Test.scala b/obp-api/src/test/scala/code/api/v1_4_0/JSONFactory1_4_0Test.scala index 086d03340..5d54a1382 100644 --- a/obp-api/src/test/scala/code/api/v1_4_0/JSONFactory1_4_0Test.scala +++ b/obp-api/src/test/scala/code/api/v1_4_0/JSONFactory1_4_0Test.scala @@ -47,7 +47,7 @@ class JSONFactory1_4_0Test extends V140ServerSetup with DefaultUsers { feature("Test JSONFactory1_4_0") { scenario("prepareDescription should work well, extract the parameters from URL") { - val description = JSONFactory1_4_0.prepareDescription("BANK_ID") + val description = JSONFactory1_4_0.prepareDescription("BANK_ID", Nil) description.contains("[BANK_ID](/glossary#Bank.bank_id): gh.29.uk") should be (true) } diff --git a/obp-commons/src/main/scala/com/openbankproject/commons/util/JsonUtils.scala b/obp-commons/src/main/scala/com/openbankproject/commons/util/JsonUtils.scala index abdab4125..61208f53d 100644 --- a/obp-commons/src/main/scala/com/openbankproject/commons/util/JsonUtils.scala +++ b/obp-commons/src/main/scala/com/openbankproject/commons/util/JsonUtils.scala @@ -683,11 +683,16 @@ object JsonUtils { * @param jValue * @return */ - def collectFieldNames(jValue: JValue): Map[String, String] = { + def collectFieldNames(jValue: JValue, types: List[(String, Class[_])]): Map[String, String] = { val buffer = scala.collection.mutable.Map[String, String]() transformField(jValue){ case (jField, path) => - buffer += (jField.name -> jField.value.toString) + types.exists(i => jField.name == i._1 && i._2.getCanonicalName().contains("Option")) match { + case false => + buffer += (s"${jField.name}*" -> jField.value.toString) + case true => + buffer += (jField.name -> jField.value.toString) + } jField } From ea38c355f5d4109815afd3e2ffa8a7ec18c80f0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Mon, 10 Oct 2022 08:28:43 +0200 Subject: [PATCH 2/4] feature/Show which fields are optional / required in API Explorer --- .../com/openbankproject/commons/util/JsonUtils.scala | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/obp-commons/src/main/scala/com/openbankproject/commons/util/JsonUtils.scala b/obp-commons/src/main/scala/com/openbankproject/commons/util/JsonUtils.scala index 61208f53d..abdab4125 100644 --- a/obp-commons/src/main/scala/com/openbankproject/commons/util/JsonUtils.scala +++ b/obp-commons/src/main/scala/com/openbankproject/commons/util/JsonUtils.scala @@ -683,16 +683,11 @@ object JsonUtils { * @param jValue * @return */ - def collectFieldNames(jValue: JValue, types: List[(String, Class[_])]): Map[String, String] = { + def collectFieldNames(jValue: JValue): Map[String, String] = { val buffer = scala.collection.mutable.Map[String, String]() transformField(jValue){ case (jField, path) => - types.exists(i => jField.name == i._1 && i._2.getCanonicalName().contains("Option")) match { - case false => - buffer += (s"${jField.name}*" -> jField.value.toString) - case true => - buffer += (jField.name -> jField.value.toString) - } + buffer += (jField.name -> jField.value.toString) jField } From ee4053f5619c07585fea6fdc0fa81e7c1fb4528b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Tue, 11 Oct 2022 11:29:21 +0200 Subject: [PATCH 3/4] feature/introduce CanGetCustomerOverview and CanGetCustomerOverviewFlat --- .../main/scala/code/api/util/ApiRole.scala | 6 ++++++ .../scala/code/api/v5_0_0/APIMethods500.scala | 20 ++++++------------- .../api/v5_0_0/CustomerOverviewTest.scala | 16 +++++++-------- 3 files changed, 20 insertions(+), 22 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 839f28d30..1c8df7055 100644 --- a/obp-api/src/main/scala/code/api/util/ApiRole.scala +++ b/obp-api/src/main/scala/code/api/util/ApiRole.scala @@ -96,6 +96,12 @@ object ApiRole { case class CanGetCustomer(requiresBankId: Boolean = true) extends ApiRole lazy val canGetCustomer = CanGetCustomer() + + case class CanGetCustomerOverview(requiresBankId: Boolean = true) extends ApiRole + lazy val canGetCustomerOverview = CanGetCustomerOverview() + + case class CanGetCustomerOverviewFlat(requiresBankId: Boolean = true) extends ApiRole + lazy val canGetCustomerOverviewFlat = CanGetCustomerOverviewFlat() case class CanCreateCustomer(requiresBankId: Boolean = true) extends ApiRole lazy val canCreateCustomer = CanCreateCustomer() diff --git a/obp-api/src/main/scala/code/api/v5_0_0/APIMethods500.scala b/obp-api/src/main/scala/code/api/v5_0_0/APIMethods500.scala index 02b61eabe..0ad770021 100644 --- a/obp-api/src/main/scala/code/api/v5_0_0/APIMethods500.scala +++ b/obp-api/src/main/scala/code/api/v5_0_0/APIMethods500.scala @@ -1040,21 +1040,17 @@ trait APIMethods500 { UnknownError ), List(apiTagCustomer, apiTagKyc ,apiTagNewStyle), - Some(List(canGetCustomer)) + Some(List(canGetCustomerOverview)) ) lazy val getCustomerOverview : OBPEndpoint = { case "banks" :: BankId(bankId) :: "customers" :: "customer-number-query" :: "overview" :: Nil JsonPost json -> req => { cc => for { - (Full(u), callContext) <- authenticatedAccess(cc) - (bank, callContext) <- NewStyle.function.getBank(bankId, callContext) - _ <- NewStyle.function.hasEntitlement(bankId.value, u.userId, canGetCustomer, callContext) - failMsg = s"$InvalidJsonFormat The Json body should be the $PostCustomerOverviewJsonV500 " - postedData <- NewStyle.function.tryons(failMsg, 400, callContext) { + postedData <- NewStyle.function.tryons(s"$InvalidJsonFormat The Json body should be the $PostCustomerOverviewJsonV500 ", 400, cc.callContext) { json.extract[PostCustomerOverviewJsonV500] } - (customer, callContext) <- NewStyle.function.getCustomerByCustomerNumber(postedData.customer_number, bank.bankId, callContext) + (customer, callContext) <- NewStyle.function.getCustomerByCustomerNumber(postedData.customer_number, bankId, cc.callContext) (customerAttributes, callContext) <- NewStyle.function.getCustomerAttributes( bankId, CustomerId(customer.customerId), @@ -1093,21 +1089,17 @@ trait APIMethods500 { UnknownError ), List(apiTagCustomer, apiTagKyc ,apiTagNewStyle), - Some(List(canGetCustomer)) + Some(List(canGetCustomerOverviewFlat)) ) lazy val getCustomerOverviewFlat : OBPEndpoint = { case "banks" :: BankId(bankId) :: "customers" :: "customer-number-query" :: "overview-flat" :: Nil JsonPost json -> req => { cc => for { - (Full(u), callContext) <- authenticatedAccess(cc) - (bank, callContext) <- NewStyle.function.getBank(bankId, callContext) - _ <- NewStyle.function.hasEntitlement(bankId.value, u.userId, canGetCustomer, callContext) - failMsg = s"$InvalidJsonFormat The Json body should be the $PostCustomerOverviewJsonV500 " - postedData <- NewStyle.function.tryons(failMsg, 400, callContext) { + postedData <- NewStyle.function.tryons(s"$InvalidJsonFormat The Json body should be the $PostCustomerOverviewJsonV500 ", 400, cc.callContext) { json.extract[PostCustomerOverviewJsonV500] } - (customer, callContext) <- NewStyle.function.getCustomerByCustomerNumber(postedData.customer_number, bank.bankId, callContext) + (customer, callContext) <- NewStyle.function.getCustomerByCustomerNumber(postedData.customer_number, bankId, cc.callContext) (customerAttributes, callContext) <- NewStyle.function.getCustomerAttributes( bankId, CustomerId(customer.customerId), diff --git a/obp-api/src/test/scala/code/api/v5_0_0/CustomerOverviewTest.scala b/obp-api/src/test/scala/code/api/v5_0_0/CustomerOverviewTest.scala index 4ec4f1b7b..3bfe5f6bd 100644 --- a/obp-api/src/test/scala/code/api/v5_0_0/CustomerOverviewTest.scala +++ b/obp-api/src/test/scala/code/api/v5_0_0/CustomerOverviewTest.scala @@ -87,13 +87,13 @@ class CustomerOverviewTest extends V500ServerSetup { val response = makePostRequest(request, write(getCustomerJson)) Then("We should get a 403") response.code should equal(403) - And("error should be " + canGetCustomer) + And("error should be " + canGetCustomerOverview) val errorMessage = response.body.extract[ErrorMessage].message errorMessage contains (UserHasMissingRoles) should be (true) - errorMessage contains (canGetCustomer.toString()) should be (true) + errorMessage contains (canGetCustomerOverview.toString()) should be (true) } scenario(s"We will call the endpoint $ApiEndpoint1 with a user credentials and a proper role", ApiEndpoint1, VersionOfApi) { - Entitlement.entitlement.vend.addEntitlement(bankId, resourceUser1.userId, CanGetCustomer.toString) + Entitlement.entitlement.vend.addEntitlement(bankId, resourceUser1.userId, CanGetCustomerOverview.toString) When(s"We make a request $VersionOfApi") val request = (v5_0_0_Request / "banks" / bankId / "customers" / "customer-number-query" / "overview").POST <@(user1) val response = makePostRequest(request, write(getCustomerJson)) @@ -106,7 +106,7 @@ class CustomerOverviewTest extends V500ServerSetup { val legalName = "Evelin Doe" val mobileNumber = "+44 123 456" val customer: CustomerJsonV310 = createCustomerEndpointV500(bankId, legalName, mobileNumber) - Entitlement.entitlement.vend.addEntitlement(bankId, resourceUser1.userId, CanGetCustomer.toString) + Entitlement.entitlement.vend.addEntitlement(bankId, resourceUser1.userId, CanGetCustomerOverview.toString) When(s"We make a request $VersionOfApi") val request = (v5_0_0_Request / "banks" / bankId / "customers" / "customer-number-query" / "overview").POST <@(user1) val response = makePostRequest(request, write(PostCustomerOverviewJsonV500(customer.customer_number))) @@ -140,13 +140,13 @@ class CustomerOverviewTest extends V500ServerSetup { val response = makePostRequest(request, write(getCustomerJson)) Then("We should get a 403") response.code should equal(403) - And("error should be " + canGetCustomer) + And("error should be " + canGetCustomerOverviewFlat) val errorMessage = response.body.extract[ErrorMessage].message errorMessage contains (UserHasMissingRoles) should be (true) - errorMessage contains (canGetCustomer.toString()) should be (true) + errorMessage contains (canGetCustomerOverviewFlat.toString()) should be (true) } scenario(s"We will call the endpoint $ApiEndpoint2 with a user credentials and a proper role", ApiEndpoint1, VersionOfApi) { - Entitlement.entitlement.vend.addEntitlement(bankId, resourceUser1.userId, CanGetCustomer.toString) + Entitlement.entitlement.vend.addEntitlement(bankId, resourceUser1.userId, CanGetCustomerOverviewFlat.toString) When(s"We make a request $VersionOfApi") val request = (v5_0_0_Request / "banks" / bankId / "customers" / "customer-number-query" / "overview-flat").POST <@(user1) val response = makePostRequest(request, write(getCustomerJson)) @@ -159,7 +159,7 @@ class CustomerOverviewTest extends V500ServerSetup { val legalName = "Evelin Doe" val mobileNumber = "+44 123 456" val customer: CustomerJsonV310 = createCustomerEndpointV500(bankId, legalName, mobileNumber) - Entitlement.entitlement.vend.addEntitlement(bankId, resourceUser1.userId, CanGetCustomer.toString) + Entitlement.entitlement.vend.addEntitlement(bankId, resourceUser1.userId, CanGetCustomerOverviewFlat.toString) When(s"We make a request $VersionOfApi") val request = (v5_0_0_Request / "banks" / bankId / "customers" / "customer-number-query" / "overview-flat").POST <@(user1) val response = makePostRequest(request, write(PostCustomerOverviewJsonV500(customer.customer_number))) From 4c3726b70ace8131b4fc80af5eab960d1f8a2b09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Wed, 12 Oct 2022 11:11:16 +0200 Subject: [PATCH 4/4] feature/Send Email when Role is granted to User --- .../code/api/util/NotificationUtil.scala | 50 +++++++++++++++++++ .../code/entitlement/MappedEntitlements.scala | 4 +- 2 files changed, 53 insertions(+), 1 deletion(-) create mode 100644 obp-api/src/main/scala/code/api/util/NotificationUtil.scala diff --git a/obp-api/src/main/scala/code/api/util/NotificationUtil.scala b/obp-api/src/main/scala/code/api/util/NotificationUtil.scala new file mode 100644 index 000000000..8b8f95e4a --- /dev/null +++ b/obp-api/src/main/scala/code/api/util/NotificationUtil.scala @@ -0,0 +1,50 @@ +package code.api.util + +import code.api.Constant +import code.entitlement.Entitlement +import code.users.Users +import code.util.Helper.MdcLoggable +import com.openbankproject.commons.model.User +import net.liftweb.common.Box +import net.liftweb.util.Mailer +import net.liftweb.util.Mailer._ + +import scala.collection.immutable.List + +object NotificationUtil extends MdcLoggable { + def sendEmailRegardingAssignedRole(userId : String, entitlement: Entitlement): Unit = { + val user = Users.users.vend.getUserByUserId(userId) + sendEmailRegardingAssignedRole(user, entitlement) + } + def sendEmailRegardingAssignedRole(user: Box[User], entitlement: Entitlement): Unit = { + val mailSent = for { + user <- user + from <- APIUtil.getPropsValue("mail.api.consumer.registered.sender.address") ?~ "Could not send mail: Missing props param for 'from'" + } yield { + val bodyOfMessage : String = s"""Dear ${user.name}, + | + |You have been granted the entitlement to use ${entitlement.roleName} on ${Constant.HostName} + | + |Cheers + |""".stripMargin + val params = PlainMailBodyType(bodyOfMessage) :: List(To(user.emailAddress)) + val subjectOfMessage = "You have been granted the role" + //this is an async call + Mailer.sendMail( + From(from), + Subject(subjectOfMessage), + params :_* + ) + } + if(mailSent.isEmpty) { + val info = + s""" + |Sending email is omitted. + |User: $user + |Props mail.api.consumer.registered.sender.address: ${APIUtil.getPropsValue("mail.api.consumer.registered.sender.address")} + |""".stripMargin + this.logger.warn(info) + } + } + +} diff --git a/obp-api/src/main/scala/code/entitlement/MappedEntitlements.scala b/obp-api/src/main/scala/code/entitlement/MappedEntitlements.scala index 97dcfa379..87e79d68f 100644 --- a/obp-api/src/main/scala/code/entitlement/MappedEntitlements.scala +++ b/obp-api/src/main/scala/code/entitlement/MappedEntitlements.scala @@ -2,7 +2,7 @@ package code.entitlement import code.api.dynamic.endpoint.helper.DynamicEntityInfo import code.api.util.ApiRole.{CanCreateEntitlementAtAnyBank, CanCreateEntitlementAtOneBank} -import code.api.util.ErrorMessages +import code.api.util.{ErrorMessages, NotificationUtil} import code.util.{MappedUUID, UUIDString} import net.liftweb.common.{Box, Failure, Full} import net.liftweb.mapper._ @@ -110,6 +110,8 @@ object MappedEntitlementsProvider extends EntitlementProvider { val addEntitlement: MappedEntitlement = MappedEntitlement.create.mBankId(bankId).mUserId(userId).mRoleName(roleName).mCreatedByProcess(createdByProcess) .saveMe() + // When a role is Granted, we should send an email to the Recipient telling them they have been granted the role. + NotificationUtil.sendEmailRegardingAssignedRole(userId: String, addEntitlement: Entitlement) Full(addEntitlement) } // Return a Box so we can handle errors later.