From 5f7bbc3e5fa9988a8f1143459c2950431fd441e4 Mon Sep 17 00:00:00 2001 From: simonredfern Date: Sat, 17 Jan 2026 09:25:04 +0100 Subject: [PATCH] ABAC Error message codes --- .../scala/code/api/util/ErrorMessages.scala | 10 ++ .../scala/code/api/v6_0_0/APIMethods600.scala | 127 ++++++++++++++++-- 2 files changed, 129 insertions(+), 8 deletions(-) diff --git a/obp-api/src/main/scala/code/api/util/ErrorMessages.scala b/obp-api/src/main/scala/code/api/util/ErrorMessages.scala index 87f28e693..572393a37 100644 --- a/obp-api/src/main/scala/code/api/util/ErrorMessages.scala +++ b/obp-api/src/main/scala/code/api/util/ErrorMessages.scala @@ -649,6 +649,16 @@ object ErrorMessages { val CannotGetUserInvitation = "OBP-37882: Cannot get user invitation." val CannotFindUserInvitation = "OBP-37883: Cannot find user invitation." + // ABAC Rule related messages (OBP-38XXX) + val AbacRuleValidationFailed = "OBP-38001: ABAC rule validation failed. The rule code could not be validated." + val AbacRuleCompilationFailed = "OBP-38002: ABAC rule compilation failed. The rule code contains syntax errors or invalid Scala code." + val AbacRuleTypeMismatch = "OBP-38003: ABAC rule type mismatch. The rule code must return a Boolean value but returns a different type." + val AbacRuleSyntaxError = "OBP-38004: ABAC rule syntax error. The rule code contains invalid syntax." + val AbacRuleFieldReferenceError = "OBP-38005: ABAC rule field reference error. The rule code references fields or objects that do not exist." + val AbacRuleCodeEmpty = "OBP-38006: ABAC rule code must not be empty." + val AbacRuleNotFound = "OBP-38007: ABAC rule not found. Please specify a valid value for ABAC_RULE_ID." + val AbacRuleNotActive = "OBP-38008: ABAC rule is not active." + val AbacRuleExecutionFailed = "OBP-38009: ABAC rule execution failed. An error occurred while executing the rule." // Transaction Request related messages (OBP-40XXX) val InvalidTransactionRequestType = "OBP-40001: Invalid value for TRANSACTION_REQUEST_TYPE" 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 9b1ca4dba..08d294bd4 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 @@ -13,8 +13,10 @@ import code.api.util.ApiTag._ import code.api.util.ErrorMessages.{$AuthenticatedUserIsRequired, InvalidDateFormat, InvalidJsonFormat, UnknownError, DynamicEntityOperationNotAllowed, _} import code.api.util.FutureUtil.EndpointContext import code.api.util.Glossary +import code.api.util.JsonSchemaGenerator import code.api.util.NewStyle.HttpCode import code.api.util.{APIUtil, CallContext, DiagnosticDynamicEntityCheck, ErrorMessages, NewStyle, RateLimitingUtil} +import net.liftweb.json import code.api.util.NewStyle.function.extractQueryParams import code.api.util.newstyle.ViewNewStyle import code.api.v3_0_0.JSONFactory300 @@ -52,7 +54,8 @@ import com.openbankproject.commons.model._ import com.openbankproject.commons.model.enums.DynamicEntityOperation._ import com.openbankproject.commons.model.enums.UserAttributeType import com.openbankproject.commons.util.{ApiVersion, ScannedApiVersion} -import net.liftweb.common.{Empty, Failure, Full} +import net.liftweb.common.{Box, Empty, Failure, Full} +import net.liftweb.util.Helpers.tryo import org.apache.commons.lang3.StringUtils import net.liftweb.http.provider.HTTPParam import net.liftweb.http.rest.RestHelper @@ -5531,7 +5534,7 @@ trait APIMethods600 { validateJson <- NewStyle.function.tryons(s"$InvalidJsonFormat", 400, callContext) { json.extract[ValidateAbacRuleJsonV600] } - _ <- NewStyle.function.tryons(s"Rule code must not be empty", 400, callContext) { + _ <- NewStyle.function.tryons(s"$AbacRuleCodeEmpty", 400, callContext) { validateJson.rule_code.trim.nonEmpty } validationResult <- Future { @@ -5544,28 +5547,40 @@ trait APIMethods600 { case Failure(errorMsg, _, _) => // Extract error details from the error message val cleanError = errorMsg.replace("Invalid ABAC rule code: ", "").replace("Failed to compile ABAC rule: ", "") + + // Determine the proper OBP error message and error type + val (obpErrorMessage, errorType) = if (cleanError.toLowerCase.contains("type mismatch") || cleanError.toLowerCase.contains("found:") && cleanError.toLowerCase.contains("required: boolean")) { + (AbacRuleTypeMismatch, "TypeError") + } else if (cleanError.toLowerCase.contains("syntax") || cleanError.toLowerCase.contains("parse")) { + (AbacRuleSyntaxError, "SyntaxError") + } else if (cleanError.toLowerCase.contains("not found") || cleanError.toLowerCase.contains("not a member")) { + (AbacRuleFieldReferenceError, "FieldReferenceError") + } else if (cleanError.toLowerCase.contains("compilation failed") || cleanError.toLowerCase.contains("reflective compilation has failed")) { + (AbacRuleCompilationFailed, "CompilationError") + } else { + (AbacRuleValidationFailed, "ValidationError") + } + Full(ValidateAbacRuleFailureJsonV600( valid = false, error = cleanError, - message = "Rule validation failed", + message = obpErrorMessage, details = ValidateAbacRuleErrorDetailsJsonV600( - error_type = if (cleanError.toLowerCase.contains("syntax")) "SyntaxError" - else if (cleanError.toLowerCase.contains("type")) "TypeError" - else "CompilationError" + error_type = errorType ) )) case Empty => Full(ValidateAbacRuleFailureJsonV600( valid = false, error = "Unknown validation error", - message = "Rule validation failed", + message = AbacRuleValidationFailed, details = ValidateAbacRuleErrorDetailsJsonV600( error_type = "UnknownError" ) )) } } map { - unboxFullOrFail(_, callContext, "Validation failed", 400) + unboxFullOrFail(_, callContext, AbacRuleValidationFailed, 400) } } yield { (validationResult, HttpCode.`200`(callContext)) @@ -6293,6 +6308,102 @@ trait APIMethods600 { } } + staticResourceDocs += ResourceDoc( + getMessageDocsJsonSchema, + implementedInApiVersion, + nameOf(getMessageDocsJsonSchema), + "GET", + "/message-docs/CONNECTOR/json-schema", + "Get Message Docs as JSON Schema", + """Returns message documentation as JSON Schema format for code generation in any language. + | + |This endpoint provides machine-readable schemas instead of just examples, making it ideal for: + |- AI-powered code generation + |- Automatic adapter creation in multiple languages + |- Type-safe client generation with tools like quicktype + | + |**Supported Connectors:** + |- rabbitmq_vOct2024 - RabbitMQ connector message schemas + |- rest_vMar2019 - REST connector message schemas + |- akka_vDec2018 - Akka connector message schemas + |- kafka_vMay2019 - Kafka connector message schemas (if available) + | + |**Code Generation Examples:** + | + |Generate Scala code with Circe: + |```bash + |curl https://api.../message-docs/rabbitmq_vOct2024/json-schema > schemas.json + |quicktype -s schema schemas.json -o Messages.scala --framework circe + |``` + | + |Generate Python code: + |```bash + |quicktype -s schema schemas.json -o messages.py --lang python + |``` + | + |Generate TypeScript code: + |```bash + |quicktype -s schema schemas.json -o messages.ts --lang typescript + |``` + | + |**Schema Structure:** + |Each message includes: + |- `process` - The connector method name (e.g., "obp.getAdapterInfo") + |- `description` - Human-readable description of what the message does + |- `outbound_schema` - JSON Schema for request messages (OBP-API -> Adapter) + |- `inbound_schema` - JSON Schema for response messages (Adapter -> OBP-API) + | + |All nested type definitions are included in the `definitions` section for reuse. + | + |**Authentication:** + |This endpoint is publicly accessible (no authentication required) to facilitate adapter development. + | + |""".stripMargin, + EmptyBody, + EmptyBody, + List( + InvalidConnector, + UnknownError + ), + List(apiTagDocumentation, apiTagApi) + ) + + lazy val getMessageDocsJsonSchema: OBPEndpoint = { + case "message-docs" :: connector :: "json-schema" :: Nil JsonGet _ => { + cc => { + implicit val ec = EndpointContext(Some(cc)) + for { + (_, callContext) <- anonymousAccess(cc) + cacheKey = s"message-docs-json-schema-$connector" + cacheValueFromRedis = Caching.getStaticSwaggerDocCache(cacheKey) + jsonSchema <- if (cacheValueFromRedis.isDefined) { + NewStyle.function.tryons(s"$UnknownError Cannot parse cached JSON Schema.", 400, callContext) { + json.parse(cacheValueFromRedis.get).asInstanceOf[JObject] + } + } else { + NewStyle.function.tryons(s"$UnknownError Cannot generate JSON Schema.", 400, callContext) { + val connectorObjectBox = tryo{Connector.getConnectorInstance(connector)} + val connectorObject = unboxFullOrFail( + connectorObjectBox, + callContext, + s"$InvalidConnector Current input is: $connector. Valid connectors include: rabbitmq_vOct2024, rest_vMar2019, akka_vDec2018" + ) + val schema = JsonSchemaGenerator.messageDocsToJsonSchema( + connectorObject.messageDocs.toList, + connector + ) + val schemaString = json.compactRender(schema) + Caching.setStaticSwaggerDocCache(cacheKey, schemaString) + schema + } + } + } yield { + (jsonSchema, HttpCode.`200`(callContext)) + } + } + } + } + } }