Skip to content

Commit

Permalink
Merge pull request #422 from amanda-ariyaratne/master
Browse files Browse the repository at this point in the history
Handle scim user patch for migrating users
  • Loading branch information
amanda-ariyaratne authored Jan 31, 2025
2 parents 29f0eb5 + 9353458 commit e4812bc
Show file tree
Hide file tree
Showing 4 changed files with 207 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -305,4 +306,9 @@ default List<Attribute> getCustomUserSchemaAttributes() throws CharonException,

return null;
}

default Map<String, String> getSyncedUserAttributes() throws CharonException {

return new HashMap<>();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 ,
Expand Down Expand Up @@ -645,7 +653,7 @@ public SCIMResponse updateWithPATCH(String existingId, String scimObjectString,
//decode the SCIM User object, encoded in the submitted payload.
List<PatchOperation> opList = decoder.decodeRequest(scimObjectString);

SCIMResourceTypeSchema schema = getSchema(userManager);;
SCIMResourceTypeSchema schema = getSchema(userManager);
List<String> allSimpleMultiValuedAttributes = ResourceManagerUtil.getAllSimpleMultiValuedAttributes(schema);

//get the user from the user core
Expand All @@ -661,6 +669,7 @@ public SCIMResponse updateWithPATCH(String existingId, String scimObjectString,

User newUser = null;

Map<String, String> syncedAttributes = userManager.getSyncedUserAttributes();
for (PatchOperation operation : opList) {

if (operation.getOperation().equals(SCIMConstants.OperationalConstants.ADD)) {
Expand Down Expand Up @@ -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<String> 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
Expand Down Expand Up @@ -786,4 +828,67 @@ private SCIMResourceTypeSchema getSchema(UserManager userManager) throws BadRequ
}
return schema;
}

private static List<String> determineScimAttributes(PatchOperation operation) {

if (operation == null) {
return Collections.emptyList();
}
List<String> 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<String> attributes) {

for (Iterator<String> 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<String> 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<String> defaultScimSchemas() {

return Arrays.asList(
SCIMConstants.CORE_SCHEMA_URI,
SCIMConstants.USER_CORE_SCHEMA_URI,
SCIMConstants.ENTERPRISE_USER_SCHEMA_URI,
SCIMConstants.SYSTEM_USER_SCHEMA_URI
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 = ".";

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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", "[email protected]"))
.put(new JSONObject().put("value", "[email protected]"))),
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", "[email protected]"))
.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<String> expectedAttributes)
throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {

Method method = UserResourceManager.class.getDeclaredMethod("determineScimAttributes", PatchOperation.class);
method.setAccessible(true);

@SuppressWarnings("unchecked")
List<String> actualAttributes = (List<String>) 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;
}
}

0 comments on commit e4812bc

Please sign in to comment.