Skip to content
Snippets Groups Projects
Commit b49c49cd authored by Cyril Dangerville's avatar Cyril Dangerville
Browse files

- Upgraded parent project: 9.0.0

- Removed workaround for https://github.com/stleary/JSON-java/issues/589 after upgrade of org.everity.json.schema dependency
parent d4f5782e
No related branches found
No related tags found
No related merge requests found
......@@ -3,7 +3,7 @@
<parent>
<groupId>org.ow2.authzforce</groupId>
<artifactId>authzforce-ce-parent</artifactId>
<version>8.5.0</version>
<version>9.0.0</version>
</parent>
<artifactId>authzforce-ce-xacml-json-model</artifactId>
<packaging>jar</packaging>
......@@ -38,7 +38,7 @@
<dependency>
<groupId>org.testng</groupId>
<artifactId>testng</artifactId>
<version>6.11</version>
<version>7.8.0</version>
<scope>test</scope>
</dependency>
<dependency>
......
......@@ -168,7 +168,7 @@ public final class LimitsCheckingJSONObject extends JSONObject
this.back();
string = sb.toString().trim();
if ("".equals(string))
if (string.isEmpty())
{
throw this.syntaxError("Missing value");
}
......
......@@ -17,6 +17,9 @@
*/
package org.ow2.authzforce.xacml.json.model;
import java.io.IOException;
import java.io.InputStream;
import org.everit.json.schema.Schema;
import org.everit.json.schema.loader.SchemaClient;
import org.everit.json.schema.loader.SchemaLoader;
......@@ -24,263 +27,146 @@ 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 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);
}
}
}
}
}
}
}
}
/**
* Same as {@link #canonicalizeResponse(JSONObject, boolean)} but with second parameter set to false.
*
* @param xacmlJsonResponse
* input XACML Response
* @return canonicalized response
*/
public static JSONObject canonicalizeResponse(final JSONObject xacmlJsonResponse)
{
return canonicalizeResponse(xacmlJsonResponse, false);
}
/**
* 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 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.
*<p>
* WARNING: this method modifies the content of {@code xacmlJsonResponse} directly
*</p>
* @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
......
......@@ -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.List;
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<Object[]> createData()
{
public Iterator<Object[]> createData() {
final List<Entry<File, File>> testDataDirLocations = Arrays.stream(SRC_XACML_JSON_DATA_DIRECTORY_LOCATIONS).map(loc -> new AbstractMap.SimpleImmutableEntry<>(new File(loc), (File) null))
.collect(Collectors.toList());
for (int i = 0; i < GEN_XACML_XML_CONFORMANCE_TEST_DATA_PARENT_DIRECTORY_LOCATIONS.length; i++)
{
.collect(Collectors.toList());
for (int i = 0; i < GEN_XACML_XML_CONFORMANCE_TEST_DATA_PARENT_DIRECTORY_LOCATIONS.length; i++) {
final String loc = GEN_XACML_XML_CONFORMANCE_TEST_DATA_PARENT_DIRECTORY_LOCATIONS[i];
try
{
try {
final int testDataParentDirIndex = i;
Files.newDirectoryStream(Paths.get(loc)).forEach(testDataDirPath -> testDataDirLocations.add(new AbstractMap.SimpleImmutableEntry<>(testDataDirPath.toFile(),
new File(SRC_XACML_XML_CONFORMANCE_TEST_DATA_PARENT_DIRECTORY_LOCATIONS[testDataParentDirIndex], testDataDirPath.getFileName().toString()))));
try (DirectoryStream<Path> dirStream = Files.newDirectoryStream(Paths.get(loc))) {
dirStream.forEach(testDataDirPath -> testDataDirLocations.add(new AbstractMap.SimpleImmutableEntry<>(testDataDirPath.toFile(),
new File(SRC_XACML_XML_CONFORMANCE_TEST_DATA_PARENT_DIRECTORY_LOCATIONS[testDataParentDirIndex], testDataDirPath.getFileName().toString()))));
}
i++;
}
catch (final IOException e)
{
catch (final IOException e) {
throw new RuntimeException("I/O error opening location '" + loc + "' as directory to iterate over its entries", e);
}
}
......@@ -100,7 +99,7 @@ public class XacmlJsonSchemaValidationTest
}
/**
*
*
* @param xacmlJsonFile XACML/JSON file
* @param expectedXacmlXmlFile expected XACML/XML file from conversion of {@code xacmlJsonFile}
* @param expectedValid
......@@ -114,50 +113,40 @@ public class XacmlJsonSchemaValidationTest
*/
@Test(dataProvider = "xacmlJsonDataProvider")
public void validateXacmlJson(final File xacmlJsonFile, final boolean expectedValid, final File expectedXacmlXmlFile, final File actualXacmlXmlFile, final ITestContext testCtx)
throws IOException, JAXBException
{
throws IOException, JAXBException {
/*
* Read properly as UTF-8 to avoid character decoding issues with org.json API
*/
try (final BufferedReader reader = Files.newBufferedReader(xacmlJsonFile.toPath(), StandardCharsets.UTF_8))
{
try (final BufferedReader reader = Files.newBufferedReader(xacmlJsonFile.toPath(), StandardCharsets.UTF_8)) {
final JSONObject json = new LimitsCheckingJSONObject(reader, MAX_JSON_STRING_LENGTH, MAX_JSON_CHILDREN_COUNT, MAX_JSON_DEPTH);
final Schema schema;
final JSONObject jsonToValidate;
if (json.has("Request"))
{
if (json.has("Request")) {
schema = XacmlJsonUtils.REQUEST_SCHEMA;
jsonToValidate = json;
}
else if (json.has("Response"))
{
else if (json.has("Response")) {
schema = XacmlJsonUtils.RESPONSE_SCHEMA;
jsonToValidate = json;
}
else if (json.has("policy"))
{
else if (json.has("policy")) {
schema = XacmlJsonUtils.POLICY_SCHEMA;
jsonToValidate = json.getJSONObject("policy");
}
else
{
else {
throw new IllegalArgumentException("Invalid XACML JSON file. Expected root key: \"Request\" or \"Response\" or \"policy\"");
}
try
{
try {
schema.validate(jsonToValidate); // throws a ValidationException if this object is invalid
if (!expectedValid)
{
if (!expectedValid) {
Assert.fail("Validation against JSON schema succeeded but expected to fail");
}
}
catch (final ValidationException e)
{
catch (final ValidationException e) {
LOGGER.error("Error validating JSON file: '{}'\n{}", xacmlJsonFile, e.toJSON().toString(2));
if (expectedValid)
{
if (expectedValid) {
Assert.fail("Validation against JSON schema failed but expected to pass");
}
}
......@@ -165,8 +154,7 @@ public class XacmlJsonSchemaValidationTest
/*
* Validation of XACML/XML output from JSON-to-XML XSLT if any
*/
if (actualXacmlXmlFile != null)
{
if (actualXacmlXmlFile != null) {
final Object expectedXacmlJaxbObj = Xacml3JaxbHelper.createXacml3Unmarshaller().unmarshal(expectedXacmlXmlFile);
final Object actualXacmlJaxbObj = Xacml3JaxbHelper.createXacml3Unmarshaller().unmarshal(actualXacmlXmlFile);
Assert.assertEquals(actualXacmlJaxbObj, expectedXacmlJaxbObj, "Source XACML/XML file and generated XACML/XML file after XML->JSON->XML (XSLT) conversion do not match");
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment