Commit 7d467a35 authored by cdanger's avatar cdanger
Browse files

Merge branch 'release/3.0.1'

parents ddb2da3f 52a32bdc
......@@ -2,6 +2,12 @@
All notable changes to this project are documented in this file following the [Keep a CHANGELOG](http://keepachangelog.com) conventions. This project adheres to [Semantic Versioning](http://semver.org).
## 3.0.1
### Fixed
- JSON schema identifiers in XACML/JSON schemas: updated to valid links on github
- XacmlJsonUtils#canonicalizeResponse() to better identify similar XACML/JSON responses
## 3.0.0
### Changed
- Upgraded parent project version: 8.0.0
......
<?xml version="1.0" encoding="ISO-8859-1"?>
<additionalHeaders>
<javadoc_style>
<firstLine>/*</firstLine>
<beforeEachLine> * </beforeEachLine>
<endLine> */</endLine>
<!--<afterEachLine></afterEachLine>-->
<!--skipLine></skipLine-->
<firstLineDetectionPattern>(\s|\t)*/\*.*$</firstLineDetectionPattern>
<lastLineDetectionPattern>.*\*/(\s|\t)*$</lastLineDetectionPattern>
<allowBlankLines>false</allowBlankLines>
<isMultiline>true</isMultiline>
<padLines>false</padLines>
</javadoc_style>
</additionalHeaders>
......@@ -7,7 +7,7 @@
</parent>
<artifactId>authzforce-ce-xacml-json-model</artifactId>
<packaging>jar</packaging>
<version>3.0.0</version>
<version>3.0.1</version>
<name>${project.groupId}:${project.artifactId}</name>
<description>AuthzForce - Data model for JSON Profile of XACML 3.0</description>
<url>${project.url}</url>
......@@ -114,6 +114,9 @@
<artifactId>license-maven-plugin</artifactId>
<configuration>
<header>LICENSE_HEADER.txt</header>
<headerDefinitions>
<headerDefinition>LICENSE_HEADER_DEFS.xml</headerDefinition>
</headerDefinitions>
<includes>
<include>src/main/java/org/ow2/authzforce/**</include>
<!-- Include test files also -->
......
/**
/*
* Copyright 2012-2021 THALES.
*
* This file is part of AuthzForce CE.
......
/**
/*
* Copyright 2012-2021 THALES.
*
* This file is part of AuthzForce CE.
......
/**
/*
* Copyright 2012-2021 THALES.
*
* This file is part of AuthzForce CE.
......@@ -17,11 +17,6 @@
*/
package org.ow2.authzforce.xacml.json.model;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Map;
import org.everit.json.schema.Schema;
import org.everit.json.schema.loader.SchemaClient;
import org.everit.json.schema.loader.SchemaLoader;
......@@ -29,141 +24,257 @@ import org.json.JSONArray;
import org.json.JSONObject;
import org.json.JSONTokener;
import java.io.*;
import java.util.*;
/**
* Instances of JSON schema as defined by JSON Profile of XACML 3.0
*
*/
public final class XacmlJsonUtils
{
private XacmlJsonUtils()
{
// hide constructor
}
/**
* 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;
static
{
final Map<String, String> mutableCatalogMap = new HashMap<>();
mutableCatalogMap.put("http://authzforce.github.io/xacml-json-profile-model/schemas/1/common-std.schema.json", "classpath:org/ow2/authzforce/xacml/json/model/common-std.schema.json");
mutableCatalogMap.put("http://authzforce.github.io/xacml-json-profile-model/schemas/1/common-ng.schema.json", "classpath:org/ow2/authzforce/xacml/json/model/common-ng.schema.json");
final SchemaClient schemaClient = new SpringBasedJsonSchemaClient(mutableCatalogMap);
try (InputStream inputStream = SpringBasedJsonSchemaClient.class.getResourceAsStream("Request.schema.json"))
{
final JSONObject rawSchema = new JSONObject(new JSONTokener(inputStream));
// final SchemaLoader schemaLoader = schemaLoaderBuilder.schemaJson(rawSchema).build();
REQUEST_SCHEMA = SchemaLoader.load(rawSchema, schemaClient); // schemaLoader.load().build();
}
catch (final IOException e)
{
throw new RuntimeException(e);
}
try (InputStream inputStream = SpringBasedJsonSchemaClient.class.getResourceAsStream("Response.schema.json"))
{
final JSONObject rawSchema = new JSONObject(new JSONTokener(inputStream));
RESPONSE_SCHEMA = SchemaLoader.load(rawSchema, schemaClient);
}
catch (final IOException e)
{
throw new RuntimeException(e);
}
try (InputStream inputStream = SpringBasedJsonSchemaClient.class.getResourceAsStream("Policy.schema.json"))
{
final JSONObject rawSchema = new JSONObject(new JSONTokener(inputStream));
POLICY_SCHEMA = SchemaLoader.load(rawSchema, schemaClient);
}
catch (final IOException e)
{
throw new RuntimeException(e);
}
}
/**
* 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 results, 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.length() == 0)
{
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)
{
jsonArrayOfAtts.forEach(attJson -> {
assert attJson instanceof JSONObject;
final JSONObject attJsonObj = (JSONObject) attJson;
attJsonObj.remove("IncludeInResult");
});
}
}
}
}
}
return xacmlJsonResponse;
}
private static final SchemaClient CLASSPATH_AWARE_SCHEMA_CLIENT = SchemaClient.classPathAwareClient();
private static Schema loadSchema(String schemaFilenameRelativeToThisClass)
{
try (InputStream inputStream = XacmlJsonUtils.class.getResourceAsStream(schemaFilenameRelativeToThisClass))
{
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);
}
}
/**
* 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;
static
{
REQUEST_SCHEMA = loadSchema("Request.schema.json");
RESPONSE_SCHEMA = loadSchema("Response.schema.json");
POLICY_SCHEMA = loadSchema("Policy.schema.json");
}
/*
*/
private static void canonicalizeObligationsOrAdvice(JSONObject xacmlResult, String obligationsOrAdviceKey, boolean floatWithTrailingZeroToInt)
{
final JSONArray obligationsOrAdvice = xacmlResult.optJSONArray(obligationsOrAdviceKey);
if(obligationsOrAdvice != null) {
if (obligationsOrAdvice.length() == 0)
{
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)
{
if (jsonArrayOfAtts.length() == 0)
{
obligationJsonObj.remove("AttributeAssignment");
} else
{
for (final Object attJson : jsonArrayOfAtts)
{
assert attJson instanceof JSONObject;
final JSONObject attJsonObj = (JSONObject) attJson;
if (floatWithTrailingZeroToInt)
{
floatWithTrailingZeroToInt(attJsonObj);
}
}
}
}
}
}
}
}
/**
* 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
* FIXME: waiting for 'org.everity.json.schema' to upgrade dependency 'org.json:json' to v20210307 or later in order to fix https://github.com/stleary/JSON-java/issues/589
*
* @param xacmlJsonResponse
* input XACML Response
* @param floatWithTrailingZeroToInt true iff floats with trailing zero (after decimal point) in AttributeValues are converted to Integer, this is originally a workaround for <a href="https://github.com/stleary/JSON-java/issues/589">issue #589 on org.json:json library</a>, used by our dependency 'org.everity.json.schema', which has been fixed in v20210307 of org.json:json; but still waiting for 'org.everity.json.schema' to upgrade.
* @return canonicalized response
*/
public static JSONObject canonicalizeResponse(final JSONObject xacmlJsonResponse, boolean floatWithTrailingZeroToInt)
{
/*
* We iterate over all results, because for each results, 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.length() == 0)
{
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.length() == 0)
{
attCatJsonObj.remove("Attribute");
} else
{
for (final Object attJson : jsonArrayOfAtts)
{
assert attJson instanceof JSONObject;
final JSONObject attJsonObj = (JSONObject) attJson;
attJsonObj.remove("IncludeInResult");
if (floatWithTrailingZeroToInt)
{
floatWithTrailingZeroToInt(attJsonObj);
}
}
}
}
}
}
}
// Handle attribute values in Obligations and AssociatedAdvice if floatWithTrailingZeroToInt
canonicalizeObligationsOrAdvice(resultJsonObj, "Obligations", floatWithTrailingZeroToInt);
canonicalizeObligationsOrAdvice(resultJsonObj, "AssociatedAdvice", floatWithTrailingZeroToInt);
}
return xacmlJsonResponse;
}
/*
Returns a number if input was a Double with zero fraction, therefore converted to an integer type (BigInteger, Integer, Long, Short); else null
FIXME: workaround for this issue: https://github.com/stleary/JSON-java/issues/589
*/
private static Number floatWithTrailingZeroToInt(Object input)
{
if (input instanceof JSONObject)
{
final JSONObject json = (JSONObject) input;
final Map<String, Number> modifiedProperties = new HashMap<>();
json.keySet().forEach(key ->
{
final Number convertedIfNonNull = floatWithTrailingZeroToInt(json.get(key));
if (convertedIfNonNull != null)
{
// Double value to be changed to an integer
modifiedProperties.put(key, convertedIfNonNull);
}
});
// apply modifications if any
modifiedProperties.forEach(json::put);
return null;
}
if (input instanceof JSONArray)
{
final JSONArray json = (JSONArray) input;
final Deque<Map.Entry<Integer, Number>> 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 static void main(String[] args) throws FileNotFoundException
{
final JSONObject jsonWithTrailing0 = XacmlJsonUtils.canonicalizeResponse(new JSONObject(new JSONTokener(new FileInputStream(new File("~/git/authzforce-ce-server/webapp/src/test/resources/xacml.samples/pdp/GeoJSON_good/response.json")))), true);
System.out.println(jsonWithTrailing0.toString());
}
*/
}
{
"$schema": "http://json-schema.org/draft-06/schema",
"$id": "http://authzforce.github.io/xacml-json-profile-model/schemas/1/Policy.schema.json",
"$id": "https://raw.githubusercontent.com/authzforce/xacml-json-model/master/src/main/resources/org/ow2/authzforce/xacml/json/model/Policy.schema.json",
"title": "JSON schema of a policy object equivalent to XACML 3.0 Policy(Set)",
"definitions": {
"VariableReference": {
......
{
"$schema": "http://json-schema.org/draft-06/schema",
"$id": "http://authzforce.github.io/xacml-json-profile-model/schemas/1/Request.schema.json",
"$id": "https://raw.githubusercontent.com/authzforce/xacml-json-model/master/src/main/resources/org/ow2/authzforce/xacml/json/model/Request.schema.json",
"title": "JSON schema of Request object defined in JSON profile of XACML 3.0 v1.0",
"definitions":
{
......
{
"$schema": "http://json-schema.org/draft-06/schema",
"$id": "http://authzforce.github.io/xacml-json-profile-model/schemas/1/Response.schema.json",
"$id": "https://raw.githubusercontent.com/authzforce/xacml-json-model/master/src/main/resources/org/ow2/authzforce/xacml/json/model/Response.schema.json",
"title": "JSON schema of Response object defined in JSON profile of XACML 3.0 v1.0",
"definitions":
{
......
{
"$schema": "http://json-schema.org/draft-06/schema",
"$id": "http://authzforce.github.io/xacml-json-profile-model/schemas/1/common-ng.schema.json",
"$id": "https://raw.githubusercontent.com/authzforce/xacml-json-model/master/src/main/resources/org/ow2/authzforce/xacml/json/model/common-ng.schema.json",
"title": "Common JSON schema to Request and Response objects defined in JSON profile of XACML 3.0 v1.0",
"definitions":
{
......
{
"$schema": "http://json-schema.org/draft-06/schema",
"$id": "http://authzforce.github.io/xacml-json-profile-model/schemas/1/common-std.schema.json",
"$id": "https://raw.githubusercontent.com/authzforce/xacml-json-model/master/src/main/resources/org/ow2/authzforce/xacml/json/model/common-std.schema.json",
"title": "Common JSON schema to Request and Response objects defined in JSON profile of XACML 3.0 v1.0",
"definitions":
{
......
/**
/*
* Copyright 2012-2021 THALES.
*
* This file is part of AuthzForce CE.
......@@ -53,33 +53,30 @@ public class LimitsCheckingJSONObjectTest
/**
* Create test data. Various JSON files.
*
*
*
* @return iterator over test data
* @throws URISyntaxException
* @throws IOException
*/
@DataProvider(name = "jsonDataProvider")
public Iterator<Object[]> createData() throws URISyntaxException, IOException
public Iterator<Object[]> createData()
{
return TestDataProvider.createData(Arrays.stream(TEST_DATA_DIRECTORY_LOCATIONS).map(loc -> new AbstractMap.SimpleImmutableEntry<>(new File(loc), (File) null)).collect(Collectors.toList()));
}
@Test(dataProvider = "jsonDataProvider")
/**
*
* @param xacmlJsonFile
* @param xacmlJsonFile XACML/JSON file (Request or Response)
* @param expectedValid
* true iff validation against JSON schema should succeed
* @param srcXacmlXmlFile
* @param srcXacmlXmlFile original source XACML/XML file
* @param genXacmlXmlFileFromXslt
* Parameter not used by this test method but value returned anyway by the dataProvider (shared with other tests).
* @param testCtx
* @throws FileNotFoundException
* @throws IOException
* @param testCtx testng Test Context
* @throws JSONException error parsing JSON
* @throws IOException error reading input XACML files
*/
@Test(dataProvider = "jsonDataProvider")
public void test(final File xacmlJsonFile, final boolean expectedValid, final File srcXacmlXmlFile, final File genXacmlXmlFileFromXslt, final ITestContext testCtx)
throws FileNotFoundException, IOException, JSONException
throws IOException, JSONException
{
/*
* Read properly as UTF-8 to avoid character decoding issues with org.json API
......
/**
/*
* Copyright 2012-2021 THALES.
*
* This file is part of AuthzForce CE.
......@@ -23,6 +23,7 @@ import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.Map.Entry;
import java.util.Objects;
public final class TestDataProvider
{
......@@ -100,7 +101,7 @@ public final class TestDataProvider
}
}
for (final File jsonFile : validXacmlJsonFilesDir.listFiles(JSON_FILE_FILTER))
for (final File jsonFile : Objects.requireNonNull(validXacmlJsonFilesDir.listFiles(JSON_FILE_FILTER)))
{
/*
* Specific test's resources directory location, used as parameter to PdpTest(String). Check for a XML version of the file.
......@@ -127,7 +128,7 @@ public final class TestDataProvider
*/
final File invalidSrcXmlFilesDir = srcXmlFilesDirRelToMvnProj == null ? null : new File(srcXmlFilesDirRelToMvnProj, INVALID_TEST_DATA_DIRECTORY_NAME);
for (final File jsonFile : invalidXacmlJsonFilesDataDir.listFiles(JSON_FILE_FILTER))
for (final File json