mirror of
https://github.com/OpenBankProject/OBP-API.git
synced 2026-02-06 11:06:49 +00:00
refactor(http4s): enhance CallContext extraction and validation chain
- Add withCallContext helper method to Http4sSupport for simplified endpoint code - Document use of http4s Vault for type-safe request attributes storage - Clarify that validated entities (bank, bankAccount, view, counterparty) are stored within CallContext - Reorder validation chain in ResourceDocMiddleware to check roles before entity validation - Add special handling for resource-docs endpoint with configurable role requirement - Extract runValidationChain method to support both middleware and endpoint wrapping patterns - Improve authentication error handling with better Box pattern matching - Add comprehensive documentation and usage examples for CallContext extraction - Enhance logging for validation chain execution and debugging
This commit is contained in:
parent
e8999ba54c
commit
dbd046bf7c
@ -34,8 +34,12 @@ import scala.language.higherKinds
|
||||
/**
|
||||
* Request attribute keys for storing CallContext in http4s requests.
|
||||
*
|
||||
* Note: Uses http4s Vault (org.typelevel.vault.Key) for type-safe request attributes.
|
||||
* Validated entities (bank, bankAccount, view, counterparty) are stored within CallContext itself.
|
||||
*/
|
||||
object Http4sRequestAttributes {
|
||||
import org.typelevel.vault.Key
|
||||
|
||||
// CallContext contains all request data and validated entities
|
||||
val callContextKey: Key[CallContext] =
|
||||
Key.newKey[IO, CallContext].unsafeRunSync()(cats.effect.unsafe.IORuntime.global)
|
||||
@ -46,6 +50,33 @@ object Http4sRequestAttributes {
|
||||
*/
|
||||
def getCallContext(req: Request[IO]): Option[CallContext] =
|
||||
req.attributes.lookup(callContextKey)
|
||||
|
||||
/**
|
||||
* Helper method to extract CallContext from http4s Request and execute business logic.
|
||||
* Simplifies endpoint code by handling the common pattern of extracting CallContext.
|
||||
*
|
||||
* Usage example:
|
||||
* {{{
|
||||
* val myEndpoint: HttpRoutes[IO] = HttpRoutes.of[IO] {
|
||||
* case req @ GET -> Root / "banks" =>
|
||||
* withCallContext(req) { cc =>
|
||||
* for {
|
||||
* result <- yourBusinessLogic(cc)
|
||||
* response <- Ok(result)
|
||||
* } yield response
|
||||
* }
|
||||
* }
|
||||
* }}}
|
||||
*
|
||||
* @param req The http4s request
|
||||
* @param f Function that takes CallContext and returns IO[Response]
|
||||
* @return IO[Response[IO]]
|
||||
*/
|
||||
def withCallContext(req: Request[IO])(f: CallContext => IO[Response[IO]]): IO[Response[IO]] = {
|
||||
IO.fromOption(req.attributes.lookup(callContextKey))(
|
||||
new RuntimeException("CallContext not found in request attributes")
|
||||
).flatMap(f)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -23,10 +23,10 @@ import scala.language.higherKinds
|
||||
*
|
||||
* VALIDATION ORDER:
|
||||
* 1. Authentication first
|
||||
* 2. BANK_ID validation (if present in path)
|
||||
* 3. ACCOUNT_ID validation (if present in path)
|
||||
* 4. VIEW_ID validation (if present in path)
|
||||
* 5. Role authorization (if roles specified in ResourceDoc)
|
||||
* 2. Roles authorization (if roles specified in ResourceDoc)
|
||||
* 3. BANK_ID validation (if present in path)
|
||||
* 4. ACCOUNT_ID validation (if present in path)
|
||||
* 5. VIEW_ID validation (if present in path)
|
||||
* 6. COUNTERPARTY_ID validation (if present in path)
|
||||
*/
|
||||
object ResourceDocMiddleware extends MdcLoggable{
|
||||
@ -36,15 +36,20 @@ object ResourceDocMiddleware extends MdcLoggable{
|
||||
private val jsonContentType: `Content-Type` = `Content-Type`(MediaType.application.json)
|
||||
|
||||
/**
|
||||
* Check if ResourceDoc requires authentication based on errorResponseBodies
|
||||
* Check if ResourceDoc requires authentication based on errorResponseBodies or property
|
||||
*/
|
||||
private def needsAuthentication(resourceDoc: ResourceDoc): Boolean = {
|
||||
// Roles always require an authenticated user to validate entitlements
|
||||
resourceDoc.errorResponseBodies.contains($AuthenticatedUserIsRequired) || resourceDoc.roles.exists(_.nonEmpty)
|
||||
// Special handling for resource-docs endpoint
|
||||
if (resourceDoc.partialFunctionName == "getResourceDocsObpV700") {
|
||||
APIUtil.getPropsAsBoolValue("resource_docs_requires_role", false)
|
||||
} else {
|
||||
// Standard check: roles always require an authenticated user to validate entitlements
|
||||
resourceDoc.errorResponseBodies.contains($AuthenticatedUserIsRequired) || resourceDoc.roles.exists(_.nonEmpty)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create middleware that applies ResourceDoc-driven validation
|
||||
* Create middleware that applies ResourceDoc-driven validation to standard HttpRoutes
|
||||
*/
|
||||
def apply(resourceDocs: ArrayBuffer[ResourceDoc]): Middleware[IO] = { routes =>
|
||||
Kleisli[HttpF, Request[IO], Response[IO]] { req =>
|
||||
@ -67,7 +72,7 @@ object ResourceDocMiddleware extends MdcLoggable{
|
||||
case Some(resourceDoc) =>
|
||||
val ccWithDoc = ResourceDocMatcher.attachToCallContext(cc, resourceDoc)
|
||||
val pathParams = ResourceDocMatcher.extractPathParams(req.uri.path, resourceDoc)
|
||||
runValidationChain(req, resourceDoc, ccWithDoc, pathParams, routes)
|
||||
runValidationChainForRoutes(req, resourceDoc, ccWithDoc, pathParams, routes)
|
||||
.map(ensureJsonContentType)
|
||||
case None =>
|
||||
routes.run(req).getOrElseF(IO.pure(Response[IO](org.http4s.Status.NotFound)))
|
||||
@ -83,9 +88,181 @@ object ResourceDocMiddleware extends MdcLoggable{
|
||||
}
|
||||
|
||||
/**
|
||||
* Run the validation chain in order: auth → bank → account → view → roles → counterparty
|
||||
* Run validation chain and return enriched CallContext.
|
||||
* Used by wrapEndpoint to validate and enrich CallContext before passing to endpoint.
|
||||
*/
|
||||
private def runValidationChain(
|
||||
resourceDoc: ResourceDoc,
|
||||
cc: CallContext,
|
||||
pathParams: Map[String, String]
|
||||
): IO[CallContext] = {
|
||||
import com.openbankproject.commons.ExecutionContext.Implicits.global
|
||||
|
||||
// Step 1: Authentication
|
||||
val needsAuth = needsAuthentication(resourceDoc)
|
||||
logger.debug(s"[ResourceDocMiddleware] needsAuthentication for ${resourceDoc.partialFunctionName}: $needsAuth")
|
||||
|
||||
val authResult: IO[Either[Throwable, (Box[User], CallContext)]] =
|
||||
if (needsAuth) {
|
||||
IO.fromFuture(IO(APIUtil.authenticatedAccess(cc))).attempt.flatMap {
|
||||
case Right((boxUser, optCC)) =>
|
||||
val updatedCC = optCC.getOrElse(cc)
|
||||
boxUser match {
|
||||
case Full(user) =>
|
||||
IO.pure(Right((boxUser, updatedCC)))
|
||||
case Empty =>
|
||||
IO.pure(Left(new RuntimeException($AuthenticatedUserIsRequired)))
|
||||
case LiftFailure(msg, _, _) =>
|
||||
IO.pure(Left(new RuntimeException(msg)))
|
||||
}
|
||||
case Left(e: APIFailureNewStyle) =>
|
||||
IO.pure(Left(e))
|
||||
case Left(e) =>
|
||||
IO.pure(Left(new RuntimeException($AuthenticatedUserIsRequired)))
|
||||
}
|
||||
} else {
|
||||
IO.fromFuture(IO(APIUtil.anonymousAccess(cc))).attempt.flatMap {
|
||||
case Right((boxUser, Some(updatedCC))) =>
|
||||
IO.pure(Right((boxUser, updatedCC)))
|
||||
case Right((boxUser, None)) =>
|
||||
IO.pure(Right((boxUser, cc)))
|
||||
case Left(e) =>
|
||||
// For anonymous access, continue with Empty user
|
||||
IO.pure(Right((Empty, cc)))
|
||||
}
|
||||
}
|
||||
|
||||
authResult.flatMap {
|
||||
case Left(error) => IO.raiseError(error)
|
||||
case Right((boxUser, cc1)) =>
|
||||
// Step 2: Role authorization
|
||||
val rolesResult: IO[Either[Throwable, CallContext]] =
|
||||
resourceDoc.roles match {
|
||||
case Some(roles) if roles.nonEmpty =>
|
||||
val shouldCheckRoles = if (resourceDoc.partialFunctionName == "getResourceDocsObpV700") {
|
||||
APIUtil.getPropsAsBoolValue("resource_docs_requires_role", false)
|
||||
} else {
|
||||
true
|
||||
}
|
||||
|
||||
if (shouldCheckRoles) {
|
||||
boxUser match {
|
||||
case Full(user) =>
|
||||
val userId = user.userId
|
||||
val bankId = pathParams.get("BANK_ID").getOrElse("")
|
||||
val hasRole = roles.exists { role =>
|
||||
val checkBankId = if (role.requiresBankId) bankId else ""
|
||||
APIUtil.hasEntitlement(checkBankId, userId, role)
|
||||
}
|
||||
if (hasRole) IO.pure(Right(cc1))
|
||||
else IO.pure(Left(new RuntimeException(UserHasMissingRoles + roles.mkString(", "))))
|
||||
case _ =>
|
||||
IO.pure(Left(new RuntimeException($AuthenticatedUserIsRequired)))
|
||||
}
|
||||
} else {
|
||||
IO.pure(Right(cc1))
|
||||
}
|
||||
case _ => IO.pure(Right(cc1))
|
||||
}
|
||||
|
||||
rolesResult.flatMap {
|
||||
case Left(error) => IO.raiseError(error)
|
||||
case Right(cc2) =>
|
||||
// Step 3: Bank validation
|
||||
val bankResult: IO[Either[Throwable, (Option[Bank], CallContext)]] =
|
||||
pathParams.get("BANK_ID") match {
|
||||
case Some(bankIdStr) =>
|
||||
IO.fromFuture(IO(NewStyle.function.getBank(BankId(bankIdStr), Some(cc2)))).attempt.flatMap {
|
||||
case Right((bank, Some(updatedCC))) =>
|
||||
IO.pure(Right((Some(bank), updatedCC)))
|
||||
case Right((bank, None)) =>
|
||||
IO.pure(Right((Some(bank), cc2)))
|
||||
case Left(e: APIFailureNewStyle) =>
|
||||
IO.pure(Left(e))
|
||||
case Left(e) =>
|
||||
IO.pure(Left(new RuntimeException(BankNotFound + ": " + bankIdStr)))
|
||||
}
|
||||
case None => IO.pure(Right((None, cc2)))
|
||||
}
|
||||
|
||||
bankResult.flatMap {
|
||||
case Left(error) => IO.raiseError(error)
|
||||
case Right((bankOpt, cc3)) =>
|
||||
// Step 4: Account validation
|
||||
val accountResult: IO[Either[Throwable, (Option[BankAccount], CallContext)]] =
|
||||
(pathParams.get("BANK_ID"), pathParams.get("ACCOUNT_ID")) match {
|
||||
case (Some(bankIdStr), Some(accountIdStr)) =>
|
||||
IO.fromFuture(IO(NewStyle.function.getBankAccount(BankId(bankIdStr), AccountId(accountIdStr), Some(cc3)))).attempt.flatMap {
|
||||
case Right((account, Some(updatedCC))) => IO.pure(Right((Some(account), updatedCC)))
|
||||
case Right((account, None)) => IO.pure(Right((Some(account), cc3)))
|
||||
case Left(e: APIFailureNewStyle) =>
|
||||
IO.pure(Left(e))
|
||||
case Left(e) =>
|
||||
IO.pure(Left(new RuntimeException(BankAccountNotFound + s": bankId=$bankIdStr, accountId=$accountIdStr")))
|
||||
}
|
||||
case _ => IO.pure(Right((None, cc3)))
|
||||
}
|
||||
|
||||
accountResult.flatMap {
|
||||
case Left(error) => IO.raiseError(error)
|
||||
case Right((accountOpt, cc4)) =>
|
||||
// Step 5: View validation
|
||||
val viewResult: IO[Either[Throwable, (Option[View], CallContext)]] =
|
||||
(pathParams.get("BANK_ID"), pathParams.get("ACCOUNT_ID"), pathParams.get("VIEW_ID")) match {
|
||||
case (Some(bankIdStr), Some(accountIdStr), Some(viewIdStr)) =>
|
||||
val bankIdAccountId = BankIdAccountId(BankId(bankIdStr), AccountId(accountIdStr))
|
||||
IO.fromFuture(IO(ViewNewStyle.checkViewAccessAndReturnView(ViewId(viewIdStr), bankIdAccountId, boxUser.toOption, Some(cc4)))).attempt.flatMap {
|
||||
case Right(view) => IO.pure(Right((Some(view), cc4)))
|
||||
case Left(e: APIFailureNewStyle) =>
|
||||
IO.pure(Left(e))
|
||||
case Left(e) =>
|
||||
IO.pure(Left(new RuntimeException(UserNoPermissionAccessView + s": viewId=$viewIdStr")))
|
||||
}
|
||||
case _ => IO.pure(Right((None, cc4)))
|
||||
}
|
||||
|
||||
viewResult.flatMap {
|
||||
case Left(error) => IO.raiseError(error)
|
||||
case Right((viewOpt, cc5)) =>
|
||||
// Step 6: Counterparty validation
|
||||
val counterpartyResult: IO[Either[Throwable, (Option[CounterpartyTrait], CallContext)]] =
|
||||
(pathParams.get("BANK_ID"), pathParams.get("ACCOUNT_ID"), pathParams.get("COUNTERPARTY_ID")) match {
|
||||
case (Some(bankIdStr), Some(accountIdStr), Some(counterpartyIdStr)) =>
|
||||
IO.fromFuture(IO(NewStyle.function.getCounterpartyTrait(BankId(bankIdStr), AccountId(accountIdStr), counterpartyIdStr, Some(cc5)))).attempt.flatMap {
|
||||
case Right((counterparty, Some(updatedCC))) => IO.pure(Right((Some(counterparty), updatedCC)))
|
||||
case Right((counterparty, None)) => IO.pure(Right((Some(counterparty), cc5)))
|
||||
case Left(e: APIFailureNewStyle) =>
|
||||
IO.pure(Left(e))
|
||||
case Left(e) =>
|
||||
IO.pure(Left(new RuntimeException(CounterpartyNotFound + s": counterpartyId=$counterpartyIdStr")))
|
||||
}
|
||||
case _ => IO.pure(Right((None, cc5)))
|
||||
}
|
||||
|
||||
counterpartyResult.flatMap {
|
||||
case Left(error) => IO.raiseError(error)
|
||||
case Right((counterpartyOpt, finalCC)) =>
|
||||
// All validations passed - return enriched CallContext
|
||||
val enrichedCC = finalCC.copy(
|
||||
bank = bankOpt,
|
||||
bankAccount = accountOpt,
|
||||
view = viewOpt,
|
||||
counterparty = counterpartyOpt
|
||||
)
|
||||
IO.pure(enrichedCC)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Run validation chain for standard HttpRoutes (returns Response).
|
||||
* Used by apply() middleware for backward compatibility.
|
||||
*/
|
||||
private def runValidationChainForRoutes(
|
||||
req: Request[IO],
|
||||
resourceDoc: ResourceDoc,
|
||||
cc: CallContext,
|
||||
|
||||
@ -105,15 +105,16 @@ object Http4s700 {
|
||||
// Route: GET /obp/v7.0.0/banks
|
||||
val getBanks: HttpRoutes[IO] = HttpRoutes.of[IO] {
|
||||
case req @ GET -> `prefixPath` / "banks" =>
|
||||
val response = for {
|
||||
cc <- IO.fromOption(req.attributes.lookup(Http4sRequestAttributes.callContextKey))(new RuntimeException("CallContext not found in request attributes"))
|
||||
result <- IO.fromFuture(IO {
|
||||
for {
|
||||
(banks, callContext) <- NewStyle.function.getBanks(Some(cc))
|
||||
} yield convertAnyToJsonString(JSONFactory400.createBanksJson(banks))
|
||||
})
|
||||
} yield result
|
||||
Ok(response)
|
||||
Http4sRequestAttributes.withCallContext(req) { cc =>
|
||||
for {
|
||||
result <- IO.fromFuture(IO {
|
||||
for {
|
||||
(banks, callContext) <- NewStyle.function.getBanks(Some(cc))
|
||||
} yield convertAnyToJsonString(JSONFactory400.createBanksJson(banks))
|
||||
})
|
||||
response <- Ok(result)
|
||||
} yield response
|
||||
}
|
||||
}
|
||||
|
||||
resourceDocs += ResourceDoc(
|
||||
@ -135,16 +136,17 @@ object Http4s700 {
|
||||
// Authentication handled by ResourceDocMiddleware based on AuthenticatedUserIsRequired
|
||||
val getCards: HttpRoutes[IO] = HttpRoutes.of[IO] {
|
||||
case req @ GET -> `prefixPath` / "cards" =>
|
||||
val response = for {
|
||||
cc <- IO.fromOption(req.attributes.lookup(Http4sRequestAttributes.callContextKey))(new RuntimeException("CallContext not found in request attributes"))
|
||||
user <- IO.fromOption(cc.user.toOption)(new RuntimeException("User not found in CallContext"))
|
||||
result <- IO.fromFuture(IO {
|
||||
for {
|
||||
(cards, callContext) <- NewStyle.function.getPhysicalCardsForUser(user, Some(cc))
|
||||
} yield convertAnyToJsonString(JSONFactory1_3_0.createPhysicalCardsJSON(cards, user))
|
||||
})
|
||||
} yield result
|
||||
Ok(response)
|
||||
Http4sRequestAttributes.withCallContext(req) { cc =>
|
||||
for {
|
||||
user <- IO.fromOption(cc.user.toOption)(new RuntimeException("User not found in CallContext"))
|
||||
result <- IO.fromFuture(IO {
|
||||
for {
|
||||
(cards, callContext) <- NewStyle.function.getPhysicalCardsForUser(user, Some(cc))
|
||||
} yield convertAnyToJsonString(JSONFactory1_3_0.createPhysicalCardsJSON(cards, user))
|
||||
})
|
||||
response <- Ok(result)
|
||||
} yield response
|
||||
}
|
||||
}
|
||||
|
||||
resourceDocs += ResourceDoc(
|
||||
@ -167,19 +169,20 @@ object Http4s700 {
|
||||
// Authentication and bank validation handled by ResourceDocMiddleware
|
||||
val getCardsForBank: HttpRoutes[IO] = HttpRoutes.of[IO] {
|
||||
case req @ GET -> `prefixPath` / "banks" / bankId / "cards" =>
|
||||
val response = for {
|
||||
cc <- IO.fromOption(req.attributes.lookup(Http4sRequestAttributes.callContextKey))(new RuntimeException("CallContext not found in request attributes"))
|
||||
user <- IO.fromOption(cc.user.toOption)(new RuntimeException("User not found in CallContext"))
|
||||
bank <- IO.fromOption(cc.bank)(new RuntimeException("Bank not found in CallContext"))
|
||||
result <- IO.fromFuture(IO {
|
||||
for {
|
||||
httpParams <- NewStyle.function.extractHttpParamsFromUrl(req.uri.renderString)
|
||||
(obpQueryParams, callContext) <- createQueriesByHttpParamsFuture(httpParams, Some(cc))
|
||||
(cards, callContext) <- NewStyle.function.getPhysicalCardsForBank(bank, user, obpQueryParams, callContext)
|
||||
} yield convertAnyToJsonString(JSONFactory1_3_0.createPhysicalCardsJSON(cards, user))
|
||||
})
|
||||
} yield result
|
||||
Ok(response)
|
||||
Http4sRequestAttributes.withCallContext(req) { cc =>
|
||||
for {
|
||||
user <- IO.fromOption(cc.user.toOption)(new RuntimeException("User not found in CallContext"))
|
||||
bank <- IO.fromOption(cc.bank)(new RuntimeException("Bank not found in CallContext"))
|
||||
result <- IO.fromFuture(IO {
|
||||
for {
|
||||
httpParams <- NewStyle.function.extractHttpParamsFromUrl(req.uri.renderString)
|
||||
(obpQueryParams, callContext) <- createQueriesByHttpParamsFuture(httpParams, Some(cc))
|
||||
(cards, callContext) <- NewStyle.function.getPhysicalCardsForBank(bank, user, obpQueryParams, callContext)
|
||||
} yield convertAnyToJsonString(JSONFactory1_3_0.createPhysicalCardsJSON(cards, user))
|
||||
})
|
||||
response <- Ok(result)
|
||||
} yield response
|
||||
}
|
||||
}
|
||||
|
||||
resourceDocs += ResourceDoc(
|
||||
@ -217,88 +220,47 @@ object Http4s700 {
|
||||
val getResourceDocsObpV700: HttpRoutes[IO] = HttpRoutes.of[IO] {
|
||||
case req @ GET -> `prefixPath` / "resource-docs" / requestedApiVersionString / "obp" =>
|
||||
import com.openbankproject.commons.ExecutionContext.Implicits.global
|
||||
val response = for {
|
||||
cc <- IO.fromOption(req.attributes.lookup(Http4sRequestAttributes.callContextKey))(new RuntimeException("CallContext not found in request attributes"))
|
||||
result <- IO.fromFuture(IO {
|
||||
// Check resource_docs_requires_role property
|
||||
val resourceDocsRequireRole = getPropsAsBoolValue("resource_docs_requires_role", false)
|
||||
|
||||
for {
|
||||
// Authentication based on property
|
||||
(boxUser, cc1) <- if (resourceDocsRequireRole)
|
||||
authenticatedAccess(cc)
|
||||
else
|
||||
anonymousAccess(cc)
|
||||
Http4sRequestAttributes.withCallContext(req) { cc =>
|
||||
for {
|
||||
result <- IO.fromFuture(IO {
|
||||
// Check resource_docs_requires_role property
|
||||
val resourceDocsRequireRole = getPropsAsBoolValue("resource_docs_requires_role", false)
|
||||
|
||||
// Role check based on property
|
||||
_ <- if (resourceDocsRequireRole) {
|
||||
NewStyle.function.hasAtLeastOneEntitlement(
|
||||
failMsg = UserHasMissingRoles + canReadResourceDoc.toString
|
||||
)("", boxUser.map(_.userId).getOrElse(""), ApiRole.canReadResourceDoc :: Nil, cc1)
|
||||
} else {
|
||||
Future.successful(())
|
||||
}
|
||||
|
||||
httpParams <- NewStyle.function.extractHttpParamsFromUrl(req.uri.renderString)
|
||||
tagsParam = httpParams.filter(_.name == "tags").map(_.values).headOption
|
||||
functionsParam = httpParams.filter(_.name == "functions").map(_.values).headOption
|
||||
localeParam = httpParams.filter(param => param.name == "locale" || param.name == "language").map(_.values).flatten.headOption
|
||||
contentParam = httpParams.filter(_.name == "content").map(_.values).flatten.flatMap(ResourceDocsAPIMethodsUtil.stringToContentParam).headOption
|
||||
apiCollectionIdParam = httpParams.filter(_.name == "api-collection-id").map(_.values).flatten.headOption
|
||||
tags = tagsParam.map(_.map(ResourceDocTag(_)))
|
||||
functions = functionsParam.map(_.toList)
|
||||
requestedApiVersion <- Future(ApiVersionUtils.valueOf(requestedApiVersionString))
|
||||
resourceDocs = ResourceDocs140.ImplementationsResourceDocs.getResourceDocsList(requestedApiVersion).getOrElse(Nil)
|
||||
filteredDocs = ResourceDocsAPIMethodsUtil.filterResourceDocs(resourceDocs, tags, functions)
|
||||
resourceDocsJson = JSONFactory1_4_0.createResourceDocsJson(filteredDocs, isVersion4OrHigher = true, localeParam)
|
||||
} yield convertAnyToJsonString(resourceDocsJson)
|
||||
})
|
||||
} yield result
|
||||
Ok(response)
|
||||
for {
|
||||
// Authentication based on property
|
||||
(boxUser, cc1) <- if (resourceDocsRequireRole)
|
||||
authenticatedAccess(cc)
|
||||
else
|
||||
anonymousAccess(cc)
|
||||
|
||||
// Role check based on property
|
||||
_ <- if (resourceDocsRequireRole) {
|
||||
NewStyle.function.hasAtLeastOneEntitlement(
|
||||
failMsg = UserHasMissingRoles + canReadResourceDoc.toString
|
||||
)("", boxUser.map(_.userId).getOrElse(""), ApiRole.canReadResourceDoc :: Nil, cc1)
|
||||
} else {
|
||||
Future.successful(())
|
||||
}
|
||||
|
||||
httpParams <- NewStyle.function.extractHttpParamsFromUrl(req.uri.renderString)
|
||||
tagsParam = httpParams.filter(_.name == "tags").map(_.values).headOption
|
||||
functionsParam = httpParams.filter(_.name == "functions").map(_.values).headOption
|
||||
localeParam = httpParams.filter(param => param.name == "locale" || param.name == "language").map(_.values).flatten.headOption
|
||||
contentParam = httpParams.filter(_.name == "content").map(_.values).flatten.flatMap(ResourceDocsAPIMethodsUtil.stringToContentParam).headOption
|
||||
apiCollectionIdParam = httpParams.filter(_.name == "api-collection-id").map(_.values).flatten.headOption
|
||||
tags = tagsParam.map(_.map(ResourceDocTag(_)))
|
||||
functions = functionsParam.map(_.toList)
|
||||
requestedApiVersion <- Future(ApiVersionUtils.valueOf(requestedApiVersionString))
|
||||
resourceDocs = ResourceDocs140.ImplementationsResourceDocs.getResourceDocsList(requestedApiVersion).getOrElse(Nil)
|
||||
filteredDocs = ResourceDocsAPIMethodsUtil.filterResourceDocs(resourceDocs, tags, functions)
|
||||
resourceDocsJson = JSONFactory1_4_0.createResourceDocsJson(filteredDocs, isVersion4OrHigher = true, localeParam)
|
||||
} yield convertAnyToJsonString(resourceDocsJson)
|
||||
})
|
||||
response <- Ok(result)
|
||||
} yield response
|
||||
}
|
||||
}
|
||||
|
||||
// Example endpoint demonstrating full validation chain with ResourceDocMiddleware
|
||||
// This endpoint requires: authentication + bank validation + account validation + view validation
|
||||
// When using ResourceDocMiddleware, these validations are automatic based on path parameters
|
||||
|
||||
// resourceDocs += ResourceDoc(
|
||||
// null,
|
||||
// implementedInApiVersion,
|
||||
// nameOf(getCounterpartyByIdWithMiddleware),
|
||||
// "GET",
|
||||
// "/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/counterparties/COUNTERPARTY_ID",
|
||||
// "Get Counterparty by Id (http4s with middleware)",
|
||||
// s"""Get counterparty by id with automatic validation via ResourceDocMiddleware.
|
||||
// |
|
||||
// |This endpoint demonstrates the COMPLETE validation chain:
|
||||
// |* Authentication (required)
|
||||
// |* Bank existence validation (BANK_ID in path)
|
||||
// |* Account existence validation (ACCOUNT_ID in path)
|
||||
// |* View access validation (VIEW_ID in path)
|
||||
// |* Counterparty existence validation (COUNTERPARTY_ID in path)
|
||||
// |
|
||||
// |${userAuthenticationMessage(true)}""",
|
||||
// EmptyBody,
|
||||
// moderatedAccountJSON,
|
||||
// List(AuthenticatedUserIsRequired, BankNotFound, BankAccountNotFound, ViewNotFound, UserNoPermissionAccessView, CounterpartyNotFound, UnknownError),
|
||||
// apiTagCounterparty :: Nil,
|
||||
// http4sPartialFunction = Some(getCounterpartyByIdWithMiddleware)
|
||||
// )
|
||||
|
||||
// // Route: GET /obp/v7.0.0/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/counterparties/COUNTERPARTY_ID
|
||||
// // When used with ResourceDocMiddleware, validation is automatic
|
||||
// val getCounterpartyByIdWithMiddleware: HttpRoutes[IO] = HttpRoutes.of[IO] {
|
||||
// case req @ GET -> `prefixPath` / "banks" / bankId / "accounts" / accountId / viewId / "counterparties" / counterpartyId =>
|
||||
// val responseJson = convertAnyToJsonString(
|
||||
// Map(
|
||||
// "bank_id" -> bankId,
|
||||
// "account_id" -> accountId,
|
||||
// "view_id" -> viewId,
|
||||
// "counterparty_id" -> counterpartyId
|
||||
// )
|
||||
// )
|
||||
// Ok(responseJson)
|
||||
// }
|
||||
|
||||
// All routes combined (without middleware - for direct use)
|
||||
val allRoutes: HttpRoutes[IO] =
|
||||
@ -308,7 +270,6 @@ object Http4s700 {
|
||||
.orElse(getCards(req))
|
||||
.orElse(getCardsForBank(req))
|
||||
.orElse(getResourceDocsObpV700(req))
|
||||
// .orElse(getAccountByIdWithMiddleware(req))
|
||||
}
|
||||
|
||||
// Routes wrapped with ResourceDocMiddleware for automatic validation
|
||||
|
||||
Loading…
Reference in New Issue
Block a user