diff --git a/scim-sdk-common/src/main/java/de/captaingoldfish/scim/sdk/common/constants/AttributeNames.java b/scim-sdk-common/src/main/java/de/captaingoldfish/scim/sdk/common/constants/AttributeNames.java
index d1a0292c..35fffe9c 100644
--- a/scim-sdk-common/src/main/java/de/captaingoldfish/scim/sdk/common/constants/AttributeNames.java
+++ b/scim-sdk-common/src/main/java/de/captaingoldfish/scim/sdk/common/constants/AttributeNames.java
@@ -276,6 +276,14 @@ public static class Custom
* @see https://github.com/Captain-P-Goldfish/SCIM-SDK/issues/416
*/
public static final String ACTIVATE_MS_AZURE_FILTER_WORKAROUND = "activateMsAzureFilterWorkaround";
+
+ /**
+ * A workaround indicator to handle MsAzures illegal value-subattribute notation. There might be cases in
+ * which this switch will cause with normal execution. That is why this must be activated explicitly.
+ *
+ * @see https://github.com/Captain-P-Goldfish/SCIM-SDK/issues/516
+ */
+ public static final String ACTIVATE_MS_AZURE_VALUE_SUB_ATTRIBUTE_WORKAROUND = "activateMsAzureValueSubAttributeWorkaround";
}
/**
diff --git a/scim-sdk-common/src/main/java/de/captaingoldfish/scim/sdk/common/resources/complex/PatchConfig.java b/scim-sdk-common/src/main/java/de/captaingoldfish/scim/sdk/common/resources/complex/PatchConfig.java
index a20b4a29..8bfe1999 100644
--- a/scim-sdk-common/src/main/java/de/captaingoldfish/scim/sdk/common/resources/complex/PatchConfig.java
+++ b/scim-sdk-common/src/main/java/de/captaingoldfish/scim/sdk/common/resources/complex/PatchConfig.java
@@ -19,7 +19,10 @@ public class PatchConfig extends ScimObjectNode
{
@Builder
- public PatchConfig(Boolean supported, Boolean activateSailsPointWorkaround, Boolean activateMsAzureWorkaround)
+ public PatchConfig(Boolean supported,
+ Boolean activateSailsPointWorkaround,
+ Boolean activateMsAzureWorkaround,
+ Boolean activateMsAzureValueSubAttributeWorkaround)
{
super(null);
setSupported(supported);
@@ -67,6 +70,7 @@ public void setActivateSailsPointWorkaround(Boolean activateSailsPointWorkaround
* A workaround to handle filter-expressions in patch-paths as attributes that will be added to the resource
*
* @see https://github.com/Captain-P-Goldfish/SCIM-SDK/issues/416
+ * @see de.captaingoldfish.scim.sdk.server.patch.msazure.MsAzurePatchFilterWorkaround
*/
public boolean isMsAzureFilterWorkaroundActive()
{
@@ -77,12 +81,36 @@ public boolean isMsAzureFilterWorkaroundActive()
* A workaround to handle filter-expressions in patch-paths as attributes that will be added to the resource
*
* @see https://github.com/Captain-P-Goldfish/SCIM-SDK/issues/416
+ * @see de.captaingoldfish.scim.sdk.server.patch.msazure.MsAzurePatchFilterWorkaround
*/
public void setMsAzureFilterWorkaroundActive(Boolean msAzureWorkaroundActive)
{
setAttribute(AttributeNames.Custom.ACTIVATE_MS_AZURE_FILTER_WORKAROUND, msAzureWorkaroundActive);
}
+ /**
+ * A workaround to handle MsAzures illegal value-subattribute notation
+ *
+ * @see https://github.com/Captain-P-Goldfish/SCIM-SDK/issues/516
+ * @see de.captaingoldfish.scim.sdk.server.patch.msazure.MsAzurePatchValueSubAttributeRebuilder
+ */
+ public boolean isMsAzureValueSubAttributeWorkaroundActive()
+ {
+ return getBooleanAttribute(AttributeNames.Custom.ACTIVATE_MS_AZURE_VALUE_SUB_ATTRIBUTE_WORKAROUND).orElse(false);
+ }
+
+ /**
+ * A workaround to handle MsAzures illegal value-subattribute notation
+ *
+ * @see https://github.com/Captain-P-Goldfish/SCIM-SDK/issues/516
+ * @see de.captaingoldfish.scim.sdk.server.patch.msazure.MsAzurePatchValueSubAttributeRebuilder
+ */
+ public void setMsAzureValueSubAttributeWorkaroundActive(Boolean msAzureValueSubAttributeWorkaroundActive)
+ {
+ setAttribute(AttributeNames.Custom.ACTIVATE_MS_AZURE_VALUE_SUB_ATTRIBUTE_WORKAROUND,
+ msAzureValueSubAttributeWorkaroundActive);
+ }
+
/**
* override lombok builder with public constructor
*/
diff --git a/scim-sdk-common/src/main/resources/de/captaingoldfish/scim/sdk/common/meta/service-provider.schema.json b/scim-sdk-common/src/main/resources/de/captaingoldfish/scim/sdk/common/meta/service-provider.schema.json
index 179131f8..59e805a3 100644
--- a/scim-sdk-common/src/main/resources/de/captaingoldfish/scim/sdk/common/meta/service-provider.schema.json
+++ b/scim-sdk-common/src/main/resources/de/captaingoldfish/scim/sdk/common/meta/service-provider.schema.json
@@ -56,7 +56,18 @@
{
"name": "activateMsAzureFilterWorkaround",
"type": "boolean",
- "description": "A workaround to handle filter-expressions in patch-paths as attributes that will be added to the resource.",
+ "description": "A workaround to handle filter-expressions in patch-paths as attributes that will be added to the resource. @see https://github.com/Captain-P-Goldfish/SCIM-SDK/issues/416",
+ "mutability": "readOnly",
+ "returned": "request",
+ "uniqueness": "none",
+ "multiValued": false,
+ "required": false,
+ "caseExact": false
+ },
+ {
+ "name": "activateMsAzureValueSubAttributeWorkaround",
+ "type": "boolean",
+ "description": "A workaround to handle MsAzures illegal value-subattribute notation. @see https://github.com/Captain-P-Goldfish/SCIM-SDK/issues/516",
"mutability": "readOnly",
"returned": "request",
"uniqueness": "none",
diff --git a/scim-sdk-server/src/main/java/de/captaingoldfish/scim/sdk/server/patch/PatchHandler.java b/scim-sdk-server/src/main/java/de/captaingoldfish/scim/sdk/server/patch/PatchHandler.java
index 4c9d5856..f24421a1 100644
--- a/scim-sdk-server/src/main/java/de/captaingoldfish/scim/sdk/server/patch/PatchHandler.java
+++ b/scim-sdk-server/src/main/java/de/captaingoldfish/scim/sdk/server/patch/PatchHandler.java
@@ -23,6 +23,7 @@
import de.captaingoldfish.scim.sdk.server.filter.AttributePathRoot;
import de.captaingoldfish.scim.sdk.server.patch.msazure.MsAzurePatchAttributeRebuilder;
import de.captaingoldfish.scim.sdk.server.patch.msazure.MsAzurePatchRemoveRebuilder;
+import de.captaingoldfish.scim.sdk.server.patch.msazure.MsAzurePatchValueSubAttributeRebuilder;
import de.captaingoldfish.scim.sdk.server.schemas.ResourceType;
import de.captaingoldfish.scim.sdk.server.utils.RequestUtils;
import lombok.Getter;
@@ -166,6 +167,14 @@ private boolean handlePatchOp(ResourceNode resource, PatchRequestOperation opera
values);
path = msAzureWorkaround.fixPath();
}
+
+ if (patchConfig.isMsAzureValueSubAttributeWorkaroundActive())
+ {
+ MsAzurePatchValueSubAttributeRebuilder msAzureWorkaround;
+ msAzureWorkaround = new MsAzurePatchValueSubAttributeRebuilder(operation.getOp(), values);
+ values = msAzureWorkaround.fixValues();
+ }
+
PatchTargetHandler patchTargetHandler = new PatchTargetHandler(patchConfig, resourceType, operation.getOp(),
path);
boolean changeWasMade = patchTargetHandler.handleOperationValues(resource, values);
diff --git a/scim-sdk-server/src/main/java/de/captaingoldfish/scim/sdk/server/patch/msazure/MsAzurePatchAttributeRebuilder.java b/scim-sdk-server/src/main/java/de/captaingoldfish/scim/sdk/server/patch/msazure/MsAzurePatchAttributeRebuilder.java
index 43ad55df..41879cbc 100644
--- a/scim-sdk-server/src/main/java/de/captaingoldfish/scim/sdk/server/patch/msazure/MsAzurePatchAttributeRebuilder.java
+++ b/scim-sdk-server/src/main/java/de/captaingoldfish/scim/sdk/server/patch/msazure/MsAzurePatchAttributeRebuilder.java
@@ -41,7 +41,7 @@
* }
*
*
- * the value in the request must not be present. Instead the request should look like this:
+ * the value in the request must not be present. Instead, the request should look like this:
*
*
* PATCH /scim/Users/2752513
diff --git a/scim-sdk-server/src/main/java/de/captaingoldfish/scim/sdk/server/patch/msazure/MsAzurePatchFilterWorkaround.java b/scim-sdk-server/src/main/java/de/captaingoldfish/scim/sdk/server/patch/msazure/MsAzurePatchFilterWorkaround.java
index cd689b7d..a95d8673 100644
--- a/scim-sdk-server/src/main/java/de/captaingoldfish/scim/sdk/server/patch/msazure/MsAzurePatchFilterWorkaround.java
+++ b/scim-sdk-server/src/main/java/de/captaingoldfish/scim/sdk/server/patch/msazure/MsAzurePatchFilterWorkaround.java
@@ -15,6 +15,61 @@
public class MsAzurePatchFilterWorkaround
{
+ /**
+ * this method is a workaround for MsAzures expected behaviour that a value is added to a multivalued complex
+ * node if it is reference in a patch-filter but not added within the attribute itself
+ *
+ *
+ * {
+ * "schemas": [
+ * "urn:ietf:params:scim:api:messages:2.0:PatchOp"
+ * ],
+ * "Operations": [
+ * {
+ * "op": "add",
+ * "path": "emails[type eq \"work\"].value",
+ * "value": "max@mustermann.de"
+ * }
+ * ]
+ * }
+ *
+ *
+ * must be rebuilt to:
+ *
+ *
+ * {
+ * "schemas": [
+ * "urn:ietf:params:scim:api:messages:2.0:PatchOp"
+ * ],
+ * "Operations": [
+ * {
+ * "op": "add",
+ * "path": "emails[type eq \"work\"].value",
+ * "value": [
+ * {
+ * "value": "max@mustermann.de"
+ * },
+ * {
+ * "type": "work"
+ * }
+ * ]
+ * }
+ * ]
+ * }
+ *
+ *
+ * The complete rebuild is not done within this method. This method will simply create the last objectNode
+ * from the filter-expression:
+ *
+ *
+ * {
+ * "type": "work"
+ * }
+ *
+ *
+ * @param path the patch filter expression node.
+ * @return the
+ */
public ScimObjectNode createAttributeFromPatchFilter(AttributePathRoot path)
{
SchemaAttribute schemaAttribute = path.getSchemaAttribute();
diff --git a/scim-sdk-server/src/main/java/de/captaingoldfish/scim/sdk/server/patch/msazure/MsAzurePatchValueSubAttributeRebuilder.java b/scim-sdk-server/src/main/java/de/captaingoldfish/scim/sdk/server/patch/msazure/MsAzurePatchValueSubAttributeRebuilder.java
new file mode 100644
index 00000000..ddbe3bdd
--- /dev/null
+++ b/scim-sdk-server/src/main/java/de/captaingoldfish/scim/sdk/server/patch/msazure/MsAzurePatchValueSubAttributeRebuilder.java
@@ -0,0 +1,165 @@
+package de.captaingoldfish.scim.sdk.server.patch.msazure;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+
+import de.captaingoldfish.scim.sdk.common.constants.AttributeNames;
+import de.captaingoldfish.scim.sdk.common.constants.enums.PatchOp;
+import de.captaingoldfish.scim.sdk.common.utils.JsonHelper;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+
+
+/**
+ * This class is a workaround handler in order to handle the broken patch requests of Microsoft Azure. Azure
+ * sends illegal patch requests that look like this:
+ *
+ *
+ * {
+ * "op": "Add",
+ * "path": "roles",
+ * "value": [
+ * {
+ * "value": "{\"id\":\"827f0d2e-be15-4d8f-a8e3-f8697239c112\",
+ * \"value\":\"DocumentMgmt-Admin\",
+ * \"displayName\":\"DocumentMgmt Admin\"}"
+ * },
+ * {
+ * "value": "{\"id\":\"8ae06bd4-35bb-4fcd-977e-14e074ad1192\",
+ * \"value\":\"Admin\",
+ * \"displayName\":\"Admin\"}"
+ * }
+ * ]
+ * }
+ *
+ *
+ * The problem in this request is the nested value-attribute that should not be there. Instead, the request
+ * should look like this:
+ *
+ *
+ * {
+ * "op": "Add",
+ * "path": "roles",
+ * "value": [
+ *
+ * "{\"id\":\"827f0d2e-be15-4d8f-a8e3-f7697239c112\",
+ * \"value\":\"DocumentMgmt-BuyerAdmin\",
+ * \"displayName\":\"DocumentMgmt BuyerAdmin\"}",
+ *
+ * "{\"id\":\"8ae06bd4-35bb-4fcd-977e-12e074ad1192\",
+ * \"value\":\"Buyer-Admin\",
+ * \"displayName\":\"Buyer Admin\"}"
+ * ]
+ * }
+ *
+ *
+ * @author Pascal Knueppel
+ * @since 07.10.2023
+ */
+@Slf4j
+@RequiredArgsConstructor
+public class MsAzurePatchValueSubAttributeRebuilder
+{
+
+ /**
+ * the patch operation that is currently executed
+ */
+ private final PatchOp patchOp;
+
+ /**
+ * the values of the patch operation. This attribute should actually be empty
+ */
+ private final List patchValues;
+
+ /**
+ * this method will try to resolve a PatchRequest as described in the class-documentation to its correct state
+ *
+ * @param patchValues the values sent in the PatchRequest
+ * @return the fixed values from the PatchRequest
+ */
+ public List fixValues()
+ {
+ // simply a check to return early from this method if a remove operation is used
+ if (PatchOp.REMOVE.equals(patchOp))
+ {
+ log.trace("[MS Azure value-subAttribute workaround] only handling 'REPLACE' and 'ADD' requests");
+ return patchValues;
+ }
+
+ if (patchValues == null || patchValues.isEmpty())
+ {
+ log.trace("[MS Azure value-subAttribute workaround] not executed for values-list is empty");
+ return patchValues;
+ }
+
+ List fixedValues = new ArrayList<>();
+ for ( String patchValue : patchValues )
+ {
+ final JsonNode jsonNode;
+ try
+ {
+ jsonNode = JsonHelper.readJsonDocument(patchValue);
+ }
+ catch (Exception ex)
+ {
+ log.trace("[MS Azure value-subAttribute workaround] ignored value-node because it is no valid JSON "
+ + "object-node");
+ fixedValues.add(patchValue);
+ continue;
+ }
+
+ boolean isObjectNode = jsonNode instanceof ObjectNode;
+ if (!isObjectNode)
+ {
+ log.trace("[MS Azure value-subAttribute workaround] ignored value because it is no JSON-ObjectNode");
+ fixedValues.add(patchValue);
+ continue;
+ }
+
+ ObjectNode objectNode = (ObjectNode)jsonNode;
+ if (objectNode.size() != 1)
+ {
+ log.trace("[MS Azure value-subAttribute workaround] ignored JSON-ObjectNode because it has less or more "
+ + "than 1 sub-nodes");
+ fixedValues.add(patchValue);
+ continue;
+ }
+
+ JsonNode innerValueNode = objectNode.get(AttributeNames.RFC7643.VALUE);
+ if (innerValueNode == null)
+ {
+ log.trace("[MS Azure value-subAttribute workaround] ignored JSON-ObjectNode because it has no value-node");
+ fixedValues.add(patchValue);
+ continue;
+ }
+
+ final JsonNode innerObjectNode;
+ try
+ {
+ innerObjectNode = JsonHelper.readJsonDocument(innerValueNode.textValue());
+ }
+ catch (Exception ex)
+ {
+ log.trace("[MS Azure value-subAttribute workaround] ignored inner value-node because it is no valid JSON-node");
+ fixedValues.add(patchValue);
+ continue;
+ }
+
+ isObjectNode = innerObjectNode instanceof ObjectNode;
+ if (!isObjectNode)
+ {
+ log.trace("[MS Azure value-subAttribute workaround] ignored inner value-node because it is no not a JSON "
+ + "object-node");
+ fixedValues.add(patchValue);
+ continue;
+ }
+
+ fixedValues.add(innerObjectNode.toString());
+ }
+
+ return fixedValues;
+ }
+}
diff --git a/scim-sdk-server/src/test/java/de/captaingoldfish/scim/sdk/server/patch/PatchAddResourceHandlerTest.java b/scim-sdk-server/src/test/java/de/captaingoldfish/scim/sdk/server/patch/PatchAddResourceHandlerTest.java
index 8b1544e1..6e986a55 100644
--- a/scim-sdk-server/src/test/java/de/captaingoldfish/scim/sdk/server/patch/PatchAddResourceHandlerTest.java
+++ b/scim-sdk-server/src/test/java/de/captaingoldfish/scim/sdk/server/patch/PatchAddResourceHandlerTest.java
@@ -5,12 +5,14 @@
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
+import java.util.UUID;
import java.util.concurrent.atomic.AtomicBoolean;
import org.hamcrest.MatcherAssert;
import org.hamcrest.Matchers;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.DynamicTest;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestFactory;
@@ -24,6 +26,7 @@
import com.fasterxml.jackson.databind.node.IntNode;
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
import com.fasterxml.jackson.databind.node.LongNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fasterxml.jackson.databind.node.TextNode;
import de.captaingoldfish.scim.sdk.common.constants.AttributeNames;
@@ -45,6 +48,7 @@
import de.captaingoldfish.scim.sdk.common.resources.complex.Name;
import de.captaingoldfish.scim.sdk.common.resources.complex.PatchConfig;
import de.captaingoldfish.scim.sdk.common.resources.multicomplex.Email;
+import de.captaingoldfish.scim.sdk.common.resources.multicomplex.PersonRole;
import de.captaingoldfish.scim.sdk.common.utils.JsonHelper;
import de.captaingoldfish.scim.sdk.server.resources.AllTypes;
import de.captaingoldfish.scim.sdk.server.schemas.ResourceType;
@@ -1576,6 +1580,140 @@ public void testHandleReplaceOnComplexTypesAsAdd()
Matchers.containsInAnyOrder("test1", "test2"));
}
+ /**
+ * this patch request is based on the github-issue: https://github.com/Captain-P-Goldfish/SCIM-SDK/issues/516.
+ * MsAzure is building illegal patch-requests that prevents a correct resolving of the patch request.
+ */
+ private String getMsAzureSubValueAttributeTestString()
+ {
+ return " { \"schemas\" : [ \"urn:ietf:params:scim:api:messages:2.0:PatchOp\" ], "//
+ + "\"Operations\" : [ "//
+ + " { \"path\" : \"roles\", "//
+ + " \"op\" : \"add\", "//
+ + " \"value\" : [ " //
+ + " {" //
+ + " \"value\": \"{\\\"$ref\\\":\\\"827f0d2e-be15-4d8f-a8e3-f7697239c112\\\","
+ + " \\\"value\\\":\\\"DocumentMgmt-BuyerAdmin\\\","
+ + " \\\"display\\\":\\\"DocumentMgmt BuyerAdmin\\\""
+ + " }\"" //
+ + " },"//
+ + " {" //
+ + " \"value\": \"{\\\"$ref\\\":\\\"8ae06bd4-35bb-4fcd-977e-12e074ad1192\\\","
+ + " \\\"value\\\":\\\"Buyer-Admin\\\","
+ + " \\\"display\\\":\\\"Buyer Admin\\\""
+ + " }\"" //
+ + " }"//
+ + " ]"//
+ + " } "//
+ + "]}";
+ }
+
+ /**
+ * This test makes sure that the illegal MsAzure Patch-Requests with the value sub-attribute object structure
+ * is resolved correctly if the feature is activated
+ *
+ * @see #getMsAzureSubValueAttributeTestString()
+ * @see https://github.com/Captain-P-Goldfish/SCIM-SDK/issues/516
+ */
+ @DisplayName("MsAzure value-subAttribute workaround is active and resolves correctly")
+ @Test
+ public void testMsAzureSubValueAttributeResolvingWithWorkaroundActive()
+ {
+ serviceProvider.getPatchConfig().setMsAzureValueSubAttributeWorkaroundActive(true);
+
+ JsonNode userSchema = JsonHelper.loadJsonDocument(ClassPathReferences.USER_SCHEMA_JSON);
+ JsonNode userResourceTypeNode = JsonHelper.loadJsonDocument(ClassPathReferences.USER_RESOURCE_TYPE_JSON);
+ JsonNode enterpriseUserSchema = JsonHelper.loadJsonDocument(ClassPathReferences.ENTERPRISE_USER_SCHEMA_JSON);
+ ResourceType userResourceType = resourceTypeFactory.registerResourceType(null,
+ userResourceTypeNode,
+ userSchema,
+ enterpriseUserSchema);
+
+ final String resourceId = UUID.randomUUID().toString();
+ final String patchRequestString = getMsAzureSubValueAttributeTestString();
+ PatchOpRequest patchOpRequest = JsonHelper.readJsonDocument(patchRequestString, PatchOpRequest.class);
+ User user = User.builder().id(resourceId).userName("goldfish").build();
+
+ PatchHandler patchHandler = new PatchHandler(serviceProvider.getPatchConfig(), userResourceType);
+ User patchedUser = patchHandler.patchResource(user, patchOpRequest);
+
+ log.info(patchedUser.toPrettyString());
+
+ List personRoles = patchedUser.getRoles();
+ Assertions.assertEquals(2, personRoles.size());
+
+ {
+ PersonRole role1 = personRoles.get(0);
+ Assertions.assertEquals("827f0d2e-be15-4d8f-a8e3-f7697239c112", role1.getRef().get());
+ Assertions.assertEquals("DocumentMgmt-BuyerAdmin", role1.getValue().get());
+ Assertions.assertEquals("DocumentMgmt BuyerAdmin", role1.getDisplay().get());
+ }
+
+ {
+ PersonRole role2 = personRoles.get(1);
+ Assertions.assertEquals("8ae06bd4-35bb-4fcd-977e-12e074ad1192", role2.getRef().get());
+ Assertions.assertEquals("Buyer-Admin", role2.getValue().get());
+ Assertions.assertEquals("Buyer Admin", role2.getDisplay().get());
+ }
+ }
+
+ /**
+ * This test makes sure that the illegal MsAzure Patch-Requests with the value sub-attribute object structure
+ * is not resolved anymore if the feature is deactivated
+ *
+ * @see #getMsAzureSubValueAttributeTestString()
+ * @see https://github.com/Captain-P-Goldfish/SCIM-SDK/issues/516
+ */
+ @DisplayName("MsAzure value-subAttribute workaround is inactive and resolves correctly")
+ @Test
+ public void testMsAzureSubValueAttributeResolvingWithWorkaroundInActive()
+ {
+ serviceProvider.getPatchConfig().setMsAzureValueSubAttributeWorkaroundActive(false);
+
+ JsonNode userSchema = JsonHelper.loadJsonDocument(ClassPathReferences.USER_SCHEMA_JSON);
+ JsonNode userResourceTypeNode = JsonHelper.loadJsonDocument(ClassPathReferences.USER_RESOURCE_TYPE_JSON);
+ JsonNode enterpriseUserSchema = JsonHelper.loadJsonDocument(ClassPathReferences.ENTERPRISE_USER_SCHEMA_JSON);
+ ResourceType userResourceType = resourceTypeFactory.registerResourceType(null,
+ userResourceTypeNode,
+ userSchema,
+ enterpriseUserSchema);
+
+ final String resourceId = UUID.randomUUID().toString();
+ final String patchRequestString = getMsAzureSubValueAttributeTestString();
+ PatchOpRequest patchOpRequest = JsonHelper.readJsonDocument(patchRequestString, PatchOpRequest.class);
+ User user = User.builder().id(resourceId).userName("goldfish").build();
+
+ PatchHandler patchHandler = new PatchHandler(serviceProvider.getPatchConfig(), userResourceType);
+ User patchedUser = patchHandler.patchResource(user, patchOpRequest);
+
+ log.info(patchedUser.toPrettyString());
+
+ List personRoles = patchedUser.getRoles();
+ Assertions.assertEquals(2, personRoles.size());
+
+ List values = patchOpRequest.getOperations().get(0).getValues();
+
+ {
+ PersonRole role1 = personRoles.get(0);
+ String expectedContent = JsonHelper.readJsonDocument(values.get(0), ObjectNode.class)
+ .get(AttributeNames.RFC7643.VALUE)
+ .textValue();
+ Assertions.assertEquals(expectedContent, role1.getValue().get());
+ Assertions.assertFalse(role1.getRef().isPresent());
+ Assertions.assertFalse(role1.getDisplay().isPresent());
+ }
+
+ {
+ PersonRole role2 = personRoles.get(1);
+ String expectedContent = JsonHelper.readJsonDocument(values.get(1), ObjectNode.class)
+ .get(AttributeNames.RFC7643.VALUE)
+ .textValue();
+ Assertions.assertEquals(expectedContent, role2.getValue().get());
+ Assertions.assertFalse(role2.getRef().isPresent());
+ Assertions.assertFalse(role2.getDisplay().isPresent());
+ }
+ }
+
/**
* this method returns a specific attribute definition that will be added to the enterprise user that is used
* as extension for the alltypes schema. this shall provoke a naming conflict with a complex type in the
diff --git a/scim-sdk-server/src/test/java/de/captaingoldfish/scim/sdk/server/patch/msazure/MsAzurePatchValueSubAttributeRebuilderTest.java b/scim-sdk-server/src/test/java/de/captaingoldfish/scim/sdk/server/patch/msazure/MsAzurePatchValueSubAttributeRebuilderTest.java
new file mode 100644
index 00000000..1b7a64a2
--- /dev/null
+++ b/scim-sdk-server/src/test/java/de/captaingoldfish/scim/sdk/server/patch/msazure/MsAzurePatchValueSubAttributeRebuilderTest.java
@@ -0,0 +1,211 @@
+package de.captaingoldfish.scim.sdk.server.patch.msazure;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+
+import de.captaingoldfish.scim.sdk.common.constants.enums.PatchOp;
+import de.captaingoldfish.scim.sdk.common.resources.multicomplex.PersonRole;
+import de.captaingoldfish.scim.sdk.common.utils.JsonHelper;
+
+
+/**
+ * @author Pascal Knueppel
+ * @since 07.10.2023
+ */
+public class MsAzurePatchValueSubAttributeRebuilderTest
+{
+
+ /**
+ * makes sure that the workaround-handler returns the original list in case of a remove-operation
+ */
+ @DisplayName("Remove operations are not handled")
+ @Test
+ public void testRemoveOperationsAreNotHandles()
+ {
+ final PatchOp patchOp = PatchOp.REMOVE;
+ final List values = new ArrayList<>(Arrays.asList("{\"value\": \"{\\\"display\\\":\\\"DocumentMgmt-BuyerAdmin\\\"}\"}",
+ "{\"value\": \"{\\\"display\\\":\\\"Buyer-Admin\\\"}\"}"));
+
+
+ MsAzurePatchValueSubAttributeRebuilder workaroundHandler = new MsAzurePatchValueSubAttributeRebuilder(patchOp,
+ values);
+ List fixedValues = workaroundHandler.fixValues();
+ Assertions.assertEquals(fixedValues, values);
+ }
+
+ /**
+ * makes sure that an empty or a null list of values will not cause any errors
+ */
+ @DisplayName("Empty or null lists do not cause errors")
+ @Test
+ public void testEmptyListsDoNotCauseErrors()
+ {
+ final PatchOp patchOp = PatchOp.ADD;
+
+ {
+ MsAzurePatchValueSubAttributeRebuilder handler = new MsAzurePatchValueSubAttributeRebuilder(patchOp,
+ new ArrayList<>());
+ List fixedValues = handler.fixValues();
+ Assertions.assertTrue(fixedValues.isEmpty());
+ }
+ {
+ MsAzurePatchValueSubAttributeRebuilder handler = new MsAzurePatchValueSubAttributeRebuilder(patchOp, null);
+ List fixedValues = handler.fixValues();
+ Assertions.assertNull(fixedValues);
+ }
+ }
+
+ /**
+ * makes sure that no error occurs if the sub-value attribute contains an invalid json structure
+ */
+ @DisplayName("Invalid-Json-Object in value-subAttribute does not cause errors")
+ @Test
+ public void testInvalidJsonObjectInSubValueCausesNoError()
+ {
+ final PatchOp patchOp = PatchOp.ADD;
+ final List values = new ArrayList<>(Arrays.asList("{\"display\" \"Admin\"", "{\"display\" \"User\""));
+
+ MsAzurePatchValueSubAttributeRebuilder workaroundHandler = new MsAzurePatchValueSubAttributeRebuilder(patchOp,
+ values);
+ List fixedValues = workaroundHandler.fixValues();
+ Assertions.assertEquals(fixedValues, values);
+ }
+
+ /**
+ * makes sure that no error occurs if the sub-value attribute is a normal patch-value request without
+ * underlying JSON-objects
+ */
+ @DisplayName("None-Json-Object in value-subAttribute does not cause errors")
+ @Test
+ public void testNonJsonObjectInSubValueCausesNoError()
+ {
+ final PatchOp patchOp = PatchOp.ADD;
+ final List values = new ArrayList<>(Arrays.asList("{\"display\": \"Admin\"}", "{\"display\": \"User\"}"));
+
+ MsAzurePatchValueSubAttributeRebuilder workaroundHandler = new MsAzurePatchValueSubAttributeRebuilder(patchOp,
+ values);
+ List fixedValues = workaroundHandler.fixValues();
+ Assertions.assertEquals(fixedValues, values);
+ }
+
+ /**
+ * makes sure that no error occurs if the value-attribute is a normal patch-value request without underlying
+ * JSON-objects
+ */
+ @DisplayName("Array-Json-Object in value-attribute does not cause errors")
+ @Test
+ public void testArrayJsonObjectInValueCausesNoError()
+ {
+ final PatchOp patchOp = PatchOp.ADD;
+ final List values = new ArrayList<>(Arrays.asList("[{\"display\": \"Admin\"}]",
+ "[{\"display\": \"User\"}]"));
+
+ MsAzurePatchValueSubAttributeRebuilder workaroundHandler = new MsAzurePatchValueSubAttributeRebuilder(patchOp,
+ values);
+ List fixedValues = workaroundHandler.fixValues();
+ Assertions.assertEquals(fixedValues, values);
+ }
+
+ /**
+ * makes sure that no error occurs if the sub-value-attribute is a normal patch-value request without
+ * underlying JSON-objects
+ */
+ @DisplayName("Array-Json-Object in value-subAttribute does not cause errors")
+ @Test
+ public void testArrayJsonObjectInSubValueCausesNoError()
+ {
+ final PatchOp patchOp = PatchOp.ADD;
+ final List values = new ArrayList<>(Arrays.asList("{\"display\": [\"Admin\"]}",
+ "{\"display\": [\"User\"]}"));
+
+ MsAzurePatchValueSubAttributeRebuilder workaroundHandler = new MsAzurePatchValueSubAttributeRebuilder(patchOp,
+ values);
+ List fixedValues = workaroundHandler.fixValues();
+ Assertions.assertEquals(fixedValues, values);
+ }
+
+ /**
+ * Makes sure that the handler is ignored if the value-sub-attribute has siblings.
+ */
+ @DisplayName("Handler does not execute if sub-value-attribute has siblings")
+ @Test
+ public void testIgnoreInnerSubValueIfMoreThanOneAttribute()
+ {
+ final PatchOp patchOp = PatchOp.ADD;
+ final List values = Collections.singletonList("{\"value\": \"{\\\"display\\\":\\\"DocumentMgmt-BuyerAdmin\\\"}\","
+ + "\"$ref\": \"123456789\"}");
+
+
+ MsAzurePatchValueSubAttributeRebuilder workaroundHandler = new MsAzurePatchValueSubAttributeRebuilder(patchOp,
+ values);
+ List fixedValues = workaroundHandler.fixValues();
+ Assertions.assertEquals(fixedValues, values);
+ }
+
+ /**
+ * makes sure that the node is not parsed if the value-sub-attribute contains illegal non parseable json
+ */
+ @DisplayName("Handler is ignored if the inner value-sub-attribute contains illegal json")
+ @Test
+ public void testInnerValueSubAttributeIsIgnoredWhenIllegalJson()
+ {
+ final PatchOp patchOp = PatchOp.ADD;
+ final List values = Collections.singletonList("{\"value\": \"{\\\"display\\\"\\\"DocumentMgmt-BuyerAdmin\\\"\"}");
+
+
+ MsAzurePatchValueSubAttributeRebuilder workaroundHandler = new MsAzurePatchValueSubAttributeRebuilder(patchOp,
+ values);
+ List fixedValues = workaroundHandler.fixValues();
+ Assertions.assertEquals(fixedValues, values);
+ }
+
+ /**
+ * makes sure that the node is not parsed if the value-sub-attribute is an array instead of an object
+ */
+ @DisplayName("Handler is ignored if the inner value-sub-attribute is an array")
+ @Test
+ public void testInnerValueSubAttributeIsIgnoredWhenArray()
+ {
+ final PatchOp patchOp = PatchOp.ADD;
+ final List values = Collections.singletonList("{\"value\": \"[{\\\"display\\\":\\\"DocumentMgmt-BuyerAdmin\\\"}]\"}");
+
+
+ MsAzurePatchValueSubAttributeRebuilder workaroundHandler = new MsAzurePatchValueSubAttributeRebuilder(patchOp,
+ values);
+ List fixedValues = workaroundHandler.fixValues();
+ Assertions.assertEquals(fixedValues, values);
+ }
+
+ /**
+ * makes sure that the node is not parsed if the value-sub-attribute is an array instead of an object
+ */
+ @DisplayName("Handler works wor ADD and REPLACE operations")
+ @ParameterizedTest
+ @ValueSource(strings = {"ADD", "REPLACE"})
+ public void testHandlerWorksForAddAndReplaceOps(PatchOp patchOp)
+ {
+ final List values = new ArrayList<>(Arrays.asList("{\"value\": \"{\\\"display\\\":\\\"DocumentMgmt-BuyerAdmin\\\"}\"}",
+ "{\"value\": \"{\\\"display\\\":\\\"Buyer-Admin\\\"}\"}"));
+
+ MsAzurePatchValueSubAttributeRebuilder workaroundHandler = new MsAzurePatchValueSubAttributeRebuilder(patchOp,
+ values);
+ List fixedValues = workaroundHandler.fixValues();
+ Assertions.assertNotEquals(fixedValues, values);
+ {
+ PersonRole personRole1 = JsonHelper.readJsonDocument(fixedValues.get(0), PersonRole.class);
+ Assertions.assertEquals("DocumentMgmt-BuyerAdmin", personRole1.getDisplay().get());
+ }
+ {
+ PersonRole personRole2 = JsonHelper.readJsonDocument(fixedValues.get(1), PersonRole.class);
+ Assertions.assertEquals("Buyer-Admin", personRole2.getDisplay().get());
+ }
+ }
+}