diff --git a/obp-api/src/main/resources/props/sample.props.template b/obp-api/src/main/resources/props/sample.props.template index 4e59da766..eced776c6 100644 --- a/obp-api/src/main/resources/props/sample.props.template +++ b/obp-api/src/main/resources/props/sample.props.template @@ -766,4 +766,7 @@ stored_procedure_connector.poolFactoryName=commons-dbcp2 # ----------------------------------------------------------------------- # Set whether DynamicEntity display name starts with underscore, default is true -dynamic_entities_have_prefix=true \ No newline at end of file +dynamic_entities_have_prefix=true + +# Url prefix of dynamic endpoints, default is dynamic. e.g if set to foobar, one url can be /obp/v4.0.0/foobar/Address +dynamic_endpoints_url_prefix=dynamic \ No newline at end of file diff --git a/obp-api/src/main/scala/bootstrap/liftweb/Boot.scala b/obp-api/src/main/scala/bootstrap/liftweb/Boot.scala index 1ef878094..29de27303 100644 --- a/obp-api/src/main/scala/bootstrap/liftweb/Boot.scala +++ b/obp-api/src/main/scala/bootstrap/liftweb/Boot.scala @@ -31,6 +31,7 @@ import java.util.{Locale, TimeZone} import code.CustomerDependants.MappedCustomerDependant import code.DynamicData.DynamicData +import code.DynamicEndpoint.DynamicEndpoint import code.accountapplication.MappedAccountApplication import code.accountattribute.MappedAccountAttribute import code.accountholders.MapperAccountHolders @@ -712,6 +713,7 @@ object ToSchemify { Authorisation, DynamicEntity, DynamicData, + DynamicEndpoint, AccountIdMapping, DirectDebit, StandingOrder 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 dfef3161e..7093f9e53 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 @@ -2019,34 +2019,8 @@ object SwaggerDefinitionsJSON { branch_id = ExampleValue.branchIdExample.value, name_suffix = ExampleValue.nameSuffixExample.value ) - - val customersJsonV300 = code.api.v3_0_0.CustomerJSONs(List(customerJsonV300)) - - val customerWithAttributesJsonV300 = CustomerWithAttributesJsonV300( - bank_id = bankIdExample.value, - customer_id = ExampleValue.customerIdExample.value, - customer_number = ExampleValue.customerNumberExample.value, - legal_name = ExampleValue.legalNameExample.value, - mobile_phone_number = ExampleValue.mobileNumberExample.value, - email = ExampleValue.emailExample.value, - face_image = customerFaceImageJson, - date_of_birth = "19900101", - relationship_status = ExampleValue.relationshipStatusExample.value, - dependants = ExampleValue.dependentsExample.value.toInt, - dob_of_dependants = List("19900101"), - credit_rating = Option(customerCreditRatingJSON), - credit_limit = Option(amountOfMoneyJsonV121), - highest_education_attained = ExampleValue.highestEducationAttainedExample.value, - employment_status = ExampleValue.employmentStatusExample.value, - kyc_status = ExampleValue.kycStatusExample.value.toBoolean, - last_ok_date = DateWithDayExampleObject, - title = ExampleValue.titleExample.value, - branch_id = ExampleValue.branchIdExample.value, - name_suffix = ExampleValue.nameSuffixExample.value, - customer_attributes = List(customerAttributeResponseJson) - ) - val customersWithAttributesJsonV300 = CustomersWithAttributesJsonV300(List(customerWithAttributesJsonV300)) + val customersJsonV300 = code.api.v3_0_0.CustomerJSONs(List(customerJsonV300)) val postCustomerJsonV310 = PostCustomerJsonV310( @@ -2123,6 +2097,32 @@ object SwaggerDefinitionsJSON { customer_attributes = List(customerAttributeResponseJson) ) + val customerWithAttributesJsonV300 = CustomerWithAttributesJsonV300( + bank_id = bankIdExample.value, + customer_id = ExampleValue.customerIdExample.value, + customer_number = ExampleValue.customerNumberExample.value, + legal_name = ExampleValue.legalNameExample.value, + mobile_phone_number = ExampleValue.mobileNumberExample.value, + email = ExampleValue.emailExample.value, + face_image = customerFaceImageJson, + date_of_birth = "19900101", + relationship_status = ExampleValue.relationshipStatusExample.value, + dependants = ExampleValue.dependentsExample.value.toInt, + dob_of_dependants = List("19900101"), + credit_rating = Option(customerCreditRatingJSON), + credit_limit = Option(amountOfMoneyJsonV121), + highest_education_attained = ExampleValue.highestEducationAttainedExample.value, + employment_status = ExampleValue.employmentStatusExample.value, + kyc_status = ExampleValue.kycStatusExample.value.toBoolean, + last_ok_date = DateWithDayExampleObject, + title = ExampleValue.titleExample.value, + branch_id = ExampleValue.branchIdExample.value, + name_suffix = ExampleValue.nameSuffixExample.value, + customer_attributes = List(customerAttributeResponseJson) + ) + + val customersWithAttributesJsonV300 = CustomersWithAttributesJsonV300(List(customerWithAttributesJsonV300)) + val putUpdateCustomerDataJsonV310 = PutUpdateCustomerDataJsonV310( face_image = customerFaceImageJson, relationship_status = ExampleValue.relationshipStatusExample.value, @@ -3631,8 +3631,7 @@ object SwaggerDefinitionsJSON { product_code = accountTypeExample.value, balance = amountOfMoneyJsonV121, branch_id = branchIdExample.value, - account_routing = accountRoutingJsonV121, - account_attributes= List(accountAttributeResponseJson) + account_routing = accountRoutingJsonV121 ) val postAccountAccessJsonV400 = PostAccountAccessJsonV400(userIdExample.value, PostViewJsonV400(ExampleValue.viewIdExample.value, true)) 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 fca1c1323..1ecb99b83 100644 --- a/obp-api/src/main/scala/code/api/util/APIUtil.scala +++ b/obp-api/src/main/scala/code/api/util/APIUtil.scala @@ -81,7 +81,7 @@ import scala.collection.JavaConverters._ import scala.collection.immutable.{List, Nil} import scala.collection.mutable.ArrayBuffer import com.openbankproject.commons.ExecutionContext.Implicits.global -import com.openbankproject.commons.util.{ApiVersion, ReflectUtils, ScannedApiVersion} +import com.openbankproject.commons.util.{ApiVersion, JsonAble, ReflectUtils, ScannedApiVersion} import com.openbankproject.commons.util.Functions.Implicits._ import org.apache.commons.lang3.StringUtils 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 9944bf854..ff9e8f55b 100644 --- a/obp-api/src/main/scala/code/api/util/ApiRole.scala +++ b/obp-api/src/main/scala/code/api/util/ApiRole.scala @@ -406,6 +406,21 @@ object ApiRole { case class CanDeleteDynamicEntity(requiresBankId: Boolean = false) extends ApiRole lazy val canDeleteDynamicEntity = CanDeleteDynamicEntity() + + case class CanGetDynamicEndpoint(requiresBankId: Boolean = false) extends ApiRole + lazy val canGetDynamicEndpoint = CanGetDynamicEndpoint() + + case class CanGetDynamicEndpoints(requiresBankId: Boolean = false) extends ApiRole + lazy val canGetDynamicEndpoints = CanGetDynamicEndpoints() + + case class CanCreateDynamicEndpoint(requiresBankId: Boolean = false) extends ApiRole + lazy val canCreateDynamicEndpoint = CanCreateDynamicEndpoint() + + case class CanUpdateDynamicEndpoint(requiresBankId: Boolean = false) extends ApiRole + lazy val canUpdateDynamicEndpoint = CanUpdateDynamicEndpoint() + + case class CanDeleteDynamicEndpoint(requiresBankId: Boolean = false) extends ApiRole + lazy val canDeleteDynamicEndpoint = CanDeleteDynamicEndpoint() case class CanCreateResetPasswordUrl(requiresBankId: Boolean = false) extends ApiRole lazy val canCreateResetPasswordUrl = CanCreateResetPasswordUrl() diff --git a/obp-api/src/main/scala/code/api/util/ApiTag.scala b/obp-api/src/main/scala/code/api/util/ApiTag.scala index 4498a2105..38f8fca72 100644 --- a/obp-api/src/main/scala/code/api/util/ApiTag.scala +++ b/obp-api/src/main/scala/code/api/util/ApiTag.scala @@ -68,6 +68,7 @@ object ApiTag { val apiTagMethodRouting = ResourceDocTag("Method-Routing") val apiTagWebUiProps = ResourceDocTag("WebUi-Props") val apiTagDynamicEntity= ResourceDocTag("Dynamic-Entity") + val apiTagDynamicEndpoint= ResourceDocTag("Dynamic-Endpoint") // To mark the Berlin Group APIs suggested order of implementation val apiTagBerlinGroupM = ResourceDocTag("Berlin-Group-M") 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 3ef1214fc..838d57ee1 100644 --- a/obp-api/src/main/scala/code/api/util/ErrorMessages.scala +++ b/obp-api/src/main/scala/code/api/util/ErrorMessages.scala @@ -41,7 +41,7 @@ object ErrorMessages { val WebUiPropsNotFound = "OBP-08002: WebUi props not found. Please specify a valid value for WEB_UI_PROPS_ID." // DynamicEntity Exceptions (OBP-09XXX) - val DynamicEntityNotFoundByDynamicEntityId = "OBP-09001: DynamicEntity not found. Please specify a valid value for dynamic_entity_id." + val DynamicEntityNotFoundByDynamicEntityId = "OBP-09001: DynamicEntity not found. Please specify a valid value for DYNAMIC_ENTITY_ID." 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." @@ -49,6 +49,9 @@ object ErrorMessages { val DynamicEntityOperationNotAllowed = "OBP-09006: Operation is not allowed, because Current DynamicEntity have upload data, must to delete all the data before this operation." val DynamicEntityInstanceValidateFail = "OBP-09007: DynamicEntity data validation failure." + val DynamicEndpointExists = "OBP-09008: DynamicEndpoint already exists." + val DynamicEndpointNotFoundByDynamicEndpointId = "OBP-09009: DynamicEndpoint not found. Please specify a valid value for DYNAMIC_ENDPOINT_ID." + // General messages (OBP-10XXX) val InvalidJsonFormat = "OBP-10001: Incorrect json format." diff --git a/obp-api/src/main/scala/code/api/util/ExampleValue.scala b/obp-api/src/main/scala/code/api/util/ExampleValue.scala index 9f577c201..683c76e6f 100644 --- a/obp-api/src/main/scala/code/api/util/ExampleValue.scala +++ b/obp-api/src/main/scala/code/api/util/ExampleValue.scala @@ -1,9 +1,12 @@ package code.api.util +import net.liftweb.json.JsonDSL._ import code.api.util.Glossary.{glossaryItems, makeGlossaryItem} import code.dynamicEntity.{DynamicEntityDefinition, DynamicEntityFooBar, DynamicEntityFullBarFields, DynamicEntityIntTypeExample, DynamicEntityStringTypeExample} import com.openbankproject.commons.model.enums.DynamicEntityFieldType +import net.liftweb.json +import net.liftweb.json.JObject case class ConnectorField(value: String, description: String) { @@ -369,6 +372,251 @@ object ExampleValue { ) lazy val dynamicEntityResponseBodyExample = dynamicEntityRequestBodyExample.copy(dynamicEntityId = Some("dynamic-entity-id")) + + private val dynamicEndpointSwagger = + """{ + | "swagger": "2.0", + | "info": { + | "version": "0.0.1", + | "title": "Example Title", + | "description": "Example Description", + | "contact": { + | "name": "Example Company", + | "email": " simon@example.com", + | "url": "https://www.tesobe.com/" + | } + | }, + | "host": "localhost:8080", + | "basePath": "/user", + | "schemes": [ + | "http" + | ], + | "consumes": [ + | "application/json" + | ], + | "produces": [ + | "application/json" + | ], + | "paths": { + | "/save": { + | "post": { + | "parameters": [ + | { + | "name": "body", + | "in": "body", + | "required": true, + | "schema": { + | "$ref": "#/definitions/user" + | } + | } + | ], + | "responses": { + | "201": { + | "description": "create user successful and return created user object", + | "schema": { + | "$ref": "#/definitions/user" + | } + | }, + | "500": { + | "description": "unexpected error", + | "schema": { + | "$ref": "#/responses/unexpectedError" + | } + | } + | } + | } + | }, + | "/getById/{userId}": { + | "get": { + | "description": "get reuested user by user ID", + | "parameters": [ + | { + | "$ref": "#/parameters/userId" + | } + | ], + | "consumes": [], + | "responses": { + | "200": { + | "description": "the successful get requested user by user ID", + | "schema": { + | "$ref": "#/definitions/user" + | } + | }, + | "400": { + | "description": "bad request", + | "schema": { + | "$ref": "#/responses/invalidRequest" + | } + | }, + | "404": { + | "description": "user not found", + | "schema": { + | "$ref": "#/definitions/APIError" + | } + | }, + | "500": { + | "description": "unexpected error", + | "schema": { + | "$ref": "#/responses/unexpectedError" + | } + | } + | } + | } + | }, + | "/listUsers": { + | "get": { + | "description": "get list of users", + | "consumes": [], + | "responses": { + | "200": { + | "description": "get all users", + | "schema": { + | "$ref": "#/definitions/users" + | } + | }, + | "404": { + | "description": "user not found", + | "schema": { + | "$ref": "#/definitions/APIError" + | } + | } + | } + | } + | }, + | "/updateUser": { + | "put": { + | "parameters": [ + | { + | "name": "body", + | "in": "body", + | "required": true, + | "schema": { + | "$ref": "#/definitions/user" + | } + | } + | ], + | "responses": { + | "200": { + | "description": "create user successful and return created user object", + | "schema": { + | "$ref": "#/definitions/user" + | } + | }, + | "500": { + | "description": "unexpected error", + | "schema": { + | "$ref": "#/responses/unexpectedError" + | } + | } + | } + | } + | }, + | "/delete/{userId}": { + | "delete": { + | "description": "delete user by user ID", + | "parameters": [ + | { + | "$ref": "#/parameters/userId" + | } + | ], + | "consumes": [], + | "responses": { + | "204": { + | "description": "the successful delete user by user ID" + | }, + | "400": { + | "description": "bad request", + | "schema": { + | "$ref": "#/responses/invalidRequest" + | } + | }, + | "500": { + | "description": "unexpected error", + | "schema": { + | "$ref": "#/responses/unexpectedError" + | } + | } + | } + | } + | } + | }, + | "definitions": { + | "user": { + | "type": "object", + | "properties": { + | "id": { + | "type": "integer", + | "description": "user ID" + | }, + | "first_name": { + | "type": "string" + | }, + | "last_name": { + | "type": "string" + | }, + | "age": { + | "type": "integer" + | }, + | "career": { + | "type": "string" + | } + | }, + | "required": [ + | "first_name", + | "last_name", + | "age" + | ] + | }, + | "users": { + | "description": "array of users", + | "type": "array", + | "items": { + | "$ref": "#/definitions/user" + | } + | }, + | "APIError": { + | "description": "content any error from API", + | "type": "object", + | "properties": { + | "errorCode": { + | "description": "content error code relate to API", + | "type": "string" + | }, + | "errorMessage": { + | "description": "content user-friendly error message", + | "type": "string" + | } + | } + | } + | }, + | "responses": { + | "unexpectedError": { + | "description": "unexpected error", + | "schema": { + | "$ref": "#/definitions/APIError" + | } + | }, + | "invalidRequest": { + | "description": "invalid request", + | "schema": { + | "$ref": "#/definitions/APIError" + | } + | } + | }, + | "parameters": { + | "userId": { + | "name": "userId", + | "in": "path", + | "required": true, + | "type": "string", + | "description": "user ID" + | } + | } + |} + |""".stripMargin + lazy val dynamicEndpointRequestBodyExample = json.parse(dynamicEndpointSwagger).asInstanceOf[JObject] + lazy val dynamicEndpointResponseBodyExample = ("dynamic_endpoint_id", "dynamic-endpoint-id") ~ ("swagger_string", dynamicEndpointRequestBodyExample) + } 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 6fa13664e..9bfa4b9ca 100644 --- a/obp-api/src/main/scala/code/api/util/NewStyle.scala +++ b/obp-api/src/main/scala/code/api/util/NewStyle.scala @@ -3,7 +3,9 @@ package code.api.util import java.util.Date import java.util.UUID.randomUUID +import akka.http.scaladsl.model.HttpMethod import code.DynamicData.DynamicDataProvider +import code.DynamicEndpoint.{DynamicEndpointProvider, DynamicEndpointT} import code.api.APIFailureNewStyle import code.api.cache.Caching import code.api.util.APIUtil.{OBPReturnType, canGrantAccessToViewCommon, canRevokeAccessToViewCommon, connectorEmptyResponse, createHttpParamsByUrlFuture, createQueriesByHttpParamsFuture, fullBoxOrException, generateUUID, unboxFull, unboxFullOrFail} @@ -13,8 +15,9 @@ import code.api.v1_4_0.OBPAPI1_4_0.Implementations1_4_0 import code.api.v2_0_0.OBPAPI2_0_0.Implementations2_0_0 import code.api.v2_1_0.OBPAPI2_1_0.Implementations2_1_0 import code.api.v2_2_0.OBPAPI2_2_0.Implementations2_2_0 -import code.api.v4_0_0.DynamicEntityInfo +import code.api.v4_0_0.{DynamicEndpointHelper, DynamicEntityInfo} import code.bankconnectors.Connector +import code.bankconnectors.rest.RestConnector_vMar2019 import code.branches.Branches.{Branch, DriveUpString, LobbyString} import code.consumer.Consumers import code.directdebit.DirectDebitTrait @@ -1919,6 +1922,10 @@ object NewStyle { } } } + def dynamicEndpointProcess(url: String, jValue: JValue, method: HttpMethod, params: Map[String, List[String]], pathParams: Map[String, String], + callContext: Option[CallContext]): OBPReturnType[Box[JValue]] = { + Connector.connector.vend.dynamicEndpointProcess(url, jValue, method, params, pathParams, callContext) + } def createDirectDebit(bankId : String, @@ -1996,6 +2003,54 @@ object NewStyle { getConnectorByName(connectorName).flatMap(_.implementedMethods.get(methodName)) } + def createDynamicEndpoint(swaggerString: String, callContext: Option[CallContext]): OBPReturnType[DynamicEndpointT] = { + Connector.connector.vend.createDynamicEndpoint( + swaggerString, + callContext + ) map { + i => (connectorEmptyResponse(i._1, callContext), i._2) + } + } + + def getDynamicEndpoint(dynamicEndpointId: String, callContext: Option[CallContext]): OBPReturnType[DynamicEndpointT] = { + val dynamicEndpointBox: Box[DynamicEndpointT] = DynamicEndpointProvider.connectorMethodProvider.vend.get(dynamicEndpointId) + val dynamicEndpoint = unboxFullOrFail(dynamicEndpointBox, callContext, DynamicEndpointNotFoundByDynamicEndpointId, 404) + Future{ + (dynamicEndpoint, callContext) + } + } + + def getDynamicEndpoints(callContext: Option[CallContext]): OBPReturnType[List[DynamicEndpointT]] = { + Connector.connector.vend.getDynamicEndpoints( + callContext + ) + } + /** + * delete one DynamicEndpoint and corresponding entitlement and dynamic entitlement + * @param dynamicEndpointId + * @param callContext + * @return + */ + def deleteDynamicEndpoint(dynamicEndpointId: String, callContext: Option[CallContext]): Future[Box[Boolean]] = { + val dynamicEndpoint: OBPReturnType[DynamicEndpointT] = this.getDynamicEndpoint(dynamicEndpointId, callContext) + for { + (entity, _) <- dynamicEndpoint + deleteSuccess = DynamicEndpointProvider.connectorMethodProvider.vend.delete(dynamicEndpointId) + + deleteEndpointResult: Box[Boolean] = if(deleteSuccess) { + val roles = DynamicEndpointHelper.getRoles(dynamicEndpointId).map(_.toString()) + DynamicEndpointHelper.removeEndpoint(dynamicEndpointId) + val rolesDeleteResult: Box[Boolean] = Entitlement.entitlement.vend.deleteEntitlements(roles) + + Box !! (rolesDeleteResult == Full(true)) + } else { + Box !! false + } + } yield { + deleteEndpointResult + } + } + def deleteCustomerAttribute(customerAttributeId : String, callContext: Option[CallContext]): OBPReturnType[Boolean] = { Connector.connector.vend.deleteCustomerAttribute(customerAttributeId, callContext) map { i => (connectorEmptyResponse(i._1, callContext), i._2) 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 739a42fe4..e3b9cb64d 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 @@ -2227,6 +2227,7 @@ trait APIMethods310 { emptyObjectJson, List( UserHasMissingRoles, + BankNotFound, UnknownError ), Catalogs(notCore, notPSD2, notOBWG), diff --git a/obp-api/src/main/scala/code/api/v3_1_0/JSONFactory3.1.0.scala b/obp-api/src/main/scala/code/api/v3_1_0/JSONFactory3.1.0.scala index 0e4bb2058..fe33545cc 100644 --- a/obp-api/src/main/scala/code/api/v3_1_0/JSONFactory3.1.0.scala +++ b/obp-api/src/main/scala/code/api/v3_1_0/JSONFactory3.1.0.scala @@ -664,8 +664,7 @@ case class CreateAccountRequestJsonV310( product_code : String, balance : AmountOfMoneyJsonV121, branch_id : String, - account_routing: AccountRoutingJsonV121, - account_attributes: List[AccountAttributeResponseJson] + account_routing: AccountRoutingJsonV121 ) case class CreateAccountResponseJsonV310( 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 31bbaeb92..a077eb3f9 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 @@ -3,6 +3,7 @@ package code.api.v4_0_0 import java.util.Date import code.DynamicData.DynamicData +import code.DynamicEndpoint.{DynamicEndpointCommons, DynamicEndpointSwagger} import code.accountattribute.AccountAttributeX import code.api.ChargePolicy import code.api.ResourceDocs1_4_0.SwaggerDefinitionsJSON._ @@ -10,7 +11,7 @@ import code.api.util.APIUtil.{fullBoxOrException, _} import code.api.util.ApiRole._ import code.api.util.ApiTag._ import code.api.util.ErrorMessages._ -import code.api.util.ExampleValue.{dynamicEntityRequestBodyExample, dynamicEntityResponseBodyExample} +import code.api.util.ExampleValue.{dynamicEndpointRequestBodyExample, dynamicEndpointResponseBodyExample, dynamicEntityRequestBodyExample, dynamicEntityResponseBodyExample} import code.api.util.NewStyle.HttpCode import code.api.util.newstyle.AttributeDefinition._ import code.api.util.newstyle.Consumer._ @@ -23,6 +24,7 @@ import code.api.v2_1_0._ import code.api.v2_2_0.{BankJSONV220, JSONFactory220} import code.api.v3_0_0.JSONFactory300 import code.api.v3_1_0.{CreateAccountRequestJsonV310, CustomerWithAttributesJsonV310, JSONFactory310, ListResult} +import code.api.v4_0_0.DynamicEndpointHelper.DynamicReq import code.api.v4_0_0.JSONFactory400.{createBankAccountJSON, createNewCoreBankAccountJson} import code.bankconnectors.Connector import code.dynamicEntity.{DynamicEntityCommons, ReferenceType} @@ -50,6 +52,7 @@ 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.json.JsonDSL._ import net.liftweb.mapper.By import net.liftweb.util.Helpers.now import net.liftweb.util.{Helpers, StringHelpers} @@ -60,6 +63,7 @@ import scala.collection.mutable.ArrayBuffer import scala.concurrent.Future import code.model._ import org.apache.commons.lang3.StringUtils +import net.liftweb.json.compactRender trait APIMethods400 { self: RestHelper => @@ -131,11 +135,11 @@ trait APIMethods400 { | INITIATED => COMPLETED |In case n persons needs to answer security challenge we have next flow of state of an `transaction request`: | INITIATED => NEXT_CHALLENGE_PENDING => ... => NEXT_CHALLENGE_PENDING => COMPLETED - | + | |The security challenge is bound to a user i.e. in case of right answer and the user is different than expected one the challenge will fail. | |Rule for calculating number of security challenges: - |If product Account attribute REQUIRED_CHALLENGE_ANSWERS=N then create N challenges + |If product Account attribute REQUIRED_CHALLENGE_ANSWERS=N then create N challenges |(one for every user that has a View where permission "can_add_transaction_request_to_any_account"=true) |In case REQUIRED_CHALLENGE_ANSWERS is not defined as an account attribute default value is 1. | @@ -363,7 +367,7 @@ trait APIMethods400 { ), Catalogs(Core, PSD2, OBWG), List(apiTagTransactionRequest, apiTagPSD2PIS, apiTagNewStyle)) - + // FREE_FORM. resourceDocs += ResourceDoc( createTransactionRequestFreeForm, @@ -424,7 +428,7 @@ trait APIMethods400 { account = BankIdAccountId(bankId, accountId) _ <- NewStyle.function.checkAuthorisationToCreateTransactionRequest(viewId, account, u, cc.callContext) - + _ <- Helper.booleanToFuture(InsufficientAuthorisationToCreateTransactionRequest) { u.hasOwnerViewAccess(BankIdAccountId(bankId, accountId)) || hasEntitlement(bankId.value, u.userId, ApiRole.canCreateAnyTransactionRequest) @@ -467,7 +471,7 @@ trait APIMethods400 { transactionRequestBodyRefundJson <- NewStyle.function.tryons(s"${InvalidJsonFormat}, it should be $ACCOUNT json format", 400, cc.callContext) { json.extract[TransactionRequestBodyRefundJsonV400] } - + transactionId = TransactionId(transactionRequestBodyRefundJson.refund.transaction_id) toBankId = BankId(transactionRequestBodyRefundJson.to.bank_id) toAccountId = AccountId(transactionRequestBodyRefundJson.to.account_id) @@ -476,28 +480,28 @@ trait APIMethods400 { transDetailsSerialized <- NewStyle.function.tryons(UnknownError, 400, callContext) { write(transactionRequestBodyRefundJson)(Serialization.formats(NoTypeHints)) } - + _ <- Helper.booleanToFuture(s"${RefundedTransaction} Current input amount is: '${transDetailsJson.value.amount}'. It can not be more than the original amount(${(transaction.amount).abs})") { (transaction.amount).abs >= transactionAmountNumber } - //TODO, we need additional field to guarantee the transaction is refunded... + //TODO, we need additional field to guarantee the transaction is refunded... // _ <- Helper.booleanToFuture(s"${RefundedTransaction}") { // !((transaction.description.toString contains(" Refund to ")) && (transaction.description.toString contains(" and transaction_id("))) // } - + //we add the extro info (counterparty name + transaction_id) for this special Refund endpoint. newDescription = s"${transactionRequestBodyRefundJson.description} - Refund for transaction_id: (${transactionId.value}) to ${transaction.otherAccount.counterpartyName}" - - //This is the refund endpoint, the original fromAccount is the `toAccount` which will receive money. + + //This is the refund endpoint, the original fromAccount is the `toAccount` which will receive money. refundToAccount = fromAccount - //This is the refund endpoint, the original toAccount is the `fromAccount` which will lose money. + //This is the refund endpoint, the original toAccount is the `fromAccount` which will lose money. refundFromAccount = toAccount - + (createdTransactionRequest, callContext) <- NewStyle.function.createTransactionRequestv400(u, viewId, refundFromAccount, refundToAccount, - transactionRequestType, + transactionRequestType, transactionRequestBodyRefundJson.copy(description = newDescription), transDetailsSerialized, sharedChargePolicy.toString, @@ -782,7 +786,7 @@ trait APIMethods400 { .findAll(By(MappedExpectedChallengeAnswer.mTransactionRequestId, transReqId.value)) .count(_.successful == true) match { case number if number >= quorum => true - case _ => + case _ => MappedTransactionRequestProvider.saveTransactionRequestStatusImpl(transReqId, TransactionRequestStatus.NEXT_CHALLENGE_PENDING.toString) false } @@ -813,9 +817,9 @@ trait APIMethods400 { implementedInApiVersion, nameOf(getDynamicEntities), "GET", - "/management/dynamic_entities", - "Get DynamicEntities", - s"""Get the all DynamicEntities.""", + "/management/dynamic-entities", + "Get Dynamic Entities", + s"""Get the all Dynamic Entities.""", emptyObjectJson, ListResult( "dynamic_entities", @@ -833,7 +837,7 @@ trait APIMethods400 { lazy val getDynamicEntities: OBPEndpoint = { - case "management" :: "dynamic_entities" :: Nil JsonGet req => { + case "management" :: "dynamic-entities" :: Nil JsonGet req => { cc => for { dynamicEntities <- Future(NewStyle.function.getDynamicEntities()) @@ -850,8 +854,8 @@ trait APIMethods400 { implementedInApiVersion, nameOf(createDynamicEntity), "POST", - "/management/dynamic_entities", - "Create DynamicEntity", + "/management/dynamic-entities", + "Create Dynamic Entity", s"""Create a DynamicEntity. | | @@ -883,7 +887,7 @@ trait APIMethods400 { Some(List(canCreateDynamicEntity))) lazy val createDynamicEntity: OBPEndpoint = { - case "management" :: "dynamic_entities" :: Nil JsonPost json -> _ => { + case "management" :: "dynamic-entities" :: Nil JsonPost json -> _ => { cc => val dynamicEntity = DynamicEntityCommons(json.asInstanceOf[JObject], None) for { @@ -901,8 +905,8 @@ trait APIMethods400 { implementedInApiVersion, nameOf(updateDynamicEntity), "PUT", - "/management/dynamic_entities/DYNAMIC_ENTITY_ID", - "Update DynamicEntity", + "/management/dynamic-entities/DYNAMIC_ENTITY_ID", + "Update Dynamic Entity", s"""Update a DynamicEntity. | | @@ -934,7 +938,7 @@ trait APIMethods400 { Some(List(canUpdateDynamicEntity))) lazy val updateDynamicEntity: OBPEndpoint = { - case "management" :: "dynamic_entities" :: dynamicEntityId :: Nil JsonPut json -> _ => { + case "management" :: "dynamic-entities" :: dynamicEntityId :: Nil JsonPut json -> _ => { cc => for { // Check whether there are uploaded data, only if no uploaded data allow to update DynamicEntity. @@ -959,8 +963,8 @@ trait APIMethods400 { implementedInApiVersion, nameOf(deleteDynamicEntity), "DELETE", - "/management/dynamic_entities/DYNAMIC_ENTITY_ID", - "Delete DynamicEntity", + "/management/dynamic-entities/DYNAMIC_ENTITY_ID", + "Delete Dynamic Entity", s"""Delete a DynamicEntity specified by DYNAMIC_ENTITY_ID. | |""", @@ -976,7 +980,7 @@ trait APIMethods400 { Some(List(canDeleteDynamicEntity))) lazy val deleteDynamicEntity: OBPEndpoint = { - case "management" :: "dynamic_entities" :: dynamicEntityId :: Nil JsonDelete _ => { + case "management" :: "dynamic-entities" :: dynamicEntityId :: Nil JsonDelete _ => { cc => for { // Check whether there are uploaded data, only if no uploaded data allow to delete DynamicEntity. @@ -1114,7 +1118,7 @@ trait APIMethods400 { json.extract[PostResetPasswordUrlJsonV400] } } yield { - val resetLink = AuthUser.passwordResetUrl(postedData.username, postedData.email, postedData.user_id) + val resetLink = AuthUser.passwordResetUrl(postedData.username, postedData.email, postedData.user_id) (ResetPasswordUrlJsonV400(resetLink), HttpCode.`201`(cc.callContext)) } } @@ -2181,7 +2185,7 @@ trait APIMethods400 { CustomerAttributeType.withName(postedData.`type`) } (customer, callContext) <- NewStyle.function.getCustomerByCustomerId(customerId, cc.callContext) - _ <- Helper.booleanToFuture(InvalidCustomerBankId.replaceAll("Bank Id.",s"Bank Id ($bankId).").replaceAll("The Customer",s"The Customer($customerId)")){customer.bankId == bankId} + _ <- Helper.booleanToFuture(InvalidCustomerBankId.replaceAll("Bank Id.",s"Bank Id ($bankId).").replaceAll("The Customer",s"The Customer($customerId)")){customer.bankId == bankId} (accountAttribute, callContext) <- NewStyle.function.getCustomerAttributeById( customerAttributeId, callContext @@ -2768,7 +2772,188 @@ trait APIMethods400 { } } + resourceDocs += ResourceDoc( + createDynamicEndpoint, + implementedInApiVersion, + nameOf(createDynamicEndpoint), + "POST", + "/management/dynamic-endpoints", + " Create DynamicEndpoint", + s"""Create a DynamicEndpoint. + | + |Create one DynamicEndpoint, + | + |""", + dynamicEndpointRequestBodyExample, + dynamicEndpointResponseBodyExample, + List( + $UserNotLoggedIn, + UserHasMissingRoles, + DynamicEndpointExists, + InvalidJsonFormat, + UnknownError + ), + Catalogs(notCore, notPSD2, notOBWG), + List(apiTagDynamicEndpoint, apiTagApi, apiTagNewStyle), + Some(List(canCreateDynamicEndpoint))) + lazy val createDynamicEndpoint: OBPEndpoint = { + case "management" :: "dynamic-endpoints" :: Nil JsonPost json -> _ => { + cc => + for { + (postedJson, openAPI) <- NewStyle.function.tryons(InvalidJsonFormat, 400, cc.callContext) { + val swaggerContent = compactRender(json) + + (DynamicEndpointSwagger(swaggerContent), DynamicEndpointHelper.parseSwaggerContent(swaggerContent)) + } + duplicatedUrl = DynamicEndpointHelper.findExistsEndpoints(openAPI).map(kv => s"${kv._1}:${kv._2}") + errorMsg = s"""$DynamicEndpointExists Duplicated ${if(duplicatedUrl.size > 1) "endpoints" else "endpoint"}: ${duplicatedUrl.mkString("; ")}""" + _ <- Helper.booleanToFuture(errorMsg) { + duplicatedUrl.isEmpty + } + (dynamicEndpoint, callContext) <- NewStyle.function.createDynamicEndpoint(postedJson.swaggerString, cc.callContext) + _ = DynamicEndpointHelper.addEndpoint(openAPI, dynamicEndpoint.dynamicEndpointId.get) + } yield { + val swaggerJson = parse(dynamicEndpoint.swaggerString) + val responseJson: JObject = ("dynamic_endpoint_id", dynamicEndpoint.dynamicEndpointId) ~ ("swagger_string", swaggerJson) + (responseJson, HttpCode.`201`(callContext)) + } + } + } + + + resourceDocs += ResourceDoc( + getDynamicEndpoint, + implementedInApiVersion, + nameOf(getDynamicEndpoint), + "GET", + "/management/dynamic-endpoints/DYNAMIC_ENDPOINT_ID", + " Get DynamicEndpoint", + s"""Get a DynamicEndpoint. + | + | + |Get one DynamicEndpoint, + | + |""", + emptyObjectJson, + dynamicEndpointResponseBodyExample, + List( + $UserNotLoggedIn, + UserHasMissingRoles, + DynamicEndpointNotFoundByDynamicEndpointId, + InvalidJsonFormat, + UnknownError + ), + Catalogs(notCore, notPSD2, notOBWG), + List(apiTagDynamicEndpoint, apiTagApi, apiTagNewStyle), + Some(List(canGetDynamicEndpoint))) + + lazy val getDynamicEndpoint: OBPEndpoint = { + case "management" :: "dynamic-endpoints" :: dynamicEndpointId :: Nil JsonGet req => { + cc => + for { + (dynamicEndpoint, callContext) <- NewStyle.function.getDynamicEndpoint(dynamicEndpointId, cc.callContext) + } yield { + val swaggerJson = parse(dynamicEndpoint.swaggerString) + val responseJson: JObject = ("dynamic_endpoint_id", dynamicEndpoint.dynamicEndpointId) ~ ("swagger_string", swaggerJson) + (responseJson, HttpCode.`200`(callContext)) + } + } + } + + resourceDocs += ResourceDoc( + getDynamicEndpoints, + implementedInApiVersion, + nameOf(getDynamicEndpoints), + "GET", + "/management/dynamic-endpoints", + " Get DynamicEndpoints", + s""" + | + |Get DynamicEndpoints. + | + |""", + emptyObjectJson, + ListResult( + "dynamic_endpoints", + List(dynamicEndpointResponseBodyExample) + ), + List( + $UserNotLoggedIn, + UserHasMissingRoles, + InvalidJsonFormat, + UnknownError + ), + Catalogs(notCore, notPSD2, notOBWG), + List(apiTagDynamicEndpoint, apiTagApi, apiTagNewStyle), + Some(List(canGetDynamicEndpoints))) + + lazy val getDynamicEndpoints: OBPEndpoint = { + case "management" :: "dynamic-endpoints" :: Nil JsonGet _ => { + cc => + for { + (dynamicEndpoints, _) <- NewStyle.function.getDynamicEndpoints(cc.callContext) + } yield { + val resultList = dynamicEndpoints.map[JObject, List[JObject]] { dynamicEndpoint=> + val swaggerJson = parse(dynamicEndpoint.swaggerString) + ("dynamic_endpoint_id", dynamicEndpoint.dynamicEndpointId) ~ ("swagger_string", swaggerJson) + } + (ListResult("dynamic_endpoints", resultList), HttpCode.`200`(cc.callContext)) + } + } + } + + resourceDocs += ResourceDoc( + deleteDynamicEndpoint, + implementedInApiVersion, + nameOf(deleteDynamicEndpoint), + "DELETE", + "/management/dynamic-endpoints/DYNAMIC_ENDPOINT_ID", + " Delete Dynamic Endpoint", + s"""Delete a DynamicEndpoint specified by DYNAMIC_ENDPOINT_ID. + | + |""", + emptyObjectJson, + emptyObjectJson, + List( + $UserNotLoggedIn, + UserHasMissingRoles, + DynamicEndpointNotFoundByDynamicEndpointId, + UnknownError + ), + Catalogs(notCore, notPSD2, notOBWG), + List(apiTagDynamicEndpoint, apiTagApi, apiTagNewStyle), + Some(List(canDeleteDynamicEndpoint))) + + lazy val deleteDynamicEndpoint : OBPEndpoint = { + case "management" :: "dynamic-endpoints" :: dynamicEndpointId :: Nil JsonDelete _ => { + cc => + for { + deleted <- NewStyle.function.deleteDynamicEndpoint(dynamicEndpointId, cc.callContext) + } yield { + (deleted, HttpCode.`204`(cc.callContext)) + } + } + } + + + lazy val dynamicEndpoint: OBPEndpoint = { + case DynamicReq(url, json, method, params, pathParams, role) => { cc => + for { + (Full(u), callContext) <- authenticatedAccess(cc) + _ <- NewStyle.function.hasEntitlement("", u.userId, role, callContext) + + (box, _) <- NewStyle.function.dynamicEndpointProcess(url, json, method, params, pathParams, callContext) + } yield { + box match { + case Full(v) => (v, HttpCode.`200`(Some(cc))) + case e: Failure => (e.messageChain, HttpCode.`200`(Some(cc))) // TODO code need change + case _ => ("fail", HttpCode.`200`(Some(cc))) + } + + } + } + } resourceDocs += ResourceDoc( createOrUpdateCustomerAttributeAttributeDefinition, diff --git a/obp-api/src/main/scala/code/api/v4_0_0/DynamicEndpointHelper.scala b/obp-api/src/main/scala/code/api/v4_0_0/DynamicEndpointHelper.scala new file mode 100644 index 000000000..dd43c7249 --- /dev/null +++ b/obp-api/src/main/scala/code/api/v4_0_0/DynamicEndpointHelper.scala @@ -0,0 +1,454 @@ +package code.api.v4_0_0 + +import java.io.File +import java.nio.charset.Charset +import java.util +import java.util.concurrent.CopyOnWriteArrayList +import java.util.regex.Pattern +import java.util.{Date, Optional} + +import akka.http.scaladsl.model.HttpMethods +import code.DynamicEndpoint.{DynamicEndpointProvider, DynamicEndpointT} +import code.api.util.APIUtil.{Catalogs, OBPEndpoint, ResourceDoc, authenticationRequiredMessage, emptyObjectJson, generateUUID, notCore, notOBWG, notPSD2} +import code.api.util.ApiTag.{ResourceDocTag, apiTagApi, apiTagNewStyle} +import code.api.util.ErrorMessages.{InvalidJsonFormat, UnknownError, UserHasMissingRoles, UserNotLoggedIn} +import code.api.util.{APIUtil, ApiRole, ApiTag, CustomJsonFormats, NewStyle} +import code.api.util.ApiRole.getOrCreateDynamicApiRole +import com.openbankproject.commons.model.enums.DynamicEntityFieldType +import com.openbankproject.commons.util.{ApiVersion, Functions} +import io.swagger.v3.oas.models.{OpenAPI, Operation, PathItem} +import io.swagger.v3.oas.models.PathItem.HttpMethod +import akka.http.scaladsl.model.{HttpMethod => AkkaHttpMethod} +import io.swagger.v3.oas.models.media.{ArraySchema, BooleanSchema, Content, DateSchema, DateTimeSchema, IntegerSchema, NumberSchema, ObjectSchema, Schema, StringSchema} +import io.swagger.v3.oas.models.parameters.RequestBody +import io.swagger.v3.oas.models.responses.ApiResponses +import io.swagger.v3.parser.OpenAPIV3Parser +import net.liftweb.http.Req +import net.liftweb.http.rest.RestHelper +import net.liftweb.json.JsonAST.{JArray, JField, JNothing, JObject} +import net.liftweb.json.JsonDSL._ +import net.liftweb.json +import net.liftweb.json.JValue +import net.liftweb.util.StringHelpers +import org.apache.commons.io.FileUtils +import org.apache.commons.lang3.StringUtils + +import scala.collection.immutable.{List, Nil} +import scala.collection.mutable +import scala.collection.mutable.ArrayBuffer +import scala.collection.JavaConverters._ +import scala.compat.java8.OptionConverters._ + + +object DynamicEndpointHelper extends RestHelper { + + /** + * dynamic endpoints url prefix + */ + val urlPrefix = APIUtil.getPropsValue("dynamic_endpoints_url_prefix", "dynamic") + + private lazy val dynamicEndpointInfos: CopyOnWriteArrayList[DynamicEndpointInfo] = { + val dynamicEndpoints: List[DynamicEndpointT] = DynamicEndpointProvider.connectorMethodProvider.vend.getAll() + val infos = dynamicEndpoints.map(it => swaggerToResourceDocs(it.swaggerString, it.dynamicEndpointId.get)) + new CopyOnWriteArrayList(infos.asJava) + } + + def getRoles(dynamicEndpointId: String): List[ApiRole] = { + val foundInfos: Option[DynamicEndpointInfo] = dynamicEndpointInfos.asScala + .find(_.id == dynamicEndpointId) + + val roles = foundInfos.toList + .flatMap(_.resourceDocs) + .map(_.roles) + .collect { + case Some(role :: _) => role + } + + roles + } + /** + * extract request body, no matter GET, POST, PUT or DELETE method + */ + object DynamicReq extends JsonTest with JsonBody { + + private val ExpressionRegx = """\{(.+?)\}""".r + /** + * unapply Request to (request url, json, http method, request parameters, path parameters, role) + * request url is current request url + * json is request body + * http method is request http method + * request parameters is http request parameters + * path parameters: /banks/{bankId}/users/{userId} bankId and userId corresponding key to value + * role is current endpoint required entitlement + * @param r HttpRequest + * @return + */ + def unapply(r: Req): Option[(String, JValue, AkkaHttpMethod, Map[String, List[String]], Map[String, String], ApiRole)] = { + val partPath = r.path.partPath + if (!testResponse_?(r) || partPath.headOption != Option(urlPrefix)) + None + else { + val akkaHttpMethod = HttpMethods.getForKeyCaseInsensitive(r.requestType.method).get + val httpMethod = HttpMethod.valueOf(r.requestType.method) + // url that match original swagger endpoint. + val url = partPath.tail.mkString("/", "/", "") + val foundDynamicEndpoint: Optional[(DynamicEndpointInfo, ResourceDoc, String)] = dynamicEndpointInfos.stream() + .map[Option[(DynamicEndpointInfo, ResourceDoc, String)]](_.findDynamicEndpoint(httpMethod, url)) + .filter(_.isDefined) + .findFirst() + .map(_.get) + + foundDynamicEndpoint.asScala + .flatMap[(String, JValue, AkkaHttpMethod, Map[String, List[String]], Map[String, String], ApiRole)] { it => + val (dynamicEndpointInfo, doc, originalUrl) = it + + val pathParams: Map[String, String] = if(originalUrl == url) { + Map.empty[String, String] + } else { + val tuples: Array[(String, String)] = StringUtils.split(originalUrl, "/").zip(partPath.tail) + tuples.collect { + case (ExpressionRegx(name), value) => name->value + }.toMap + } + + val Some(role::_) = doc.roles + body(r).toOption + .orElse(Some(JNothing)) + .map(t => (dynamicEndpointInfo.targetUrl(url), t, akkaHttpMethod, r.params, pathParams, role)) + } + + } + } + } + + def addEndpoint(openAPI: OpenAPI, id: String): Boolean = { + val endpointInfo = swaggerToResourceDocs(openAPI, id) + dynamicEndpointInfos.add(endpointInfo) + } + + def removeEndpoint(id: String): Boolean = { + dynamicEndpointInfos.asScala.find(_.id == id) match { + case Some(v) => dynamicEndpointInfos.remove(v) + case _ => false + } + } + + def findExistsEndpoints(openAPI: OpenAPI): List[(HttpMethod, String)] = { + for { + (path, pathItem) <- openAPI.getPaths.asScala.toList + (method: HttpMethod, _) <- pathItem.readOperationsMap.asScala + if dynamicEndpointInfos.stream().anyMatch(_.existsEndpoint(method, path)) + } yield (method, path) + + } + + private def swaggerToResourceDocs(content: String, id: String): DynamicEndpointInfo = { + val openAPI: OpenAPI = parseSwaggerContent(content) + swaggerToResourceDocs(openAPI, id) + } + + private def swaggerToResourceDocs(openAPI: OpenAPI, id: String): DynamicEndpointInfo = { + val tags: List[ResourceDocTag] = List(ApiTag.apiTagDynamicEndpoint, apiTagApi, apiTagNewStyle) + + val serverUrl = { + val servers = openAPI.getServers + assert(!servers.isEmpty, s"swagger host is mandatory, but current swagger host is empty, id=$id") + servers.get(0).getUrl + } + + val paths: mutable.Map[String, PathItem] = openAPI.getPaths.asScala + def entitlementSuffix(path: String) = Math.abs(path.hashCode).toString.substring(0, 3) // to avoid different swagger have same entitlement + val docs: mutable.Iterable[(ResourceDoc, String)] = for { + (path, pathItem) <- paths + (method: HttpMethod, op: Operation) <- pathItem.readOperationsMap.asScala + } yield { + val implementedInApiVersion = ApiVersion.v4_0_0 + val partialFunction: OBPEndpoint = APIMethods400.Implementations4_0_0.genericEndpoint // this function is just placeholder, not need a real value. + val partialFunctionName: String = s"$method-$path".replaceAll("\\W", "_") + val requestVerb: String = method.name() + val requestUrl: String = buildRequestUrl(path) + val summary: String = Option(pathItem.getSummary) + .filter(StringUtils.isNotBlank) + .getOrElse(buildSummary(method, op, path)) + val description: String = Option(pathItem.getDescription) + .filter(StringUtils.isNotBlank) + .orElse(Option(op.getDescription)) + .filter(StringUtils.isNotBlank) + .map(_.capitalize) + .getOrElse(summary) + + s""" + | + |MethodRouting settings example: + |``` + |{ + | "is_bank_id_exact_match":false, + | "method_name":"dynamicEndpointProcess", + | "connector_name":"rest_vMar2019", + | "bank_id_pattern":".*", + | "parameters":[ + | { + | "key":"url_pattern", + | "value":"$serverUrl$path" + | }, + | { + | "key":"http_method", + | "value":"$requestVerb" + | } + | { + | "key":"url", + | "value":"http://mydomain.com/xxx" + | } + | ] + |} + |``` + | + |""".stripMargin + val exampleRequestBody: Product = getRequestExample(openAPI, op.getRequestBody) + val successResponseBody: Product = getResponseExample(openAPI, op.getResponses) + val errorResponseBodies: List[String] = List( + UserNotLoggedIn, + UserHasMissingRoles, + UnknownError + ) + val catalogs: Catalogs = Catalogs(notCore, notPSD2, notOBWG) + + val roles: Option[List[ApiRole]] = { + val roleName = s"Can$summary${entitlementSuffix(path)}" + .replaceFirst("Can(Create|Update|Get|Delete)", "Can$1Dynamic") + .replace(" ", "") + Some(List( + ApiRole.getOrCreateDynamicApiRole(roleName) + )) + } + val connectorMethods = Some(List("dynamicEndpointProcess")) + val doc = ResourceDoc( + partialFunction, + implementedInApiVersion, + partialFunctionName, + requestVerb, + requestUrl, + summary, + description, + exampleRequestBody, + successResponseBody, + errorResponseBodies, + catalogs, + tags, + roles, + connectorMethods = connectorMethods + ) + (doc, path) + } + + DynamicEndpointInfo(id, docs, serverUrl) + } + + private val PathParamRegx = """\{(.+?)\}""".r + private val WordBoundPattern = Pattern.compile("(?<=[a-z])(?=[A-Z])|-") + + private def buildRequestUrl(path: String): String = { + val url = StringUtils.split(s"$urlPrefix/$path", "/") + url.map { + case PathParamRegx(param) => WordBoundPattern.matcher(param).replaceAll("_").toUpperCase() + case v => v + }.mkString("/", "/", "") + } + + def parseSwaggerContent(content: String): OpenAPI = { + val tempSwaggerFile = File.createTempFile("temp", ".swagger") + FileUtils.write(tempSwaggerFile, content, Charset.forName("utf-8")) + val openAPI: OpenAPI = new OpenAPIV3Parser().read(tempSwaggerFile.getAbsolutePath) + // Delete temp file when program exits, only if delete fail. + if(!FileUtils.deleteQuietly(tempSwaggerFile)){ + tempSwaggerFile.deleteOnExit() + } + openAPI + } + + def doc: ArrayBuffer[ResourceDoc] = { + val docs = ArrayBuffer[ResourceDoc]() + dynamicEndpointInfos.forEach { info => + info.resourceDocs.foreach { doc => + docs += doc + } + } + docs + } + + private def buildSummary(method: HttpMethod, op: Operation, path: String): String = method match { + case _ if StringUtils.isNotBlank(op.getSummary) => op.getSummary + case HttpMethod.GET | HttpMethod.DELETE => + val opName = if(method == HttpMethod.GET) "Get" else "Delete" + op.getResponses.asScala + .find(_._1.startsWith("20")) + .flatMap(it => getRef(it._2.getContent, it._2.get$ref()) ) + .map(StringUtils.substringAfterLast(_, "/")) + .map(entityName => s"$opName $entityName") + .orElse(Option(op.getDescription)) + .filter(StringUtils.isNotBlank) + .orElse(Option(s"$opName $path")) + .map(_.replaceFirst("(?i)((get|delete)\\s+\\S+).*", "$1")) + .map(capitalize) + .get + + case m@(HttpMethod.POST | HttpMethod.PUT) => + val opName = if(m == HttpMethod.POST) "Create" else "Update" + + getRef(op.getRequestBody.getContent, op.getRequestBody.get$ref()) + .map(StringUtils.substringAfterLast(_, "/")) + .map(entityName => s"$opName $entityName") + .orElse(Option(op.getDescription)) + .filter(StringUtils.isNotBlank) + .orElse(Option(s"$method $path")) + .map(capitalize) + .get + case _ => throw new RuntimeException(s"Support HTTP METHOD: GET, POST, PUT, DELETE, current method is $method") + } + private def capitalize(str: String): String = + StringUtils.split(str, " ").map(_.capitalize).mkString(" ") + + private def getRequestExample(openAPI: OpenAPI, body: RequestBody): Product = { + if(body == null || body.getContent == null) { + JObject() + } else { + getExample(openAPI, getRef(body.getContent, body.get$ref()).orNull) + } + } + private def getResponseExample(openAPI: OpenAPI, apiResponses: ApiResponses): Product = { + if(apiResponses == null || apiResponses.isEmpty) { + JObject() + } else { + val ref: Option[String] = apiResponses.asScala + .find(_._1.startsWith("20")) + .flatMap(it => getRef(it._2.getContent, it._2.get$ref())) + getExample(openAPI, ref.orNull) + } + } + + private def getRef(content: Content, $ref: String): Option[String] = { + if(StringUtils.isNoneBlank($ref)) { + Option($ref) + } else { + val schemaRef: Option[String] = Option(content.get("application/json")) + .flatMap(it => Option[Schema[_]](it.getSchema)) + .map(_.get$ref()) + .filter(StringUtils.isNoneBlank(_)) + + if(schemaRef.isDefined) { + Option(schemaRef.get) + } else { + val supportMediaTypes = content.values().asScala + supportMediaTypes.collectFirst { + case mediaType if mediaType.getSchema != null && StringUtils.isNotBlank(mediaType.getSchema.get$ref()) => + mediaType.getSchema.get$ref() + } + } + } + + } + private val RegexDefinitions = """(?:#/components/schemas(?:/#definitions)?/)(.+)""".r + private val RegexResponse = """#/responses/(.+)""".r + + private def getExample(openAPI: OpenAPI, ref: String): Product = { + implicit val formats = CustomJsonFormats.formats + ref match { + case null => JObject() + + case RegexResponse(refName) => + val response = openAPI.getComponents.getResponses.get(refName) + val ref = getRef(response.getContent, response.get$ref()) + getExample(openAPI, ref.get) + + case RegexDefinitions(refName) => + openAPI.getComponents.getSchemas.get(refName) match { + case o: ObjectSchema => + val properties: util.Map[String, Schema[_]] = o.getProperties + + val jFields: mutable.Iterable[JField] = properties.asScala.map { kv => + val (name, value) = kv + val valueExample = if(value.getClass == classOf[Schema[_]]) getExample(openAPI, value.get$ref()) else getPropertyExample(value) + JField(name, json.Extraction.decompose(valueExample)) + } + JObject(jFields.toList) + + case a: ArraySchema => + Option(a.getExample) + .map(json.Extraction.decompose(_).asInstanceOf[JObject]) + .getOrElse { + val schema: Schema[_] = a.getItems + val singleItem: Any = if(schema.getClass == classOf[Schema[_]]) getExample(openAPI, schema.get$ref()) else getPropertyExample(schema) + val jItem = json.Extraction.decompose(singleItem) + jItem :: Nil + } + } + + } + } + + private def getPropertyExample(schema: Schema[_]) = schema match { + case b: BooleanSchema => Option(b.getExample).getOrElse(true) + case d: DateSchema => Option(d.getExample).getOrElse { + APIUtil.DateWithDayFormat.format(new Date()) + } + case t: DateTimeSchema => Option(t.getExample).getOrElse { + APIUtil.DateWithSecondsFormat.format(new Date()) + } + case i: IntegerSchema => Option(i.getExample).getOrElse(1) + case n: NumberSchema => Option(n.getExample).getOrElse(1.2) + case s: StringSchema => Option(s.getExample).getOrElse("string") + case _ => throw new RuntimeException(s"Not support type $schema, please support it if necessary.") + } +} + +/** + * + * @param id DynamicEntity id value + * @param docsToUrl ResourceDoc to url that defined in swagger content + * @param serverUrl base url that defined in swagger content + */ +case class DynamicEndpointInfo(id: String, docsToUrl: mutable.Iterable[(ResourceDoc, String)], serverUrl: String) { + val resourceDocs: mutable.Iterable[ResourceDoc] = docsToUrl.map(_._1) + + private val existsUrlToMethod: mutable.Iterable[(HttpMethod, String, ResourceDoc)] = + docsToUrl + .map(it => { + val (doc, path) = it + (HttpMethod.valueOf(doc.requestVerb), path, doc) + }) + + def findDynamicEndpoint(newMethod: HttpMethod, newUrl: String): Option[(DynamicEndpointInfo, ResourceDoc, String)] = existsUrlToMethod.find(it => { + val (method, url, _) = it + isSameUrl(newUrl, url) && newMethod == method + }).map(it => (this, it._3, it._2)) + + def existsEndpoint(newMethod: HttpMethod, newUrl: String): Boolean = findDynamicEndpoint(newMethod, newUrl).isDefined + + def targetUrl(url: String): String = s"""$serverUrl$url""" + + /** + * check whether two url is the same: + * isSameUrl("/abc/efg", "/abc/efg") == true + * isSameUrl("/abc/efg", "/abc/{id}") == true + * isSameUrl("/abc/{userId}", "/abc/{id}") == true + * isSameUrl("/abc/{userId}/", "/abc/{id}") == true + * isSameUrl("/def/abc/", "/abc/{id}") == false + * @param pathX + * @param pathY + * @return + */ + private def isSameUrl(pathX: String, pathY: String) = { + val splitPathX = StringUtils.split(pathX, '/') + val splitPathY = StringUtils.split(pathY, '/') + if(splitPathX.size != splitPathY.size) { + false + } else { + splitPathX.zip(splitPathY).forall {kv => + val (partX, partY) = kv + partX == partY || + (partX.startsWith("{") && partX.endsWith("}")) || + (partY.startsWith("{") && partY.endsWith("}")) + } + } + } + +} \ No newline at end of file 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 deee1304f..bf3fa7cf2 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 @@ -32,7 +32,7 @@ object MockerConnector { def definitionsMap = NewStyle.function.getDynamicEntities().map(it => (it.entityName, DynamicEntityInfo(it.metadataJson, it.entityName))).toMap - def doc = { + def doc: ArrayBuffer[ResourceDoc] = { val docs: Seq[ResourceDoc] = definitionsMap.values.flatMap(createDocs).toSeq collection.mutable.ArrayBuffer(docs:_*) } diff --git a/obp-api/src/main/scala/code/api/v4_0_0/JSONFactory4.0.0.scala b/obp-api/src/main/scala/code/api/v4_0_0/JSONFactory4.0.0.scala index e6625deb5..a3598d1c2 100644 --- a/obp-api/src/main/scala/code/api/v4_0_0/JSONFactory4.0.0.scala +++ b/obp-api/src/main/scala/code/api/v4_0_0/JSONFactory4.0.0.scala @@ -36,18 +36,16 @@ import code.api.v1_2_1.{BankRoutingJsonV121, JSONFactory, UserJSONV121, ViewJSON import code.api.v1_4_0.JSONFactory1_4_0.TransactionRequestAccountJsonV140 import code.api.v2_0_0.TransactionRequestChargeJsonV200 import code.api.v2_1_0.ResourceUserJSON -import code.api.v2_2_0.ConsumerJson import code.api.v3_0_0.JSONFactory300.createAccountRoutingsJSON import code.api.v3_0_0.{CustomerAttributeResponseJsonV300, ViewBasicV300} import code.api.v3_1_0.AccountAttributeResponseJson import code.api.v3_1_0.JSONFactory310.createAccountAttributeJson import code.directdebit.DirectDebitTrait import code.entitlement.Entitlement -import code.model.{Consumer, ModeratedBankAccount, ModeratedBankAccountCore} +import code.model.{Consumer, ModeratedBankAccountCore} import code.standingorders.StandingOrderTrait import code.transactionChallenge.MappedExpectedChallengeAnswer import code.transactionrequests.TransactionRequests.TransactionChallengeTypes -import code.users.Users import com.openbankproject.commons.model._ import net.liftweb.common.{Box, Full} diff --git a/obp-api/src/main/scala/code/api/v4_0_0/OBPAPI4_0_0.scala b/obp-api/src/main/scala/code/api/v4_0_0/OBPAPI4_0_0.scala index 3e3983bfb..919c71345 100644 --- a/obp-api/src/main/scala/code/api/v4_0_0/OBPAPI4_0_0.scala +++ b/obp-api/src/main/scala/code/api/v4_0_0/OBPAPI4_0_0.scala @@ -55,7 +55,7 @@ object OBPAPI4_0_0 extends OBPRestHelper with APIMethods130 with APIMethods140 w // Possible Endpoints from 4.0.0, exclude one endpoint use - method,exclude multiple endpoints use -- method, // e.g getEndpoints(Implementations4_0_0) -- List(Implementations4_0_0.genericEndpoint, Implementations4_0_0.root) - val endpointsOf4_0_0 = getEndpoints(Implementations4_0_0) - Implementations4_0_0.genericEndpoint + val endpointsOf4_0_0 = getEndpoints(Implementations4_0_0) - Implementations4_0_0.genericEndpoint - Implementations4_0_0.dynamicEndpoint lazy val excludeEndpoints = nameOf(Implementations1_2_1.addPermissionForUserForBankAccountForMultipleViews) :: @@ -67,7 +67,7 @@ object OBPAPI4_0_0 extends OBPRestHelper with APIMethods130 with APIMethods140 w // if old version ResourceDoc objects have the same name endpoint with new version, omit old version ResourceDoc. def allResourceDocs = collectResourceDocs(OBPAPI3_1_0.allResourceDocs, Implementations4_0_0.resourceDocs, - MockerConnector.doc) + MockerConnector.doc, DynamicEndpointHelper.doc) .filterNot(it => it.partialFunctionName.matches(excludeEndpoints.mkString("|"))) //TODO exclude two endpoints, after training we need add logic to exclude endpoints @@ -85,6 +85,8 @@ object OBPAPI4_0_0 extends OBPRestHelper with APIMethods130 with APIMethods140 w registerRoutes(routes, allResourceDocs, apiPrefix, true) oauthServe(apiPrefix{Implementations4_0_0.genericEndpoint}, None) + oauthServe(apiPrefix{Implementations4_0_0.dynamicEndpoint}, None) + logger.info(s"version $version has been run! There are ${routes.length} routes.") // specified response for OPTIONS request. diff --git a/obp-api/src/main/scala/code/bankconnectors/Connector.scala b/obp-api/src/main/scala/code/bankconnectors/Connector.scala index 5d0efb57d..4e3070156 100644 --- a/obp-api/src/main/scala/code/bankconnectors/Connector.scala +++ b/obp-api/src/main/scala/code/bankconnectors/Connector.scala @@ -3,6 +3,7 @@ package code.bankconnectors import java.util.Date import java.util.UUID.randomUUID +import code.DynamicEndpoint.DynamicEndpointT import code.accountholders.{AccountHolders, MapperAccountHolders} import code.api.attributedefinition.AttributeDefinition import code.api.{APIFailure, APIFailureNewStyle} @@ -59,6 +60,7 @@ import scala.concurrent.duration._ import scala.math.{BigDecimal, BigInt} import scala.util.Random import scala.reflect.runtime.universe.{MethodSymbol, typeOf} +import _root_.akka.http.scaladsl.model.HttpMethod /* So we can switch between different sources of resources e.g. @@ -2181,6 +2183,9 @@ trait Connector extends MdcLoggable { requestBody: Option[JObject], entityId: Option[String], callContext: Option[CallContext]): OBPReturnType[Box[JValue]] = Future{(Failure(setUnimplementedError), callContext)} + + def dynamicEndpointProcess(url: String, jValue: JValue, method: HttpMethod, params: Map[String, List[String]], pathParams: Map[String, String], + callContext: Option[CallContext]): OBPReturnType[Box[JValue]] = Future{(Failure(setUnimplementedError), callContext)} def createDirectDebit(bankId: String, accountId: String, @@ -2210,4 +2215,16 @@ trait Connector extends MdcLoggable { def deleteCustomerAttribute(customerAttributeId: String, callContext: Option[CallContext]): OBPReturnType[Box[Boolean]] = Future{(Failure(setUnimplementedError), callContext)} + + def createDynamicEndpoint(swaggerString: String, callContext: Option[CallContext]): OBPReturnType[Box[DynamicEndpointT]] = Future { + (Failure(setUnimplementedError), callContext) + } + + def getDynamicEndpoint(dynamicEndpointId: String, callContext: Option[CallContext]): OBPReturnType[Box[DynamicEndpointT]] = Future { + (Failure(setUnimplementedError), callContext) + } + + def getDynamicEndpoints(callContext: Option[CallContext]): OBPReturnType[List[DynamicEndpointT]] = Future { + (List.empty[DynamicEndpointT], callContext) + } } diff --git a/obp-api/src/main/scala/code/bankconnectors/LocalMappedConnector.scala b/obp-api/src/main/scala/code/bankconnectors/LocalMappedConnector.scala index 9f1b75f43..c9f2ee271 100644 --- a/obp-api/src/main/scala/code/bankconnectors/LocalMappedConnector.scala +++ b/obp-api/src/main/scala/code/bankconnectors/LocalMappedConnector.scala @@ -5,6 +5,7 @@ import java.util.UUID.randomUUID import scala.concurrent.duration._ import code.DynamicData.DynamicDataProvider +import code.DynamicEndpoint.{DynamicEndpointProvider, DynamicEndpointT} import code.accountapplication.AccountApplicationX import code.accountattribute.AccountAttributeX import code.accountholders.{AccountHolders, MapperAccountHolders} @@ -78,7 +79,6 @@ import net.liftweb.mapper.{By, _} import net.liftweb.util.Helpers.{hours, now, time, tryo} import net.liftweb.util.Mailer import net.liftweb.util.Mailer.{From, PlainMailBodyType, Subject, To} -import org.apache.commons.lang3.StringUtils import org.mindrot.jbcrypt.BCrypt import scalacache.ScalaCache import scalacache.guava.GuavaCache @@ -91,6 +91,7 @@ import scala.language.postfixOps import scala.math.{BigDecimal, BigInt} import scala.util.Random +import _root_.akka.http.scaladsl.model.HttpMethod object LocalMappedConnector extends Connector with MdcLoggable { @@ -3158,6 +3159,13 @@ object LocalMappedConnector extends Connector with MdcLoggable { } } + /* delegate to rest connector + */ + override def dynamicEndpointProcess(url: String, jValue: JValue, method: HttpMethod, params: Map[String, List[String]], pathParams: Map[String, String], + callContext: Option[CallContext]): OBPReturnType[Box[JValue]] = { + Connector.getConnectorInstance("rest_vMar2019").dynamicEndpointProcess(url,jValue, method, params, pathParams, callContext) + } + override def createDirectDebit(bankId: String, accountId: String, customerId: String, @@ -4144,6 +4152,18 @@ object LocalMappedConnector extends Connector with MdcLoggable { } Full(res) } + + override def createDynamicEndpoint(swaggerString: String, callContext: Option[CallContext]): OBPReturnType[Box[DynamicEndpointT]] = Future { + (DynamicEndpointProvider.connectorMethodProvider.vend.create(swaggerString), callContext) + } + + override def getDynamicEndpoint(dynamicEndpointId: String, callContext: Option[CallContext]): OBPReturnType[Box[DynamicEndpointT]] = Future { + (DynamicEndpointProvider.connectorMethodProvider.vend.get(dynamicEndpointId), callContext) + } + + override def getDynamicEndpoints(callContext: Option[CallContext]): OBPReturnType[List[DynamicEndpointT]] = Future { + (DynamicEndpointProvider.connectorMethodProvider.vend.getAll(), callContext) + } override def deleteCustomerAttribute(customerAttributeId: String, callContext: Option[CallContext] ): OBPReturnType[Box[Boolean]] = { CustomerAttributeX.customerAttributeProvider.vend.deleteCustomerAttribute(customerAttributeId) map { ( _, callContext) } diff --git a/obp-api/src/main/scala/code/bankconnectors/package.scala b/obp-api/src/main/scala/code/bankconnectors/package.scala index 2e6ef4e22..20020c7fb 100644 --- a/obp-api/src/main/scala/code/bankconnectors/package.scala +++ b/obp-api/src/main/scala/code/bankconnectors/package.scala @@ -1,7 +1,9 @@ package code import java.lang.reflect.Method +import java.util.regex.Pattern +import akka.http.scaladsl.model.HttpMethod import code.api.{APIFailureNewStyle, ApiVersionHolder} import code.api.util.{CallContext, NewStyle} import code.methodrouting.{MethodRouting, MethodRoutingT} @@ -17,7 +19,7 @@ import scala.collection.mutable.ArrayBuffer import scala.reflect.runtime.universe.{MethodSymbol, Type, typeOf} import code.api.util.ErrorMessages.InvalidConnectorResponseForMissingRequiredValues import code.api.util.APIUtil.fullBoxOrException -import com.openbankproject.commons.util.ApiVersion +import com.openbankproject.commons.util.{ApiVersion, ReflectUtils} import com.openbankproject.commons.util.ReflectUtils._ import com.openbankproject.commons.util.Functions.Implicits._ import net.liftweb.util.ThreadGlobal @@ -97,6 +99,18 @@ package object bankconnectors extends MdcLoggable { NewStyle.function.getMethodRoutings(Some(methodName)) .find(_.parameters.exists(it => it.key == "entityName" && it.value == entityName)) } + case _ if methodName == "dynamicEndpointProcess" => { + val Array(url: String, _, method: HttpMethod, _*) = args + NewStyle.function.getMethodRoutings(Some(methodName)) + .find(routing => { + routing.parameters.exists(it => it.key == "http_method" && it.value.equalsIgnoreCase(method.value)) && + routing.parameters.exists(it => it.key == "url")&& + routing.parameters.exists( + it => it.key == "url_pattern" && + (it.value == url || Pattern.compile(it.value).matcher(url).matches()) + ) + }) + } case None => NewStyle.function.getMethodRoutings(Some(methodName), Some(false)) .find {routing => val bankIdPattern = routing.bankIdPattern @@ -123,7 +137,7 @@ package object bankconnectors extends MdcLoggable { case name => Connector.getConnectorInstance(name) } val methodSymbol = connector.implementedMethods(methodName).alternatives match { - case m::Nil if m.isInstanceOf[MethodSymbol] => m.asMethod + case m::Nil if m.isMethod => m.asMethod case _ => findMethodByArgs(connector, methodName, args:_*) .getOrElse(sys.error(s"not found matched method, method name: ${methodName}, params: ${args.mkString(",")}")) @@ -179,7 +193,8 @@ package object bankconnectors extends MdcLoggable { processObj match { case None => None - case Some(value) => { + + case Some(value) if ReflectUtils.isObpObject(value) => { val argNameToValues: Map[String, Any] = getConstructorArgs(value) //find from current object constructor args // orElse: if current object constructor args not found value, recursive search args @@ -192,6 +207,8 @@ package object bankconnectors extends MdcLoggable { .find(it => it.isDefined) } } + + case _ => None } } 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 8a4c0e8e2..e909bc495 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 @@ -61,8 +61,10 @@ import code.model.dataAccess.internalMapping.MappedAccountIdMappingProvider import code.util.{Helper, JsonUtils} import com.openbankproject.commons.model.enums.{AccountAttributeType, CardAttributeType, DynamicEntityOperation, ProductAttributeType} import com.openbankproject.commons.util.{ReflectUtils, RequiredFieldValidation} -import net.liftweb.json._ +import net.liftweb.json +import net.liftweb.json.{JValue, _} import net.liftweb.json.Extraction.decompose +import org.apache.commons.lang3.StringUtils trait RestConnector_vMar2019 extends Connector with KafkaHelper with MdcLoggable { //this one import is for implicit convert, don't delete @@ -9324,6 +9326,81 @@ trait RestConnector_vMar2019 extends Connector with KafkaHelper with MdcLoggable val result: OBPReturnType[Box[JValue]] = sendRequest[InBound](url, HttpMethods.POST, req, callContext).map(convertToTuple(callContext)) result } + + + override def dynamicEndpointProcess(url: String, jValue: JValue, method: HttpMethod, params: Map[String, List[String]], pathParams: Map[String, String], + callContext: Option[CallContext]): OBPReturnType[Box[JValue]] = { + val urlInMethodRouting: Option[String] = MethodRoutingHolder.methodRouting match { + case _: EmptyBox => None + case Full(routing) => routing.parameters.find(_.key == "url").map(_.value) + } + val pathVariableRex = """\{:(.+?)\}""".r + val targetUrl = urlInMethodRouting.map { urlInRouting => + val tuples: Iterator[(String, String)] = pathVariableRex.findAllMatchIn(urlInRouting).map{ regex => + val expression = regex.group(0) + val paramName = regex.group(1) + val paramValue = + if(paramName.startsWith("body.")) { + val path = StringUtils.substringAfter(paramName, "body.") + val value = JsonUtils.getValueByPath(jValue, path) + JsonUtils.toString(value) + } else { + pathParams.get(paramName) + .orElse(params.get(paramName).flatMap(_.headOption)).getOrElse(throw new RuntimeException(s"param $paramName not exists.")) + } + expression -> paramValue + } + + (urlInRouting /: tuples) {(pre, kv)=> + pre.replace(kv._1, kv._2) + } + }.getOrElse(url) + + val paramNameToValue = for { + (name, values) <- params + value <- values + param = s"$name=$value" + } yield param + + val paramUrl: String = + if(params.isEmpty){ + targetUrl + } else if(targetUrl.contains("?")) { + targetUrl + "&" + paramNameToValue.mkString("&") + } else { + targetUrl + "?" + paramNameToValue.mkString("&") + } + + val jsonToSend = if(jValue == JNothing) "" else compactRender(jValue) + val request = prepareHttpRequest(paramUrl, method, HttpProtocol("HTTP/1.1"), jsonToSend).withHeaders(callContext) + logger.debug(s"RestConnector_vMar2019 request is : $request") + val responseFuture = makeHttpRequest(request) + + val result: Future[(Box[JValue], Option[CallContext])] = responseFuture.map { + case response@HttpResponse(status, _, entity@_, _) => (status, entity) + }.flatMap { + case (status, entity) if status.isSuccess() => + this.extractBody(entity) + .map{ + case v if StringUtils.isBlank(v) => (Empty, callContext) + case v => (Full(json.parse(v)), callContext) + } + case (status, entity) => { + val future: Future[Box[Box[JValue]]] = extractBody(entity) map { msg => + tryo { + val failure: Box[JValue] = ParamFailure(msg, APIFailureNewStyle(msg, status.intValue())) + failure + } ~> APIFailureNewStyle(msg, status.intValue()) + } + future.map{ + case Full(v) => (v, callContext) + case e: EmptyBox => (e, callContext) + } + } + } + + result + } //In RestConnector, we use the headers to propagate the parameters to Adapter. The parameters come from the CallContext.outboundAdapterAuthInfo.userAuthContext diff --git a/obp-api/src/main/scala/code/dynamicEndpoint/DynamicEndpointProvider.scala b/obp-api/src/main/scala/code/dynamicEndpoint/DynamicEndpointProvider.scala new file mode 100644 index 000000000..f38c239e2 --- /dev/null +++ b/obp-api/src/main/scala/code/dynamicEndpoint/DynamicEndpointProvider.scala @@ -0,0 +1,34 @@ +package code.DynamicEndpoint + +import com.openbankproject.commons.model.{Converter, JsonFieldReName} +import net.liftweb.common.Box +import net.liftweb.util.SimpleInjector + +object DynamicEndpointProvider extends SimpleInjector { + + val connectorMethodProvider = new Inject(buildOne _) {} + + def buildOne: MappedDynamicEndpointProvider.type = MappedDynamicEndpointProvider +} + +trait DynamicEndpointT { + def dynamicEndpointId: Option[String] + def swaggerString: String +} + +case class DynamicEndpointCommons( + dynamicEndpointId: Option[String] = None, + swaggerString: String + ) extends DynamicEndpointT with JsonFieldReName + +object DynamicEndpointCommons extends Converter[DynamicEndpointT, DynamicEndpointCommons] + +case class DynamicEndpointSwagger(swaggerString: String, dynamicEndpointId: Option[String] = None) + +trait DynamicEndpointProvider { + def create(swaggerString: String): Box[DynamicEndpointT] + def update(dynamicEndpointId: String, swaggerString: String): Box[DynamicEndpointT] + def get(dynamicEndpointId: String): Box[DynamicEndpointT] + def getAll(): List[DynamicEndpointT] + def delete(dynamicEndpointId: String): Boolean +} \ No newline at end of file diff --git a/obp-api/src/main/scala/code/dynamicEndpoint/MapppedDynamicEndpointProvider.scala b/obp-api/src/main/scala/code/dynamicEndpoint/MapppedDynamicEndpointProvider.scala new file mode 100644 index 000000000..e7ccbd6e4 --- /dev/null +++ b/obp-api/src/main/scala/code/dynamicEndpoint/MapppedDynamicEndpointProvider.scala @@ -0,0 +1,40 @@ +package code.DynamicEndpoint + +import code.api.util.CustomJsonFormats +import code.util.MappedUUID +import net.liftweb.common.Box +import net.liftweb.mapper._ +import net.liftweb.util.Helpers.tryo + +object MappedDynamicEndpointProvider extends DynamicEndpointProvider with CustomJsonFormats{ + override def create(swaggerString: String): Box[DynamicEndpointT] = { + tryo{DynamicEndpoint.create.SwaggerString(swaggerString).saveMe()} + } + override def update(dynamicEndpointId: String, swaggerString: String): Box[DynamicEndpointT] = { + DynamicEndpoint.find(By(DynamicEndpoint.DynamicEndpointId, dynamicEndpointId)).map(_.SwaggerString(swaggerString).saveMe()) + } + + override def get(dynamicEndpointId: String): Box[DynamicEndpointT] = DynamicEndpoint.find(By(DynamicEndpoint.DynamicEndpointId, dynamicEndpointId)) + + override def getAll(): List[DynamicEndpointT] = DynamicEndpoint.findAll() + + override def delete(dynamicEndpointId: String): Boolean = DynamicEndpoint.bulkDelete_!!(By(DynamicEndpoint.DynamicEndpointId, dynamicEndpointId)) + +} + +class DynamicEndpoint extends DynamicEndpointT with LongKeyedMapper[DynamicEndpoint] with IdPK { + + override def getSingleton = DynamicEndpoint + + object DynamicEndpointId extends MappedUUID(this) + + object SwaggerString extends MappedText(this) + + override def dynamicEndpointId: Option[String] = Option(DynamicEndpointId.get) + override def swaggerString: String = SwaggerString.get +} + +object DynamicEndpoint extends DynamicEndpoint with LongKeyedMetaMapper[DynamicEndpoint] { + override def dbIndexes = UniqueIndex(DynamicEndpointId) :: super.dbIndexes +} + diff --git a/obp-api/src/main/scala/code/entitlement/Entilement.scala b/obp-api/src/main/scala/code/entitlement/Entilement.scala index 3fe8aa7dd..b67888ce7 100644 --- a/obp-api/src/main/scala/code/entitlement/Entilement.scala +++ b/obp-api/src/main/scala/code/entitlement/Entilement.scala @@ -32,6 +32,7 @@ trait EntitlementProvider { def getEntitlementsByRoleFuture(roleName: String) : Future[Box[List[Entitlement]]] def addEntitlement(bankId: String, userId: String, roleName: String) : Box[Entitlement] def deleteDynamicEntityEntitlement(entityName: String) : Box[Boolean] + def deleteEntitlements(entityNames: List[String]) : Box[Boolean] } trait Entitlement { @@ -54,6 +55,7 @@ class RemotedataEntitlementsCaseClasses { case class getEntitlementsByRoleFuture(roleName: String) case class addEntitlement(bankId: String, userId: String, roleName: String) case class deleteDynamicEntityEntitlement(entityName: String) + case class deleteEntitlements(entityNames: List[String]) } object RemotedataEntitlementsCaseClasses extends RemotedataEntitlementsCaseClasses \ No newline at end of file diff --git a/obp-api/src/main/scala/code/entitlement/MappedEntitlements.scala b/obp-api/src/main/scala/code/entitlement/MappedEntitlements.scala index abdd36726..c195ab7cd 100644 --- a/obp-api/src/main/scala/code/entitlement/MappedEntitlements.scala +++ b/obp-api/src/main/scala/code/entitlement/MappedEntitlements.scala @@ -94,8 +94,12 @@ object MappedEntitlementsProvider extends EntitlementProvider { override def deleteDynamicEntityEntitlement(entityName: String): Box[Boolean] = { val roleNames = DynamicEntityInfo.roleNames(entityName) + deleteEntitlements(roleNames) + } + + override def deleteEntitlements(entityNames: List[String]) : Box[Boolean] = { Box.tryo{ - MappedEntitlement.bulkDelete_!!(ByList(MappedEntitlement.mRoleName, roleNames)) + MappedEntitlement.bulkDelete_!!(ByList(MappedEntitlement.mRoleName, entityNames)) } } diff --git a/obp-api/src/main/scala/code/remotedata/RemotedataEntitlements.scala b/obp-api/src/main/scala/code/remotedata/RemotedataEntitlements.scala index a0508baf0..b1ad08809 100644 --- a/obp-api/src/main/scala/code/remotedata/RemotedataEntitlements.scala +++ b/obp-api/src/main/scala/code/remotedata/RemotedataEntitlements.scala @@ -56,4 +56,8 @@ object RemotedataEntitlements extends ObpActorInit with EntitlementProvider { (actor ? cc.deleteDynamicEntityEntitlement(entityName)).mapTo[Box[Boolean]] ) + override def deleteEntitlements(entityNames: List[String]) : Box[Boolean] = getValueFromFuture( + (actor ? cc.deleteEntitlements(entityNames)).mapTo[Box[Boolean]] + ) + } diff --git a/obp-api/src/main/scala/code/remotedata/RemotedataEntitlementsActor.scala b/obp-api/src/main/scala/code/remotedata/RemotedataEntitlementsActor.scala index e43e7ce14..97f8bb9bf 100644 --- a/obp-api/src/main/scala/code/remotedata/RemotedataEntitlementsActor.scala +++ b/obp-api/src/main/scala/code/remotedata/RemotedataEntitlementsActor.scala @@ -63,6 +63,10 @@ class RemotedataEntitlementsActor extends Actor with ObpActorHelper with MdcLogg logger.debug(s"deleteDynamicEntityEntitlement($entityName)") sender ! (mapper.deleteDynamicEntityEntitlement(entityName)) + case cc.deleteEntitlements(entityNames) => + logger.debug(s"deleteEntitlements($entityNames)") + sender ! (mapper.deleteEntitlements(entityNames)) + case message => logger.warn("[AKKA ACTOR ERROR - REQUEST NOT RECOGNIZED] " + message) } diff --git a/obp-api/src/main/scala/code/util/JsonUtils.scala b/obp-api/src/main/scala/code/util/JsonUtils.scala index 8a73c93ad..f865bbba8 100644 --- a/obp-api/src/main/scala/code/util/JsonUtils.scala +++ b/obp-api/src/main/scala/code/util/JsonUtils.scala @@ -163,6 +163,7 @@ object JsonUtils { } else { JField(newName, jObj) } + case _ => throw new RuntimeException(s"Not support json value type, value is: $jValue") } } @@ -420,7 +421,7 @@ object JsonUtils { * @param pathExpress path, can be prefix by - or !, e.g: "-result.count" "!value.isDeleted" * @return given nested field value */ - private def getValueByPath(jValue: JValue, pathExpress: String): JValue = { + def getValueByPath(jValue: JValue, pathExpress: String): JValue = { pathExpress match { case str if str.trim == "$root" || str.trim.isEmpty => jValue // if path is "$root" or "", return whole original json case RegexBoolean(b) => JBool(b.toBoolean) @@ -479,4 +480,14 @@ object JsonUtils { case v => v.values.toString == expectValue } } + + def toString(jValue: JValue) = jValue match{ + case JString(s) => s + case JInt(num) => num.toString() + case JDouble(num) => num.toString() + case JBool(b) => b.toString() + case JNothing => "" + case JNull => "null" + case v => json.compactRender(v) + } } 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 6cebabc96..4f4527e87 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 @@ -152,7 +152,7 @@ class DynamicEntityTest extends V400ServerSetup { feature("Add a DynamicEntity v4.0.4- Unauthorized access") { scenario("We will call the endpoint without user credentials", ApiEndpoint1, VersionOfApi) { When("We make a request v4.0.0") - val request400 = (v4_0_0_Request / "management" / "dynamic_entities").POST + val request400 = (v4_0_0_Request / "management" / "dynamic-entities").POST val response400 = makePostRequest(request400, write(rightEntity)) Then("We should get a 400") response400.code should equal(400) @@ -163,7 +163,7 @@ class DynamicEntityTest extends V400ServerSetup { feature("Update a DynamicEntity v4.0.4- Unauthorized access") { scenario("We will call the endpoint without user credentials", ApiEndpoint2, VersionOfApi) { When("We make a request v4.0.0") - val request400 = (v4_0_0_Request / "management" / "dynamic_entities"/ "some-method-routing-id").PUT + val request400 = (v4_0_0_Request / "management" / "dynamic-entities"/ "some-method-routing-id").PUT val response400 = makePutRequest(request400, write(rightEntity)) Then("We should get a 400") response400.code should equal(400) @@ -174,7 +174,7 @@ class DynamicEntityTest extends V400ServerSetup { feature("Get DynamicEntities v4.0.4- Unauthorized access") { scenario("We will call the endpoint without user credentials", ApiEndpoint3, VersionOfApi) { When("We make a request v4.0.0") - val request400 = (v4_0_0_Request / "management" / "dynamic_entities").GET + val request400 = (v4_0_0_Request / "management" / "dynamic-entities").GET val response400 = makeGetRequest(request400) Then("We should get a 400") response400.code should equal(400) @@ -185,7 +185,7 @@ class DynamicEntityTest extends V400ServerSetup { feature("Delete the DynamicEntity specified by METHOD_ROUTING_ID v4.0.4- Unauthorized access") { scenario("We will call the endpoint without user credentials", ApiEndpoint4, VersionOfApi) { When("We make a request v4.0.0") - val request400 = (v4_0_0_Request / "management" / "dynamic_entities" / "METHOD_ROUTING_ID").DELETE + val request400 = (v4_0_0_Request / "management" / "dynamic-entities" / "METHOD_ROUTING_ID").DELETE val response400 = makeDeleteRequest(request400) Then("We should get a 400") response400.code should equal(400) @@ -198,7 +198,7 @@ class DynamicEntityTest extends V400ServerSetup { feature("Add a DynamicEntity v4.0.4- Unauthorized access - Authorized access") { scenario("We will call the endpoint without the proper Role " + canCreateDynamicEntity, ApiEndpoint1, VersionOfApi) { When("We make a request v4.0.0 without a Role " + canCreateDynamicEntity) - val request400 = (v4_0_0_Request / "management" / "dynamic_entities").POST <@(user1) + val request400 = (v4_0_0_Request / "management" / "dynamic-entities").POST <@(user1) val response400 = makePostRequest(request400, write(rightEntity)) Then("We should get a 403") response400.code should equal(403) @@ -209,7 +209,7 @@ class DynamicEntityTest extends V400ServerSetup { scenario("We will call the endpoint with the proper Role " + canCreateDynamicEntity , ApiEndpoint1, ApiEndpoint2, ApiEndpoint3, ApiEndpoint4, VersionOfApi) { Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, CanCreateDynamicEntity.toString) When("We make a request v4.0.0") - val request = (v4_0_0_Request / "management" / "dynamic_entities").POST <@(user1) + val request = (v4_0_0_Request / "management" / "dynamic-entities").POST <@(user1) val response = makePostRequest(request, write(rightEntity)) Then("We should get a 201") response.code should equal(201) @@ -244,7 +244,7 @@ class DynamicEntityTest extends V400ServerSetup { { // update success - val request400 = (v4_0_0_Request / "management" / "dynamic_entities" / dynamicEntityId ).PUT <@(user1) + val request400 = (v4_0_0_Request / "management" / "dynamic-entities" / dynamicEntityId ).PUT <@(user1) val response400 = makePutRequest(request400, compactRender(updateRequest)) Then("We should get a 200") response400.code should equal(200) @@ -254,7 +254,7 @@ class DynamicEntityTest extends V400ServerSetup { { // update a not exists DynamicEntity - val request404 = (v4_0_0_Request / "management" / "dynamic_entities" / "not-exists-id" ).PUT <@(user1) + 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 404") response404.code should equal(404) @@ -263,7 +263,7 @@ class DynamicEntityTest extends V400ServerSetup { { // update a DynamicEntity with wrong required field name - val request400 = (v4_0_0_Request / "management" / "dynamic_entities" / dynamicEntityId ).PUT <@(user1) + val request400 = (v4_0_0_Request / "management" / "dynamic-entities" / dynamicEntityId ).PUT <@(user1) val response400 = makePutRequest(request400, compactRender(wrongRequiredEntity)) Then("We should get a 400") @@ -273,7 +273,7 @@ class DynamicEntityTest extends V400ServerSetup { { // update a DynamicEntity with wrong type of description - val request400 = (v4_0_0_Request / "management" / "dynamic_entities" / dynamicEntityId ).PUT <@(user1) + val request400 = (v4_0_0_Request / "management" / "dynamic-entities" / dynamicEntityId ).PUT <@(user1) val response400 = makePutRequest(request400, compactRender(wrongDescriptionEntity)) Then("We should get a 400") @@ -283,7 +283,7 @@ class DynamicEntityTest extends V400ServerSetup { { // update a DynamicEntity with wrong type of property description - val request400 = (v4_0_0_Request / "management" / "dynamic_entities" / dynamicEntityId ).PUT <@(user1) + val request400 = (v4_0_0_Request / "management" / "dynamic-entities" / dynamicEntityId ).PUT <@(user1) val response400 = makePutRequest(request400, compactRender(wrongPropertyDescriptionEntity)) Then("We should get a 400") @@ -293,7 +293,7 @@ class DynamicEntityTest extends V400ServerSetup { Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, CanGetDynamicEntities.toString) When("We make a request v4.0.0 with the Role " + canGetDynamicEntities) - val requestGet = (v4_0_0_Request / "management" / "dynamic_entities").GET <@(user1) + val requestGet = (v4_0_0_Request / "management" / "dynamic-entities").GET <@(user1) val responseGet = makeGetRequest(requestGet) Then("We should get a 200") responseGet.code should equal(200) @@ -308,7 +308,7 @@ class DynamicEntityTest extends V400ServerSetup { Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, CanDeleteDynamicEntity.toString) When("We make a request v4.0.0 with the Role " + canDeleteDynamicEntity) - val requestDelete400 = (v4_0_0_Request / "management" / "dynamic_entities" / dynamicEntityId).DELETE <@(user1) + val requestDelete400 = (v4_0_0_Request / "management" / "dynamic-entities" / dynamicEntityId).DELETE <@(user1) val responseDelete400 = makeDeleteRequest(requestDelete400) Then("We should get a 200") responseDelete400.code should equal(200) diff --git a/obp-api/src/test/scala/code/api/v4_0_0/DynamicendPointsTest.scala b/obp-api/src/test/scala/code/api/v4_0_0/DynamicendPointsTest.scala new file mode 100644 index 000000000..a6d9cc83f --- /dev/null +++ b/obp-api/src/test/scala/code/api/v4_0_0/DynamicendPointsTest.scala @@ -0,0 +1,284 @@ +package code.api.v4_0_0 + +import code.api.util.APIUtil.OAuth._ +import code.api.util.ApiRole._ +import code.api.util.ErrorMessages.{DynamicEndpointExists, UserHasMissingRoles, UserNotLoggedIn} +import code.api.util.ExampleValue +import code.api.v4_0_0.OBPAPI4_0_0.Implementations4_0_0 +import code.entitlement.Entitlement +import com.github.dwickern.macros.NameOf.nameOf +import com.openbankproject.commons.model.ErrorMessage +import com.openbankproject.commons.util.ApiVersion +import net.liftweb.json.JsonAST.JField +import net.liftweb.json.Serialization.write +import org.scalatest.Tag + +class DynamicEndpointsTest extends V400ServerSetup { + /** + * Test tags + * Example: To run tests with tag "getPermissions": + * mvn test -D tagsToInclude + * + * This is made possible by the scalatest maven plugin + */ + object VersionOfApi extends Tag(ApiVersion.v4_0_0.toString) + object ApiEndpoint1 extends Tag(nameOf(Implementations4_0_0.createDynamicEndpoint)) + object ApiEndpoint2 extends Tag(nameOf(Implementations4_0_0.getDynamicEndpoints)) + object ApiEndpoint3 extends Tag(nameOf(Implementations4_0_0.getDynamicEndpoint)) + object ApiEndpoint4 extends Tag(nameOf(Implementations4_0_0.deleteDynamicEndpoint)) + + + feature(s"test $ApiEndpoint1 version $VersionOfApi - Unauthorized access") { + scenario("We will call the endpoint without user credentials", ApiEndpoint1, VersionOfApi) { + val postDynamicEndpointRequestBodyExample = ExampleValue.dynamicEndpointRequestBodyExample + + When("We make a request v4.0.0") + val request400 = (v4_0_0_Request / "management" / "dynamic-endpoints").POST + val response400 = makePostRequest(request400, write(postDynamicEndpointRequestBodyExample)) + Then("We should get a 400") + response400.code should equal(400) + response400.body.extract[ErrorMessage].message should equal(UserNotLoggedIn) + } + } + + feature(s"test $ApiEndpoint1 version $VersionOfApi - authorized access- missing role") { + scenario("We will call the endpoint with user credentials", ApiEndpoint1, VersionOfApi) { + val postDynamicEndpointRequestBodyExample = ExampleValue.dynamicEndpointRequestBodyExample + + When("We make a request v4.0.0") + val request = (v4_0_0_Request / "management" / "dynamic-endpoints").POST<@ (user1) + val response = makePostRequest(request, write(postDynamicEndpointRequestBodyExample)) + Then("We should get a 403") + response.code should equal(403) + response.body.extract[ErrorMessage].message.toString contains (UserHasMissingRoles) should be (true) + } + } + + feature(s"test $ApiEndpoint1 version $VersionOfApi - authorized access - with role - should be success!") { + scenario("We will call the endpoint with user credentials", ApiEndpoint1, VersionOfApi) { + When("We make a request v4.0.0") + val postDynamicEndpointRequestBodyExample = ExampleValue.dynamicEndpointRequestBodyExample + + When("We make a request v4.0.0") + val request = (v4_0_0_Request / "management" / "dynamic-endpoints").POST<@ (user1) + val response = makePostRequest(request, write(postDynamicEndpointRequestBodyExample)) + Then("We should get a 403") + response.code should equal(403) + response.body.extract[ErrorMessage].message.toString contains (UserHasMissingRoles) should be (true) + + Then("We grant the role to the user1") + Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, canCreateDynamicEndpoint.toString) + + val responseWithRole = makePostRequest(request, write(postDynamicEndpointRequestBodyExample)) + Then("We should get a 201") + responseWithRole.code should equal(201) + responseWithRole.body.toString contains("dynamic_endpoint_id") should be (true) + responseWithRole.body.toString contains("swagger_string") should be (true) + responseWithRole.body.toString contains("Example Title") should be (true) + responseWithRole.body.toString contains("Example Description") should be (true) + responseWithRole.body.toString contains("Example Company") should be (true) + } + } + + feature(s"test $ApiEndpoint2 version $VersionOfApi - Unauthorized access") { + scenario("We will call the endpoint without user credentials", ApiEndpoint2, VersionOfApi) { + When("We make a request v4.0.0") + val request400 = (v4_0_0_Request / "management" / "dynamic-endpoints").GET + val response400 = makeGetRequest(request400) + Then("We should get a 400") + response400.code should equal(400) + response400.body.extract[ErrorMessage].message should equal(UserNotLoggedIn) + } + } + + feature(s"test $ApiEndpoint2 version $VersionOfApi - authorized access- missing role") { + scenario("We will call the endpoint with user credentials", ApiEndpoint2, VersionOfApi) { + When("We make a request v4.0.0") + val request = (v4_0_0_Request / "management" / "dynamic-endpoints").GET<@ (user1) + val response = makeGetRequest(request) + Then("We should get a 400") + response.code should equal(403) + response.body.extract[ErrorMessage].message.toString contains (UserHasMissingRoles) should be (true) + } + } + + feature(s"test $ApiEndpoint2 version $VersionOfApi - authorized access - with role - should be success!") { + scenario("We will call the endpoint with user credentials", ApiEndpoint2, VersionOfApi) { + When("We make a request v4.0.0") + val postDynamicEndpointRequestBodyExample = ExampleValue.dynamicEndpointRequestBodyExample + + When("We make a request v4.0.0") + val request = (v4_0_0_Request / "management" / "dynamic-endpoints").POST<@ (user1) + val response = makePostRequest(request, write(postDynamicEndpointRequestBodyExample)) + Then("We should get a 403") + response.code should equal(403) + response.body.extract[ErrorMessage].message.toString contains (UserHasMissingRoles) should be (true) + + Then("We grant the role to the user1") + Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, CanGetDynamicEndpoints.toString) + Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, CanCreateDynamicEndpoint.toString) + + + val newSwagger = postDynamicEndpointRequestBodyExample.transformField { + case JField(name, value) if name.startsWith("/") => JField(s"$name/abc", value) + } + + val responseWithRole = makePostRequest(request, write(newSwagger)) + Then("We should get a 201") + responseWithRole.code should equal(201) + + + val duplicatedRequest = makePostRequest(request, write(newSwagger)) + Then("We should get a 400") + duplicatedRequest.code should equal(400) + duplicatedRequest.body.extract[ErrorMessage].message.toString contains (DynamicEndpointExists) should be (true) + + + val request400 = (v4_0_0_Request / "management" / "dynamic-endpoints").GET<@ (user1) + val response400 = makeGetRequest(request400) + response400.code should be (200) + response400.body.toString contains("Example Title") should be (true) + response400.body.toString contains("Example Description") should be (true) + response400.body.toString contains("Example Company") should be (true) + + } + } + + feature(s"test $ApiEndpoint3 version $VersionOfApi - Unauthorized access") { + scenario("We will call the endpoint without user credentials", ApiEndpoint3, VersionOfApi) { + When("We make a request v4.0.0") + val request400 = (v4_0_0_Request / "management" / "dynamic-endpoints"/ "some-id").GET + val response400 = makeGetRequest(request400) + Then("We should get a 400") + response400.code should equal(400) + response400.body.extract[ErrorMessage].message should equal(UserNotLoggedIn) + } + } + + feature(s"test $ApiEndpoint3 version $VersionOfApi - authorized access- missing role") { + scenario("We will call the endpoint with user credentials", ApiEndpoint3, VersionOfApi) { + When("We make a request v4.0.0") + val request = (v4_0_0_Request / "management" / "dynamic-endpoints" /"some-id").GET<@ (user1) + val response = makeGetRequest(request) + Then("We should get a 400") + response.code should equal(403) + response.body.extract[ErrorMessage].message.toString contains (UserHasMissingRoles) should be (true) + } + } + + feature(s"test $ApiEndpoint3 version $VersionOfApi - authorized access - with role - should be success!") { + scenario("We will call the endpoint with user credentials", ApiEndpoint3, VersionOfApi) { + When("We make a request v4.0.0") + val postDynamicEndpointRequestBodyExample = ExampleValue.dynamicEndpointRequestBodyExample + + When("We make a request v4.0.0") + val request = (v4_0_0_Request / "management" / "dynamic-endpoints").POST<@ (user1) + val response = makePostRequest(request, write(postDynamicEndpointRequestBodyExample)) + Then("We should get a 403") + response.code should equal(403) + response.body.extract[ErrorMessage].message.toString contains (UserHasMissingRoles) should be (true) + + Then("We grant the role to the user1") + Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, CanGetDynamicEndpoint.toString) + Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, CanCreateDynamicEndpoint.toString) + + val newSwagger = postDynamicEndpointRequestBodyExample.transformField { + case JField(name, value) if name.startsWith("/") => JField(s"$name/def", value) + } + + val responseWithRole = makePostRequest(request, write(newSwagger)) + Then("We should get a 201") + responseWithRole.code should equal(201) + + + val duplicatedRequest = makePostRequest(request, write(newSwagger)) + Then("We should get a 400") + duplicatedRequest.code should equal(400) + duplicatedRequest.body.extract[ErrorMessage].message.toString contains (DynamicEndpointExists) should be (true) + + + val id = responseWithRole.body.\\("dynamic_endpoint_id").values.get("dynamic_endpoint_id").head.toString + + val request400 = (v4_0_0_Request / "management" / "dynamic-endpoints" /id).GET<@ (user1) + val response400 = makeGetRequest(request400) + response400.code should be (200) + response400.body.toString contains("dynamic_endpoint_id") should be (true) + response400.body.toString contains("swagger_string") should be (true) + response400.body.toString contains("Example Title") should be (true) + response400.body.toString contains("Example Description") should be (true) + response400.body.toString contains("Example Company") should be (true) + + } + } + + feature(s"test $ApiEndpoint4 version $VersionOfApi - Unauthorized access") { + scenario("We will call the endpoint without user credentials", ApiEndpoint4, VersionOfApi) { + When("We make a request v4.0.0") + val request400 = (v4_0_0_Request / "management" / "dynamic-endpoints"/ "some-id").DELETE + val response400 = makeDeleteRequest(request400) + Then("We should get a 400") + response400.code should equal(400) + response400.body.extract[ErrorMessage].message should equal(UserNotLoggedIn) + } + } + + feature(s"test $ApiEndpoint4 version $VersionOfApi - authorized access- missing role") { + scenario("We will call the endpoint with user credentials", ApiEndpoint4, VersionOfApi) { + When("We make a request v4.0.0") + val request = (v4_0_0_Request / "management" / "dynamic-endpoints" /"some-id").DELETE<@ (user1) + val response = makeDeleteRequest(request) + Then("We should get a 400") + response.code should equal(403) + response.body.extract[ErrorMessage].message.toString contains (UserHasMissingRoles) should be (true) + } + } + + feature(s"test $ApiEndpoint4 version $VersionOfApi - authorized access - with role - should be success!") { + scenario("We will call the endpoint with user credentials", ApiEndpoint4, VersionOfApi) { + When("We make a request v4.0.0") + val postDynamicEndpointRequestBodyExample = ExampleValue.dynamicEndpointRequestBodyExample + + When("We make a request v4.0.0") + val request = (v4_0_0_Request / "management" / "dynamic-endpoints").POST<@ (user1) + val response = makePostRequest(request, write(postDynamicEndpointRequestBodyExample)) + Then("We should get a 403") + response.code should equal(403) + response.body.extract[ErrorMessage].message.toString contains (UserHasMissingRoles) should be (true) + + Then("We grant the role to the user1") + Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, CanGetDynamicEndpoint.toString) + Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, CanCreateDynamicEndpoint.toString) + Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, CanDeleteDynamicEndpoint.toString) + + val newSwagger = postDynamicEndpointRequestBodyExample.transformField { + case JField(name, value) if name.startsWith("/") => JField(s"$name/def2", value) + } + + val responseWithRole = makePostRequest(request, write(newSwagger)) + Then("We should get a 201") + responseWithRole.code should equal(201) + + + val id = responseWithRole.body.\\("dynamic_endpoint_id").values.get("dynamic_endpoint_id").head.toString + + val request400 = (v4_0_0_Request / "management" / "dynamic-endpoints" /id).GET<@ (user1) + val response400 = makeGetRequest(request400) + response400.code should be (200) + response400.body.toString contains("dynamic_endpoint_id") should be (true) + response400.body.toString contains("swagger_string") should be (true) + response400.body.toString contains("Example Title") should be (true) + response400.body.toString contains("Example Description") should be (true) + response400.body.toString contains("Example Company") should be (true) + + + val requestDelete = (v4_0_0_Request / "management" / "dynamic-endpoints" /id).DELETE<@ (user1) + val responseDelete = makeDeleteRequest(requestDelete) + responseDelete.code should be (204) + + val responseGetAgain = makeGetRequest(request400) + responseGetAgain.code should be (404) + + + } + } +} diff --git a/obp-api/src/test/scala/code/connector/RestConnector_vMar2019_FrozenTest.scala b/obp-api/src/test/scala/code/connector/RestConnector_vMar2019_FrozenTest.scala index c8939ad98..e52490a45 100644 --- a/obp-api/src/test/scala/code/connector/RestConnector_vMar2019_FrozenTest.scala +++ b/obp-api/src/test/scala/code/connector/RestConnector_vMar2019_FrozenTest.scala @@ -6,6 +6,7 @@ import java.net.URI import code.bankconnectors.rest.RestConnector_vMar2019 import code.connector.RestConnector_vMar2019_FrozenUtil.{connectorMethodNames, persistFilePath, typeNameToFieldsInfo} import com.openbankproject.commons.util.ReflectUtils +import net.liftweb.common.Logger import org.apache.commons.io.IOUtils import org.scalatest.matchers.{MatchResult, Matcher} import org.scalatest.{BeforeAndAfter, FlatSpec, Matchers, Tag} @@ -19,14 +20,19 @@ import scala.reflect.runtime.universe._ class RestConnector_vMar2019_FrozenTest extends FlatSpec with Matchers with BeforeAndAfter { private var connectorMethodNamesPersisted: List[String] = _ private var typeNameToFieldsInfoPersisted: Map[String, Map[String, String]] = _ + private val logger = Logger(classOf[RestConnector_vMar2019_FrozenTest]) before { - val in = new ObjectInputStream(new FileInputStream(persistFilePath)) + var in: ObjectInputStream = null try { + in = new ObjectInputStream(new FileInputStream(persistFilePath)) in.readUTF() connectorMethodNamesPersisted = in.readObject().asInstanceOf[List[String]] typeNameToFieldsInfoPersisted = in.readObject().asInstanceOf[Map[String, Map[String, String]]] - } finally { + } catch { + case e: Throwable => + logger.error("read frozen file fail.", e) + }finally { IOUtils.closeQuietly(in) } } @@ -85,7 +91,7 @@ object RestConnector_vMar2019_FrozenUtil { .filter(_.overrides.nonEmpty) .filter(_.paramLists.flatten.nonEmpty) .map(_.name.toString) - .toList + .toList.filterNot(_ == "dynamicEndpointProcess") // typeNameToFieldsInfo sturcture is: (typeFullName, Map(fieldName->fieldTypeName)) val typeNameToFieldsInfo: Map[String, Map[String, String]] = {