> modifiedItems = new ArrayDeque<>(json.length());
- int index = 0;
- for (final Object item : json)
- {
- final Number convertedIfNonNull = floatWithTrailingZeroToInt(item);
- if (convertedIfNonNull != null)
- {
- // Double value to be changed to an integer
- modifiedItems.addLast(new AbstractMap.SimpleImmutableEntry<>(index, convertedIfNonNull));
- }
- index++;
- }
- // apply modifications if any
- modifiedItems.forEach(e -> json.put(e.getKey(), e.getValue()));
- return null;
- }
-
- if (input instanceof Double)
- {
- // FIXME: workaround for this issue: https://github.com/stleary/JSON-java/issues/589
- // if there is some trailing zero, this Double is considered equivalent to an int
- // The corresponding int is obtained after serializing/deserializing
- final String serialized = JSONObject.valueToString(input);
- final Object deserialized = JSONObject.stringToValue(serialized);
- if (!(deserialized instanceof Double))
- {
- // value was converted to an int
- assert deserialized instanceof Number;
- return (Number) deserialized;
- }
- }
-
- // nothing to change
- return null;
- }
-
- private XacmlJsonUtils()
- {
- // hide constructor
- }
+public final class XacmlJsonUtils {
+ /**
+ * JSON schema for validating Requests according to JSON Profile of XACML 3.0
+ */
+ public static final Schema REQUEST_SCHEMA;
+
+ /**
+ * JSON schema for validating Responses according to JSON Profile of XACML 3.0
+ */
+ public static final Schema RESPONSE_SCHEMA;
+
+ /**
+ * JSON schema for validating Policies according to AuthzForce/JSON policy format for XACML Policy(Set) (see Policy.schema.json)
+ */
+ public static final Schema POLICY_SCHEMA;
+
+ private static final SchemaClient CLASSPATH_AWARE_SCHEMA_CLIENT = SchemaClient.classPathAwareClient();
+
+ static {
+ REQUEST_SCHEMA = loadSchema("Request.schema.json");
+ RESPONSE_SCHEMA = loadSchema("Response.schema.json");
+ POLICY_SCHEMA = loadSchema("Policy.schema.json");
+ }
+
+ private XacmlJsonUtils() {
+ // hide constructor
+ }
+
+ private static Schema loadSchema(String schemaFilenameRelativeToThisClass) {
+ try (InputStream inputStream = XacmlJsonUtils.class.getResourceAsStream(schemaFilenameRelativeToThisClass)) {
+ if (inputStream == null) {
+ throw new RuntimeException("No resource name '" + schemaFilenameRelativeToThisClass + "' found on the classpath");
+ }
+
+ final JSONObject rawSchema = new JSONObject(new JSONTokener(inputStream));
+ return SchemaLoader.builder().schemaJson(rawSchema).schemaClient(CLASSPATH_AWARE_SCHEMA_CLIENT).resolutionScope("classpath://org/ow2/authzforce/xacml/json/model/").build().load().build();
+ }
+ catch (final IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /*
+
+ */
+ private static void canonicalizeObligationsOrAdvice(JSONObject xacmlResult, String obligationsOrAdviceKey) {
+ final JSONArray obligationsOrAdvice = xacmlResult.optJSONArray(obligationsOrAdviceKey);
+ if (obligationsOrAdvice != null) {
+ if (obligationsOrAdvice.isEmpty()) {
+ xacmlResult.remove(obligationsOrAdviceKey);
+ }
+ else {
+ for (final Object obligation : obligationsOrAdvice) {
+ assert obligation instanceof JSONObject;
+ final JSONObject obligationJsonObj = (JSONObject) obligation;
+ final JSONArray jsonArrayOfAtts = obligationJsonObj.optJSONArray("AttributeAssignment");
+ if (jsonArrayOfAtts != null && jsonArrayOfAtts.isEmpty()) {
+ obligationJsonObj.remove("AttributeAssignment");
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Canonicalize a XACML/JSON response, typically for comparison with another one. In particular, it removes every Result's status as we choose to ignore the Status. Indeed, a PDP implementation
+ * might return a perfectly XACML-compliant response but with extra StatusCode/Message/Detail that we would not expect.
+ *
+ * WARNING: this method modifies the content of {@code xacmlJsonResponse} directly
+ *
+ * @param xacmlJsonResponse
+ * input XACML Response
+ * @return canonicalized response
+ */
+ public static JSONObject canonicalizeResponse(final JSONObject xacmlJsonResponse) {
+ /*
+ * We iterate over all results, because for each result, we don't compare everything. In particular, we choose to ignore the StatusMessage, StatusDetail and any nested StatusCode. Indeed, a
+ * PDP implementation might return a perfectly XACML-compliant response but with extra StatusCode/Message/Detail that we would not expect.
+ */
+ for (final Object resultObj : xacmlJsonResponse.getJSONArray("Response")) {
+ final JSONObject resultJsonObj = (JSONObject) resultObj;
+ // Status
+ final JSONObject statusJsonObj = resultJsonObj.optJSONObject("Status");
+ if (statusJsonObj != null) {
+ // remove Status if StatusCode OK (optional, default implicit therefore useless)
+ final JSONObject statusCodeJsonObj = statusJsonObj.getJSONObject("StatusCode");
+ final String statusCodeVal = statusCodeJsonObj.getString("Value");
+ if (statusCodeVal.equals("urn:oasis:names:tc:xacml:1.0:status:ok")) {
+ // Status OK is useless, simplify
+ resultJsonObj.remove("Status");
+ }
+ else {
+ // remove any nested status code, StatusMessage and StatusDetail
+ statusCodeJsonObj.remove("StatusCode");
+ statusJsonObj.remove("StatusMessage");
+ statusJsonObj.remove("StatusDetail");
+ }
+ }
+
+ // remove empty Category array if any
+ final JSONArray jsonArrayOfAttCats = resultJsonObj.optJSONArray("Category");
+ if (jsonArrayOfAttCats != null) {
+ if (jsonArrayOfAttCats.isEmpty()) {
+ resultJsonObj.remove("Category");
+ }
+ else {
+ /*
+ * Remove any IncludeInResult property which is useless and optional in XACML/JSON. (NB.: IncludeInResult is mandatory in XACML/XML schema but optional in JSON Profile).
+ */
+ for (final Object attCatJson : jsonArrayOfAttCats) {
+ assert attCatJson instanceof JSONObject;
+ final JSONObject attCatJsonObj = (JSONObject) attCatJson;
+ final JSONArray jsonArrayOfAtts = attCatJsonObj.optJSONArray("Attribute");
+ if (jsonArrayOfAtts != null) {
+ if (jsonArrayOfAtts.isEmpty()) {
+ attCatJsonObj.remove("Attribute");
+ }
+ else {
+ for (final Object attJson : jsonArrayOfAtts) {
+ assert attJson instanceof JSONObject;
+ final JSONObject attJsonObj = (JSONObject) attJson;
+ attJsonObj.remove("IncludeInResult");
+ }
+ }
+ }
+ }
+ }
+ }
+
+ // Handle attribute values in Obligations and AssociatedAdvice if floatWithTrailingZeroToInt
+ canonicalizeObligationsOrAdvice(resultJsonObj, "Obligations");
+ canonicalizeObligationsOrAdvice(resultJsonObj, "AssociatedAdvice");
+ }
+
+ return xacmlJsonResponse;
+ }
/*
public static void main(String[] args) throws FileNotFoundException
diff --git a/src/test/java/org/ow2/authzforce/xacml/json/model/test/XacmlJsonSchemaValidationTest.java b/src/test/java/org/ow2/authzforce/xacml/json/model/test/XacmlJsonSchemaValidationTest.java
index e9cf7d8..bc16466 100644
--- a/src/test/java/org/ow2/authzforce/xacml/json/model/test/XacmlJsonSchemaValidationTest.java
+++ b/src/test/java/org/ow2/authzforce/xacml/json/model/test/XacmlJsonSchemaValidationTest.java
@@ -17,25 +17,13 @@
*/
package org.ow2.authzforce.xacml.json.model.test;
-import org.everit.json.schema.Schema;
-import org.everit.json.schema.ValidationException;
-import org.json.JSONObject;
-import org.ow2.authzforce.xacml.Xacml3JaxbHelper;
-import org.ow2.authzforce.xacml.json.model.LimitsCheckingJSONObject;
-import org.ow2.authzforce.xacml.json.model.XacmlJsonUtils;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.testng.Assert;
-import org.testng.ITestContext;
-import org.testng.annotations.DataProvider;
-import org.testng.annotations.Test;
-
-import javax.xml.bind.JAXBException;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
+import java.nio.file.DirectoryStream;
import java.nio.file.Files;
+import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.AbstractMap;
import java.util.Arrays;
@@ -44,8 +32,21 @@
import java.util.Map.Entry;
import java.util.stream.Collectors;
-public class XacmlJsonSchemaValidationTest
-{
+import jakarta.xml.bind.JAXBException;
+import org.everit.json.schema.Schema;
+import org.everit.json.schema.ValidationException;
+import org.json.JSONObject;
+import org.ow2.authzforce.xacml.Xacml3JaxbHelper;
+import org.ow2.authzforce.xacml.json.model.LimitsCheckingJSONObject;
+import org.ow2.authzforce.xacml.json.model.XacmlJsonUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.testng.Assert;
+import org.testng.ITestContext;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+public class XacmlJsonSchemaValidationTest {
private static final Logger LOGGER = LoggerFactory.getLogger(XacmlJsonSchemaValidationTest.class);
private static final int MAX_JSON_STRING_LENGTH = 65536;
@@ -61,37 +62,35 @@ public class XacmlJsonSchemaValidationTest
* Source XACML/JSON files (not generated from XACML/XML files)
*/
private static final String[] SRC_XACML_JSON_DATA_DIRECTORY_LOCATIONS = { "src/test/resources/xacml+json.samples/Policies", "src/test/resources/xacml+json.samples/Requests",
- "src/test/resources/xacml+json.samples/Responses" };
+ "src/test/resources/xacml+json.samples/Responses" };
private static final String[] SRC_XACML_XML_CONFORMANCE_TEST_DATA_PARENT_DIRECTORY_LOCATIONS = { "src/test/resources/xacml+xml.samples/xacml-3.0-ct/mandatory",
- "src/test/resources/xacml+xml.samples/xacml-3.0-ct/optional" };
+ "src/test/resources/xacml+xml.samples/xacml-3.0-ct/optional" };
private static final String[] GEN_XACML_XML_CONFORMANCE_TEST_DATA_PARENT_DIRECTORY_LOCATIONS = { "target/generated-test-resources/xacml-xslt-outputs/xacml-3.0-ct/mandatory",
- "target/generated-test-resources/xacml-xslt-outputs/xacml-3.0-ct/optional" };
+ "target/generated-test-resources/xacml-xslt-outputs/xacml-3.0-ct/optional" };
/**
* Create test data. Various Requests/Responses in XACML JSON Profile defined format
- *
- *
+ *
+ *
* @return iterator over test data
*/
@DataProvider(name = "xacmlJsonDataProvider")
- public Iterator