mirror of
https://github.com/OpenBankProject/OBP-API.git
synced 2026-02-06 13:07:02 +00:00
Merge 1cfcbf3442 into c1e616750f
This commit is contained in:
commit
cc8f0e5e5d
@ -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
|
||||
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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 <<? List(("tags", "Consumer"))
|
||||
val responseGetOpenAPI = makeGetRequest(requestGetOpenAPI)
|
||||
responseGetOpenAPI.code should equal(200)
|
||||
}
|
||||
|
||||
scenario("OpenAPI JSON - unauthenticated rejected when resource_docs_requires_role is true", ApiEndpoint1, VersionOfApi) {
|
||||
setPropsValues(
|
||||
"resource_docs_requires_role" -> "true",
|
||||
)
|
||||
val requestGetOpenAPI = (ResourceDocsV6_0Request / "resource-docs" / "v6.0.0" / "openapi").GET <<? List(("tags", "Consumer"))
|
||||
val responseGetOpenAPI = makeGetRequest(requestGetOpenAPI)
|
||||
responseGetOpenAPI.code should equal(401)
|
||||
responseGetOpenAPI.body.toString should include(AuthenticatedUserIsRequired)
|
||||
}
|
||||
|
||||
scenario("OpenAPI YAML - raw response: public access when resource_docs_requires_role is false", ApiEndpoint1, VersionOfApi) {
|
||||
setPropsValues(
|
||||
"resource_docs_requires_role" -> "false",
|
||||
)
|
||||
val requestGetOpenAPIYAML = (ResourceDocsV6_0Request / "resource-docs" / "v6.0.0" / "openapi.yaml").GET <<? List(("tags", "Consumer"))
|
||||
val responseGetOpenAPIYAML = makeGetRequest(requestGetOpenAPIYAML)
|
||||
responseGetOpenAPIYAML.code should equal(200)
|
||||
// body should be non-empty YAML
|
||||
responseGetOpenAPIYAML.body.toString.trim.nonEmpty should be (true)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -186,16 +186,45 @@ trait SendServerRequests {
|
||||
// Check that every response has a correlationId at Response Header
|
||||
val list = response.getHeaders(ResponseHeader.`Correlation-Id`).asScala.toList
|
||||
list match {
|
||||
case Nil => 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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user