diff --git a/modules/charon-core/src/main/java/org/wso2/charon3/core/extensions/UserManager.java b/modules/charon-core/src/main/java/org/wso2/charon3/core/extensions/UserManager.java index 95949dbc2..e01f7aa21 100644 --- a/modules/charon-core/src/main/java/org/wso2/charon3/core/extensions/UserManager.java +++ b/modules/charon-core/src/main/java/org/wso2/charon3/core/extensions/UserManager.java @@ -31,6 +31,7 @@ import org.wso2.charon3.core.utils.codeutils.PatchOperation; import org.wso2.charon3.core.utils.codeutils.SearchRequest; +import java.util.HashMap; import java.util.List; import java.util.Map; @@ -305,4 +306,9 @@ default List getCustomUserSchemaAttributes() throws CharonException, return null; } + + default Map getSyncedUserAttributes() throws CharonException { + + return new HashMap<>(); + } } diff --git a/modules/charon-core/src/main/java/org/wso2/charon3/core/protocol/endpoints/UserResourceManager.java b/modules/charon-core/src/main/java/org/wso2/charon3/core/protocol/endpoints/UserResourceManager.java index e37a8c218..f3d73cbff 100644 --- a/modules/charon-core/src/main/java/org/wso2/charon3/core/protocol/endpoints/UserResourceManager.java +++ b/modules/charon-core/src/main/java/org/wso2/charon3/core/protocol/endpoints/UserResourceManager.java @@ -18,6 +18,8 @@ import org.apache.commons.lang.StringUtils; +import org.json.JSONArray; +import org.json.JSONObject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.wso2.charon3.core.attributes.Attribute; @@ -49,11 +51,17 @@ import org.wso2.charon3.core.utils.codeutils.SearchRequest; import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.HashMap; +import java.util.Iterator; import java.util.List; import java.util.Map; +import static org.wso2.charon3.core.schema.SCIMConstants.OperationalConstants.COLON; +import static org.wso2.charon3.core.schema.SCIMConstants.OperationalConstants.DOT_SEPARATOR; + /** * REST API exposed by Charon-Core to perform operations on UserResource. * Any SCIM service provider can call this API perform relevant CRUD operations on USER , @@ -645,7 +653,7 @@ public SCIMResponse updateWithPATCH(String existingId, String scimObjectString, //decode the SCIM User object, encoded in the submitted payload. List opList = decoder.decodeRequest(scimObjectString); - SCIMResourceTypeSchema schema = getSchema(userManager);; + SCIMResourceTypeSchema schema = getSchema(userManager); List allSimpleMultiValuedAttributes = ResourceManagerUtil.getAllSimpleMultiValuedAttributes(schema); //get the user from the user core @@ -661,6 +669,7 @@ public SCIMResponse updateWithPATCH(String existingId, String scimObjectString, User newUser = null; + Map syncedAttributes = userManager.getSyncedUserAttributes(); for (PatchOperation operation : opList) { if (operation.getOperation().equals(SCIMConstants.OperationalConstants.ADD)) { @@ -698,6 +707,39 @@ public SCIMResponse updateWithPATCH(String existingId, String scimObjectString, } else { throw new BadRequestException("Unknown operation.", ResponseCodeConstants.INVALID_SYNTAX); } + + if (syncedAttributes == null) { + continue; + } + List scimAttributes = determineScimAttributes(operation); + for (String scimAttribute : scimAttributes) { + String syncedAttribute = syncedAttributes.get(scimAttribute); + + if (syncedAttribute == null) { + continue; + } + + int lastColonIndex = syncedAttribute.lastIndexOf(COLON); + String baseAttributeName = (lastColonIndex != -1) + ? syncedAttribute.substring(0, lastColonIndex) : StringUtils.EMPTY; + String subAttributeName = (lastColonIndex != -1) + ? syncedAttribute.substring(lastColonIndex + 1) : syncedAttribute; + String[] subAttributes = subAttributeName.split("\\."); + + switch (subAttributes.length) { + case 1: + newUser.deleteSubAttribute(baseAttributeName, subAttributes[0]); + break; + case 2: + newUser.deleteSubSubAttribute(baseAttributeName, subAttributes[0], subAttributes[1]); + break; + default: + break; + } + + copyOfOldUser = (User) CopyUtil.deepCopy(newUser); + syncedAttributes.remove(syncedAttribute); + } } //get the URIs of required attributes which must be given a value @@ -786,4 +828,67 @@ private SCIMResourceTypeSchema getSchema(UserManager userManager) throws BadRequ } return schema; } + + private static List determineScimAttributes(PatchOperation operation) { + + if (operation == null) { + return Collections.emptyList(); + } + List attributes = new ArrayList<>(); + String path = operation.getPath(); + Object values = operation.getValues(); + + if (values instanceof JSONObject) { + extractScimAttributes((JSONObject) values, path, attributes); + } else if (values instanceof JSONArray) { + extractScimAttributes((JSONArray) values, path, attributes); + } else if (values != null) { + attributes.add(path); + } + + return attributes.isEmpty() && path != null ? Collections.singletonList(path) : attributes; + } + + private static void extractScimAttributes(JSONObject jsonObject, String basePath, List attributes) { + + for (Iterator it = jsonObject.keys(); it.hasNext(); ) { + String key = it.next(); + Object value = jsonObject.get(key); + String separator = DOT_SEPARATOR; + if (defaultScimSchemas().contains(basePath)) { + separator = COLON; + } + String newPath = (basePath != null) ? basePath + separator + key : key; + + if (value instanceof JSONObject) { + extractScimAttributes((JSONObject) value, newPath, attributes); + } else if (value instanceof JSONArray) { + extractScimAttributes((JSONArray) value, newPath, attributes); + } else { + attributes.add(newPath); + } + } + } + + private static void extractScimAttributes(JSONArray jsonArray, String basePath, List attributes) { + + for (int i = 0; i < jsonArray.length(); i++) { + Object value = jsonArray.get(i); + if (value instanceof JSONObject) { + extractScimAttributes((JSONObject) value, basePath, attributes); + } else if (!(value instanceof JSONArray)) { + attributes.add(basePath); + } + } + } + + private static List defaultScimSchemas() { + + return Arrays.asList( + SCIMConstants.CORE_SCHEMA_URI, + SCIMConstants.USER_CORE_SCHEMA_URI, + SCIMConstants.ENTERPRISE_USER_SCHEMA_URI, + SCIMConstants.SYSTEM_USER_SCHEMA_URI + ); + } } diff --git a/modules/charon-core/src/main/java/org/wso2/charon3/core/schema/SCIMConstants.java b/modules/charon-core/src/main/java/org/wso2/charon3/core/schema/SCIMConstants.java index 44f31afaf..319a6ec9f 100644 --- a/modules/charon-core/src/main/java/org/wso2/charon3/core/schema/SCIMConstants.java +++ b/modules/charon-core/src/main/java/org/wso2/charon3/core/schema/SCIMConstants.java @@ -862,6 +862,7 @@ public static class OperationalConstants { public static final String COLON = ":"; public static final String URL_SEPARATOR = "/"; + public static final String DOT_SEPARATOR = "."; } } diff --git a/modules/charon-core/src/test/java/org/wso2/charon3/core/protocol/endpoints/UserResourceManagerTest.java b/modules/charon-core/src/test/java/org/wso2/charon3/core/protocol/endpoints/UserResourceManagerTest.java index 5fa532e0b..993e206a9 100644 --- a/modules/charon-core/src/test/java/org/wso2/charon3/core/protocol/endpoints/UserResourceManagerTest.java +++ b/modules/charon-core/src/test/java/org/wso2/charon3/core/protocol/endpoints/UserResourceManagerTest.java @@ -18,6 +18,7 @@ package org.wso2.charon3.core.protocol.endpoints; +import org.json.JSONArray; import org.json.JSONObject; import org.mockito.MockedStatic; import org.mockito.Mockito; @@ -47,9 +48,14 @@ import org.wso2.charon3.core.schema.ServerSideValidator; import org.wso2.charon3.core.utils.CopyUtil; import org.wso2.charon3.core.utils.ResourceManagerUtil; +import org.wso2.charon3.core.utils.codeutils.PatchOperation; import org.wso2.charon3.core.utils.codeutils.SearchRequest; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -1355,4 +1361,92 @@ public void testUpdateWithPATCHRemove(String existingId, String scimObjectString userManager, attributes, excludeAttributes); Assert.assertEquals(scimResponse.getResponseStatus(), expectedScimResponseStatus); } + + @DataProvider(name = "scimAttributeData") + public Object[][] scimAttributeData() { + return new Object[][]{ + {createPatchOperation("urn:ietf:params:scim:schemas:core:2.0:User:emails", null), + Collections.singletonList("urn:ietf:params:scim:schemas:core:2.0:User:emails") + }, + {createPatchOperation("urn:ietf:params:scim:schemas:core:2.0:User:userName", "john_doe"), + Collections.singletonList("urn:ietf:params:scim:schemas:core:2.0:User:userName") + }, + {createPatchOperation("urn:ietf:params:scim:schemas:core:2.0:User:addresses", + new JSONObject() + .put("home", new JSONObject() + .put("details", new JSONObject() + .put("street", "123 Main St") + .put("zip", "12345"))) + .put("work", new JSONObject().put("street", "456 Work Rd"))), + Arrays.asList( + "urn:ietf:params:scim:schemas:core:2.0:User:addresses.home.details.street", + "urn:ietf:params:scim:schemas:core:2.0:User:addresses.home.details.zip", + "urn:ietf:params:scim:schemas:core:2.0:User:addresses.work.street") + }, + {createPatchOperation("urn:ietf:params:scim:schemas:extension:enterprise:2.0:User", + new JSONObject() + .put("department", "Engineering") + .put("organization", new JSONObject().put("name", "WSO2"))), + Arrays.asList( + "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User:department", + "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User:organization.name") + }, + {createPatchOperation("urn:ietf:params:scim:schemas:core:2.0:User:emails", + new JSONArray() + .put(new JSONObject().put("primary", true).put("value", "test@example.com")) + .put(new JSONObject().put("value", "work@example.com"))), + Arrays.asList( + "urn:ietf:params:scim:schemas:core:2.0:User:emails.primary", + "urn:ietf:params:scim:schemas:core:2.0:User:emails.value", + "urn:ietf:params:scim:schemas:core:2.0:User:emails.value") + }, + {createPatchOperation("urn:ietf:params:scim:schemas:core:2.0:User:numbers", + new JSONArray() + .put(new JSONObject().put("type", "mobile").put("number", "123456")) + .put("invalid_entry")), + Arrays.asList( + "urn:ietf:params:scim:schemas:core:2.0:User:numbers.type", + "urn:ietf:params:scim:schemas:core:2.0:User:numbers.number", + "urn:ietf:params:scim:schemas:core:2.0:User:numbers") + }, + {createPatchOperation("urn:ietf:params:scim:schemas:core:2.0:User", + new JSONArray() + .put(new JSONObject().put("email", "test@example.com")) + .put(new JSONObject().put("phone", "123-456-7890"))), + Arrays.asList( + "urn:ietf:params:scim:schemas:core:2.0:User:email", + "urn:ietf:params:scim:schemas:core:2.0:User:phone") + }, + {createPatchOperation("urn:ietf:params:scim:schemas:core:2.0:User:customAttributes", + new JSONObject()), + Collections.singletonList("urn:ietf:params:scim:schemas:core:2.0:User:customAttributes") + }, + {createPatchOperation("urn:ietf:params:scim:schemas:core:2.0:User:emptyArray", + new JSONArray()), + Collections.singletonList("urn:ietf:params:scim:schemas:core:2.0:User:emptyArray") + } + }; + } + + @Test(dataProvider = "scimAttributeData") + public void testDetermineScimAttributes(PatchOperation operation, List expectedAttributes) + throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { + + Method method = UserResourceManager.class.getDeclaredMethod("determineScimAttributes", PatchOperation.class); + method.setAccessible(true); + + @SuppressWarnings("unchecked") + List actualAttributes = (List) method.invoke(null, operation); + Assert.assertEqualsNoOrder(actualAttributes.toArray(), expectedAttributes.toArray(), + "The SCIM attributes do not match"); + } + + + private static PatchOperation createPatchOperation(String path, Object values) { + + PatchOperation operation = new PatchOperation(); + operation.setPath(path); + operation.setValues(values); + return operation; + } }