diff --git a/obp-api/src/main/resources/props/sample.props.template b/obp-api/src/main/resources/props/sample.props.template index 4c081fde8..087163b68 100644 --- a/obp-api/src/main/resources/props/sample.props.template +++ b/obp-api/src/main/resources/props/sample.props.template @@ -720,9 +720,10 @@ super_admin_user_ids=USER_ID1,USER_ID2, # For a VERSION (the version in path e.g. /obp/v4.0.0) to be allowed, it must be: # 1) Absent from here (high priority): -# Note the default is empty, not the example here. # Black List of Versions -#api_disabled_versions=[OBPv3.0.0,BGv1.3] +# Since December 2025 we are removing older versions of OBP standards by default. Note any endpoints defined in these versions are still +# available via calling v5.1.0 or v6.0.0. We're doing this so API Explorer (II) loads faster etc. +#api_disabled_versions=["OBPv1.2.1,OBPv1.3.0,OBPv1.4.0,OBPv2.0.0,OBPv2.1.0,OBPv2.2.0,OBPv3.0.0,OBPv3.1.0,OBPv4.0.0,OBPv5.0.0"] # 2) Present here OR this entry must be empty: # Note the default is empty, not the example here. diff --git a/obp-api/src/main/scala/code/api/util/APIUtil.scala b/obp-api/src/main/scala/code/api/util/APIUtil.scala index f4af4b3f6..28c55e03e 100644 --- a/obp-api/src/main/scala/code/api/util/APIUtil.scala +++ b/obp-api/src/main/scala/code/api/util/APIUtil.scala @@ -2678,7 +2678,14 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{ case JField("ccy", x) => JField("currency", x) } - def getDisabledVersions() : List[String] = APIUtil.getPropsValue("api_disabled_versions").getOrElse("").replace("[", "").replace("]", "").split(",").toList.filter(_.nonEmpty) + def getDisabledVersions() : List[String] = { + val defaultDisabledVersions = "OBPv1.2.1,OBPv1.3.0,OBPv1.4.0,OBPv2.0.0,OBPv2.1.0,OBPv2.2.0,OBPv3.0.0,OBPv3.1.0,OBPv4.0.0,OBPv5.0.0" + val disabledVersions = APIUtil.getPropsValue("api_disabled_versions").getOrElse(defaultDisabledVersions).replace("[", "").replace("]", "").split(",").toList.filter(_.nonEmpty) + if (disabledVersions.nonEmpty) { + logger.info(s"Disabled API versions: ${disabledVersions.mkString(", ")}") + } + disabledVersions + } def getDisabledEndpointOperationIds() : List[String] = APIUtil.getPropsValue("api_disabled_endpoints").getOrElse("").replace("[", "").replace("]", "").split(",").toList.filter(_.nonEmpty) 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 fb0b4e5c4..bb32e82aa 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 @@ -26,7 +26,7 @@ import code.api.v5_0_0.JSONFactory500 import code.api.v5_0_0.{ViewJsonV500, ViewsJsonV500} import code.api.v5_1_0.{JSONFactory510, PostCustomerLegalNameJsonV510} import code.api.dynamic.entity.helper.{DynamicEntityHelper, DynamicEntityInfo} -import code.api.v6_0_0.JSONFactory600.{DynamicEntityDiagnosticsJsonV600, DynamicEntityIssueJsonV600, GroupJsonV600, GroupMembershipJsonV600, GroupMembershipsJsonV600, GroupsJsonV600, PostGroupJsonV600, PostGroupMembershipJsonV600, PostResetPasswordUrlJsonV600, PutGroupJsonV600, ReferenceTypeJsonV600, ReferenceTypesJsonV600, ResetPasswordUrlJsonV600, RoleWithEntitlementCountJsonV600, RolesWithEntitlementCountsJsonV600, ValidateUserEmailJsonV600, ValidateUserEmailResponseJsonV600, createActiveCallLimitsJsonV600, createCallLimitJsonV600, createCurrentUsageJson} +import code.api.v6_0_0.JSONFactory600.{DynamicEntityDiagnosticsJsonV600, DynamicEntityIssueJsonV600, GroupJsonV600, GroupMembershipJsonV600, GroupMembershipsJsonV600, GroupsJsonV600, PostGroupJsonV600, PostGroupMembershipJsonV600, PostResetPasswordUrlJsonV600, PutGroupJsonV600, ReferenceTypeJsonV600, ReferenceTypesJsonV600, ResetPasswordUrlJsonV600, RoleWithEntitlementCountJsonV600, RolesWithEntitlementCountsJsonV600, ScannedApiVersionJsonV600, ValidateUserEmailJsonV600, ValidateUserEmailResponseJsonV600, createActiveCallLimitsJsonV600, createCallLimitJsonV600, createCurrentUsageJson} import code.api.v6_0_0.OBPAPI6_0_0 import code.metrics.APIMetrics import code.bankconnectors.LocalMappedConnectorInternal @@ -65,6 +65,7 @@ import scala.collection.immutable.{List, Nil} import scala.collection.mutable.ArrayBuffer import scala.concurrent.Future import scala.concurrent.duration._ +import scala.jdk.CollectionConverters._ import scala.util.Random @@ -1245,6 +1246,90 @@ trait APIMethods600 { } } + staticResourceDocs += ResourceDoc( + getScannedApiVersions, + implementedInApiVersion, + nameOf(getScannedApiVersions), + "GET", + "/api/versions", + "Get Scanned API Versions", + s"""Get all scanned API versions available in this codebase. + | + |This endpoint returns all API versions that have been discovered/scanned, along with their active status. + | + |**Response Fields:** + | + |* `url_prefix`: The URL prefix for the version (e.g., "obp", "berlin-group", "open-banking") + |* `api_standard`: The API standard name (e.g., "OBP", "BG", "UK", "STET") + |* `api_short_version`: The version number (e.g., "v4.0.0", "v1.3") + |* `fully_qualified_version`: The fully qualified version combining standard and version (e.g., "OBPv4.0.0", "BGv1.3") + |* `is_active`: Boolean indicating if the version is currently enabled and accessible + | + |**Active Status:** + | + |* `is_active=true`: Version is enabled and can be accessed via its URL prefix + |* `is_active=false`: Version is scanned but disabled (via `api_disabled_versions` props) + | + |**Use Cases:** + | + |* Discover what API versions are available in the codebase + |* Check which versions are currently enabled + |* Verify that disabled versions configuration is working correctly + |* API documentation and discovery + | + |**Note:** This differs from v4.0.0's `/api/versions` endpoint which shows all scanned versions without is_active status. + | + |""", + EmptyBody, + ListResult( + "scanned_api_versions", + List( + ScannedApiVersionJsonV600(url_prefix = "obp", api_standard = "OBP", api_short_version = "v1.2.1", fully_qualified_version = "OBPv1.2.1", is_active = true), + ScannedApiVersionJsonV600(url_prefix = "obp", api_standard = "OBP", api_short_version = "v1.3.0", fully_qualified_version = "OBPv1.3.0", is_active = true), + ScannedApiVersionJsonV600(url_prefix = "obp", api_standard = "OBP", api_short_version = "v1.4.0", fully_qualified_version = "OBPv1.4.0", is_active = true), + ScannedApiVersionJsonV600(url_prefix = "obp", api_standard = "OBP", api_short_version = "v2.0.0", fully_qualified_version = "OBPv2.0.0", is_active = true), + ScannedApiVersionJsonV600(url_prefix = "obp", api_standard = "OBP", api_short_version = "v2.1.0", fully_qualified_version = "OBPv2.1.0", is_active = true), + ScannedApiVersionJsonV600(url_prefix = "obp", api_standard = "OBP", api_short_version = "v2.2.0", fully_qualified_version = "OBPv2.2.0", is_active = true), + ScannedApiVersionJsonV600(url_prefix = "obp", api_standard = "OBP", api_short_version = "v3.0.0", fully_qualified_version = "OBPv3.0.0", is_active = true), + ScannedApiVersionJsonV600(url_prefix = "obp", api_standard = "OBP", api_short_version = "v3.1.0", fully_qualified_version = "OBPv3.1.0", is_active = true), + ScannedApiVersionJsonV600(url_prefix = "obp", api_standard = "OBP", api_short_version = "v4.0.0", fully_qualified_version = "OBPv4.0.0", is_active = true), + ScannedApiVersionJsonV600(url_prefix = "obp", api_standard = "OBP", api_short_version = "v5.0.0", fully_qualified_version = "OBPv5.0.0", is_active = true), + ScannedApiVersionJsonV600(url_prefix = "obp", api_standard = "OBP", api_short_version = "v5.1.0", fully_qualified_version = "OBPv5.1.0", is_active = true), + ScannedApiVersionJsonV600(url_prefix = "obp", api_standard = "OBP", api_short_version = "v6.0.0", fully_qualified_version = "OBPv6.0.0", is_active = true), + ScannedApiVersionJsonV600(url_prefix = "berlin-group", api_standard = "BG", api_short_version = "v1.3", fully_qualified_version = "BGv1.3", is_active = false) + ) + ), + List( + UnknownError + ), + List(apiTagDocumentation, apiTagApi), + Some(Nil) + ) + + lazy val getScannedApiVersions: OBPEndpoint = { + case "api" :: "versions" :: Nil JsonGet _ => { cc => + implicit val ec = EndpointContext(Some(cc)) + Future { + val versions: List[ScannedApiVersionJsonV600] = + ApiVersion.allScannedApiVersion.asScala.toList + .filter(version => version.urlPrefix.trim.nonEmpty) + .map { version => + ScannedApiVersionJsonV600( + url_prefix = version.urlPrefix, + api_standard = version.apiStandard, + api_short_version = version.apiShortVersion, + fully_qualified_version = version.fullyQualifiedVersion, + is_active = versionIsAllowed(version) + ) + } + ( + ListResult("scanned_api_versions", versions), + HttpCode.`200`(cc.callContext) + ) + } + } + } + staticResourceDocs += ResourceDoc( createCustomer, implementedInApiVersion, diff --git a/obp-api/src/main/scala/code/api/v6_0_0/JSONFactory6.0.0.scala b/obp-api/src/main/scala/code/api/v6_0_0/JSONFactory6.0.0.scala index 7a2d75184..30b69c059 100644 --- a/obp-api/src/main/scala/code/api/v6_0_0/JSONFactory6.0.0.scala +++ b/obp-api/src/main/scala/code/api/v6_0_0/JSONFactory6.0.0.scala @@ -646,4 +646,12 @@ case class PostResetPasswordUrlJsonV600(username: String, email: String, user_id case class ResetPasswordUrlJsonV600(reset_password_url: String) +case class ScannedApiVersionJsonV600( + url_prefix: String, + api_standard: String, + api_short_version: String, + fully_qualified_version: String, + is_active: Boolean +) + } diff --git a/obp-api/src/main/scala/code/api/v6_0_0/OBPAPI6_0_0.scala b/obp-api/src/main/scala/code/api/v6_0_0/OBPAPI6_0_0.scala index 747a2d928..6b1868e7d 100644 --- a/obp-api/src/main/scala/code/api/v6_0_0/OBPAPI6_0_0.scala +++ b/obp-api/src/main/scala/code/api/v6_0_0/OBPAPI6_0_0.scala @@ -77,11 +77,36 @@ object OBPAPI6_0_0 extends OBPRestHelper // Exclude v5.1.0 root endpoint since v6.0.0 has its own lazy val endpointsOf5_1_0_without_root = OBPAPI5_1_0.routes.filterNot(_ == Implementations5_1_0.root) + /* + * IMPORTANT: Endpoint Exclusion Pattern + * + * excludeEndpoints is used to filter out old endpoints when v6.0.0 has a DIFFERENT URL pattern. + * + * WHEN TO EXCLUDE: + * - Old and new endpoints have DIFFERENT URLs (e.g., v4.0.0: /users/:username vs v6.0.0: /providers/:provider/users/:username) + * - The old endpoint should not be accessible via v6.0.0 at all + * + * WHEN NOT TO EXCLUDE: + * - Old and new endpoints have the SAME URL and HTTP method (e.g., GET /api/versions) + * - In this case, collectResourceDocs() automatically deduplicates by (URL, method) and keeps newest version + * - Excluding by function name would remove BOTH versions since they share the same name! + * + * Why? The routing works as follows: + * 1. endpoints list = endpointsOf6_0_0 ++ endpointsOf5_1_0_without_root (contains BOTH old and new) + * 2. allResourceDocs = collectResourceDocs() deduplicates docs by (URL, method), keeps newest + * 3. excludeEndpoints filters ResourceDocs by partialFunctionName (removes by name, not by version) + * 4. getAllowedEndpoints() filters endpoints to only those with matching ResourceDocs + * + * Pattern: Add nameOf(Implementations{version}.endpointName) :: with a comment explaining why + */ lazy val excludeEndpoints = nameOf(Implementations3_0_0.getUserByUsername) :: // following 4 endpoints miss Provider parameter in the URL, we introduce new ones in V600. nameOf(Implementations3_1_0.getBadLoginStatus) :: nameOf(Implementations3_1_0.unlockUser) :: nameOf(Implementations4_0_0.lockUser) :: + // NOTE: getScannedApiVersions is NOT excluded here because it has the same URL in both v4.0.0 and v6.0.0 + // collectResourceDocs() automatically deduplicates by (URL, HTTP method) and keeps the newest version (v6.0.0) + // Excluding by function name would incorrectly filter out BOTH versions since they share the same function name nameOf(Implementations4_0_0.createUserWithAccountAccess) :: // following 3 endpoints miss ViewId parameter in the URL, we introduce new ones in V600. nameOf(Implementations4_0_0.grantUserAccessToView) :: nameOf(Implementations4_0_0.revokeUserAccessToView) ::