diff --git a/obp-api/src/main/resources/props/sample.props.template b/obp-api/src/main/resources/props/sample.props.template index 3f9b3c274..40f3d3acb 100644 --- a/obp-api/src/main/resources/props/sample.props.template +++ b/obp-api/src/main/resources/props/sample.props.template @@ -423,6 +423,9 @@ webui_dummy_customer_logins = Customer Logins\ \ Please ask a member of the Open Bank Project team for more logins if you require. You can use this [application](https://sofit.openbankproject.com) which also uses OAuth to browse your transaction data (use the above username/password).\ +# when this value is set to true and webui_dummy_customer_logins value not empty, the register consumer key success page will show dummy customers Direct Login tokens. +webui_show_dummy_customer_tokens=false + # when developer register the consumer successfully, it will show this message to developer on the webpage or email. webui_register_consumer_success_message_webpage = Thanks for registering your consumer with the Open Bank Project API! Here is your developer information. Please save it in a secure location. webui_register_consumer_success_message_email = Thank you for registering to use the Open Bank Project API. diff --git a/obp-api/src/main/scala/code/api/OBPRestHelper.scala b/obp-api/src/main/scala/code/api/OBPRestHelper.scala index ff7697347..b315e0a0d 100644 --- a/obp-api/src/main/scala/code/api/OBPRestHelper.scala +++ b/obp-api/src/main/scala/code/api/OBPRestHelper.scala @@ -267,7 +267,7 @@ trait OBPRestHelper extends RestHelper with MdcLoggable { fn(cc.copy(user = Full(u), consumer=consumer)) }// Authentication is successful case _ => { - var (httpCode, message, directLoginParameters) = DirectLogin.validator("protectedResource", DirectLogin.getHttpMethod) + var (httpCode, message, directLoginParameters) = DirectLogin.validator("protectedResource") Full(errorJsonResponse(message, httpCode)) } } 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 07adedcca..674b0c797 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 @@ -245,17 +245,17 @@ object SwaggerDefinitionsJSON { ) ) - val transactionTypeId = TransactionTypeId(value = "123") + val transactionTypeIdSwagger = TransactionTypeId(value = "123") - val bankId = BankId(value = "gh.uk.9j") + val bankIdSwagger = BankId(value = "gh.uk.9j") - val transactionRequestId = TransactionRequestId(value = "123") + val transactionRequestIdSwagger = TransactionRequestId(value = "123") - val counterpartyId = CounterpartyId(value = "123") + val counterpartyIdSwagger = CounterpartyId(value = "123") - val accountId = model.AccountId(value = "123") + val accountIdSwagger = model.AccountId(value = "123") - val viewId = ViewId(value = "owner") + val viewIdSwagger = ViewId(value = "owner") // from code.TransactionTypes.TransactionType, not from normal version Factory @@ -264,8 +264,8 @@ object SwaggerDefinitionsJSON { import code.TransactionTypes.TransactionType._ val transactionType = TransactionType( - id = transactionTypeId, - bankId = bankId, + id = transactionTypeIdSwagger, + bankId = bankIdSwagger, shortCode = "80080", summary = SANDBOX_TAN.toString, description = "This is the sandbox mode, charging litter money.", @@ -386,7 +386,7 @@ object SwaggerDefinitionsJSON { ) val transactionRequest = TransactionRequest( - id= transactionRequestId, + id= transactionRequestIdSwagger, `type`= "String", from= transactionRequestAccount, body= transactionRequestBodyAllTypes, @@ -397,11 +397,11 @@ object SwaggerDefinitionsJSON { challenge= transactionRequestChallenge, charge= transactionRequestCharge, charge_policy= "String", - counterparty_id= counterpartyId, + counterparty_id= counterpartyIdSwagger, name= "String", - this_bank_id= bankId, - this_account_id= accountId, - this_view_id= viewId, + this_bank_id= bankIdSwagger, + this_account_id= accountIdSwagger, + this_view_id= viewIdSwagger, other_account_routing_scheme= "String", other_account_routing_address= "String", other_bank_routing_scheme= "String", @@ -1044,7 +1044,7 @@ object SwaggerDefinitionsJSON { ) val transactionRequestJson = TransactionRequestJson( - id = transactionRequestId, + id = transactionRequestIdSwagger, `type` = "String", from = transactionRequestAccount, details = transactionRequestBodyJson, @@ -1056,11 +1056,11 @@ object SwaggerDefinitionsJSON { challenge = transactionRequestChallenge, charge = transactionRequestCharge, charge_policy = "String", - counterparty_id = counterpartyId, + counterparty_id = counterpartyIdSwagger, name = "String", - this_bank_id = bankId, - this_account_id = accountId, - this_view_id = viewId, + this_bank_id = bankIdSwagger, + this_account_id = accountIdSwagger, + this_view_id = viewIdSwagger, other_account_routing_scheme = "String", other_account_routing_address = "String", other_bank_routing_scheme = "String", @@ -1671,7 +1671,7 @@ object SwaggerDefinitionsJSON { ) val transactionTypeJsonV200 = TransactionTypeJsonV200( - id = transactionTypeId, + id = transactionTypeIdSwagger, bank_id = bankIdExample.value, short_code = "PlaceholderString", summary = "PlaceholderString", @@ -2841,7 +2841,7 @@ object SwaggerDefinitionsJSON { ) val transactionInnerJson = TransactionInnerJson( - AccountId = accountId.value, + AccountId = accountIdSwagger.value, TransactionId = "123", TransactionReference = "Ref 1", Amount = amountOfMoneyJsonV121, diff --git a/obp-api/src/main/scala/code/api/directlogin.scala b/obp-api/src/main/scala/code/api/directlogin.scala index 645702fa7..834aa58ad 100644 --- a/obp-api/src/main/scala/code/api/directlogin.scala +++ b/obp-api/src/main/scala/code/api/directlogin.scala @@ -94,36 +94,7 @@ object DirectLogin extends RestHelper with MdcLoggable { //Handling get request for a token case Req("my" :: "logins" :: "direct" :: Nil,_ , PostRequest) => { - //Extract the directLogin parameters from the header and test if the request is valid - var (httpCode, message, directLoginParameters) = validator("authorizationToken", getHttpMethod) - - if (httpCode == 200) { - val userId:Long = (for {id <- getUserId(directLoginParameters)} yield id).getOrElse(0) - - if (userId == 0) { - message = ErrorMessages.InvalidLoginCredentials - httpCode = 401 - } else if (userId == AuthUser.usernameLockedStateCode) { - message = ErrorMessages.UsernameHasBeenLocked - httpCode = 401 - } else { - val jwtPayloadAsJson = - """{ - "":"" - }""" - - val jwtClaims: JWTClaimsSet = JWTClaimsSet.parse(jwtPayloadAsJson) - val (token:String, secret:String) = generateTokenAndSecret(jwtClaims) - - //Save the token that we have generated - if (saveAuthorizationToken(directLoginParameters, token, secret, userId)) { - message = token - } else { - httpCode = 500 - message = "invalid" - } - } - } + val (httpCode: Int, message: String) = createToken(getAllParameters) if (httpCode == 200) successJsonResponse(Extraction.decompose(JSONFactory.createTokenJSON(message)), 201) @@ -132,6 +103,45 @@ object DirectLogin extends RestHelper with MdcLoggable { } } + /** + * according username, password, consumer_key to generate a DirectLogin token + * @param allParameters map {"username": "some_username", "password": "some_password", "consumer_key": "some_consumer_key"} + * @return httpCode and token value + */ + def createToken(allParameters: Map[String, String]) = { + //Extract the directLogin parameters from the header and test if the request is valid + var (httpCode, message, directLoginParameters) = validator("authorizationToken", allParameters) + + if (httpCode == 200) { + val userId: Long = (for {id <- getUserId(directLoginParameters)} yield id).getOrElse(0) + + if (userId == 0) { + message = ErrorMessages.InvalidLoginCredentials + httpCode = 401 + } else if (userId == AuthUser.usernameLockedStateCode) { + message = ErrorMessages.UsernameHasBeenLocked + httpCode = 401 + } else { + val jwtPayloadAsJson = + """{ + "":"" + }""" + + val jwtClaims: JWTClaimsSet = JWTClaimsSet.parse(jwtPayloadAsJson) + val (token: String, secret: String) = generateTokenAndSecret(jwtClaims) + + //Save the token that we have generated + if (saveAuthorizationToken(directLoginParameters, token, secret, userId)) { + message = token + } else { + httpCode = 500 + message = "invalid" + } + } + } + (httpCode, message) + } + def getHttpMethod = S.request match { case Full(s) => s.post_? match { case true => "POST" @@ -217,7 +227,7 @@ object DirectLogin extends RestHelper with MdcLoggable { //Check if the request (access token or request token) is valid and return a tuple - def validator(requestType : String, httpMethod : String) : (Int, String, Map[String,String]) = { + def validator(requestType : String, allParameters: Map[String, String] = getAllParameters) : (Int, String, Map[String,String]) = { def validAccessToken(tokenKey: String) = { Tokens.tokens.vend.getTokenByKeyAndType(tokenKey, TokenType.Access) match { @@ -229,12 +239,10 @@ object DirectLogin extends RestHelper with MdcLoggable { var message = "" var httpCode: Int = 500 - val parameters = getAllParameters - //are all the necessary directLogin parameters present? - val missingParams = missingDirectLoginParameters(parameters, requestType) + val missingParams = missingDirectLoginParameters(allParameters, requestType) //guard maximum length and content of strings (a-z, 0-9 etc.) for parameters - val validParams = validDirectLoginParameters(parameters) + val validParams = validDirectLoginParameters(allParameters) if (missingParams.nonEmpty) { message = ErrorMessages.DirectLoginMissingParameters + missingParams.mkString(", ") @@ -246,18 +254,18 @@ object DirectLogin extends RestHelper with MdcLoggable { } else if ( requestType == "protectedResource" && - ! validAccessToken(parameters.getOrElse("token", "")) + ! validAccessToken(allParameters.getOrElse("token", "")) ) { - message = ErrorMessages.DirectLoginInvalidToken + parameters.getOrElse("token", "") + message = ErrorMessages.DirectLoginInvalidToken + allParameters.getOrElse("token", "") httpCode = 401 } //check if the application is registered and active else if ( requestType == "authorizationToken" && APIUtil.getPropsAsBoolValue("direct_login_consumer_key_mandatory", true) && - ! APIUtil.registeredApplication(parameters.getOrElse("consumer_key", ""))) { + ! APIUtil.registeredApplication(allParameters.getOrElse("consumer_key", ""))) { - logger.error("application: " + parameters.getOrElse("consumer_key", "") + " not found") + logger.error("application: " + allParameters.getOrElse("consumer_key", "") + " not found") message = ErrorMessages.InvalidConsumerKey httpCode = 401 } @@ -265,7 +273,7 @@ object DirectLogin extends RestHelper with MdcLoggable { httpCode = 200 if(message.nonEmpty) logger.error("error message : " + message) - (httpCode, message, parameters) + (httpCode, message, allParameters) } @@ -377,7 +385,7 @@ object DirectLogin extends RestHelper with MdcLoggable { case Full(r) => r.request.method case _ => "GET" } - val (httpCode, message, directLoginParameters) = validator("protectedResource", httpMethod) + val (httpCode, message, directLoginParameters) = validator("protectedResource") if (httpCode == 400 || httpCode == 401) ParamFailure(message, Empty, Empty, APIFailure(message, httpCode)) @@ -450,7 +458,7 @@ object DirectLogin extends RestHelper with MdcLoggable { case _ => "GET" } - val (httpCode, message, directLoginParameters) = validator("protectedResource", httpMethod) + val (httpCode, message, directLoginParameters) = validator("protectedResource") val consumer: Option[Consumer] = for { tokenId: String <- directLoginParameters.get("token") 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 61580e653..a296bd9cc 100644 --- a/obp-api/src/main/scala/code/api/util/ApiRole.scala +++ b/obp-api/src/main/scala/code/api/util/ApiRole.scala @@ -381,17 +381,17 @@ object ApiRole { case class CanAddKycStatus(requiresBankId: Boolean = true) extends ApiRole lazy val canAddKycStatus = CanAddKycStatus() - case class CanGetKycChecks(requiresBankId: Boolean = true) extends ApiRole - lazy val canGetKycChecks = CanGetKycChecks() + case class CanGetAnyKycChecks(requiresBankId: Boolean = false) extends ApiRole + lazy val canGetAnyKycChecks = CanGetAnyKycChecks() - case class CanGetKycDocuments(requiresBankId: Boolean = true) extends ApiRole - lazy val canGetKycDocuments = CanGetKycDocuments() + case class CanGetAnyKycDocuments(requiresBankId: Boolean = false) extends ApiRole + lazy val canGetAnyKycDocuments = CanGetAnyKycDocuments() - case class CanGetKycMedia(requiresBankId: Boolean = true) extends ApiRole - lazy val canGetKycMedia = CanGetKycMedia() + case class CanGetAnyKycMedia(requiresBankId: Boolean = false) extends ApiRole + lazy val canGetAnyKycMedia = CanGetAnyKycMedia() - case class CanGetKycStatuses(requiresBankId: Boolean = true) extends ApiRole - lazy val canGetKycStatuses = CanGetKycStatuses() + case class CanGetAnyKycStatuses(requiresBankId: Boolean = false) extends ApiRole + lazy val canGetAnyKycStatuses = CanGetAnyKycStatuses() private val roles = canSearchAllTransactions :: @@ -512,10 +512,10 @@ object ApiRole { canAddKycDocument :: canAddKycMedia :: canAddKycStatus :: - canGetKycChecks :: - canGetKycDocuments :: - canGetKycMedia :: - canGetKycStatuses :: + canGetAnyKycChecks :: + canGetAnyKycDocuments :: + canGetAnyKycMedia :: + canGetAnyKycStatuses :: Nil lazy val rolesMappedToClasses = roles.map(_.getClass) 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 1ee249f7f..d4ab1c47f 100644 --- a/obp-api/src/main/scala/code/api/util/ErrorMessages.scala +++ b/obp-api/src/main/scala/code/api/util/ErrorMessages.scala @@ -36,9 +36,10 @@ object ErrorMessages { // DynamicEntity Exceptions (OBP-09XXX) val DynamicEntityNotFoundByDynamicEntityId = "OBP-09001: DynamicEntity not found. Please specify a valid value for dynamic_entity_id." - val DynamicEntityEntityNameAlreadyExists = "OBP-09002: DynamicEntity's entityName already exists. Please specify a different value for entityName." - val DynamicEntityEntityNotExists = "OBP-09003: DynamicEntity not exists. Please check entityName." + val DynamicEntityNameAlreadyExists = "OBP-09002: DynamicEntity's entityName already exists. Please specify a different value for entityName." + val DynamicEntityNotExists = "OBP-09003: DynamicEntity not exists. Please check entityName." val DynamicEntityMissArgument = "OBP-09004: DynamicEntity process related argument is missing." + val EntityNotFoundByEntityId = "OBP-09005: Entity not found. Please specify a valid value for entityId." // General messages (OBP-10XXX) val InvalidJsonFormat = "OBP-10001: Incorrect json format." 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 a6a9d4c69..01791c73b 100644 --- a/obp-api/src/main/scala/code/api/util/NewStyle.scala +++ b/obp-api/src/main/scala/code/api/util/NewStyle.scala @@ -32,7 +32,7 @@ import com.openbankproject.commons.model.enums.DynamicEntityOperation.{CREATE, U import com.openbankproject.commons.model.enums.{AccountAttributeType, CardAttributeType, DynamicEntityOperation, ProductAttributeType} import com.openbankproject.commons.model.{AccountApplication, Bank, Customer, CustomerAddress, Product, ProductCollection, ProductCollectionItem, TaxResidence, UserAuthContext, UserAuthContextUpdate, _} import com.tesobe.CacheKeyFromArguments -import net.liftweb.common.{Box, Empty, Full} +import net.liftweb.common.{Box, Empty, Full, ParamFailure} import net.liftweb.http.provider.HTTPParam import net.liftweb.json import net.liftweb.json.{JArray, JBool, JDouble, JInt, JObject, JString, JValue} @@ -1400,27 +1400,51 @@ object NewStyle { } } - def createOrUpdateDynamicEntity(dynamicEntity: DynamicEntityT, callContext: Option[CallContext]): Future[Box[DynamicEntityT]] = { + private def createDynamicEntity(dynamicEntity: DynamicEntityT, callContext: Option[CallContext]): Future[Box[DynamicEntityT]] = { val existsDynamicEntity = DynamicEntityProvider.connectorMethodProvider.vend.getByEntityName(dynamicEntity.entityName) - val isEntityNameNotChange = existsDynamicEntity.isEmpty || - existsDynamicEntity.filter(_.dynamicEntityId == dynamicEntity.dynamicEntityId).isDefined + if(existsDynamicEntity.isDefined) { + val errorMsg = s"$DynamicEntityNameAlreadyExists current entityName is '${dynamicEntity.entityName}'." + return Helper.booleanToFuture(errorMsg)(existsDynamicEntity.isEmpty).map(_.asInstanceOf[Box[DynamicEntityT]]) + } - isEntityNameNotChange match { - case true => Future { - DynamicEntityProvider.connectorMethodProvider.vend.createOrUpdate(dynamicEntity) - } - case false => { - val entityNameExists = existsDynamicEntity.isDefined - // validate whether entityName is exists - val errorMsg = s"$DynamicEntityEntityNameAlreadyExists current entityName is '${dynamicEntity.entityName}'." - Helper.booleanToFuture(errorMsg)(!entityNameExists).map { _ => - DynamicEntityProvider.connectorMethodProvider.vend.createOrUpdate(dynamicEntity) - } - } + Future { + DynamicEntityProvider.connectorMethodProvider.vend.createOrUpdate(dynamicEntity) } } + private def updateDynamicEntity(dynamicEntity: DynamicEntityT, dynamicEntityId: String , callContext: Option[CallContext]): Future[Box[DynamicEntityT]] = { + val originEntity = DynamicEntityProvider.connectorMethodProvider.vend.getById(dynamicEntityId) + // if can't find by id, return 404 error + val idNotExistsMsg = s"$DynamicEntityNotFoundByDynamicEntityId dynamicEntityId = ${dynamicEntity.dynamicEntityId.get}." + + if (originEntity.isEmpty) { + return Helper.booleanToFuture(idNotExistsMsg, 404)(originEntity.isDefined).map(_.asInstanceOf[Box[DynamicEntityT]]) + } + + val originEntityName = originEntity.map(_.entityName).orNull + // if entityName changed and the new entityName already exists, return error message + if(dynamicEntity.entityName != originEntityName) { + val existsDynamicEntity = DynamicEntityProvider.connectorMethodProvider.vend.getByEntityName(dynamicEntity.entityName) + + if(existsDynamicEntity.isDefined) { + val errorMsg = s"$DynamicEntityNameAlreadyExists current entityName is '${dynamicEntity.entityName}'." + return Helper.booleanToFuture(errorMsg)(existsDynamicEntity.isEmpty).map(_.asInstanceOf[Box[DynamicEntityT]]) + } + } + + Future { + DynamicEntityProvider.connectorMethodProvider.vend.createOrUpdate(dynamicEntity) + } + + } + + def createOrUpdateDynamicEntity(dynamicEntity: DynamicEntityT, callContext: Option[CallContext]): Future[Box[DynamicEntityT]] = + dynamicEntity.dynamicEntityId match { + case Some(dynamicEntityId) => updateDynamicEntity(dynamicEntity, dynamicEntityId, callContext) + case None => createDynamicEntity(dynamicEntity, callContext) + } + def deleteDynamicEntity(dynamicEntityId: String): Future[Box[Boolean]] = Future { DynamicEntityProvider.connectorMethodProvider.vend.delete(dynamicEntityId) } diff --git a/obp-api/src/main/scala/code/api/v1_2_1/APIMethods121.scala b/obp-api/src/main/scala/code/api/v1_2_1/APIMethods121.scala index fd49bc711..86af6fa99 100644 --- a/obp-api/src/main/scala/code/api/v1_2_1/APIMethods121.scala +++ b/obp-api/src/main/scala/code/api/v1_2_1/APIMethods121.scala @@ -1398,7 +1398,7 @@ trait APIMethods121 { BankAccountNotFound, InvalidJsonFormat, NoViewPermission, - "the view " + viewId + "does not allow adding more info", + "the view " + viewIdSwagger + "does not allow adding more info", "More Info cannot be added", UnknownError ), diff --git a/obp-api/src/main/scala/code/api/v2_0_0/APIMethods200.scala b/obp-api/src/main/scala/code/api/v2_0_0/APIMethods200.scala index 40947dc96..d6659938a 100644 --- a/obp-api/src/main/scala/code/api/v2_0_0/APIMethods200.scala +++ b/obp-api/src/main/scala/code/api/v2_0_0/APIMethods200.scala @@ -350,7 +350,7 @@ trait APIMethods200 { (Full(u), callContext) <- authorizedAccess(cc) (bank, callContext) <- NewStyle.function.getBank(BankId(defaultBankId), callContext) } yield { - val privateViewsUserCanAccessAtOneBank = Views.views.vend.privateViewsUserCanAccess(u).filter(_.bankId == bankId) + val privateViewsUserCanAccessAtOneBank = Views.views.vend.privateViewsUserCanAccess(u).filter(_.bankId == BankId(defaultBankId)) val privateAaccountsForOneBank = bank.privateAccounts(privateViewsUserCanAccessAtOneBank) val result = corePrivateAccountsAtOneBankResult(CallerContext(corePrivateAccountsAtOneBank), codeContext, u, privateAaccountsForOneBank, privateViewsUserCanAccessAtOneBank) (result, HttpCode.`200`(callContext)) @@ -453,7 +453,7 @@ trait APIMethods200 { List(UserNotLoggedIn, CustomerNotFoundByCustomerId, UnknownError), Catalogs(notCore, notPSD2, notOBWG), List(apiTagKyc, apiTagCustomer), - Some(List(canGetKycDocuments)) + Some(List(canGetAnyKycDocuments)) ) // TODO Add Role @@ -463,7 +463,7 @@ trait APIMethods200 { cc => { for { (Full(u), callContext) <- authorizedAccess(cc) - _ <- NewStyle.function.hasEntitlement(bankId.value, u.userId, ApiRole.canGetKycDocuments, callContext) + _ <- NewStyle.function.hasEntitlement("", u.userId, ApiRole.canGetAnyKycDocuments, callContext) (customer, callContext) <- NewStyle.function.getCustomerByCustomerId(customerId, callContext) (kycDocuments, callContxt) <- NewStyle.function.getKycDocuments(customerId, callContext) } yield { @@ -490,14 +490,14 @@ trait APIMethods200 { List(UserNotLoggedIn, CustomerNotFoundByCustomerId, UnknownError), Catalogs(notCore, notPSD2, notOBWG), List(apiTagKyc, apiTagCustomer), - Some(List(canGetKycMedia))) + Some(List(canGetAnyKycMedia))) lazy val getKycMedia : OBPEndpoint = { case "customers" :: customerId :: "kyc_media" :: Nil JsonGet _ => { cc => { for { (Full(u), callContext) <- authorizedAccess(cc) - _ <- NewStyle.function.hasEntitlement(bankId.value, u.userId, ApiRole.canGetKycMedia, callContext) + _ <- NewStyle.function.hasEntitlement("", u.userId, ApiRole.canGetAnyKycMedia, callContext) (customer, callContext) <- NewStyle.function.getCustomerByCustomerId(customerId, callContext) (kycMedias, callContxt) <- NewStyle.function.getKycMedias(customerId, callContext) } yield { @@ -523,17 +523,15 @@ trait APIMethods200 { List(UserNotLoggedIn, CustomerNotFoundByCustomerId, UnknownError), Catalogs(notCore, notPSD2, notOBWG), List(apiTagKyc, apiTagCustomer), - Some(List(canGetKycChecks)) + Some(List(canGetAnyKycChecks)) ) - // TODO Add Role - lazy val getKycChecks : OBPEndpoint = { case "customers" :: customerId :: "kyc_checks" :: Nil JsonGet _ => { cc => { for { (Full(u), callContext) <- authorizedAccess(cc) - _ <- NewStyle.function.hasEntitlement(bankId.value, u.userId, ApiRole.canGetKycChecks, callContext) + _ <- NewStyle.function.hasEntitlement("", u.userId, ApiRole.canGetAnyKycChecks, callContext) (customer, callContext) <- NewStyle.function.getCustomerByCustomerId(customerId, callContext) (kycChecks, callContxt) <- NewStyle.function.getKycChecks(customerId, callContext) } yield { @@ -558,7 +556,7 @@ trait APIMethods200 { List(UserNotLoggedIn, CustomerNotFoundByCustomerId, UnknownError), Catalogs(notCore, notPSD2, notOBWG), List(apiTagKyc, apiTagCustomer), - Some(List(canGetKycStatuses)) + Some(List(canGetAnyKycStatuses)) ) lazy val getKycStatuses : OBPEndpoint = { @@ -566,7 +564,7 @@ trait APIMethods200 { cc => { for { (Full(u), callContext) <- authorizedAccess(cc) - _ <- NewStyle.function.hasEntitlement(bankId.value, u.userId, ApiRole.canGetKycStatuses, callContext) + _ <- NewStyle.function.hasEntitlement("", u.userId, ApiRole.canGetAnyKycStatuses, callContext) (customer, callContext) <- NewStyle.function.getCustomerByCustomerId(customerId, callContext) (kycStatuses, callContxt) <- NewStyle.function.getKycStatuses(customerId, callContext) } yield { diff --git a/obp-api/src/main/scala/code/api/v3_0_0/APIMethods300.scala b/obp-api/src/main/scala/code/api/v3_0_0/APIMethods300.scala index f98822b03..dcb1c5be0 100644 --- a/obp-api/src/main/scala/code/api/v3_0_0/APIMethods300.scala +++ b/obp-api/src/main/scala/code/api/v3_0_0/APIMethods300.scala @@ -483,7 +483,7 @@ trait APIMethods300 { moderatedCoreAccountsJsonV300, List(UserNotLoggedIn,UnknownError), Catalogs(notCore, notPSD2, notOBWG), - List(apiTagAccountFirehose, apiTagAccount, apiTagFirehoseData, apiTagNewStyle), + List(apiTagAccount, apiTagAccountFirehose, apiTagFirehoseData, apiTagNewStyle), Some(List(canUseFirehoseAtAnyBank)) ) @@ -550,7 +550,7 @@ trait APIMethods300 { transactionsJsonV300, List(UserNotLoggedIn, FirehoseViewsNotAllowedOnThisInstance, UserHasMissingRoles, UnknownError), Catalogs(notCore, notPSD2, notOBWG), - List(apiTagAccountFirehose, apiTagAccount, apiTagFirehoseData, apiTagNewStyle), + List(apiTagTransaction, apiTagAccountFirehose, apiTagTransactionFirehose, apiTagFirehoseData, apiTagNewStyle), Some(List(canUseFirehoseAtAnyBank))) lazy val getFirehoseTransactionsForBankAccount : OBPEndpoint = { @@ -1776,7 +1776,7 @@ trait APIMethods300 { val msg = s"$InvalidJsonFormat The Json body should be the $CreateEntitlementRequestJSON " x => unboxFullOrFail(x, callContext, msg) } - _ <- Future { if (postedData.bank_id == "") Full() else NewStyle.function.getBank(bankId, callContext)} + _ <- Future { if (postedData.bank_id == "") Full() else NewStyle.function.getBank(BankId(postedData.bank_id), callContext)} _ <- Helper.booleanToFuture(failMsg = IncorrectRoleName + postedData.role_name + ". Possible roles are " + ApiRole.availableRoles.sorted.mkString(", ")) { availableRoles.exists(_ == postedData.role_name) 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 43be8f08a..fecfac0aa 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 @@ -470,7 +470,7 @@ trait APIMethods310 { transactionsJsonV300, List(UserNotLoggedIn, FirehoseViewsNotAllowedOnThisInstance, UserHasMissingRoles, UnknownError), Catalogs(notCore, notPSD2, notOBWG), - List(apiTagAccountFirehose, apiTagAccount, apiTagFirehoseData, apiTagNewStyle), + List(apiTagCustomer, apiTagFirehoseData, apiTagNewStyle), Some(List(canUseFirehoseAtAnyBank))) lazy val getFirehoseCustomers : OBPEndpoint = { diff --git a/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala b/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala index a535f57c7..89fdf067d 100644 --- a/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala +++ b/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala @@ -1,6 +1,5 @@ package code.api.v4_0_0 -import code.api.ChargePolicy import code.api.ResourceDocs1_4_0.SwaggerDefinitionsJSON._ import code.api.util.APIUtil._ import code.api.util.ApiRole._ @@ -13,7 +12,8 @@ import code.api.v1_4_0.JSONFactory1_4_0.{ChallengeAnswerJSON, TransactionRequest import code.api.v2_1_0._ import code.api.v3_0_0.JSONFactory300 import code.api.v3_1_0.ListResult -import code.dynamicEntity.{DynamicEntityCommons, DynamicEntityDefinition} +import code.api.{APIFailureNewStyle, ChargePolicy} +import code.dynamicEntity.DynamicEntityCommons import code.model.dataAccess.AuthUser import code.model.toUserExtended import code.transactionrequests.TransactionRequests.TransactionChallengeTypes._ @@ -25,9 +25,8 @@ import com.github.dwickern.macros.NameOf.nameOf import com.openbankproject.commons.model._ import com.openbankproject.commons.model.enums.DynamicEntityFieldType import com.openbankproject.commons.model.enums.DynamicEntityOperation._ -import net.liftweb.common.{Box, Failure, Full} +import net.liftweb.common.{Box, Full, ParamFailure} import net.liftweb.http.rest.RestHelper -import net.liftweb.json.JsonAST.JValue import net.liftweb.json.Serialization.write import net.liftweb.json._ import net.liftweb.util.StringHelpers @@ -720,13 +719,6 @@ trait APIMethods400 { } } - private def validateDynamicEntityJson(metadataJson: JValue) = { - val jFields = metadataJson.asInstanceOf[JObject].obj - require(jFields.size == 1, "json format for create or update DynamicEntity is not correct, it should have a single key value for structure definition") - val JField(_, definition) = jFields.head - definition.extract[DynamicEntityDefinition] - } - resourceDocs += ResourceDoc( createDynamicEntity, implementedInApiVersion, @@ -763,17 +755,12 @@ trait APIMethods400 { for { (Full(u), callContext) <- authorizedAccess(cc) _ <- NewStyle.function.hasEntitlement("", u.userId, canCreateDynamicEntity, callContext) - failMsg = s"$InvalidJsonFormat The Json body should be the same structure as request body example." - postedData <- NewStyle.function.tryons(failMsg, 400, callContext) { - validateDynamicEntityJson(json) - val JField(entityName, _) = json.asInstanceOf[JObject].obj.head - DynamicEntityCommons(entityName, compactRender(json)) - } - - Full(dynamicEntity) <- NewStyle.function.createOrUpdateDynamicEntity(postedData, callContext) + jsonObject = json.asInstanceOf[JObject] + dynamicEntity = DynamicEntityCommons(jsonObject, None) + Full(result) <- NewStyle.function.createOrUpdateDynamicEntity(dynamicEntity, callContext) } yield { - val commonsData: DynamicEntityCommons = dynamicEntity + val commonsData: DynamicEntityCommons = result (commonsData.jValue, HttpCode.`201`(callContext)) } } @@ -817,18 +804,11 @@ trait APIMethods400 { (Full(u), callContext) <- authorizedAccess(cc) _ <- NewStyle.function.hasEntitlement("", u.userId, canUpdateDynamicEntity, callContext) - failMsg = s"$InvalidJsonFormat The Json body should be the same structure as request body example." - putData <- NewStyle.function.tryons(failMsg, 400, callContext) { - validateDynamicEntityJson(json) - val JField(name, _) = json.asInstanceOf[JObject].obj.head - DynamicEntityCommons(name, compactRender(json), Some(dynamicEntityId)) - } - - (_, _) <- NewStyle.function.getDynamicEntityById(dynamicEntityId, callContext) - - Full(dynamicEntity) <- NewStyle.function.createOrUpdateDynamicEntity(putData, callContext) + jsonObject = json.asInstanceOf[JObject] + dynamicEntity = DynamicEntityCommons(jsonObject, Some(dynamicEntityId)) + Full(result) <- NewStyle.function.createOrUpdateDynamicEntity(dynamicEntity, callContext) } yield { - val commonsData: DynamicEntityCommons = dynamicEntity + val commonsData: DynamicEntityCommons = result (commonsData.jValue, HttpCode.`200`(callContext)) } } @@ -870,9 +850,11 @@ trait APIMethods400 { } } } - private def unboxResult[T](box: Box[T]): T = { - if(box.isInstanceOf[Failure]) { - throw new Exception(box.asInstanceOf[Failure].msg) + + + private def unboxResult[T: Manifest](box: Box[T]): T = { + if(box.isInstanceOf[ParamFailure[_]]) { + fullBoxOrException[T](box) } box.openOrThrowException("impossible error") @@ -881,9 +863,8 @@ trait APIMethods400 { case EntityName(entityName) :: Nil JsonGet req => { cc => val listName = StringHelpers.snakify(English.plural(entityName)) for { - (box: Box[JArray], _) <- NewStyle.function.invokeDynamicConnector(GET_ALL, entityName, None, None, Some(cc)) -// resultList = APIUtil.unboxFullOrFail(box, Some(cc)) - resultList = unboxResult(box) + (box, _) <- NewStyle.function.invokeDynamicConnector(GET_ALL, entityName, None, None, Some(cc)) + resultList: JArray = unboxResult(box.asInstanceOf[Box[JArray]]) } yield { import net.liftweb.json.JsonDSL._ val jValue: JObject = listName -> resultList @@ -892,36 +873,32 @@ trait APIMethods400 { } case EntityName(entityName, id) JsonGet req => {cc => for { - (box: Box[JObject], _) <- NewStyle.function.invokeDynamicConnector(GET_ONE, entityName, None, Some(id), Some(cc)) -// entity = APIUtil.unboxFullOrFail(box, Some(cc)) - entity = unboxResult(box) + (box, _) <- NewStyle.function.invokeDynamicConnector(GET_ONE, entityName, None, Some(id), Some(cc)) + entity: JValue = unboxResult(box.asInstanceOf[Box[JValue]]) } yield { (entity, HttpCode.`200`(Some(cc))) } } case EntityName(entityName) :: Nil JsonPost json -> _ => {cc => for { - (box: Box[JObject], _) <- NewStyle.function.invokeDynamicConnector(CREATE, entityName, Some(json.asInstanceOf[JObject]), None, Some(cc)) -// entity = APIUtil.unboxFullOrFail(box, Some(cc)) - entity = unboxResult(box) + (box, _) <- NewStyle.function.invokeDynamicConnector(CREATE, entityName, Some(json.asInstanceOf[JObject]), None, Some(cc)) + entity: JValue = unboxResult(box.asInstanceOf[Box[JValue]]) } yield { (entity, HttpCode.`201`(Some(cc))) } } case EntityName(entityName, id) JsonPut json -> _ => { cc => for { - (box: Box[JObject], _) <- NewStyle.function.invokeDynamicConnector(UPDATE, entityName, Some(json.asInstanceOf[JObject]), Some(id), Some(cc)) -// entity = APIUtil.unboxFullOrFail(box, Some(cc)) - entity = unboxResult(box) + (box: Box[JValue], _) <- NewStyle.function.invokeDynamicConnector(UPDATE, entityName, Some(json.asInstanceOf[JObject]), Some(id), Some(cc)) + entity: JValue = unboxResult(box.asInstanceOf[Box[JValue]]) } yield { (entity, HttpCode.`200`(Some(cc))) } } case EntityName(entityName, id) JsonDelete req => { cc => for { - (box: Box[JValue], _) <- NewStyle.function.invokeDynamicConnector(DELETE, entityName, None, Some(id), Some(cc)) -// deleteResult = APIUtil.unboxFullOrFail(box, Some(cc)) - deleteResult = unboxResult(box) + (box, _) <- NewStyle.function.invokeDynamicConnector(DELETE, entityName, None, Some(id), Some(cc)) + deleteResult: JBool = unboxResult(box.asInstanceOf[Box[JBool]]) } yield { (deleteResult, HttpCode.`200`(Some(cc))) } diff --git a/obp-api/src/main/scala/code/api/v4_0_0/DynamicEntityHelper.scala b/obp-api/src/main/scala/code/api/v4_0_0/DynamicEntityHelper.scala index 34cd39381..2f57b2836 100644 --- a/obp-api/src/main/scala/code/api/v4_0_0/DynamicEntityHelper.scala +++ b/obp-api/src/main/scala/code/api/v4_0_0/DynamicEntityHelper.scala @@ -220,16 +220,8 @@ case class DynamicEntityInfo(definition: String, entityName: String) { val fieldNameToType: Map[String, Class[_]] = fieldNameToTypeName .mapValues(jsonTypeMap(_)) - val requiredFieldNames: Set[String] = (entity \ "required").asInstanceOf[JArray].arr.map(_.asInstanceOf[JString].s).toSet - val fields = result.obj.filter(it => fieldNameToType.keySet.contains(it.name)) - def check(v: Boolean, msg: String) = if (!v) throw new RuntimeException(msg) - // if there are field type are not match the definitions, there must be bug. - fields.foreach(it => check(fieldNameToType(it.name).isInstance(it.value), s"""$InvalidJsonFormat "${it.name}" required type is "${fieldNameToTypeName(it.name)}".""")) - // if there are required field not presented, must be some bug. - requiredFieldNames.foreach(it => check(fields.exists(_.name == it), s"""$InvalidJsonFormat required field "$it" not presented.""")) - (id, fields.exists(_.name == idName)) match { case (Some(idValue), false) => JObject(JField(idName, JString(idValue)) :: fields) case _ => JObject(fields) diff --git a/obp-api/src/main/scala/code/bankconnectors/ConnectorEndpoints.scala b/obp-api/src/main/scala/code/bankconnectors/ConnectorEndpoints.scala index 2be0a303a..944bb1745 100644 --- a/obp-api/src/main/scala/code/bankconnectors/ConnectorEndpoints.scala +++ b/obp-api/src/main/scala/code/bankconnectors/ConnectorEndpoints.scala @@ -17,6 +17,7 @@ import net.liftweb.json.JValue import net.liftweb.json.JsonAST.JNothing import org.apache.commons.lang3.StringUtils +import scala.annotation.tailrec import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.Future import scala.language.postfixOps @@ -55,16 +56,14 @@ object ConnectorEndpoints extends RestHelper{ val value = invokeMethod(methodSymbol, paramValues :_*) // convert any to Future[(Box[_], Option[CallContext])] type - val futureValue: Future[(Box[_], Option[CallContext])] = toStandaredFuture(value) + val futureValue: Future[(Box[_], Option[CallContext])] = toStandardFuture(value) for { - (Full(data), callContext) <- futureValue.map {it => - APIUtil.fullBoxOrException(it._1 ~> APIFailureNewStyle("", 400, optionCC.map(_.toLight))) - it - } + (boxedData, _) <- futureValue + data = APIUtil.fullBoxOrException(boxedData ~> APIFailureNewStyle("", 400, optionCC.map(_.toLight))) inboundAdapterCallContext = nameOf(InboundAdapterCallContext) //convert first letter to small case - inboundAdapterCallContextKey = Character.toLowerCase(inboundAdapterCallContext.charAt(0)) + inboundAdapterCallContext.substring(1) + inboundAdapterCallContextKey = StringUtils.uncapitalize(inboundAdapterCallContext) inboundAdapterCallContextValue = InboundAdapterCallContext(cc.correlationId) } yield { // NOTE: if any filed type is BigDecimal, it is can't be serialized by lift json @@ -184,7 +183,8 @@ object ConnectorEndpoints extends RestHelper{ mirrorObj.reflectMethod(method).apply(args :_*) } - def toStandaredFuture(obj: Any): Future[(Box[_], Option[CallContext])] = { + @tailrec + def toStandardFuture(obj: Any): Future[(Box[_], Option[CallContext])] = { obj match { case null => Future((Empty, None)) case future: Future[_] => { @@ -202,10 +202,10 @@ object ConnectorEndpoints extends RestHelper{ } case Full(data) => { data match { - case _: (_, _) => toStandaredFuture(Future(obj)) + case _: (_, _) => toStandardFuture(Future(obj)) case _ => { val fillCallContext = obj.asInstanceOf[Box[_]].map((_, None)) - toStandaredFuture(Future(fillCallContext)) + toStandardFuture(Future(fillCallContext)) } } } diff --git a/obp-api/src/main/scala/code/bankconnectors/LocalMappedConnector.scala b/obp-api/src/main/scala/code/bankconnectors/LocalMappedConnector.scala index ebfb502a9..a650d86b5 100644 --- a/obp-api/src/main/scala/code/bankconnectors/LocalMappedConnector.scala +++ b/obp-api/src/main/scala/code/bankconnectors/LocalMappedConnector.scala @@ -23,7 +23,7 @@ import code.cards.MappedPhysicalCard import code.context.{UserAuthContextProvider, UserAuthContextUpdateProvider} import code.customer._ import code.customeraddress.CustomerAddressX -import code.dynamicEntity.DynamicEntityProvider +import code.dynamicEntity.{DynamicEntityProvider, DynamicEntityT} import code.fx.{FXRate, MappedFXRate, fx} import code.kycchecks.KycChecks import code.kycdocuments.KycDocuments @@ -60,8 +60,7 @@ import com.openbankproject.commons.model.{AccountApplication, AccountAttribute, import com.tesobe.CacheKeyFromArguments import com.tesobe.model.UpdateBankAccount import net.liftweb.common._ -import net.liftweb.json -import net.liftweb.json.{JArray, JBool, JDouble, JInt, JObject, JString, JValue} +import net.liftweb.json.{JArray, JBool, JObject, JValue} import net.liftweb.mapper.{By, _} import net.liftweb.util.Helpers.{tryo, _} import net.liftweb.util.Mailer @@ -196,10 +195,8 @@ object LocalMappedConnector extends Connector with MdcLoggable { ) } yield true } - val errorMessage = sendingResult map { - case f: Failure => f.msg - case Empty => "" - } + val errorMessage = sendingResult.filter(_.isInstanceOf[Failure]).map(_.asInstanceOf[Failure].msg) + if(sendingResult.forall(_ == Full(true))) hashedPassword else (Failure(errorMessage.toSet.mkString(" <- ")), callContext) case None => // All versions which precede v4.0.0 i.e. to keep backward compatibility createHashedPassword("123") @@ -2801,50 +2798,30 @@ object LocalMappedConnector extends Connector with MdcLoggable { val dynamicEntityBox = DynamicEntityProvider.connectorMethodProvider.vend.getByEntityName(entityName) // do validate, any validate process fail will return immediately if(dynamicEntityBox.isEmpty) { - return Helper.booleanToFuture(s"$DynamicEntityEntityNotExists entity's name is '$entityName'")(dynamicEntityBox.isDefined) + return Helper.booleanToFuture(s"$DynamicEntityNotExists entity's name is '$entityName'")(false) .map(it => (it.map(_.asInstanceOf[JValue]), callContext)) - } else if(entityId.isDefined && !persistedEntities.contains(entityId.get -> entityName)) { - val id = entityId.get - val idName = StringUtils.uncapitalize(entityName) + "Id" + } - return Helper.booleanToFuture(s"$InvalidUrl not exists $entityName of $idName = $id")(false) - .map(it => (it.map(_.asInstanceOf[JValue]), callContext)) - } else if(requestBody.isDefined) { - val dynamicEntity = dynamicEntityBox.openOrThrowException(DynamicEntityEntityNotExists) - - val jsonTypeMap = Map[String, Class[_]]( - ("boolean", classOf[JBool]), - ("string", classOf[JString]), - ("array", classOf[JArray]), - ("integer", classOf[JInt]), - ("number", classOf[JDouble]), - ) - val definitionJson = json.parse(dynamicEntity.metadataJson).asInstanceOf[JObject] - val entity = (definitionJson \ entityName).asInstanceOf[JObject] - val requiredFieldNames: Set[String] = (entity \ "required").asInstanceOf[JArray].arr.map(_.asInstanceOf[JString].s).toSet - - val fieldNameToTypeName: Map[String, String] = (entity \ "properties") - .asInstanceOf[JObject] - .obj - .map(field => (field.name, (field.value \ "type").asInstanceOf[JString].s)) - .toMap - - val fieldNameToType: Map[String, Class[_]] = fieldNameToTypeName - .mapValues(jsonTypeMap(_)) - val bodyJson = requestBody.getOrElse(throw new RuntimeException(s"$DynamicEntityMissArgument please supply the requestBody.")) - val fields = bodyJson.obj.filter(it => fieldNameToType.keySet.contains(it.name)) - - // if there are field type are not match the definitions, there must be bug. - val invalidTypes = fields.filterNot(it => fieldNameToType(it.name).isInstance(it.value)) - val invalidTypeNames = invalidTypes.map(_.name).mkString("[", ",", "]") - val missingRequiredFields = requiredFieldNames.filterNot(it => fields.exists(_.name == it)) - val missingFieldNames = missingRequiredFields.mkString("[", ",", "]") - - if(invalidTypes.nonEmpty) { - return Helper.booleanToFuture(s"$InvalidJsonFormat these field type not correct: $invalidTypeNames")(invalidTypes.isEmpty) + if(operation == CREATE || operation == UPDATE) { + if(requestBody.isEmpty) { + return Helper.booleanToFuture(s"$InvalidJsonFormat requestBody is required for $operation operation.")(false) .map(it => (it.map(_.asInstanceOf[JValue]), callContext)) - } else if(missingRequiredFields.nonEmpty) { - return Helper.booleanToFuture(s"$InvalidJsonFormat some required fields are missing: $missingFieldNames")(missingRequiredFields.isEmpty) + } + val dynamicEntity: DynamicEntityT = dynamicEntityBox.openOrThrowException(DynamicEntityNotExists) + val validateResult: Either[String, Unit] = dynamicEntity.validateEntityJson(requestBody.get) + if(validateResult.isLeft) { + return Helper.booleanToFuture(s"$InvalidJsonFormat details: ${validateResult.left.get}")(validateResult.isRight) + .map(it => (it.map(_.asInstanceOf[JValue]), callContext)) + } + } + if(operation == GET_ONE || operation == UPDATE || operation == DELETE) { + if (entityId.isEmpty) { + return Helper.booleanToFuture(s"$InvalidJsonFormat entityId is required for $operation operation.")(entityId.isEmpty || StringUtils.isBlank(entityId.get)) + .map(it => (it.map(_.asInstanceOf[JValue]), callContext)) + } + if (!persistedEntities.contains(entityId.get -> entityName)) { + val id = entityId.get + return Helper.booleanToFuture(s"$EntityNotFoundByEntityId please check: entityId = $id", 404)(false) .map(it => (it.map(_.asInstanceOf[JValue]), callContext)) } } @@ -2860,7 +2837,8 @@ object LocalMappedConnector extends Connector with MdcLoggable { } case CREATE | UPDATE => { val body = requestBody.getOrElse(throw new RuntimeException(s"$DynamicEntityMissArgument please supply the requestBody.")) - val persistedEntity = MockerConnector.persist(entityName, body, entityId) + val id = if(operation == CREATE) None else entityId + val persistedEntity = MockerConnector.persist(entityName, body, id) Full(persistedEntity) } case DELETE => { diff --git a/obp-api/src/main/scala/code/bankconnectors/rest/RestConnector_vMar2019.scala b/obp-api/src/main/scala/code/bankconnectors/rest/RestConnector_vMar2019.scala index fc8915fea..8e64c27c4 100644 --- a/obp-api/src/main/scala/code/bankconnectors/rest/RestConnector_vMar2019.scala +++ b/obp-api/src/main/scala/code/bankconnectors/rest/RestConnector_vMar2019.scala @@ -9242,6 +9242,63 @@ trait RestConnector_vMar2019 extends Connector with KafkaHelper with MdcLoggable } //---------------- dynamic end ---------------------please don't modify this line + + private val availableOperation = DynamicEntityOperation.values.map(it => s""""$it"""").mkString("[", ", ", "]") + + messageDocs += dynamicEntityProcessDoc + def dynamicEntityProcessDoc = MessageDoc( + process = "obp.dynamicEntityProcess", + messageFormat = messageFormat, + description = s"operate committed dynamic entity data, the available value of 'operation' can be: ${availableOperation}", + outboundTopic = None, + inboundTopic = None, + exampleOutboundMessage = ( + OutBoundDynamicEntityProcessDoc(outboundAdapterCallContext = OutboundAdapterCallContext(correlationId=correlationIdExample.value, + sessionId=Some(sessionIdExample.value), + consumerId=Some(consumerIdExample.value), + generalContext=Some(List( BasicGeneralContext(key=keyExample.value, + value=valueExample.value))), + outboundAdapterAuthInfo=Some( OutboundAdapterAuthInfo(userId=Some(userIdExample.value), + username=Some(usernameExample.value), + linkedCustomers=Some(List( BasicLinkedCustomer(customerId=customerIdExample.value, + customerNumber=customerNumberExample.value, + legalName=legalNameExample.value))), + userAuthContext=Some(List( BasicUserAuthContext(key=keyExample.value, + value=valueExample.value))), + authViews=Some(List( AuthView(view= ViewBasic(id=viewIdExample.value, + name=viewNameExample.value, + description=viewDescriptionExample.value), + account= AccountBasic(id=accountIdExample.value, + accountRoutings=List( AccountRouting(scheme=accountRoutingSchemeExample.value, + address=accountRoutingAddressExample.value)), + customerOwners=List( InternalBasicCustomer(bankId=bankIdExample.value, + customerId=customerIdExample.value, + customerNumber=customerNumberExample.value, + legalName=legalNameExample.value, + dateOfBirth=parseDate(dateOfBirthExample.value).getOrElse(sys.error("dateOfBirthExample.value is not validate date format.")))), + userOwners=List( InternalBasicUser(userId=userIdExample.value, + emailAddress=emailExample.value, + name=usernameExample.value))))))))), + operation = DynamicEntityOperation.UPDATE, + entityName = "FooBar", + requestBody = Some(FooBar(name = "James Brown", number = 1234567890)), + entityId = Some("foobar-id-value")) + ), + exampleInboundMessage = ( + InBoundDynamicEntityProcessDoc(inboundAdapterCallContext= InboundAdapterCallContext(correlationId=correlationIdExample.value, + sessionId=Some(sessionIdExample.value), + generalContext=Some(List( BasicGeneralContext(key=keyExample.value, + value=valueExample.value)))), + status= Status(errorCode=statusErrorCodeExample.value, + backendMessages=List( InboundStatusMessage(source=sourceExample.value, + status=inboundStatusMessageStatusExample.value, + errorCode=inboundStatusMessageErrorCodeExample.value, + text=inboundStatusMessageTextExample.value))), + data=FooBar(name = "James Brown", number = 1234567890, fooBarId = Some("foobar-id-value"))) + ), + adapterImplementation = Some(AdapterImplementation("- Core", 1)) + ) + override def dynamicEntityProcess(operation: DynamicEntityOperation, entityName: String, requestBody: Option[JObject], @@ -9365,7 +9422,7 @@ trait RestConnector_vMar2019 extends Connector with KafkaHelper with MdcLoggable val future: Future[Box[Box[T]]] = extractBody(entity) map { msg => tryo { val errorMsg = parse(msg).extract[ErrorMessage] - val failure: Box[T] = ParamFailure(errorMsg.message, "") + val failure: Box[T] = ParamFailure("", APIFailureNewStyle(errorMsg.message, status.intValue())) failure } ~> APIFailureNewStyle(msg, status.intValue()) } diff --git a/obp-api/src/main/scala/code/dynamicEntity/DynamicEntityProvider.scala b/obp-api/src/main/scala/code/dynamicEntity/DynamicEntityProvider.scala index e4b21a5f8..46b45d084 100644 --- a/obp-api/src/main/scala/code/dynamicEntity/DynamicEntityProvider.scala +++ b/obp-api/src/main/scala/code/dynamicEntity/DynamicEntityProvider.scala @@ -1,11 +1,11 @@ package code.dynamicEntity +import code.api.util.ErrorMessages.InvalidJsonFormat import com.openbankproject.commons.model.enums.DynamicEntityFieldType import com.openbankproject.commons.model.{Converter, JsonFieldReName} import net.liftweb.common.Box -import net.liftweb.json.JsonAST.JString import net.liftweb.json.JsonDSL._ -import net.liftweb.json.{JField, JObject, JsonAST} +import net.liftweb.json.{JArray, JBool, JDouble, JField, JInt, JNothing, JNull, JObject, JString, compactRender, parse} import net.liftweb.util.SimpleInjector object DynamicEntityProvider extends SimpleInjector { @@ -19,13 +19,11 @@ trait DynamicEntityT { def dynamicEntityId: Option[String] def entityName: String def metadataJson: String -} -case class DynamicEntityCommons(entityName: String, - metadataJson: String, - dynamicEntityId: Option[String] = None, - ) extends DynamicEntityT with JsonFieldReName { - private val definition: JObject = net.liftweb.json.parse(metadataJson).asInstanceOf[JObject] + + //---------util methods + + private lazy val definition: JObject = parse(metadataJson).asInstanceOf[JObject] //convert metadataJson to JValue, so the final json field metadataJson have no escaped " to \", have good readable lazy val jValue = dynamicEntityId match { case Some(id) => { @@ -35,18 +33,165 @@ case class DynamicEntityCommons(entityName: String, } case None => definition } + + /** + * validate the commit json whether fulfil DynamicEntity schema + * @param entityJson commit json object to add new instance of given dynamic entity + * @return return Success[Unit], or return Left[String] error message + */ + def validateEntityJson(entityJson: JObject): Either[String, Unit] = { + val required: List[String] = (definition \ entityName \ "required").asInstanceOf[JArray].arr.map(_.asInstanceOf[JString].s) + + val missingProperties = required diff entityJson.obj.map(_.name) + + if(missingProperties.nonEmpty) { + return Left(s"$InvalidJsonFormat The 'required' field's not be fulfilled, missing properties: ${missingProperties.mkString(", ")}") + } + + val invalidPropertyMsg = (definition \ entityName \ "properties").asInstanceOf[JObject].obj + .map(it => { + val JField(propertyName, propertyDef: JObject) = it + val propertyTypeName = (propertyDef \ "type").asInstanceOf[JString].s + (propertyName, propertyTypeName) + }) + .map(it => { + val (propertyName, propertyType) = it + val propertyValue = entityJson \ propertyName + propertyType match { + case _ if propertyValue == JNothing || propertyValue == JNull => "" // required properties already checked. + case "string" if !propertyValue.isInstanceOf[JString] => s"$InvalidJsonFormat The type of '$propertyName' should be string" + case "number" if !propertyValue.isInstanceOf[JDouble] => s"$InvalidJsonFormat The type of '$propertyName' should be number" + case "integer" if !propertyValue.isInstanceOf[JInt] => s"$InvalidJsonFormat The type of '$propertyName' should be integer" + case "boolean" if !propertyValue.isInstanceOf[JBool] => s"$InvalidJsonFormat The type of '$propertyName' should be boolean" + case "array" if !propertyValue.isInstanceOf[JArray] => s"$InvalidJsonFormat The type of '$propertyName' should be array" + case "object" if !propertyValue.isInstanceOf[JObject] => s"$InvalidJsonFormat The type of '$propertyName' should be object" + case _ => "" + } + }) + .filter(_.nonEmpty) + .mkString("; ") + if(invalidPropertyMsg.nonEmpty) { + Left(invalidPropertyMsg) + } else { + Right(Unit) + } + } +} + +case class DynamicEntityCommons(entityName: String, + metadataJson: String, + dynamicEntityId: Option[String] = None + ) extends DynamicEntityT with JsonFieldReName + +object DynamicEntityCommons extends Converter[DynamicEntityT, DynamicEntityCommons] { + + /** + * create DynamicEntityCommons object, and do validation + * + * @param jsonObject the follow schema json: + * {{{ + * { + * "FooBar": { + * "required": [ + * "name" + * ], + * "properties": { + * "name": { + * "type": "string", + * "example": "James Brown" + * }, + * "number": { + * "type": "integer", + * "example": "698761728934" + * } + * } + * } + * } + * }}} + * @param dynamicEntityId + * @return object of DynamicEntityCommons + */ + def apply(jsonObject: JObject, dynamicEntityId: Option[String]): DynamicEntityCommons = { + + def checkFormat(requirement: Boolean, message: String) = { + if (!requirement) throw new IllegalArgumentException(message) + } + + val fields = jsonObject.obj + + // validate whether json is object and have a single field, currently support one entity definition + checkFormat(fields.nonEmpty, s"$InvalidJsonFormat The Json root object should have a single entity, but current have none.") + checkFormat(fields.size == 1, s"$InvalidJsonFormat The Json root object should have a single entity, but current entityNames: ${fields.map(_.name).mkString(", ")}") + + val JField(entityName, metadataJson) = fields.head + + // validate entityName corresponding value is json object + val metadataStr = compactRender(metadataJson) + checkFormat(metadataJson.isInstanceOf[JObject], s"$InvalidJsonFormat The $entityName should have an object value, but current value is: $metadataStr") + + val required = metadataJson \ "required" + + // validate 'required' field exists and is a json array[string] + checkFormat(required != JNothing , s"$InvalidJsonFormat There must be 'required' field in $entityName, and type is json array[string]") + checkFormat(required.isInstanceOf[JArray] && required.asInstanceOf[JArray].arr.forall(_.isInstanceOf[JString]), s"$InvalidJsonFormat The 'required' field's type of $entityName should be array[string]") + + val properties = metadataJson \ "properties" + + // validate 'properties' field exists and is json object + checkFormat(properties != JNothing , s"$InvalidJsonFormat There must be 'required' field in $entityName, and type is array[string]") + checkFormat(properties.isInstanceOf[JObject], s"$InvalidJsonFormat The 'properties' field's type of $entityName should be json object") + + val propertiesObj = properties.asInstanceOf[JObject] + + val requiredFields = required.asInstanceOf[JArray].arr.map(_.asInstanceOf[JString].s) + + val allFields = propertiesObj.obj + + val missingRequiredFields = requiredFields diff allFields.map(_.name) + + checkFormat(missingRequiredFields.isEmpty , s"$InvalidJsonFormat missing properties: ${missingRequiredFields.mkString(", ")}") + + // validate there is no required field missing in properties + val notFoundRequiredField = requiredFields.diff(allFields.map(_.name)) + checkFormat(metadataJson.isInstanceOf[JObject], s"$InvalidJsonFormat In the $entityName, all 'required' fields should be present, these are missing: ${notFoundRequiredField.mkString(", ")}") + + // validate all properties have a type and example + allFields.foreach(field => { + val JField(fieldName, value) = field + checkFormat(value.isInstanceOf[JObject], s"$InvalidJsonFormat The property of $fieldName's type should be json object") + + // 'type' exists and value should be one of allowed type + val fieldType = value \ "type" + checkFormat(fieldType.isInstanceOf[JString] && fieldType.asInstanceOf[JString].s.nonEmpty, s"$InvalidJsonFormat The property of $fieldName's 'type' field should be exists and type is json string") + checkFormat(allowedFieldType.contains(fieldType.asInstanceOf[JString].s), s"$InvalidJsonFormat The property of $fieldName's 'type' field should be json string and value should be one of: ${allowedFieldType.mkString(", ")}") + + // example is exists + val fieldExample = value \ "example" + checkFormat(fieldExample != JNothing, s"$InvalidJsonFormat The property of $fieldName's 'example' field should be exists") + }) + + DynamicEntityCommons(entityName, compactRender(jsonObject), dynamicEntityId) + } + + private val allowedFieldType: Set[String] = Set( + "string", + "number", + "integer", + "boolean", + "array", +// "object", + ) } /** - * an example schema of DynamicEntity, this is as request body example usage + * example case classes, as an example schema of DynamicEntity, for request body example usage * @param FooBar */ case class DynamicEntityFooBar(FooBar: DynamicEntityDefinition, dynamicEntityId: Option[String] = None) case class DynamicEntityDefinition(required: List[String],properties: DynamicEntityFullBarFields) case class DynamicEntityFullBarFields(name: DynamicEntityTypeExample, number: DynamicEntityTypeExample) case class DynamicEntityTypeExample(`type`: DynamicEntityFieldType, example: String) - -object DynamicEntityCommons extends Converter[DynamicEntityT, DynamicEntityCommons] +//-------------------example case class end trait DynamicEntityProvider { diff --git a/obp-api/src/main/scala/code/snippet/ConsumerRegistration.scala b/obp-api/src/main/scala/code/snippet/ConsumerRegistration.scala index 282a247aa..c849af3e0 100644 --- a/obp-api/src/main/scala/code/snippet/ConsumerRegistration.scala +++ b/obp-api/src/main/scala/code/snippet/ConsumerRegistration.scala @@ -26,6 +26,7 @@ TESOBE (http://www.tesobe.com/) */ package code.snippet +import code.api.DirectLogin import code.api.util.{APIUtil, ErrorMessages} import code.consumer.Consumers import code.model._ @@ -37,6 +38,8 @@ import net.liftweb.util.Helpers._ import net.liftweb.util.{FieldError, Helpers} import code.webuiprops.MappedWebUiPropsProvider.getWebUiPropsValue +import scala.collection.mutable.ListBuffer + class ConsumerRegistration extends MdcLoggable { private object nameVar extends RequestVar("") @@ -89,6 +92,29 @@ class ConsumerRegistration extends MdcLoggable { val urlOAuthEndpoint = APIUtil.getPropsValue("hostname", "") + "/oauth/initiate" val urlDirectLoginEndpoint = APIUtil.getPropsValue("hostname", "") + "/my/logins/direct" val createDirectLoginToken = getWebUiPropsValue("webui_create_directlogin_token_url", "") + val dummyCustomersInfo = getWebUiPropsValue("webui_dummy_customer_logins", "") + val isShowDummyCustomerTokens = getWebUiPropsValue("webui_show_dummy_customer_tokens", "false").toBoolean + val dummyUsersTokens: String = (isShowDummyCustomerTokens, dummyCustomersInfo) match { + case(true, v) if v.nonEmpty => { + val regex = """\{"user_name"\s*:\s*"(.+?)".+?"password"\s*:\s*"(.+?)".+?\}""".r + val matcher = regex.pattern.matcher(v) + val tokens = ListBuffer[String]() + while(matcher.find()) { + val userName = matcher.group(1) + val password = matcher.group(2) + val consumerKey = consumer.key.get + val (code, token) = DirectLogin.createToken(Map(("username", userName), ("password", password), ("consumer_key", consumerKey))) + val authHeader = code match { + case 200 => s"""$userName auth header --> Authorization: DirectLogin token="$token"""" + case _ => s"""$userName - -> username or password is invalid, generate token fail""" + } + tokens += authHeader + } + tokens.mkString(""" | """) + } + case _ => "" + } + val registerConsumerSuccessMessageWebpage = getWebUiPropsValue( "webui_register_consumer_success_message_webpage", "Thanks for registering your consumer with the Open Bank Project API! Here is your developer information. Please save it in a secure location.") @@ -111,7 +137,13 @@ class ConsumerRegistration extends MdcLoggable { "#directlogin-endpoint a [href]" #> urlDirectLoginEndpoint & "#post-consumer-registration-more-info-link a *" #> registrationMoreInfoText & "#post-consumer-registration-more-info-link a [href]" #> registrationMoreInfoUrl & - "#register-consumer-input" #> "" + "#register-consumer-input" #> "" & { + if(dummyUsersTokens.isEmpty) { + ".preparedTokens" #> dummyUsersTokens + } else { + "#preparedTokens *" #> dummyUsersTokens + } + } } def showRegistrationResults(result : Consumer) = { diff --git a/obp-api/src/main/webapp/consumer-registration.html b/obp-api/src/main/webapp/consumer-registration.html index 236cade8a..31d48a41d 100644 --- a/obp-api/src/main/webapp/consumer-registration.html +++ b/obp-api/src/main/webapp/consumer-registration.html @@ -134,10 +134,14 @@ Berlin 13359, Germany
Direct Login Endpoint
endpoint
+
+
Prepared user tokens
+
+
Direct Login Documentation
How to use Direct Login -
+
diff --git a/obp-api/src/test/scala/code/api/v4_0_0/DynamicEntityTest.scala b/obp-api/src/test/scala/code/api/v4_0_0/DynamicEntityTest.scala index 4278b2256..1eac7644f 100644 --- a/obp-api/src/test/scala/code/api/v4_0_0/DynamicEntityTest.scala +++ b/obp-api/src/test/scala/code/api/v4_0_0/DynamicEntityTest.scala @@ -159,6 +159,13 @@ class DynamicEntityTest extends V400ServerSetup { val response = makePostRequest(request, write(rightEntity)) Then("We should get a 201") response.code should equal(201) + + { // create duplicated entityName FooBar, cause 400 + val response400 = makePostRequest(request, write(rightEntity)) + response400.code should equal(400) + response400.body.extract[ErrorMessage].message should startWith (DynamicEntityNameAlreadyExists) + } + val responseJson = response.body val dynamicEntityId = (responseJson \ "dynamicEntityId").asInstanceOf[JString].s val dynamicEntityIdJObject: JObject = "dynamicEntityId" -> dynamicEntityId @@ -193,11 +200,11 @@ class DynamicEntityTest extends V400ServerSetup { { // update a not exists DynamicEntity - val request400 = (v4_0_0_Request / "management" / "dynamic_entities" / "not-exists-id" ).PUT <@(user1) - val response400 = makePutRequest(request400, compactRender(updateRequest)) + val request404 = (v4_0_0_Request / "management" / "dynamic_entities" / "not-exists-id" ).PUT <@(user1) + val response404 = makePutRequest(request404, compactRender(updateRequest)) Then("We should get a 400") - response400.code should equal(400) - response400.body.extract[ErrorMessage].message should startWith (DynamicEntityNotFoundByDynamicEntityId) + response404.code should equal(404) + response404.body.extract[ErrorMessage].message should startWith (DynamicEntityNotFoundByDynamicEntityId) } { diff --git a/obp-commons/src/main/scala/com/openbankproject/commons/dto/JsonsTransfer.scala b/obp-commons/src/main/scala/com/openbankproject/commons/dto/JsonsTransfer.scala index 10381839b..8fb382cce 100644 --- a/obp-commons/src/main/scala/com/openbankproject/commons/dto/JsonsTransfer.scala +++ b/obp-commons/src/main/scala/com/openbankproject/commons/dto/JsonsTransfer.scala @@ -1165,4 +1165,13 @@ case class OutBoundDynamicEntityProcess (outboundAdapterCallContext: OutboundAda entityName: String, requestBody: Option[JObject], entityId: Option[String]) extends TopicTrait -case class InBoundDynamicEntityProcess (inboundAdapterCallContext: InboundAdapterCallContext, status: Status, data: JValue) extends InBoundTrait[JValue] \ No newline at end of file +case class InBoundDynamicEntityProcess (inboundAdapterCallContext: InboundAdapterCallContext, status: Status, data: JValue) extends InBoundTrait[JValue] + +// because swagger generate not support JValue type, so here supply too xxxDoc TO generate correct request and response body example +case class FooBar(name: String, number: Int, fooBarId: Option[String] = None) +case class OutBoundDynamicEntityProcessDoc (outboundAdapterCallContext: OutboundAdapterCallContext, + operation: DynamicEntityOperation, + entityName: String, + requestBody: Option[FooBar], + entityId: Option[String]) extends TopicTrait +case class InBoundDynamicEntityProcessDoc (inboundAdapterCallContext: InboundAdapterCallContext, status: Status, data: FooBar) extends InBoundTrait[FooBar] \ No newline at end of file diff --git a/obp-commons/src/main/scala/com/openbankproject/commons/util/ReflectUtils.scala b/obp-commons/src/main/scala/com/openbankproject/commons/util/ReflectUtils.scala index 593ac8bb2..bda44dc3c 100644 --- a/obp-commons/src/main/scala/com/openbankproject/commons/util/ReflectUtils.scala +++ b/obp-commons/src/main/scala/com/openbankproject/commons/util/ReflectUtils.scala @@ -1,6 +1,6 @@ package com.openbankproject.commons.util -import net.liftweb.common.{Box, Empty, Full} +import net.liftweb.common.{Box, Empty, Failure, Full} import scala.concurrent.ExecutionContext.Implicits.global import scala.annotation.tailrec @@ -10,6 +10,7 @@ import scala.language.postfixOps import scala.reflect.runtime.universe._ import scala.reflect.runtime.{universe => ru} import scala.util.Success +import net.liftweb.json.JValue object ReflectUtils { private[this] val mirror: ru.Mirror = ru.runtimeMirror(getClass().getClassLoader) @@ -506,9 +507,18 @@ object ReflectUtils { def toValueObject(t: Any): Any = { t match { case null => null + case v: JValue => v + case Some(v) => toValueObject(v) + case Full(v) => toValueObject(v) + case None|Empty => null + case v: Failure => v + case Left(v) => Left(toValueObject(v)) + case v: Right[_, _] => v.map(toValueObject) + case v: Success[_]=> v.map(toValueObject) + case scala.util.Failure(v) => v case it: Iterable[_] => it.map(toValueObject) case array: Array[_] => array.map(toValueObject) - case v if(getType(v).typeSymbol.asClass.isCaseClass) => v + case v if getType(v).typeSymbol.asClass.isCaseClass => v case other => { val mirrorObj = mirror.reflect(other) mirrorObj.symbol.info.decls