mirror of
https://github.com/OpenBankProject/OBP-API.git
synced 2026-02-06 11:06:49 +00:00
refactor/(http4s): improve documentation and code clarity in error handling and support utilities
- Enhance ErrorResponseConverter documentation with detailed handler descriptions and response format details - Add comprehensive comments explaining error type handling (APIFailureNewStyle, Box Failure, unknown exceptions) - Document correlation-Id header inclusion and HTTP status code mapping in error responses - Simplify error matching logic in toHttp4sResponse using pattern matching - Improve Http4sSupport file documentation with clear component descriptions - Add usage examples for RequestOps implicit class in endpoint implementations - Clarify CallContext storage mechanism using http4s Vault (type-safe key-value store) - Document validated entity storage (user, bank, bankAccount, view, counterparty) within CallContext - Add inline comments explaining ResourceDocMatcher functionality and request matching process - Improve code readability with consistent formatting and clearer method documentation
This commit is contained in:
parent
83d90bfc44
commit
0415d13b1a
@ -13,7 +13,16 @@ import org.typelevel.ci.CIString
|
||||
|
||||
/**
|
||||
* Converts OBP errors to http4s Response[IO].
|
||||
* Uses Lift JSON for serialization (consistent with OBP codebase).
|
||||
*
|
||||
* Handles:
|
||||
* - APIFailureNewStyle (structured errors with code and message)
|
||||
* - Box Failure (Lift framework errors)
|
||||
* - Unknown exceptions
|
||||
*
|
||||
* All responses include:
|
||||
* - JSON body with code and message
|
||||
* - Correlation-Id header for request tracing
|
||||
* - Appropriate HTTP status code
|
||||
*/
|
||||
object ErrorResponseConverter {
|
||||
import net.liftweb.json.Formats
|
||||
@ -23,7 +32,7 @@ object ErrorResponseConverter {
|
||||
private val jsonContentType: `Content-Type` = `Content-Type`(MediaType.application.json)
|
||||
|
||||
/**
|
||||
* OBP standard error response format
|
||||
* OBP standard error response format.
|
||||
*/
|
||||
case class OBPErrorResponse(
|
||||
code: Int,
|
||||
@ -31,7 +40,7 @@ object ErrorResponseConverter {
|
||||
)
|
||||
|
||||
/**
|
||||
* Convert error response to JSON string
|
||||
* Convert error response to JSON string using Lift JSON.
|
||||
*/
|
||||
private def toJsonString(error: OBPErrorResponse): String = {
|
||||
val json = ("code" -> error.code) ~ ("message" -> error.message)
|
||||
@ -39,19 +48,18 @@ object ErrorResponseConverter {
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert an error to http4s Response[IO]
|
||||
* Convert any error to http4s Response[IO].
|
||||
*/
|
||||
def toHttp4sResponse(error: Throwable, callContext: CallContext): IO[Response[IO]] = {
|
||||
error match {
|
||||
case e: APIFailureNewStyle =>
|
||||
apiFailureToResponse(e, callContext)
|
||||
case e =>
|
||||
unknownErrorToResponse(e, callContext)
|
||||
case e: APIFailureNewStyle => apiFailureToResponse(e, callContext)
|
||||
case _ => unknownErrorToResponse(error, callContext)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert APIFailureNewStyle to http4s Response
|
||||
* Convert APIFailureNewStyle to http4s Response.
|
||||
* Uses failCode as HTTP status and failMsg as error message.
|
||||
*/
|
||||
def apiFailureToResponse(failure: APIFailureNewStyle, callContext: CallContext): IO[Response[IO]] = {
|
||||
val errorJson = OBPErrorResponse(failure.failCode, failure.failMsg)
|
||||
@ -65,7 +73,8 @@ object ErrorResponseConverter {
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert Box Failure to http4s Response
|
||||
* Convert Lift Box Failure to http4s Response.
|
||||
* Returns 400 Bad Request with failure message.
|
||||
*/
|
||||
def boxFailureToResponse(failure: LiftFailure, callContext: CallContext): IO[Response[IO]] = {
|
||||
val errorJson = OBPErrorResponse(400, failure.msg)
|
||||
@ -78,7 +87,8 @@ object ErrorResponseConverter {
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert unknown error to http4s Response
|
||||
* Convert unknown error to http4s Response.
|
||||
* Returns 500 Internal Server Error.
|
||||
*/
|
||||
def unknownErrorToResponse(e: Throwable, callContext: CallContext): IO[Response[IO]] = {
|
||||
val errorJson = OBPErrorResponse(500, s"$UnknownError: ${e.getMessage}")
|
||||
@ -91,7 +101,7 @@ object ErrorResponseConverter {
|
||||
}
|
||||
|
||||
/**
|
||||
* Create error response with specific status code and message
|
||||
* Create error response with specific status code and message.
|
||||
*/
|
||||
def createErrorResponse(statusCode: Int, message: String, callContext: CallContext): IO[Response[IO]] = {
|
||||
val errorJson = OBPErrorResponse(statusCode, message)
|
||||
|
||||
@ -22,57 +22,56 @@ import scala.concurrent.Future
|
||||
import scala.language.higherKinds
|
||||
|
||||
/**
|
||||
* Http4s support for ResourceDoc-driven validation.
|
||||
* Http4s support utilities for OBP API.
|
||||
*
|
||||
* This file contains:
|
||||
* - Http4sCallContextBuilder: Builds shared CallContext from http4s Request[IO]
|
||||
* - Http4sRequestAttributes: Provides CallContext access from http4s requests
|
||||
* - ResourceDocMatcher: Matches http4s requests to ResourceDoc entries
|
||||
* This file contains three main components:
|
||||
*
|
||||
* Validated entities (User, Bank, BankAccount, View, Counterparty) are stored
|
||||
* directly in CallContext fields, making them available throughout the call chain.
|
||||
* 1. Http4sRequestAttributes: Request attribute management and endpoint helpers
|
||||
* - Stores CallContext in http4s request Vault
|
||||
* - Provides helper methods to simplify endpoint implementations
|
||||
* - Validated entities are stored in CallContext fields
|
||||
*
|
||||
* 2. Http4sCallContextBuilder: Builds CallContext from http4s Request[IO]
|
||||
* - Extracts headers, auth params, and request metadata
|
||||
* - Supports DirectLogin, OAuth, and Gateway authentication
|
||||
*
|
||||
* 3. ResourceDocMatcher: Matches requests to ResourceDoc entries
|
||||
* - Finds ResourceDoc by HTTP verb and URL pattern
|
||||
* - Extracts path parameters (BANK_ID, ACCOUNT_ID, etc.)
|
||||
* - Attaches ResourceDoc to CallContext for metrics/rate limiting
|
||||
*/
|
||||
|
||||
/**
|
||||
* Request attribute keys and helpers for accessing CallContext in http4s requests.
|
||||
* Request attributes and helper methods for http4s endpoints.
|
||||
*
|
||||
* CallContext is stored in http4s request attributes using Vault (type-safe key-value store).
|
||||
* Validated entities (bank, bankAccount, view, counterparty) are stored within CallContext itself.
|
||||
*
|
||||
* Usage in endpoints:
|
||||
* {{{
|
||||
* import Http4sRequestAttributes.RequestOps
|
||||
*
|
||||
* val myEndpoint: HttpRoutes[IO] = HttpRoutes.of[IO] {
|
||||
* case req @ GET -> Root / "banks" =>
|
||||
* implicit val cc: CallContext = req.callContext
|
||||
* for {
|
||||
* result <- yourBusinessLogic // cc is implicitly available
|
||||
* response <- Ok(result)
|
||||
* } yield response
|
||||
* }
|
||||
* }}}
|
||||
* CallContext is stored in request attributes using http4s Vault (type-safe key-value store).
|
||||
* Validated entities (user, bank, bankAccount, view, counterparty) are stored within CallContext.
|
||||
*/
|
||||
object Http4sRequestAttributes {
|
||||
import org.typelevel.vault.Key
|
||||
|
||||
/**
|
||||
* Vault key for storing CallContext in http4s request attributes.
|
||||
* CallContext contains all request data and validated entities.
|
||||
* CallContext contains request data and validated entities (user, bank, account, view, counterparty).
|
||||
*/
|
||||
val callContextKey: Key[CallContext] =
|
||||
Key.newKey[IO, CallContext].unsafeRunSync()(cats.effect.unsafe.IORuntime.global)
|
||||
|
||||
/**
|
||||
* Implicit class that adds CallContext accessor to Request[IO].
|
||||
* Import RequestOps to enable `req.callContext` syntax.
|
||||
* Implicit class that adds .callContext accessor to Request[IO].
|
||||
*
|
||||
* Usage:
|
||||
* {{{
|
||||
* import Http4sRequestAttributes.RequestOps
|
||||
*
|
||||
* case req @ GET -> Root / "banks" =>
|
||||
* implicit val cc: CallContext = req.callContext
|
||||
* // Use cc for business logic
|
||||
* }}}
|
||||
*/
|
||||
implicit class RequestOps(val req: Request[IO]) extends AnyVal {
|
||||
/**
|
||||
* Extract CallContext from request attributes.
|
||||
* Throws RuntimeException if CallContext is not found (should never happen with ResourceDocMiddleware).
|
||||
*
|
||||
* @return CallContext containing validated user, bank, account, view, counterparty
|
||||
* Throws RuntimeException if not found (should never happen with ResourceDocMiddleware).
|
||||
*/
|
||||
def callContext: CallContext = {
|
||||
req.attributes.lookup(callContextKey).getOrElse(
|
||||
@ -82,31 +81,26 @@ object Http4sRequestAttributes {
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper methods to simplify endpoint implementations.
|
||||
* These eliminate boilerplate for common patterns in http4s endpoints.
|
||||
* Helper methods to eliminate boilerplate in endpoint implementations.
|
||||
*
|
||||
* These methods handle:
|
||||
* - CallContext extraction from request
|
||||
* - User/Bank extraction from CallContext
|
||||
* - Future execution with IO.fromFuture
|
||||
* - JSON serialization with Lift JSON
|
||||
* - Ok response creation
|
||||
*/
|
||||
object EndpointHelpers {
|
||||
import net.liftweb.json.{Extraction, Formats}
|
||||
import net.liftweb.json.JsonAST.prettyRender
|
||||
|
||||
/**
|
||||
* Execute a Future-based business logic function and return JSON response.
|
||||
* Handles Future execution, JSON conversion, and Ok response creation.
|
||||
* Execute Future-based business logic and return JSON response.
|
||||
*
|
||||
* Usage:
|
||||
* {{{
|
||||
* case req @ GET -> Root / "banks" =>
|
||||
* executeAndRespond(req) { implicit cc =>
|
||||
* for {
|
||||
* (banks, callContext) <- NewStyle.function.getBanks(Some(cc))
|
||||
* } yield JSONFactory400.createBanksJson(banks)
|
||||
* }
|
||||
* }}}
|
||||
* Handles: Future execution, JSON conversion, Ok response.
|
||||
*
|
||||
* @param req The http4s request
|
||||
* @param f Business logic function that takes CallContext and returns Future[A]
|
||||
* @param formats Implicit JSON formats for serialization
|
||||
* @tparam A The result type (will be converted to JSON)
|
||||
* @param req http4s request
|
||||
* @param f Business logic: CallContext => Future[A]
|
||||
* @return IO[Response[IO]] with JSON body
|
||||
*/
|
||||
def executeAndRespond[A](req: Request[IO])(f: CallContext => Future[A])(implicit formats: Formats): IO[Response[IO]] = {
|
||||
@ -119,23 +113,12 @@ object Http4sRequestAttributes {
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute business logic that requires validated User from CallContext.
|
||||
* Extracts User from CallContext, executes business logic, and returns JSON response.
|
||||
* Execute business logic requiring validated User.
|
||||
*
|
||||
* Usage:
|
||||
* {{{
|
||||
* case req @ GET -> Root / "cards" =>
|
||||
* withUser(req) { (user, cc) =>
|
||||
* for {
|
||||
* (cards, callContext) <- NewStyle.function.getPhysicalCardsForUser(user, Some(cc))
|
||||
* } yield JSONFactory1_3_0.createPhysicalCardsJSON(cards, user)
|
||||
* }
|
||||
* }}}
|
||||
* Extracts User from CallContext, executes logic, returns JSON response.
|
||||
*
|
||||
* @param req The http4s request
|
||||
* @param f Business logic function that takes (User, CallContext) and returns Future[A]
|
||||
* @param formats Implicit JSON formats for serialization
|
||||
* @tparam A The result type (will be converted to JSON)
|
||||
* @param req http4s request
|
||||
* @param f Business logic: (User, CallContext) => Future[A]
|
||||
* @return IO[Response[IO]] with JSON body
|
||||
*/
|
||||
def withUser[A](req: Request[IO])(f: (User, CallContext) => Future[A])(implicit formats: Formats): IO[Response[IO]] = {
|
||||
@ -149,23 +132,12 @@ object Http4sRequestAttributes {
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute business logic that requires validated Bank from CallContext.
|
||||
* Extracts Bank from CallContext, executes business logic, and returns JSON response.
|
||||
* Execute business logic requiring validated Bank.
|
||||
*
|
||||
* Usage:
|
||||
* {{{
|
||||
* case req @ GET -> Root / "banks" / bankId / "accounts" =>
|
||||
* withBank(req) { (bank, cc) =>
|
||||
* for {
|
||||
* (accounts, callContext) <- NewStyle.function.getBankAccounts(bank, Some(cc))
|
||||
* } yield JSONFactory400.createAccountsJson(accounts)
|
||||
* }
|
||||
* }}}
|
||||
* Extracts Bank from CallContext, executes logic, returns JSON response.
|
||||
*
|
||||
* @param req The http4s request
|
||||
* @param f Business logic function that takes (Bank, CallContext) and returns Future[A]
|
||||
* @param formats Implicit JSON formats for serialization
|
||||
* @tparam A The result type (will be converted to JSON)
|
||||
* @param req http4s request
|
||||
* @param f Business logic: (Bank, CallContext) => Future[A]
|
||||
* @return IO[Response[IO]] with JSON body
|
||||
*/
|
||||
def withBank[A](req: Request[IO])(f: (Bank, CallContext) => Future[A])(implicit formats: Formats): IO[Response[IO]] = {
|
||||
@ -179,23 +151,12 @@ object Http4sRequestAttributes {
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute business logic that requires both User and Bank from CallContext.
|
||||
* Extracts both from CallContext, executes business logic, and returns JSON response.
|
||||
* Execute business logic requiring both User and Bank.
|
||||
*
|
||||
* Usage:
|
||||
* {{{
|
||||
* case req @ GET -> Root / "banks" / bankId / "cards" =>
|
||||
* withUserAndBank(req) { (user, bank, cc) =>
|
||||
* for {
|
||||
* (cards, callContext) <- NewStyle.function.getPhysicalCardsForBank(bank, user, obpQueryParams, Some(cc))
|
||||
* } yield JSONFactory1_3_0.createPhysicalCardsJSON(cards, user)
|
||||
* }
|
||||
* }}}
|
||||
* Extracts both from CallContext, executes logic, returns JSON response.
|
||||
*
|
||||
* @param req The http4s request
|
||||
* @param f Business logic function that takes (User, Bank, CallContext) and returns Future[A]
|
||||
* @param formats Implicit JSON formats for serialization
|
||||
* @tparam A The result type (will be converted to JSON)
|
||||
* @param req http4s request
|
||||
* @param f Business logic: (User, Bank, CallContext) => Future[A]
|
||||
* @return IO[Response[IO]] with JSON body
|
||||
*/
|
||||
def withUserAndBank[A](req: Request[IO])(f: (User, Bank, CallContext) => Future[A])(implicit formats: Formats): IO[Response[IO]] = {
|
||||
|
||||
@ -20,14 +20,17 @@ import scala.language.higherKinds
|
||||
* ResourceDoc-driven validation middleware for http4s.
|
||||
*
|
||||
* This middleware wraps http4s routes with automatic validation based on ResourceDoc metadata.
|
||||
* Validation is performed in a specific order to ensure security and proper error responses.
|
||||
*
|
||||
* VALIDATION ORDER:
|
||||
* 1. Authentication first
|
||||
* 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)
|
||||
* 1. Authentication - Check if user is authenticated (if required by ResourceDoc)
|
||||
* 2. Authorization - Verify user has required roles/entitlements
|
||||
* 3. Bank validation - Validate BANK_ID path parameter (if present)
|
||||
* 4. Account validation - Validate ACCOUNT_ID path parameter (if present)
|
||||
* 5. View validation - Validate VIEW_ID and check user access (if present)
|
||||
* 6. Counterparty validation - Validate COUNTERPARTY_ID (if present)
|
||||
*
|
||||
* Validated entities are stored in CallContext fields for use in endpoint handlers.
|
||||
*/
|
||||
object ResourceDocMiddleware extends MdcLoggable{
|
||||
|
||||
@ -36,20 +39,26 @@ object ResourceDocMiddleware extends MdcLoggable{
|
||||
private val jsonContentType: `Content-Type` = `Content-Type`(MediaType.application.json)
|
||||
|
||||
/**
|
||||
* Check if ResourceDoc requires authentication based on errorResponseBodies or property
|
||||
* Check if ResourceDoc requires authentication.
|
||||
*
|
||||
* Authentication is required if:
|
||||
* - ResourceDoc errorResponseBodies contains $AuthenticatedUserIsRequired
|
||||
* - ResourceDoc has roles (roles always require authenticated user)
|
||||
* - Special case: resource-docs endpoint checks resource_docs_requires_role property
|
||||
*/
|
||||
private def needsAuthentication(resourceDoc: ResourceDoc): Boolean = {
|
||||
// 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 to standard HttpRoutes
|
||||
* Create middleware that applies ResourceDoc-driven validation.
|
||||
*
|
||||
* @param resourceDocs Collection of ResourceDoc entries for matching
|
||||
* @return Middleware that wraps HttpRoutes with validation
|
||||
*/
|
||||
def apply(resourceDocs: ArrayBuffer[ResourceDoc]): Middleware[IO] = { routes =>
|
||||
Kleisli[HttpF, Request[IO], Response[IO]] { req =>
|
||||
@ -58,7 +67,13 @@ object ResourceDocMiddleware extends MdcLoggable{
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate request and route to handler if validation passes
|
||||
* Validate request and route to handler if validation passes.
|
||||
*
|
||||
* Steps:
|
||||
* 1. Build CallContext from request
|
||||
* 2. Find matching ResourceDoc
|
||||
* 3. Run validation chain
|
||||
* 4. Route to handler with enriched CallContext
|
||||
*/
|
||||
private def validateAndRoute(
|
||||
req: Request[IO],
|
||||
@ -80,6 +95,9 @@ object ResourceDocMiddleware extends MdcLoggable{
|
||||
} yield response
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure response has JSON content type.
|
||||
*/
|
||||
private def ensureJsonContentType(response: Response[IO]): Response[IO] = {
|
||||
response.contentType match {
|
||||
case Some(contentType) if contentType.mediaType == MediaType.application.json => response
|
||||
@ -88,179 +106,18 @@ object ResourceDocMiddleware extends MdcLoggable{
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* Run validation chain for HttpRoutes and return Response.
|
||||
*
|
||||
* This method performs all validation steps in order:
|
||||
* 1. Authentication (if required)
|
||||
* 2. Role authorization (if roles specified)
|
||||
* 3. Bank validation (if BANK_ID in path)
|
||||
* 4. Account validation (if ACCOUNT_ID in path)
|
||||
* 5. View validation (if VIEW_ID in path)
|
||||
* 6. Counterparty validation (if COUNTERPARTY_ID in path)
|
||||
*
|
||||
* On success: Enriches CallContext with validated entities and routes to handler
|
||||
* On failure: Returns error response immediately
|
||||
*/
|
||||
private def runValidationChainForRoutes(
|
||||
req: Request[IO],
|
||||
@ -270,10 +127,10 @@ object ResourceDocMiddleware extends MdcLoggable{
|
||||
routes: HttpRoutes[IO]
|
||||
): IO[Response[IO]] = {
|
||||
|
||||
// Step 1: Authentication
|
||||
val needsAuth = needsAuthentication(resourceDoc)
|
||||
logger.debug(s"[ResourceDocMiddleware] needsAuthentication for ${resourceDoc.partialFunctionName}: $needsAuth")
|
||||
|
||||
// Step 1: Authentication
|
||||
val authResult: IO[Either[Response[IO], (Box[User], CallContext)]] =
|
||||
if (needsAuth) {
|
||||
IO.fromFuture(IO(APIUtil.authenticatedAccess(cc))).attempt.flatMap {
|
||||
@ -305,24 +162,19 @@ object ResourceDocMiddleware extends MdcLoggable{
|
||||
} else {
|
||||
IO.fromFuture(IO(APIUtil.anonymousAccess(cc))).attempt.flatMap {
|
||||
case Right((boxUser, Some(updatedCC))) =>
|
||||
logger.debug(s"[ResourceDocMiddleware] anonymousAccess succeeded with user: $boxUser")
|
||||
IO.pure(Right((boxUser, updatedCC)))
|
||||
case Right((boxUser, None)) =>
|
||||
logger.debug(s"[ResourceDocMiddleware] anonymousAccess succeeded with user: $boxUser (no updated CC)")
|
||||
IO.pure(Right((boxUser, cc)))
|
||||
case Left(e) =>
|
||||
// For anonymous access, we don't fail on auth errors - just continue with Empty user
|
||||
// This allows endpoints without $AuthenticatedUserIsRequired to work without authentication
|
||||
logger.debug(s"[ResourceDocMiddleware] anonymousAccess threw exception (ignoring for anonymous): ${e.getClass.getName}: ${e.getMessage.take(100)}")
|
||||
// For anonymous endpoints, continue with Empty user even if auth fails
|
||||
IO.pure(Right((Empty, cc)))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
authResult.flatMap {
|
||||
authResult.flatMap {
|
||||
case Left(errorResponse) => IO.pure(errorResponse)
|
||||
case Right((boxUser, cc1)) =>
|
||||
// Step 2: Role authorization - BEFORE business logic validation
|
||||
// Step 2: Role authorization
|
||||
val rolesResult: IO[Either[Response[IO], CallContext]] =
|
||||
resourceDoc.roles match {
|
||||
case Some(roles) if roles.nonEmpty =>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user