diff --git a/components/api-resource-mgt/org.wso2.carbon.identity.api.resource.mgt/src/main/java/org/wso2/carbon/identity/api/resource/mgt/util/AuthorizationDetailsTypesUtil.java b/components/api-resource-mgt/org.wso2.carbon.identity.api.resource.mgt/src/main/java/org/wso2/carbon/identity/api/resource/mgt/util/AuthorizationDetailsTypesUtil.java index 78a18921d422..7cb413f03bec 100644 --- a/components/api-resource-mgt/org.wso2.carbon.identity.api.resource.mgt/src/main/java/org/wso2/carbon/identity/api/resource/mgt/util/AuthorizationDetailsTypesUtil.java +++ b/components/api-resource-mgt/org.wso2.carbon.identity.api.resource.mgt/src/main/java/org/wso2/carbon/identity/api/resource/mgt/util/AuthorizationDetailsTypesUtil.java @@ -19,6 +19,9 @@ package org.wso2.carbon.identity.api.resource.mgt.util; import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.ToNumberPolicy; +import com.google.gson.ToNumberStrategy; import com.google.gson.reflect.TypeToken; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -36,6 +39,23 @@ public class AuthorizationDetailsTypesUtil { private static final Log LOG = LogFactory.getLog(AuthorizationDetailsTypesUtil.class); private static final Gson GSON = new Gson(); + /* + * By default, Gson parses every JSON number of a Map as a Double. JSON schema validators such as + * Vert.x cast integer keywords (e.g. minItems, maxItems) directly to Integer, hence integral values (including + * ones written as 1.0) are returned as Integer, or as Long when they exceed the Integer range. + */ + private static final ToNumberStrategy INTEGRAL_PRESERVING_NUMBER_STRATEGY = in -> { + final Number number = ToNumberPolicy.LONG_OR_DOUBLE.readNumber(in); + final double doubleValue = number.doubleValue(); + if (doubleValue >= Integer.MIN_VALUE && doubleValue <= Integer.MAX_VALUE + && doubleValue == Math.rint(doubleValue)) { + return (int) doubleValue; + } + return number; + }; + private static final Gson INTEGRAL_PRESERVING_GSON = new GsonBuilder() + .setObjectToNumberStrategy(INTEGRAL_PRESERVING_NUMBER_STRATEGY) + .create(); private static final Type SCHEMA_TYPE = new TypeToken>() { }.getType(); /** @@ -49,6 +69,19 @@ public static Map parseSchema(final String schema) { return GSON.fromJson(schema, SCHEMA_TYPE); } + /** + * Parses a JSON schema into a map structure to be used for authorization details schema validation. Unlike + * {@link #parseSchema(String)}, integral numbers are returned as {@code Integer} (or {@code Long}) instead of + * {@code Double}, as JSON schema validators expect integer keywords such as {@code minItems} to be integers. + * + * @param schema the JSON schema string to be parsed. It must be a valid JSON string. + * @return a {@code Map} representing the parsed JSON schema. + */ + public static Map parseSchemaForValidation(final String schema) { + + return INTEGRAL_PRESERVING_GSON.fromJson(schema, SCHEMA_TYPE); + } + /** * Converts a map representing a schema into its JSON string representation. * diff --git a/components/api-resource-mgt/org.wso2.carbon.identity.api.resource.mgt/src/test/java/org/wso2/carbon/identity/api/resource/mgt/AuthorizationDetailsTypesUtilTest.java b/components/api-resource-mgt/org.wso2.carbon.identity.api.resource.mgt/src/test/java/org/wso2/carbon/identity/api/resource/mgt/AuthorizationDetailsTypesUtilTest.java new file mode 100644 index 000000000000..48897670f2cd --- /dev/null +++ b/components/api-resource-mgt/org.wso2.carbon.identity.api.resource.mgt/src/test/java/org/wso2/carbon/identity/api/resource/mgt/AuthorizationDetailsTypesUtilTest.java @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2026, WSO2 LLC. (http://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.wso2.carbon.identity.api.resource.mgt; + +import org.testng.Assert; +import org.testng.annotations.Test; +import org.wso2.carbon.identity.api.resource.mgt.util.AuthorizationDetailsTypesUtil; + +import java.util.List; +import java.util.Map; + +/** + * Test class for {@link AuthorizationDetailsTypesUtil}. + */ +public class AuthorizationDetailsTypesUtilTest { + + private static final String SCHEMA = "{\"type\":\"object\",\"required\":[\"type\"],\"minProperties\":1," + + "\"properties\":{\"type\":{\"type\":\"string\",\"const\":\"test_type\"}," + + "\"actions\":{\"type\":\"array\",\"items\":{\"type\":\"string\"},\"minItems\":1,\"maxItems\":3.0}," + + "\"amount\":{\"type\":\"number\",\"minimum\":0.5,\"maximum\":9999999999}}," + + "\"allOf\":[{\"maxProperties\":5}]}"; + + @Test + @SuppressWarnings("unchecked") + public void testParseSchemaForValidationPreservesIntegralValues() { + + final Map schema = AuthorizationDetailsTypesUtil.parseSchemaForValidation(SCHEMA); + final Map properties = (Map) schema.get("properties"); + final Map actions = (Map) properties.get("actions"); + final Map amount = (Map) properties.get("amount"); + final Map allOf = ((List>) schema.get("allOf")).get(0); + + Assert.assertEquals(schema.get("minProperties"), 1); + Assert.assertEquals(actions.get("minItems"), 1); + Assert.assertEquals(actions.get("maxItems"), 3); + Assert.assertEquals(allOf.get("maxProperties"), 5); + Assert.assertEquals(amount.get("minimum"), 0.5d); + Assert.assertEquals(amount.get("maximum"), 9999999999L); + } + + @Test + @SuppressWarnings("unchecked") + public void testParseSchemaKeepsExistingNumberFormat() { + + final Map schema = AuthorizationDetailsTypesUtil.parseSchema(SCHEMA); + final Map actions = (Map) ((Map) schema.get("properties")) + .get("actions"); + + Assert.assertEquals(schema.get("minProperties"), 1.0d); + Assert.assertEquals(actions.get("minItems"), 1.0d); + Assert.assertEquals(actions.get("maxItems"), 3.0d); + } +} diff --git a/components/api-resource-mgt/org.wso2.carbon.identity.api.resource.mgt/src/test/resources/testng.xml b/components/api-resource-mgt/org.wso2.carbon.identity.api.resource.mgt/src/test/resources/testng.xml index 4d8e1a82e607..aede5dbcf9bd 100644 --- a/components/api-resource-mgt/org.wso2.carbon.identity.api.resource.mgt/src/test/resources/testng.xml +++ b/components/api-resource-mgt/org.wso2.carbon.identity.api.resource.mgt/src/test/resources/testng.xml @@ -26,6 +26,7 @@ + diff --git a/components/application-mgt/org.wso2.carbon.identity.application.mgt/src/main/java/org/wso2/carbon/identity/application/mgt/dao/impl/AuthorizedAPIDAOImpl.java b/components/application-mgt/org.wso2.carbon.identity.application.mgt/src/main/java/org/wso2/carbon/identity/application/mgt/dao/impl/AuthorizedAPIDAOImpl.java index a9a95e72f490..3948e2584c4a 100644 --- a/components/application-mgt/org.wso2.carbon.identity.application.mgt/src/main/java/org/wso2/carbon/identity/application/mgt/dao/impl/AuthorizedAPIDAOImpl.java +++ b/components/application-mgt/org.wso2.carbon.identity.application.mgt/src/main/java/org/wso2/carbon/identity/application/mgt/dao/impl/AuthorizedAPIDAOImpl.java @@ -1170,7 +1170,7 @@ private AuthorizationDetailsType buildAuthorizationDetailsTypeWithSchema(ResultS final AuthorizationDetailsType authorizationDetailsType = this.buildAuthorizationDetailsType(resultSet); if (authorizationDetailsType != null) { - authorizationDetailsType.setSchema(AuthorizationDetailsTypesUtil.parseSchema(resultSet + authorizationDetailsType.setSchema(AuthorizationDetailsTypesUtil.parseSchemaForValidation(resultSet .getString(ApplicationConstants.ApplicationTableColumns.AUTHORIZATION_DETAILS_SCHEMA))); }