diff --git a/modules/charon-core/src/main/java/org/wso2/charon3/core/encoder/JSONDecoder.java b/modules/charon-core/src/main/java/org/wso2/charon3/core/encoder/JSONDecoder.java index 118dbbcbd..579e7837e 100644 --- a/modules/charon-core/src/main/java/org/wso2/charon3/core/encoder/JSONDecoder.java +++ b/modules/charon-core/src/main/java/org/wso2/charon3/core/encoder/JSONDecoder.java @@ -15,6 +15,7 @@ */ package org.wso2.charon3.core.encoder; +import org.apache.commons.lang.StringUtils; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; @@ -53,8 +54,10 @@ import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; import static org.wso2.charon3.core.schema.SCIMDefinitions.DataType.BINARY; import static org.wso2.charon3.core.schema.SCIMDefinitions.DataType.BOOLEAN; @@ -881,6 +884,7 @@ public BulkRequestData decodeBulkData(String bulkResourceString) throws BadReque List rolesEndpointOperationList = new ArrayList<>(); int failOnErrorsAttribute; List schemas = new ArrayList<>(); + Set encounteredBulkIds = new HashSet<>(); JSONObject decodedObject; try { @@ -917,9 +921,14 @@ public BulkRequestData decodeBulkData(String bulkResourceString) throws BadReque if (requestMethod.equals(SCIMConstants.OperationalConstants.POST)) { - if (!member.optString(SCIMConstants.OperationalConstants.BULK_ID).equals("") && - member.optString(SCIMConstants.OperationalConstants.BULK_ID) != null) { + String bulkId = member.optString(SCIMConstants.OperationalConstants.BULK_ID); + if (StringUtils.isNotEmpty(bulkId)) { + if (encounteredBulkIds.contains(bulkId)) { + String error = "Duplicate bulkId found: " + bulkId; + throw new BadRequestException(error, ResponseCodeConstants.INVALID_VALUE); + } + encounteredBulkIds.add(bulkId); setRequestData(requestType, requestMethod, requestVersion, member, usersEndpointOperationList, groupsEndpointOperationList, rolesEndpointOperationList); } else { diff --git a/modules/charon-core/src/main/java/org/wso2/charon3/core/protocol/BulkRequestProcessor.java b/modules/charon-core/src/main/java/org/wso2/charon3/core/protocol/BulkRequestProcessor.java index 7a0a1624f..2875527ef 100644 --- a/modules/charon-core/src/main/java/org/wso2/charon3/core/protocol/BulkRequestProcessor.java +++ b/modules/charon-core/src/main/java/org/wso2/charon3/core/protocol/BulkRequestProcessor.java @@ -17,6 +17,11 @@ */ package org.wso2.charon3.core.protocol; +import org.apache.commons.lang.StringUtils; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + import org.wso2.charon3.core.exceptions.BadRequestException; import org.wso2.charon3.core.extensions.RoleManager; import org.wso2.charon3.core.extensions.UserManager; @@ -30,6 +35,10 @@ import org.wso2.charon3.core.protocol.endpoints.UserResourceManager; import org.wso2.charon3.core.schema.SCIMConstants; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + /** * */ @@ -42,7 +51,6 @@ public class BulkRequestProcessor { private UserManager userManager; private RoleManager roleManager; - public UserResourceManager getUserResourceManager() { return userResourceManager; } @@ -120,34 +128,36 @@ public BulkResponseData processBulkRequests(BulkRequestData bulkRequestData) thr for (BulkRequestContent bulkRequestContent : bulkRequestData.getUserOperationRequests()) { if (failOnError == 0) { - bulkResponseData.addUserOperation - (getBulkResponseContent(bulkRequestContent, userResourceManager)); + bulkResponseData.addUserOperation(getBulkResponseContent(bulkRequestContent, userResourceManager)); } else { if (errors < failOnError) { - bulkResponseData.addUserOperation - (getBulkResponseContent(bulkRequestContent, userResourceManager)); + bulkResponseData.addUserOperation(getBulkResponseContent(bulkRequestContent, userResourceManager)); } } } + Map userIdMappings = getUserIdBulkIdMapping(bulkResponseData.getUserOperationResponse()); + for (BulkRequestContent bulkRequestContent : bulkRequestData.getGroupOperationRequests()) { if (failOnError == 0) { - bulkResponseData.addGroupOperation - (getBulkResponseContent(bulkRequestContent, groupResourceManager)); + bulkResponseData.addGroupOperation( + getBulkResponseContent(bulkRequestContent, userIdMappings, groupResourceManager)); } else { if (errors < failOnError) { - bulkResponseData.addGroupOperation - (getBulkResponseContent(bulkRequestContent, groupResourceManager)); + bulkResponseData.addGroupOperation( + getBulkResponseContent(bulkRequestContent, userIdMappings, groupResourceManager)); } } } for (BulkRequestContent bulkRequestContent : bulkRequestData.getRoleOperationRequests()) { if (failOnError == 0) { - bulkResponseData.addRoleOperation(getBulkResponseContent(bulkRequestContent, roleResourceManager)); + bulkResponseData.addRoleOperation(getBulkResponseContent(bulkRequestContent, userIdMappings, + roleResourceManager)); } else { if (errors < failOnError) { - bulkResponseData.addRoleOperation(getBulkResponseContent(bulkRequestContent, roleResourceManager)); + bulkResponseData.addRoleOperation(getBulkResponseContent(bulkRequestContent, + userIdMappings, roleResourceManager)); } } } @@ -155,71 +165,86 @@ public BulkResponseData processBulkRequests(BulkRequestData bulkRequestData) thr return bulkResponseData; } + private BulkResponseContent getBulkResponseContent(BulkRequestContent bulkRequestContent, + ResourceManager resourceManager) throws BadRequestException { - private BulkResponseContent getBulkResponseContent - (BulkRequestContent bulkRequestContent, ResourceManager resourceManager) - throws BadRequestException { - - BulkResponseContent bulkResponseContent = null; - SCIMResponse response; - - if (bulkRequestContent.getMethod().equals(SCIMConstants.OperationalConstants.POST)) { - - if (bulkRequestContent.getPath().contains(SCIMConstants.ROLE_ENDPOINT)) { - response = resourceManager.createRole(bulkRequestContent.getData(), roleManager); - } else { - response = resourceManager.create(bulkRequestContent.getData(), userManager, null, null); - } - - bulkResponseContent = createBulkResponseContent - (response, SCIMConstants.OperationalConstants.POST, bulkRequestContent); - errorsCheck(response); - - } else if (bulkRequestContent.getMethod().equals(SCIMConstants.OperationalConstants.PUT)) { - - String resourceId = extractIDFromPath(bulkRequestContent.getPath()); - if (bulkRequestContent.getPath().contains(SCIMConstants.ROLE_ENDPOINT)) { - response = resourceManager.updateWithPUTRole(resourceId, bulkRequestContent.getData(), roleManager); - } else { - response = resourceManager - .updateWithPUT(resourceId, bulkRequestContent.getData(), userManager, null, null); - } + return getBulkResponseContent(bulkRequestContent, null, resourceManager); + } - bulkResponseContent = createBulkResponseContent - (response, SCIMConstants.OperationalConstants.PUT, bulkRequestContent); - errorsCheck(response); + private BulkResponseContent getBulkResponseContent(BulkRequestContent bulkRequestContent, + Map userIdMappings, + ResourceManager resourceManager) + throws BadRequestException { + + BulkResponseContent bulkResponseContent = null; + SCIMResponse response; + processBulkRequestContent(bulkRequestContent, userIdMappings, bulkRequestContent.getMethod()); + + switch (bulkRequestContent.getMethod()) { + case SCIMConstants.OperationalConstants.POST: + if (bulkRequestContent.getPath().contains(SCIMConstants.ROLE_ENDPOINT)) { + response = resourceManager.createRole(bulkRequestContent.getData(), roleManager); + } else { + response = resourceManager.create(bulkRequestContent.getData(), userManager, + null, null); + } - } else if (bulkRequestContent.getMethod().equals(SCIMConstants.OperationalConstants.PATCH)) { + bulkResponseContent = createBulkResponseContent(response, SCIMConstants.OperationalConstants.POST, + bulkRequestContent); + errorsCheck(response); + break; + + case SCIMConstants.OperationalConstants.PUT: { + String resourceId = extractIDFromPath(bulkRequestContent.getPath()); + if (bulkRequestContent.getPath().contains(SCIMConstants.ROLE_ENDPOINT)) { + response = resourceManager.updateWithPUTRole(resourceId, bulkRequestContent.getData(), + roleManager); + } else { + response = resourceManager.updateWithPUT(resourceId, bulkRequestContent.getData(), userManager, + null, null); + } - String resourceId = extractIDFromPath(bulkRequestContent.getPath()); - if (bulkRequestContent.getPath().contains(SCIMConstants.ROLE_ENDPOINT)) { - response = resourceManager.updateWithPATCHRole(resourceId, bulkRequestContent.getData(), roleManager); - } else { - response = resourceManager - .updateWithPATCH(resourceId, bulkRequestContent.getData(), userManager, null, null); - } + bulkResponseContent = createBulkResponseContent(response, SCIMConstants.OperationalConstants.PUT, + bulkRequestContent); + errorsCheck(response); + break; + } - bulkResponseContent = createBulkResponseContent - (response, SCIMConstants.OperationalConstants.PATCH, bulkRequestContent); - errorsCheck(response); + case SCIMConstants.OperationalConstants.PATCH: { + String resourceId = extractIDFromPath(bulkRequestContent.getPath()); + if (bulkRequestContent.getPath().contains(SCIMConstants.ROLE_ENDPOINT)) { + response = resourceManager.updateWithPATCHRole(resourceId, bulkRequestContent.getData(), + roleManager); + } else { + response = resourceManager.updateWithPATCH(resourceId, bulkRequestContent.getData(), userManager, + null, null); + } - } else if (bulkRequestContent.getMethod().equals(SCIMConstants.OperationalConstants.DELETE)) { + bulkResponseContent = createBulkResponseContent(response, SCIMConstants.OperationalConstants.PATCH, + bulkRequestContent); + errorsCheck(response); + break; + } - String resourceId = extractIDFromPath(bulkRequestContent.getPath()); - if (bulkRequestContent.getPath().contains(SCIMConstants.ROLE_ENDPOINT)) { - response = resourceManager.deleteRole(resourceId, roleManager); - } else { - response = resourceManager.delete(resourceId, userManager); - } + case SCIMConstants.OperationalConstants.DELETE: { + String resourceId = extractIDFromPath(bulkRequestContent.getPath()); + if (bulkRequestContent.getPath().contains(SCIMConstants.ROLE_ENDPOINT)) { + response = resourceManager.deleteRole(resourceId, roleManager); + } else { + response = resourceManager.delete(resourceId, userManager); + } - bulkResponseContent = createBulkResponseContent - (response, SCIMConstants.OperationalConstants.DELETE, bulkRequestContent); - errorsCheck(response); - } - return bulkResponseContent; - } + bulkResponseContent = createBulkResponseContent(response, SCIMConstants.OperationalConstants.DELETE, + bulkRequestContent); + errorsCheck(response); + break; + } + } + return bulkResponseContent; + } private String extractIDFromPath(String path) throws BadRequestException { + String [] parts = path.split("[/]"); if (parts[2] != null) { return parts[2]; @@ -231,8 +256,8 @@ private String extractIDFromPath(String path) throws BadRequestException { private BulkResponseContent createBulkResponseContent(SCIMResponse response, String method, BulkRequestContent requestContent) { - BulkResponseContent bulkResponseContent = new BulkResponseContent(); + BulkResponseContent bulkResponseContent = new BulkResponseContent(); bulkResponseContent.setScimResponse(response); bulkResponseContent.setMethod(method); if (response.getHeaderParamMap() != null) { @@ -242,7 +267,6 @@ private BulkResponseContent createBulkResponseContent(SCIMResponse response, Str bulkResponseContent.setVersion(requestContent.getVersion()); return bulkResponseContent; - } private void errorsCheck(SCIMResponse response) { @@ -252,4 +276,128 @@ private void errorsCheck(SCIMResponse response) { } } + /** + * This method is used to process the bulk request content. + * This method will replace the bulk id with the created user id. + * + * @param bulkRequestContent Bulk request content. + * @param userIDMappings User id bulk id mapping. + * @param method HTTP method. + * @throws BadRequestException Bad request exception. + */ + private void processBulkRequestContent(BulkRequestContent bulkRequestContent, Map userIDMappings, + String method) throws BadRequestException { + + try { + if (userIDMappings == null || userIDMappings.isEmpty() || + SCIMConstants.OperationalConstants.DELETE.equals(method)) { + return; + } + + JSONObject dataJson = new JSONObject(bulkRequestContent.getData()); + String usersOrMembersKey = getUsersOrMembersKey(bulkRequestContent.getPath()); + JSONArray usersArray = getUserArray(dataJson, method, usersOrMembersKey); + + if (usersArray != null) { + String bulkIdPrefix = + SCIMConstants.OperationalConstants.BULK_ID + SCIMConstants.OperationalConstants.COLON; + for (int i = 0; i < usersArray.length(); i++) { + JSONObject user = usersArray.getJSONObject(i); + String userValue = user.getString(SCIMConstants.OperationalConstants.VALUE); + if (userValue.startsWith(bulkIdPrefix)) { + String userBulkId = userValue.substring(bulkIdPrefix.length()); + String userId = userIDMappings.get(userBulkId); + if (StringUtils.isNotBlank(userId)) { + user.put(SCIMConstants.OperationalConstants.VALUE, userId); + } + } + } + } + bulkRequestContent.setData(dataJson.toString()); + } catch (JSONException e) { + throw new BadRequestException("Error while parsing the data field of the bulk request content", + ResponseCodeConstants.INVALID_SYNTAX); + } + } + + private String getUsersOrMembersKey(String path) { + + return path.contains(SCIMConstants.ROLE_ENDPOINT) ? SCIMConstants.RoleSchemaConstants.USERS : + SCIMConstants.GroupSchemaConstants.MEMBERS; + } + + /** + * This method is used to get the user array from the data JSON object. + * + * @param dataJson SCIM data JSON object. + * @param method HTTP method. + * @param usersOrMembersKey Users or members key. + * @return User array. + * @throws JSONException JSON exception. + */ + private JSONArray getUserArray(JSONObject dataJson, String method, String usersOrMembersKey) throws JSONException { + + if (SCIMConstants.OperationalConstants.PATCH.equals(method)) { + return getUserArrayForPatch(dataJson, usersOrMembersKey); + } + return dataJson.optJSONArray(usersOrMembersKey); + } + + private JSONArray getUserArrayForPatch(JSONObject dataJson, String usersOrMembersKey) throws JSONException { + + JSONArray operations = dataJson.optJSONArray(SCIMConstants.OperationalConstants.OPERATIONS); + if (operations == null) { + return null; + } + for (int i = 0; i < operations.length(); i++) { + JSONObject operation = operations.getJSONObject(i); + if (isValidOperation(operation, usersOrMembersKey)) { + if (operation.has(SCIMConstants.OperationalConstants.PATH)) { + return operation.optJSONArray(SCIMConstants.OperationalConstants.VALUE); + } + JSONObject valueObject = operation.optJSONObject(SCIMConstants.OperationalConstants.VALUE); + if (valueObject != null) { + return valueObject.optJSONArray(usersOrMembersKey); + } + } + } + return null; + } + + private boolean isValidOperation(JSONObject operation, String path) throws JSONException { + + String operationType = operation.optString(SCIMConstants.OperationalConstants.OP); + String operationPath = operation.optString(SCIMConstants.OperationalConstants.PATH, StringUtils.EMPTY); + + return (SCIMConstants.OperationalConstants.ADD.equalsIgnoreCase(operationType) || + SCIMConstants.OperationalConstants.REPLACE.equalsIgnoreCase(operationType)) && + (operationPath.equals(path) || operationPath.isEmpty()); + } + + /** + * This method is used to get user id bulk id mapping from the bulk user operation response. + * + * @param bulkUserOperationResponse Bulk user operation response. + * @return Bulk id user id mapping. + */ + private static Map getUserIdBulkIdMapping(List bulkUserOperationResponse) { + + Map userIdMappings = new HashMap<>(); + for (BulkResponseContent bulkResponse : bulkUserOperationResponse) { + String bulkId = bulkResponse.getBulkID(); + + SCIMResponse response = bulkResponse.getScimResponse(); + if (response.getResponseStatus() == ResponseCodeConstants.CODE_CREATED) { + String locationHeader = response.getHeaderParamMap().get(SCIMConstants.LOCATION_HEADER); + + if (locationHeader != null) { + String[] locationHeaderParts = + locationHeader.split(SCIMConstants.OperationalConstants.URL_SEPARATOR); + String userId = locationHeaderParts[locationHeaderParts.length - 1]; + userIdMappings.put(bulkId, userId); + } + } + } + return userIdMappings; + } } 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 78c225eb7..a6378621d 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 @@ -775,6 +775,8 @@ public static class OperationalConstants { public static final String PUT = "PUT"; public static final String PATCH = "PATCH"; + public static final String COLON = ":"; + public static final String URL_SEPARATOR = "/"; } }