diff --git a/obp-api/src/main/resources/props/sample.props.template b/obp-api/src/main/resources/props/sample.props.template index c0f151d836..77a420868f 100644 --- a/obp-api/src/main/resources/props/sample.props.template +++ b/obp-api/src/main/resources/props/sample.props.template @@ -1047,6 +1047,8 @@ featured_apis=elasticSearchWarehouseV300 # rabbitmq_connector.username=obp # rabbitmq_connector.password=obp # rabbitmq_connector.virtual_host=/ +# rabbitmq_connector.request_queue=obp_rpc_queue +# rabbitmq_connector.response_queue_prefix=obp_reply_queue # -- RabbitMQ Adapter -------------------------------------------- #rabbitmq.adapter.enabled=false diff --git a/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/ResourceDocs140.scala b/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/ResourceDocs140.scala index bec0f2adeb..4f4e46793a 100644 --- a/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/ResourceDocs140.scala +++ b/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/ResourceDocs140.scala @@ -2,7 +2,7 @@ package code.api.ResourceDocs1_4_0 import scala.language.reflectiveCalls import code.api.Constant.HostName -import code.api.OBPRestHelper +import code.api.{OBPRestHelper, ResponseHeader} import code.api.cache.Caching import code.api.util.APIUtil._ import code.api.util.{APIUtil, ApiVersionUtils, YAMLUtils} @@ -236,7 +236,7 @@ object ResourceDocs300 extends OBPRestHelper with ResourceDocsAPIMethods with Md yamlResult } - val headers = List("Content-Type" -> YAMLUtils.getYAMLContentType) + val headers = List("Content-Type" -> YAMLUtils.getYAMLContentType, (ResponseHeader.`Correlation-Id` -> getCorrelationId())) val bytes = yamlString.getBytes("UTF-8") InMemoryResponse(bytes, headers, Nil, 200) } diff --git a/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/ResourceDocsAPIMethods.scala b/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/ResourceDocsAPIMethods.scala index 6488006040..26822fb7f8 100644 --- a/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/ResourceDocsAPIMethods.scala +++ b/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/ResourceDocsAPIMethods.scala @@ -677,6 +677,16 @@ trait ResourceDocsAPIMethods extends MdcLoggable with APIMethods220 with APIMeth implicit val ec = EndpointContext(Some(cc)) val (resourceDocTags, partialFunctions, locale, contentParam, apiCollectionIdParam) = ResourceDocsAPIMethodsUtil.getParams() for { + (u: Box[User], callContext: Option[CallContext]) <- if (resourceDocsRequireRole) { + authenticatedAccess(cc) + } else { + anonymousAccess(cc) + } + _ <- if (resourceDocsRequireRole) { + NewStyle.function.hasAtLeastOneEntitlement(failMsg = UserHasMissingRoles + canReadResourceDoc.toString)("", u.map(_.userId).getOrElse(""), ApiRole.canReadResourceDoc :: Nil, cc.callContext) + } else { + Future(()) + } requestedApiVersion <- NewStyle.function.tryons(s"$InvalidApiVersionString Current Version is $requestedApiVersionString", 400, cc.callContext) { ApiVersionUtils.valueOf(requestedApiVersionString) } @@ -871,6 +881,16 @@ trait ResourceDocsAPIMethods extends MdcLoggable with APIMethods220 with APIMeth } else { Future.successful(true) } + (u: Box[User], callContext: Option[CallContext]) <- if (resourceDocsRequireRole) { + authenticatedAccess(cc) + } else { + anonymousAccess(cc) + } + _ <- if (resourceDocsRequireRole) { + NewStyle.function.hasAtLeastOneEntitlement(failMsg = UserHasMissingRoles + canReadResourceDoc.toString)("", u.map(_.userId).getOrElse(""), ApiRole.canReadResourceDoc :: Nil, cc.callContext) + } else { + Future(()) + } requestedApiVersion <- NewStyle.function.tryons(s"$InvalidApiVersionString Current Version is $requestedApiVersionString", 400, cc.callContext) { ApiVersionUtils.valueOf(requestedApiVersionString) } 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 07a31d9291..5ce7fa0e24 100644 --- a/obp-api/src/main/scala/code/api/util/ApiRole.scala +++ b/obp-api/src/main/scala/code/api/util/ApiRole.scala @@ -459,9 +459,15 @@ object ApiRole extends MdcLoggable{ case class CanDeleteEntitlementRequestsAtAnyBank(requiresBankId: Boolean = false) extends ApiRole lazy val canDeleteEntitlementRequestsAtAnyBank = CanDeleteEntitlementRequestsAtAnyBank() + case class CanDeleteEntitlementRequestsAtOneBank(requiresBankId: Boolean = true) extends ApiRole + lazy val canDeleteEntitlementRequestsAtOneBank = CanDeleteEntitlementRequestsAtOneBank() + case class CanGetEntitlementRequestsAtAnyBank(requiresBankId: Boolean = false) extends ApiRole lazy val canGetEntitlementRequestsAtAnyBank = CanGetEntitlementRequestsAtAnyBank() + case class CanGetEntitlementRequestsAtOneBank(requiresBankId: Boolean = true) extends ApiRole + lazy val canGetEntitlementRequestsAtOneBank = CanGetEntitlementRequestsAtOneBank() + case class CanUseAccountFirehoseAtAnyBank(requiresBankId: Boolean = false) extends ApiRole lazy val canUseAccountFirehoseAtAnyBank = CanUseAccountFirehoseAtAnyBank() @@ -471,6 +477,9 @@ object ApiRole extends MdcLoggable{ case class CanUseCustomerFirehoseAtAnyBank(requiresBankId: Boolean = false) extends ApiRole lazy val canUseCustomerFirehoseAtAnyBank = CanUseCustomerFirehoseAtAnyBank() + case class CanUseCustomerFirehose(requiresBankId: Boolean = true) extends ApiRole + lazy val canUseCustomerFirehose = CanUseCustomerFirehose() + case class CanReadAggregateMetrics (requiresBankId: Boolean = false) extends ApiRole lazy val canReadAggregateMetrics = CanReadAggregateMetrics() @@ -483,6 +492,9 @@ object ApiRole extends MdcLoggable{ case class CanDeleteScopeAtAnyBank(requiresBankId: Boolean = false) extends ApiRole lazy val canDeleteScopeAtAnyBank = CanDeleteScopeAtAnyBank() + case class CanDeleteScopeAtOneBank(requiresBankId: Boolean = true) extends ApiRole + lazy val canDeleteScopeAtOneBank = CanDeleteScopeAtOneBank() + case class CanUnlockUser (requiresBankId: Boolean = false) extends ApiRole lazy val canUnlockUser = CanUnlockUser() @@ -889,9 +901,15 @@ object ApiRole extends MdcLoggable{ case class CanGetTransactionRequestAtAnyBank(requiresBankId: Boolean = false) extends ApiRole lazy val canGetTransactionRequestAtAnyBank = CanGetTransactionRequestAtAnyBank() + case class CanGetTransactionRequestAtOneBank(requiresBankId: Boolean = true) extends ApiRole + lazy val canGetTransactionRequestAtOneBank = CanGetTransactionRequestAtOneBank() + case class CanUpdateTransactionRequestStatusAtAnyBank(requiresBankId: Boolean = false) extends ApiRole lazy val canUpdateTransactionRequestStatusAtAnyBank = CanUpdateTransactionRequestStatusAtAnyBank() + case class CanUpdateTransactionRequestStatusAtOneBank(requiresBankId: Boolean = true) extends ApiRole + lazy val canUpdateTransactionRequestStatusAtOneBank = CanUpdateTransactionRequestStatusAtOneBank() + case class CanGetDoubleEntryTransactionAtOneBank(requiresBankId: Boolean = true) extends ApiRole lazy val canGetDoubleEntryTransactionAtOneBank = CanGetDoubleEntryTransactionAtOneBank() @@ -1159,6 +1177,9 @@ object ApiRole extends MdcLoggable{ case class CanGetAccountsMinimalForCustomerAtAnyBank(requiresBankId: Boolean = false) extends ApiRole lazy val canGetAccountsMinimalForCustomerAtAnyBank = CanGetAccountsMinimalForCustomerAtAnyBank() + case class CanGetAccountsMinimalForCustomerAtOneBank(requiresBankId: Boolean = true) extends ApiRole + lazy val canGetAccountsMinimalForCustomerAtOneBank = CanGetAccountsMinimalForCustomerAtOneBank() + case class CanUpdateConsentStatusAtOneBank(requiresBankId: Boolean = true) extends ApiRole lazy val canUpdateConsentStatusAtOneBank = CanUpdateConsentStatusAtOneBank() case class CanUpdateConsentStatusAtAnyBank(requiresBankId: Boolean = false) extends ApiRole diff --git a/obp-api/src/main/scala/code/api/v3_0_0/APIMethods300.scala b/obp-api/src/main/scala/code/api/v3_0_0/APIMethods300.scala index aa81d1f8aa..8e7693af3a 100644 --- a/obp-api/src/main/scala/code/api/v3_0_0/APIMethods300.scala +++ b/obp-api/src/main/scala/code/api/v3_0_0/APIMethods300.scala @@ -1922,7 +1922,7 @@ trait APIMethods300 { UnknownError ), List(apiTagRole, apiTagEntitlement, apiTagUser), - Some(List(canGetEntitlementRequestsAtAnyBank))) + Some(List(canGetEntitlementRequestsAtOneBank, canGetEntitlementRequestsAtAnyBank))) lazy val getAllEntitlementRequests : OBPEndpoint = { case "entitlement-requests" :: Nil JsonGet _ => { @@ -1961,7 +1961,7 @@ trait APIMethods300 { UnknownError ), List(apiTagRole, apiTagEntitlement, apiTagUser), - Some(List(canGetEntitlementRequestsAtAnyBank))) + Some(List(canGetEntitlementRequestsAtOneBank, canGetEntitlementRequestsAtAnyBank))) lazy val getEntitlementRequests : OBPEndpoint = { case "users" :: userId :: "entitlement-requests" :: Nil JsonGet _ => { @@ -2035,16 +2035,19 @@ trait APIMethods300 { UnknownError ), List(apiTagRole, apiTagEntitlement, apiTagUser), - Some(List(canDeleteEntitlementRequestsAtAnyBank))) + Some(List(canDeleteEntitlementRequestsAtOneBank, canDeleteEntitlementRequestsAtAnyBank))) lazy val deleteEntitlementRequest : OBPEndpoint = { case "entitlement-requests" :: entitlementRequestId :: Nil JsonDelete _ => { cc => implicit val ec = EndpointContext(Some(cc)) - val allowedEntitlements = canDeleteEntitlementRequestsAtAnyBank :: Nil + val allowedEntitlements = canDeleteEntitlementRequestsAtOneBank :: canDeleteEntitlementRequestsAtAnyBank :: Nil val allowedEntitlementsTxt = UserHasMissingRoles + allowedEntitlements.mkString(" or ") for { (Full(u), callContext) <- authenticatedAccess(cc) - _ <- NewStyle.function.hasAtLeastOneEntitlement(failMsg = allowedEntitlementsTxt)("", u.userId, allowedEntitlements, callContext) + entitlementRequest <- EntitlementRequest.entitlementRequest.vend.getEntitlementRequestFuture(entitlementRequestId) map { + connectorEmptyResponse(_, callContext) + } + _ <- NewStyle.function.hasAtLeastOneEntitlement(failMsg = allowedEntitlementsTxt)(entitlementRequest.bankId, u.userId, allowedEntitlements, callContext) deleteEntitlementRequest <- EntitlementRequest.entitlementRequest.vend.deleteEntitlementRequestFuture(entitlementRequestId) map { connectorEmptyResponse(_, callContext) } @@ -2349,7 +2352,8 @@ trait APIMethods300 { EmptyBody, EmptyBody, List(AuthenticatedUserIsRequired, EntitlementNotFound, UnknownError), - List(apiTagScope, apiTagConsumer)) + List(apiTagScope, apiTagConsumer), + Some(List(canDeleteScopeAtOneBank, canDeleteScopeAtAnyBank))) lazy val deleteScope: OBPEndpoint = { case "consumers" :: consumerId :: "scope" :: scopeId :: Nil JsonDelete _ => { @@ -2359,13 +2363,15 @@ trait APIMethods300 { consumer <- Future{callContext.get.consumer} map { x => unboxFullOrFail(x, callContext, InvalidConsumerCredentials) } - _ <- Future {NewStyle.function.hasEntitlementAndScope("", u.userId, consumer.id.get.toString, canDeleteScopeAtAnyBank, callContext)} map ( fullBoxOrException(_)) scope <- Future{ Scope.scope.vend.getScopeById(scopeId) ?~! ScopeNotFound } map { val msg = s"$ScopeNotFound Current Value is $scopeId" x => unboxFullOrFail(x, callContext, msg) } + _ <- Future {NewStyle.function.hasEntitlementAndScope(scope.bankId, u.userId, consumer.id.get.toString, canDeleteScopeAtOneBank, callContext)} map (fullBoxOrException(_)) recoverWith { + case _ => Future {NewStyle.function.hasEntitlementAndScope("", u.userId, consumer.id.get.toString, canDeleteScopeAtAnyBank, callContext)} map (fullBoxOrException(_)) + } _ <- Helper.booleanToFuture(failMsg = ConsumerDoesNotHaveScope, cc=callContext) { scope.scopeId ==scopeId } - _ <- Future {Scope.scope.vend.deleteScope(Full(scope))} + _ <- Future {Scope.scope.vend.deleteScope(Full(scope))} } yield (JsRaw(""), HttpCode.`200`(callContext)) } 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 e65e1079a9..29fba4cb91 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 @@ -394,7 +394,7 @@ trait APIMethods310 { customerJSONs, List(AuthenticatedUserIsRequired, CustomerFirehoseNotAllowedOnThisInstance, UserHasMissingRoles, UnknownError), List(apiTagCustomer, apiTagFirehoseData), - Some(List(canUseCustomerFirehoseAtAnyBank))) + Some(List(ApiRole.canUseCustomerFirehose, canUseCustomerFirehoseAtAnyBank))) lazy val getFirehoseCustomers : OBPEndpoint = { //get private accounts for all banks @@ -405,7 +405,7 @@ trait APIMethods310 { _ <- Helper.booleanToFuture(failMsg = AccountFirehoseNotAllowedOnThisInstance , cc=callContext) { allowCustomerFirehose } - _ <- NewStyle.function.hasEntitlement("", u.userId, ApiRole.canUseCustomerFirehoseAtAnyBank, callContext) + _ <- NewStyle.function.hasAtLeastOneEntitlement(bankId.value, u.userId, ApiRole.canUseCustomerFirehose :: canUseCustomerFirehoseAtAnyBank :: Nil, callContext) (_, callContext) <- NewStyle.function.getBank(bankId, callContext) allowedParams = List("sort_direction", "limit", "offset", "from_date", "to_date") httpParams <- NewStyle.function.extractHttpParamsFromUrl(cc.url) 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 efd26036ac..ed8d8742a9 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 @@ -10063,7 +10063,7 @@ trait APIMethods400 extends MdcLoggable { UnknownError ), List(apiTagAccount), - Some(List(canGetAccountsMinimalForCustomerAtAnyBank)) + Some(List(canGetAccountsMinimalForCustomerAtOneBank, canGetAccountsMinimalForCustomerAtAnyBank)) ) lazy val getAccountsMinimalByCustomerId: OBPEndpoint = { @@ -10071,10 +10071,12 @@ trait APIMethods400 extends MdcLoggable { cc => implicit val ec = EndpointContext(Some(cc)) for { - (_, callContext) <- getCustomerByCustomerId( + (Full(u), callContext) <- authenticatedAccess(cc) + (customer, callContext) <- getCustomerByCustomerId( customerId, - cc.callContext + callContext ) + _ <- NewStyle.function.hasAtLeastOneEntitlement(customer.bankId, u.userId, canGetAccountsMinimalForCustomerAtOneBank :: canGetAccountsMinimalForCustomerAtAnyBank :: Nil, callContext) (userCustomerLinks, callContext) <- getUserCustomerLinks( customerId, callContext diff --git a/obp-api/src/main/scala/code/api/v5_1_0/APIMethods510.scala b/obp-api/src/main/scala/code/api/v5_1_0/APIMethods510.scala index 3ce13dbf07..c3264073a6 100644 --- a/obp-api/src/main/scala/code/api/v5_1_0/APIMethods510.scala +++ b/obp-api/src/main/scala/code/api/v5_1_0/APIMethods510.scala @@ -4259,7 +4259,7 @@ trait APIMethods510 { UnknownError ), List(apiTagTransactionRequest, apiTagPSD2PIS, apiTagPsd2), - Some(List(canGetTransactionRequestAtAnyBank)) + Some(List(canGetTransactionRequestAtOneBank, canGetTransactionRequestAtAnyBank)) ) lazy val getTransactionRequestById: OBPEndpoint = { @@ -4267,7 +4267,9 @@ trait APIMethods510 { cc => implicit val ec = EndpointContext(Some(cc)) for { - (transactionRequest, callContext) <- NewStyle.function.getTransactionRequestImpl(requestId, cc.callContext) + (Full(u), callContext) <- authenticatedAccess(cc) + (transactionRequest, callContext) <- NewStyle.function.getTransactionRequestImpl(requestId, callContext) + _ <- NewStyle.function.hasAtLeastOneEntitlement(transactionRequest.from.bank_id, u.userId, canGetTransactionRequestAtOneBank :: canGetTransactionRequestAtAnyBank :: Nil, callContext) } yield { val json = JSONFactory210.createTransactionRequestWithChargeJSON(transactionRequest) (json, HttpCode.`200`(callContext)) @@ -4377,7 +4379,7 @@ trait APIMethods510 { UnknownError ), List(apiTagTransactionRequest), - Some(List(canUpdateTransactionRequestStatusAtAnyBank)) + Some(List(canUpdateTransactionRequestStatusAtOneBank, canUpdateTransactionRequestStatusAtAnyBank)) ) lazy val updateTransactionRequestStatus : OBPEndpoint = { @@ -4386,11 +4388,14 @@ trait APIMethods510 { implicit val ec = EndpointContext(Some(cc)) val failMsg = s"$InvalidJsonFormat The Json body should be the $PostTransactionRequestStatusJsonV510" for { - postedData <- NewStyle.function.tryons(failMsg, 400, cc.callContext) { + (Full(u), callContext) <- authenticatedAccess(cc) + postedData <- NewStyle.function.tryons(failMsg, 400, callContext) { json.extract[PostTransactionRequestStatusJsonV510] } - _ <- NewStyle.function.saveTransactionRequestStatusImpl(transactionRequestId, postedData.status, cc.callContext) - (transactionRequest, callContext) <- NewStyle.function.getTransactionRequestImpl(transactionRequestId, cc.callContext) + (existingTransactionRequest, callContext) <- NewStyle.function.getTransactionRequestImpl(transactionRequestId, callContext) + _ <- NewStyle.function.hasAtLeastOneEntitlement(existingTransactionRequest.from.bank_id, u.userId, canUpdateTransactionRequestStatusAtOneBank :: canUpdateTransactionRequestStatusAtAnyBank :: Nil, callContext) + _ <- NewStyle.function.saveTransactionRequestStatusImpl(transactionRequestId, postedData.status, callContext) + (transactionRequest, callContext) <- NewStyle.function.getTransactionRequestImpl(transactionRequestId, callContext) } yield { (TransactionRequestStatusJsonV510(transactionRequest.id.value, transactionRequest.status), HttpCode.`200`(callContext)) } diff --git a/obp-api/src/main/scala/code/api/v6_0_0/APIMethods600.scala b/obp-api/src/main/scala/code/api/v6_0_0/APIMethods600.scala index 0b5bf92604..6ff7de422d 100644 --- a/obp-api/src/main/scala/code/api/v6_0_0/APIMethods600.scala +++ b/obp-api/src/main/scala/code/api/v6_0_0/APIMethods600.scala @@ -3457,6 +3457,7 @@ trait APIMethods600 { cc => implicit val ec = EndpointContext(Some(cc)) for { (Full(u), callContext) <- authenticatedAccess(cc) + _ <- NewStyle.function.hasEntitlement("", u.userId, ApiRole.canGetRolesWithEntitlementCountsAtAllBanks, callContext) // Get all available roles allRoles = ApiRole.availableRoles.sorted diff --git a/obp-api/src/main/scala/code/bankconnectors/rabbitmq/RabbitMQUtils.scala b/obp-api/src/main/scala/code/bankconnectors/rabbitmq/RabbitMQUtils.scala index 52d0b1975e..d321379910 100644 --- a/obp-api/src/main/scala/code/bankconnectors/rabbitmq/RabbitMQUtils.scala +++ b/obp-api/src/main/scala/code/bankconnectors/rabbitmq/RabbitMQUtils.scala @@ -53,8 +53,8 @@ object RabbitMQUtils extends MdcLoggable{ private implicit val formats = code.api.util.CustomJsonFormats.nullTolerateFormats - val RPC_QUEUE_NAME: String = "obp_rpc_queue" - val RPC_REPLY_TO_QUEUE_NAME_PREFIX: String = "obp_reply_queue" + val RPC_QUEUE_NAME: String = APIUtil.getPropsValue("rabbitmq_connector.request_queue", "obp_rpc_queue") + val RPC_REPLY_TO_QUEUE_NAME_PREFIX: String = APIUtil.getPropsValue("rabbitmq_connector.response_queue_prefix", "obp_reply_queue") class ResponseCallback(val rabbitCorrelationId: String, channel: Channel) extends DeliverCallback { @@ -92,14 +92,30 @@ object RabbitMQUtils extends MdcLoggable{ val rabbitRequestJsonString: String = write(outBound) // convert OutBound to json string val connection = RabbitMQConnectionPool.borrowConnection() + // Check if queue already exists using a temporary channel (passive declare closes channel on failure) + val queueExists = try { + val tempChannel = connection.createChannel() + try { + tempChannel.queueDeclarePassive(RPC_QUEUE_NAME) + true + } finally { + if (tempChannel.isOpen) tempChannel.close() + } + } catch { + case _: java.io.IOException => false + } + val channel = connection.createChannel() // channel is not thread safe, so we always create new channel for each message. - channel.queueDeclare( - RPC_QUEUE_NAME, // Queue name - true, // durable: non-persis, here set durable = true - false, // exclusive: non-excl4, here set exclusive = false - false, // autoDelete: delete, here set autoDelete = false - rpcQueueArgs // extra arguments, - ) + // Only declare queue if it doesn't already exist (avoids argument conflicts with external adapters) + if (!queueExists) { + channel.queueDeclare( + RPC_QUEUE_NAME, // Queue name + true, // durable: non-persis, here set durable = true + false, // exclusive: non-excl4, here set exclusive = false + false, // autoDelete: delete, here set autoDelete = false + rpcQueueArgs // extra arguments, + ) + } val replyQueueName:String = channel.queueDeclare( s"${RPC_REPLY_TO_QUEUE_NAME_PREFIX}_${messageId.replace("obp_","")}_${UUID.randomUUID.toString}", // Queue name, it will be a unique name for each queue @@ -112,6 +128,7 @@ object RabbitMQUtils extends MdcLoggable{ val rabbitResponseJsonFuture = { try { logger.debug(s"${RabbitMQConnector_vOct2024.toString} outBoundJson: $messageId = $rabbitRequestJsonString") + logger.info(s"[RabbitMQ] Sending message to queue: $RPC_QUEUE_NAME, messageId: $messageId, replyTo: $replyQueueName") val rabbitMQCorrelationId = UUID.randomUUID().toString val rabbitMQProps = new BasicProperties.Builder() @@ -121,6 +138,7 @@ object RabbitMQUtils extends MdcLoggable{ .replyTo(replyQueueName) .build() channel.basicPublish("", RPC_QUEUE_NAME, rabbitMQProps, rabbitRequestJsonString.getBytes("UTF-8")) + logger.info(s"[RabbitMQ] Message published, correlationId: $rabbitMQCorrelationId, waiting for response on: $replyQueueName") val responseCallback = new ResponseCallback(rabbitMQCorrelationId, channel) channel.basicConsume(replyQueueName, true, responseCallback, cancelCallback) diff --git a/obp-api/src/test/scala/code/api/ResourceDocs1_4_0/SwaggerDocsTest.scala b/obp-api/src/test/scala/code/api/ResourceDocs1_4_0/SwaggerDocsTest.scala index b96a1acf37..ee3326acec 100644 --- a/obp-api/src/test/scala/code/api/ResourceDocs1_4_0/SwaggerDocsTest.scala +++ b/obp-api/src/test/scala/code/api/ResourceDocs1_4_0/SwaggerDocsTest.scala @@ -1,9 +1,12 @@ package code.api.ResourceDocs1_4_0 import code.api.ResourceDocs1_4_0.ResourceDocs140.ImplementationsResourceDocs +import code.api.util.ErrorMessages.{AuthenticatedUserIsRequired, UserHasMissingRoles} import code.api.util.{ApiRole, CustomJsonFormats} import code.setup.{DefaultUsers, PropsReset} import com.github.dwickern.macros.NameOf.nameOf +import code.api.util.APIUtil.OAuth._ +import code.entitlement.Entitlement import com.openbankproject.commons.util.{ApiVersion, Functions} import io.swagger.parser.OpenAPIParser import net.liftweb.json @@ -256,4 +259,83 @@ class SwaggerDocsTest extends ResourceDocsV140ServerSetup with PropsReset with D (errors, warnings, allMessages) } -} + + // Additional tests to verify that the Swagger/OpenAPI endpoints respect the resource_docs_requires_role prop. + // These are minimal checks that mirror the behaviour validated elsewhere (Lift/http4s tests). + feature(s"Swagger & OpenAPI access control for resource_docs_requires_role") { + scenario("Swagger - public access when resource_docs_requires_role is false", ApiEndpoint1, VersionOfApi) { + setPropsValues( + "resource_docs_requires_role" -> "false", + ) + val requestGetSwagger = (ResourceDocsV5_1Request / "resource-docs" / "v5.1.0" / "swagger").GET + val responseGetSwagger = makeGetRequest(requestGetSwagger) + responseGetSwagger.code should equal(200) + } + + scenario("Swagger - unauthenticated rejected when resource_docs_requires_role is true", ApiEndpoint1, VersionOfApi) { + setPropsValues( + "resource_docs_requires_role" -> "true", + ) + val requestGetSwagger = (ResourceDocsV5_1Request / "resource-docs" / "v5.1.0" / "swagger").GET + val responseGetSwagger = makeGetRequest(requestGetSwagger) + // Lift endpoints typically return 401 with AuthenticatedUserIsRequired message when auth required + responseGetSwagger.code should equal(401) + responseGetSwagger.body.toString should include(AuthenticatedUserIsRequired) + } + + scenario("Swagger - authenticated but missing role gets 403", ApiEndpoint1, VersionOfApi) { + setPropsValues( + "resource_docs_requires_role" -> "true", + ) + val requestGetSwagger = (ResourceDocsV5_1Request / "resource-docs" / "v5.1.0" / "swagger").GET <@ (user1) + val responseGetSwagger = makeGetRequest(requestGetSwagger) + responseGetSwagger.code should equal(403) + responseGetSwagger.body.toString should include(UserHasMissingRoles) + responseGetSwagger.body.toString should include(ApiRole.canReadResourceDoc.toString()) + } + + scenario("Swagger - authenticated and entitled canReadResourceDoc returns 200", ApiEndpoint1, VersionOfApi) { + setPropsValues( + "resource_docs_requires_role" -> "true", + ) + // grant the entitlement to the resource user used in tests + Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, ApiRole.canReadResourceDoc.toString) + val requestGetSwagger = (ResourceDocsV5_1Request / "resource-docs" / "v5.1.0" / "swagger").GET <@ (user1) + val responseGetSwagger = makeGetRequest(requestGetSwagger) + responseGetSwagger.code should equal(200) + } + + // OpenAPI JSON checks (v6.0.0 used elsewhere for OpenAPI tests) + scenario("OpenAPI JSON - public access when resource_docs_requires_role is false", ApiEndpoint1, VersionOfApi) { + setPropsValues( + "resource_docs_requires_role" -> "false", + ) + val requestGetOpenAPI = (ResourceDocsV6_0Request / "resource-docs" / "v6.0.0" / "openapi").GET < "true", + ) + val requestGetOpenAPI = (ResourceDocsV6_0Request / "resource-docs" / "v6.0.0" / "openapi").GET < "false", + ) + val requestGetOpenAPIYAML = (ResourceDocsV6_0Request / "resource-docs" / "v6.0.0" / "openapi.yaml").GET < throw new Exception(s"There is no ${ResponseHeader.`Correlation-Id`} in response header. Couldn't parse response from ${req.url} : $body") + case Nil => + // Improve diagnostic information: include HTTP status, all response headers and a snippet of the body. + val status = response.getStatusCode + val headersStr = try { + // response.getHeaders().entries() returns a Java collection of header entries + response.getHeaders().entries().asScala.map(h => s"${h.getKey}: ${h.getValue}").mkString(", ") + } catch { + case _: Throwable => "unable to read headers" + } + val bodySnippet = if (body == null) { + "" + } else { + val maxLen = 1000 + if (body.length > maxLen) body.take(maxLen) + "..." else body + } + throw new Exception( + s"""There is no ${ResponseHeader.`Correlation-Id`} in response header. + |Couldn't parse response from ${req.url} + |status=$status + |headers=[$headersStr] + |body-snippet=${bodySnippet}""".stripMargin + ) case _ => } - val parsedBody = tryo { - parse(body) - } - parsedBody match { - case Full(b) => APIResponse(response.getStatusCode, b, Some(response.getHeaders())) - case _ => throw new Exception(s"couldn't parse response from ${req.url} : $body") + // Handle YAML responses: don't try to parse as JSON. Wrap YAML as a JString so tests + // that expect a JValue can still receive the body. + val contentTypeList = response.getHeaders("Content-Type").asScala.toList.map(_.toLowerCase) + val isYaml = contentTypeList.exists(_.contains("yaml")) + if (isYaml) { + APIResponse(response.getStatusCode, JString(body), Some(response.getHeaders())) + } else { + val parsedBody = tryo { + parse(body) + } + parsedBody match { + case Full(b) => APIResponse(response.getStatusCode, b, Some(response.getHeaders())) + case _ => throw new Exception(s"couldn't parse response from ${req.url} : $body") + } } } }