From 718a47598a8e1bd14291afb954ad9a10bdca1f35 Mon Sep 17 00:00:00 2001 From: Lashen1227 Date: Mon, 18 May 2026 10:00:46 +0530 Subject: [PATCH 01/12] feat: enhance user claim handling in pre-update profile actions --- .../internal/util/OperationComparator.java | 3 +- .../PreUpdateProfileRequestBuilder.java | 66 +- .../PreUpdateProfileResponseProcessor.java | 567 +++++++++++++++++- 3 files changed, 628 insertions(+), 8 deletions(-) diff --git a/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/internal/util/OperationComparator.java b/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/internal/util/OperationComparator.java index 02e37bf27631..fecf39430312 100644 --- a/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/internal/util/OperationComparator.java +++ b/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/internal/util/OperationComparator.java @@ -54,7 +54,8 @@ public static boolean compare(AllowedOperation allowedOp, PerformableOperation p for (String allowedPath : allowedOp.getPaths()) { if (performableOp.getPath().equals(allowedPath) || - performableOperationBasePath.equals(allowedPath)) { + performableOperationBasePath.equals(allowedPath) || + performableOp.getPath().startsWith(allowedPath)) { return true; } } diff --git a/components/user-mgt/org.wso2.carbon.identity.user.pre.update.profile.action/src/main/java/org/wso2/carbon/identity/user/pre/update/profile/action/internal/execution/PreUpdateProfileRequestBuilder.java b/components/user-mgt/org.wso2.carbon.identity.user.pre.update.profile.action/src/main/java/org/wso2/carbon/identity/user/pre/update/profile/action/internal/execution/PreUpdateProfileRequestBuilder.java index 6a63b8c28e1c..9280bd736d45 100644 --- a/components/user-mgt/org.wso2.carbon.identity.user.pre.update.profile.action/src/main/java/org/wso2/carbon/identity/user/pre/update/profile/action/internal/execution/PreUpdateProfileRequestBuilder.java +++ b/components/user-mgt/org.wso2.carbon.identity.user.pre.update.profile.action/src/main/java/org/wso2/carbon/identity/user/pre/update/profile/action/internal/execution/PreUpdateProfileRequestBuilder.java @@ -23,8 +23,10 @@ import org.wso2.carbon.identity.action.execution.api.model.ActionExecutionRequest; import org.wso2.carbon.identity.action.execution.api.model.ActionExecutionRequestContext; import org.wso2.carbon.identity.action.execution.api.model.ActionType; +import org.wso2.carbon.identity.action.execution.api.model.AllowedOperation; import org.wso2.carbon.identity.action.execution.api.model.Event; import org.wso2.carbon.identity.action.execution.api.model.FlowContext; +import org.wso2.carbon.identity.action.execution.api.model.Operation; import org.wso2.carbon.identity.action.execution.api.model.Organization; import org.wso2.carbon.identity.action.execution.api.model.Tenant; import org.wso2.carbon.identity.action.execution.api.model.User; @@ -68,6 +70,7 @@ public class PreUpdateProfileRequestBuilder implements ActionExecutionRequestBui private static final String ROLE_CLAIM_URI = "http://wso2.org/claims/roles"; private static final String GROUP_CLAIM_URI = "http://wso2.org/claims/groups"; + public static final String USER_CLAIMS_PATH_PREFIX = "/user/claims/"; @Override public ActionType getSupportedActionType() { @@ -83,13 +86,40 @@ public ActionExecutionRequest buildActionExecutionRequest(FlowContext flowContex UserActionContext userActionContext = flowContext.getValue(UserActionContext.USER_ACTION_CONTEXT_REFERENCE_KEY, UserActionContext.class); PreUpdateProfileAction preUpdateProfileAction = (PreUpdateProfileAction) actionExecutionContext.getAction(); + Event event = getEvent(userActionContext, preUpdateProfileAction); return new ActionExecutionRequest.Builder() .actionType(getSupportedActionType()) - .event(getEvent(userActionContext, preUpdateProfileAction)) + .event(event) + .allowedOperations(getAllowedOperations(event)) .build(); } + private List getAllowedOperations(Event event) throws ActionExecutionRequestBuilderException { + + List addOrReplacePaths = new ArrayList<>(); + List removePaths = new ArrayList<>(); + + addOrReplacePaths.add(USER_CLAIMS_PATH_PREFIX); + removePaths.add(USER_CLAIMS_PATH_PREFIX); + + List allowedOperations = new ArrayList<>(); + + allowedOperations.add(createAllowedOperation(Operation.ADD, addOrReplacePaths)); + allowedOperations.add(createAllowedOperation(Operation.REMOVE, removePaths)); + allowedOperations.add(createAllowedOperation(Operation.REPLACE, addOrReplacePaths)); + + return allowedOperations; + } + + private AllowedOperation createAllowedOperation(Operation op, List paths) { + + AllowedOperation operation = new AllowedOperation(); + operation.setOp(op); + operation.setPaths(new ArrayList<>(paths)); + return operation; + } + private Event getEvent(UserActionContext userActionContext, PreUpdateProfileAction preUpdateProfileAction) throws ActionExecutionRequestBuilderException { @@ -237,13 +267,37 @@ private void setClaimsInUserBuilder(User.Builder userBuilder, Map entry : updatingUserClaimsInRequest.entrySet()) { + String claimKey = entry.getKey(); + if (isRoleOrGroupClaim(claimKey) || claimValues.containsKey(claimKey) && + StringUtils.isNotBlank(claimValues.get(claimKey))) { + continue; + } + + Object updatingClaimValue = entry.getValue(); + UpdatingUserClaim claim; + if (isMultiValuedClaim(claimKey)) { + if (!(updatingClaimValue instanceof String[])) { + throw new ActionExecutionRequestBuilderException( + "Invalid claim value format for multi-valued claim: " + claimKey + + " Only String[] types are expected."); + } + claim = new UpdatingUserClaim(claimKey, new String[0], (String[]) updatingClaimValue); + } else { + claim = new UpdatingUserClaim(claimKey, null, String.valueOf(updatingClaimValue)); + } + userClaimValuesToSetInEvent.add(claim); } } diff --git a/components/user-mgt/org.wso2.carbon.identity.user.pre.update.profile.action/src/main/java/org/wso2/carbon/identity/user/pre/update/profile/action/internal/execution/PreUpdateProfileResponseProcessor.java b/components/user-mgt/org.wso2.carbon.identity.user.pre.update.profile.action/src/main/java/org/wso2/carbon/identity/user/pre/update/profile/action/internal/execution/PreUpdateProfileResponseProcessor.java index 75d2d57cb027..c168532b5817 100644 --- a/components/user-mgt/org.wso2.carbon.identity.user.pre.update.profile.action/src/main/java/org/wso2/carbon/identity/user/pre/update/profile/action/internal/execution/PreUpdateProfileResponseProcessor.java +++ b/components/user-mgt/org.wso2.carbon.identity.user.pre.update.profile.action/src/main/java/org/wso2/carbon/identity/user/pre/update/profile/action/internal/execution/PreUpdateProfileResponseProcessor.java @@ -24,15 +24,59 @@ import org.wso2.carbon.identity.action.execution.api.model.ActionInvocationSuccessResponse; import org.wso2.carbon.identity.action.execution.api.model.ActionType; import org.wso2.carbon.identity.action.execution.api.model.FlowContext; +import org.wso2.carbon.identity.action.execution.api.model.Operation; +import org.wso2.carbon.identity.action.execution.api.model.PerformableOperation; import org.wso2.carbon.identity.action.execution.api.model.Success; import org.wso2.carbon.identity.action.execution.api.model.SuccessStatus; import org.wso2.carbon.identity.action.execution.api.service.ActionExecutionResponseProcessor; +import org.wso2.carbon.identity.application.authentication.framework.util.FrameworkUtils; +import org.wso2.carbon.identity.claim.metadata.mgt.ClaimMetadataManagementService; +import org.wso2.carbon.identity.claim.metadata.mgt.exception.ClaimMetadataException; +import org.wso2.carbon.identity.claim.metadata.mgt.model.LocalClaim; +import org.wso2.carbon.identity.claim.metadata.mgt.util.ClaimConstants; +import org.wso2.carbon.identity.core.context.IdentityContext; +import org.wso2.carbon.identity.user.pre.update.profile.action.internal.component.PreUpdateProfileActionServiceComponentHolder; +import org.wso2.carbon.identity.user.pre.update.profile.action.internal.model.PreUpdateProfileEvent; +import org.wso2.carbon.user.api.UserRealm; +import org.wso2.carbon.user.api.UserStoreManager; +import org.wso2.carbon.user.core.UniqueIDUserStoreManager; +import org.wso2.carbon.user.core.UserCoreConstants; +import org.wso2.carbon.user.core.service.RealmService; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.regex.Pattern; +import java.util.stream.Collectors; /** * Pre Update Profile Action Response Processor. */ public class PreUpdateProfileResponseProcessor implements ActionExecutionResponseProcessor { + private static final String ID_CLAIM_URI = "http://wso2.org/claims/userid"; + private static final String NAME_CLAIM_URI = "http://wso2.org/claims/username"; + private static final String CREATED_CLAIM_URI = "http://wso2.org/claims/created"; + private static final String FAMILY_NAME_CLAIM_URI = "http://wso2.org/claims/lastname"; + private static final String GROUP_CLAIM_URI = "http://wso2.org/claims/groups"; + private static final String ROLE_CLAIM_URI = "http://wso2.org/claims/roles"; + private static final String IDENTITY_CLAIM_URI_PREFIX = "http://wso2.org/claims/identity/"; + private static final String URI = "uri"; + private static final String VALUE = "value"; + private static final String USER_CLAIMS_TO_BE_ADDED = "userClaimsToBeAdded"; + private static final String USER_CLAIMS_TO_BE_MODIFIED = "userClaimsToBeModified"; + private static final String USER_CLAIMS_TO_BE_REMOVED = "userClaimsToBeRemoved"; + private static final String MULTI_VALUED_CLAIMS_TO_BE_ADDED = "multiValuedClaimsToBeAdded"; + private static final String MULTI_VALUED_CLAIMS_TO_BE_REMOVED = "multiValuedClaimsToBeRemoved"; + private static final String USER_CLAIMS_PATH_PREFIX = "/user/claims/"; + private static final String VALUE_PATH_SEGMENT = "/value/"; + private static final String ARRAY_APPEND_PATH_SEGMENT = "/-"; + @Override public ActionType getSupportedActionType() { @@ -41,9 +85,530 @@ public ActionType getSupportedActionType() { @Override public ActionExecutionStatus processSuccessResponse(FlowContext flowContext, - ActionExecutionResponseContext responseContext) + ActionExecutionResponseContext responseContext) throws ActionExecutionResponseProcessorException { + List operationsToPerform = responseContext.getActionInvocationResponse().getOperations(); + Map userClaimsToBeAdded = new HashMap<>(); + Map userClaimsToBeModified = new HashMap<>(); + Map userClaimsToBeRemoved = new HashMap<>(); + Map> simpleMultiValuedClaimsToBeAdded = new HashMap<>(); + Map> simpleMultiValuedClaimsToBeRemoved = new HashMap<>(); + + if (operationsToPerform != null && !operationsToPerform.isEmpty()) { + UniqueIDUserStoreManager userStoreManager = getUserStoreManager(); + for (PerformableOperation operation : operationsToPerform) { + switch (operation.getOp()) { + case ADD: + populateAddOperationResult(operation, responseContext, userClaimsToBeAdded, + userClaimsToBeModified, simpleMultiValuedClaimsToBeAdded, userStoreManager); + break; + case REPLACE: + populateModifyOperationResult(operation, responseContext, userClaimsToBeModified, + simpleMultiValuedClaimsToBeRemoved, simpleMultiValuedClaimsToBeAdded, userStoreManager); + break; + case REMOVE: + populateRemoveOperationResult(operation, responseContext, userClaimsToBeModified, + userClaimsToBeRemoved, simpleMultiValuedClaimsToBeRemoved); + break; + default: + break; + } + } + } + + flowContext.add(USER_CLAIMS_TO_BE_ADDED, userClaimsToBeAdded); + flowContext.add(USER_CLAIMS_TO_BE_MODIFIED, userClaimsToBeModified); + flowContext.add(USER_CLAIMS_TO_BE_REMOVED, userClaimsToBeRemoved); + flowContext.add(MULTI_VALUED_CLAIMS_TO_BE_ADDED, simpleMultiValuedClaimsToBeAdded); + flowContext.add(MULTI_VALUED_CLAIMS_TO_BE_REMOVED, simpleMultiValuedClaimsToBeRemoved); + return new SuccessStatus.Builder().setResponseContext(flowContext.getContextData()).build(); } + + private void populateAddOperationResult(PerformableOperation operation, + ActionExecutionResponseContext + responseContext, + Map userClaimsToBeAdded, + Map userClaimsToBeModified, + Map> simpleMultiValuedClaimsToBeAdded, + UniqueIDUserStoreManager userStoreManager) + throws ActionExecutionResponseProcessorException { + + String userId = responseContext.getActionEvent().getUser().getId(); + PreUpdateProfileEvent.FlowInitiatorType initiatorType = ((PreUpdateProfileEvent) responseContext + .getActionEvent()).getInitiatorType(); + + String claimUri; + String path = operation.getPath(); + + // Determine claim URI: from path (path-based format) or from value map. + if (path != null && path.startsWith(USER_CLAIMS_PATH_PREFIX) && + path.length() > USER_CLAIMS_PATH_PREFIX.length()) { + claimUri = getClaimUriFromPath(path); + } else if (operation.getValue() instanceof LinkedHashMap) { + LinkedHashMap valueMap = (LinkedHashMap) operation.getValue(); + claimUri = Optional.ofNullable(valueMap.get(URI)) + .filter(value -> value instanceof String) + .map(value -> (String) value) + .orElseThrow(() -> new ActionExecutionResponseProcessorException("Missing or wrong format for " + + "claim uri in operation")); + } else { + throw new ActionExecutionResponseProcessorException("Operation path does not contain claim URI and " + + "operation value is not a map"); + } + + Optional localClaim = isLocalClaim(claimUri); + validateGroupAndRoleClaims(claimUri); + validateImmutableClaims(claimUri); + validateFlowInitiatorClaims(claimUri, localClaim); + + if (!isMultiValuedClaim(localClaim)) { + // Extract single claim value from String directly or from value map. + String claimValue; + if (operation.getValue() instanceof String) { + claimValue = (String) operation.getValue(); + } else if (operation.getValue() instanceof LinkedHashMap) { + LinkedHashMap valueMap = (LinkedHashMap) operation.getValue(); + claimValue = Optional.ofNullable(valueMap.get(VALUE)) + .filter(value -> value instanceof String) + .map(value -> (String) value) + .orElseThrow( + () -> new ActionExecutionResponseProcessorException("Missing or wrong format for " + + "single valued claim in operation")); + } else { + throw new ActionExecutionResponseProcessorException("Missing or wrong format for " + + "claim value in add operation"); + } + + Map userClaimValues = getUserClaimValues(userId, claimUri, userStoreManager); + // Validate if the claim value already exists for the user + if (operation.getOp() == Operation.ADD && userClaimValues.get(claimUri) != null && + !claimValue.trim().isEmpty()) { + userClaimsToBeModified.put(claimUri, claimValue.trim()); + } else { + userClaimsToBeAdded.put(claimUri, claimValue.trim()); + } + } else { + // Extract multi-valued claim value from List directly or from value map. + List claimValue; + if (operation.getValue() instanceof LinkedHashMap) { + LinkedHashMap valueMap = (LinkedHashMap) operation.getValue(); + claimValue = Optional.ofNullable(valueMap.get(VALUE)) + .filter(value -> value instanceof List) + .map(value -> (List) value) + .filter(list -> list.stream().allMatch(e -> e instanceof String)) + .map(list -> (List) list) + .orElseThrow(() -> new ActionExecutionResponseProcessorException( + "Missing or wrong format for multi valued claim in operation")); + } else if (operation.getValue() instanceof List) { + List rawList = (List) operation.getValue(); + if (!rawList.stream().allMatch(e -> e instanceof String)) { + throw new ActionExecutionResponseProcessorException( + "Missing or wrong format for multi valued claim in operation"); + } + @SuppressWarnings("unchecked") + List typedList = (List) operation.getValue(); + claimValue = typedList; + } else { + throw new ActionExecutionResponseProcessorException( + "Missing or wrong format for multi valued claim in add operation"); + } + populateMultivaluedClaimsForAddOperation(localClaim, claimValue, userId, userClaimsToBeModified, + simpleMultiValuedClaimsToBeAdded, initiatorType, userStoreManager); + } + } + + private void populateModifyOperationResult(PerformableOperation operation, + ActionExecutionResponseContext + responseContext, + Map userClaimsToBeModified, + Map> simpleMultiValuedClaimsToBeRemoved, + Map> simpleMultiValuedClaimsToBeAdded, + UniqueIDUserStoreManager userStoreManager) + throws ActionExecutionResponseProcessorException { + + String userId = responseContext.getActionEvent().getUser().getId(); + PreUpdateProfileEvent.FlowInitiatorType initiatorType = ((PreUpdateProfileEvent) responseContext + .getActionEvent()).getInitiatorType(); + + String path = operation.getPath(); + String claimUri = getClaimUriFromPath(path); + + Optional localClaim = isLocalClaim(claimUri); + validateGroupAndRoleClaims(claimUri); + validateImmutableClaims(claimUri); + validateFlowInitiatorClaims(claimUri, localClaim); + String separator = FrameworkUtils.getMultiAttributeSeparator(); + + if (!isMultiValuedClaim(localClaim)) { + // For single valued claims, value is directly the new value string. + String claimValue; + if (operation.getValue() instanceof String) { + claimValue = (String) operation.getValue(); + } else if (operation.getValue() instanceof LinkedHashMap) { + LinkedHashMap valueMap = (LinkedHashMap) operation.getValue(); + claimValue = Optional.ofNullable(valueMap.get(VALUE)) + .filter(value -> value instanceof String) + .map(value -> (String) value) + .orElseThrow( + () -> new ActionExecutionResponseProcessorException("Missing or wrong format for " + + "claim value in operation")); + } else { + throw new ActionExecutionResponseProcessorException("Missing or wrong format for " + + "claim value in replace operation"); + } + userClaimsToBeModified.put(claimUri, claimValue.trim()); + } else { + populateMultiValuedClaimsForReplaceOperation(operation, initiatorType, userClaimsToBeModified, + simpleMultiValuedClaimsToBeAdded, simpleMultiValuedClaimsToBeRemoved, userId, claimUri, separator, + userStoreManager); + } + } + + private void populateRemoveOperationResult(PerformableOperation operation, + ActionExecutionResponseContext + responseContext, + Map userClaimsToBeModified, + Map userClaimsToBeRemoved, + Map> simpleMultiValuedClaimsToBeRemoved) + throws ActionExecutionResponseProcessorException { + + String path = operation.getPath(); + PreUpdateProfileEvent.FlowInitiatorType initiatorType = ((PreUpdateProfileEvent) responseContext + .getActionEvent()).getInitiatorType(); + + // Extract the claim URI and optionally the specific value to remove for + // multivalued claims. + String claimUri = getClaimUriFromPath(path); + String valueToRemove = getValueToRemoveFromPath(path); + + Optional localClaim = isLocalClaim(claimUri); + validateGroupAndRoleClaims(claimUri); + validateImmutableClaims(claimUri); + + if (!isMultiValuedClaim(localClaim)) { + userClaimsToBeRemoved.put(claimUri, ""); + userClaimsToBeModified.remove(claimUri); + } else { + populateMultiValuedClaimsForRemoveOperation(initiatorType, claimUri, valueToRemove, userClaimsToBeRemoved, + simpleMultiValuedClaimsToBeRemoved); + } + } + + private void populateMultivaluedClaimsForAddOperation(Optional localClaim, List claimValue, + String userId, + Map userClaimsToBeModified, + Map> simpleMultiValuedClaimsToBeAdded, + PreUpdateProfileEvent.FlowInitiatorType initiatorType, + UniqueIDUserStoreManager userStoreManager) + throws ActionExecutionResponseProcessorException { + + String claimUri = localClaim.get().getClaimURI(); + String separator = FrameworkUtils.getMultiAttributeSeparator(); + Map userClaimValues = getUserClaimValues(userId, claimUri, userStoreManager); + List filteredValues = getFilteredModifyingClaimValues(userClaimValues, claimUri, claimValue, + separator); + if (initiatorType == PreUpdateProfileEvent.FlowInitiatorType.ADMIN || + initiatorType == PreUpdateProfileEvent.FlowInitiatorType.APPLICATION) { + simpleMultiValuedClaimsToBeAdded.put(claimUri, filteredValues); + } else if (initiatorType == PreUpdateProfileEvent.FlowInitiatorType.USER) { + String addingClaimValue = (userClaimValues.get(claimUri) == null) ? String.join(separator, filteredValues) + : userClaimValues.get(claimUri) + separator + String.join(separator, filteredValues); + userClaimsToBeModified.put(claimUri, addingClaimValue); + } + } + + private void populateMultiValuedClaimsForReplaceOperation(PerformableOperation operation, + PreUpdateProfileEvent.FlowInitiatorType initiatorType, + Map userClaimsToBeModified, + Map> + simpleMultiValuedClaimsToBeAdded, + Map> + simpleMultiValuedClaimsToBeRemoved, + String userId, String claimUri, String separator, + UniqueIDUserStoreManager userStoreManager) + throws ActionExecutionResponseProcessorException { + + // For multivalued replace, value can be a map with "value" (list) and optional + // "oldValue" (string), or directly a list of strings for replacing the entire array. + List claimValue; + String oldValueName = null; + + if (operation.getValue() instanceof LinkedHashMap) { + LinkedHashMap valueMap = (LinkedHashMap) operation.getValue(); + claimValue = Optional.ofNullable(valueMap.get(VALUE)) + .filter(value -> value instanceof List) + .map(value -> (List) value) + .filter(list -> list.stream().allMatch(e -> e instanceof String)) + .map(list -> (List) list) + .orElseThrow(() -> new ActionExecutionResponseProcessorException( + "Missing or wrong format for multi valued claim in operation")); + + oldValueName = Optional.ofNullable(valueMap.get("oldValue")) + .filter(value -> value instanceof String) + .map(value -> (String) value) + .orElse(null); + } else if (operation.getValue() instanceof List) { + List rawList = (List) operation.getValue(); + if (!rawList.stream().allMatch(e -> e instanceof String)) { + throw new ActionExecutionResponseProcessorException( + "Missing or wrong format for multi valued claim in replace operation"); + } + claimValue = (List) operation.getValue(); + } else { + throw new ActionExecutionResponseProcessorException( + "Missing or wrong format for multi valued claim in replace operation"); + } + + if (oldValueName == null) { + // Replacing the entire array: calculate deltas to avoid changing unaffected values. + Map userClaimValues = getUserClaimValues(userId, claimUri, userStoreManager); + List existingValues = new ArrayList<>(); + if (userClaimValues.get(claimUri) != null && !userClaimValues.get(claimUri).isEmpty()) { + existingValues.addAll(Arrays.asList(userClaimValues.get(claimUri).split(Pattern.quote(separator)))); + } + + if (claimValue.size() < existingValues.size()) { + throw new ActionExecutionResponseProcessorException( + "To replace a multivalued attribute, the full array must be passed to " + + "prevent accidental deletion of existing values."); + } + + List addedValues = claimValue.stream() + .map(String::trim) + .filter(s -> !s.isEmpty()) + .filter(s -> !existingValues.contains(s)) + .collect(Collectors.toList()); + + List removedValues = existingValues.stream() + .map(String::trim) + .filter(s -> !s.isEmpty()) + .filter(s -> !claimValue.contains(s)) + .collect(Collectors.toList()); + + if (initiatorType == PreUpdateProfileEvent.FlowInitiatorType.ADMIN || + initiatorType == PreUpdateProfileEvent.FlowInitiatorType.APPLICATION) { + if (!removedValues.isEmpty()) { + simpleMultiValuedClaimsToBeRemoved.put(claimUri, removedValues); + } + if (!addedValues.isEmpty()) { + simpleMultiValuedClaimsToBeAdded.put(claimUri, addedValues); + } + } else if (initiatorType == PreUpdateProfileEvent.FlowInitiatorType.USER) { + String modifyingClaimValue = claimValue.stream() + .map(String::trim) + .filter(s -> !s.isEmpty()) + .collect(Collectors.joining(separator)); + userClaimsToBeModified.put(claimUri, modifyingClaimValue); + } + } else { + // Replacing a specific value in the array + if (initiatorType == PreUpdateProfileEvent.FlowInitiatorType.ADMIN || + initiatorType == PreUpdateProfileEvent.FlowInitiatorType.APPLICATION) { + Map userClaimValues = getUserClaimValues(userId, claimUri, userStoreManager); + List filteredClaims = getFilteredModifyingClaimValues(userClaimValues, claimUri, + claimValue, separator); + + if (!filteredClaims.isEmpty()) { + simpleMultiValuedClaimsToBeRemoved.put(claimUri, Arrays.asList(oldValueName)); + List trimmedFilteredClaims = filteredClaims.stream() + .map(String::trim) + .collect(Collectors.toList()); + simpleMultiValuedClaimsToBeAdded.put(claimUri, trimmedFilteredClaims); + } + } else if (initiatorType == PreUpdateProfileEvent.FlowInitiatorType.USER) { + Map userClaimValues = getUserClaimValues(userId, claimUri, userStoreManager); + List filteredClaims = getFilteredModifyingClaimValues(userClaimValues, claimUri, + claimValue, separator); + + List existingValues = new ArrayList<>(); + if (userClaimValues.get(claimUri) != null && !userClaimValues.get(claimUri).isEmpty()) { + existingValues.addAll(Arrays.asList(userClaimValues.get(claimUri).split(Pattern.quote(separator)))); + } + + existingValues.remove(oldValueName); + existingValues.addAll(filteredClaims); + String modifyingClaimValue = existingValues.stream() + .map(String::trim) + .filter(s -> !s.isEmpty()) + .collect(Collectors.joining(separator)); + userClaimsToBeModified.put(claimUri, modifyingClaimValue); + } + } + } + + private void populateMultiValuedClaimsForRemoveOperation( + PreUpdateProfileEvent.FlowInitiatorType initiatorType, + String claimUri, + String valueToRemove, + Map userClaimsToBeRemoved, + Map> simpleMultiValuedClaimsToBeRemoved) + throws ActionExecutionResponseProcessorException { + + if (valueToRemove != null) { + throw new ActionExecutionResponseProcessorException( + "Removing a specific value from a multivalued claim is not supported."); + } + + userClaimsToBeRemoved.put(claimUri, ""); + } + + private Optional isLocalClaim(String claimUri) throws ActionExecutionResponseProcessorException { + + ClaimMetadataManagementService claimMetadataManagementService = PreUpdateProfileActionServiceComponentHolder + .getInstance().getClaimManagementService(); + String tenantDomain = IdentityContext.getThreadLocalIdentityContext().getTenantDomain(); + try { + Optional localClaim = claimMetadataManagementService.getLocalClaim(claimUri, tenantDomain); + if (!localClaim.isPresent()) { + throw new ActionExecutionResponseProcessorException("Local claim not found for claim uri: " + claimUri); + } + return localClaim; + } catch (ClaimMetadataException e) { + throw new ActionExecutionResponseProcessorException("Error while retrieving localClaim for claim uri: " + + claimUri, e); + } + } + + private boolean isMultiValuedClaim(Optional localClaim) { + + return localClaim.isPresent() && + Boolean.parseBoolean(localClaim.get().getClaimProperty(ClaimConstants.MULTI_VALUED_PROPERTY)); + } + + private String getClaimUriFromPath(String path) + throws ActionExecutionResponseProcessorException { + + if (path.startsWith(USER_CLAIMS_PATH_PREFIX)) { + + String remainder = path.substring(USER_CLAIMS_PATH_PREFIX.length()); + // Handle SCIM array append syntax: /claimUri/- + if (remainder.endsWith(ARRAY_APPEND_PATH_SEGMENT)) { + return remainder.substring(0, remainder.length() - 2); + } + + int valueIndex = remainder.indexOf(VALUE_PATH_SEGMENT); + if (valueIndex != -1) { + return remainder.substring(0, valueIndex); + } + + return remainder; + } + + throw new ActionExecutionResponseProcessorException("Invalid path format: " + path); + } + + /** + * Extract the specific value to remove from a multivalued claim path. + * + * Path format: /user/claims/{claimURI}/value/{valueToRemove} + * e.g., /user/claims/http://wso2.org/claims/emails/value/bob@example.com + * + * @param path The operation path. + * @return The value to remove, or null if path doesn't contain a value segment. + */ + private String getValueToRemoveFromPath(String path) { + + if (path.startsWith(USER_CLAIMS_PATH_PREFIX)) { + String remainder = path.substring(USER_CLAIMS_PATH_PREFIX.length()); + int valueIndex = remainder.indexOf(VALUE_PATH_SEGMENT); + if (valueIndex != -1) { + return remainder.substring(valueIndex + VALUE_PATH_SEGMENT.length()); + } + } + return null; + } + + private UniqueIDUserStoreManager getUserStoreManager() throws ActionExecutionResponseProcessorException { + + String tenantDomain = IdentityContext.getThreadLocalIdentityContext().getTenantDomain(); + RealmService realmService = PreUpdateProfileActionServiceComponentHolder.getInstance().getRealmService(); + + if (realmService == null) { + throw new ActionExecutionResponseProcessorException("Realm service is unavailable."); + } + + try { + int tenantId = realmService.getTenantManager().getTenantId(tenantDomain); + UserRealm userRealm = realmService.getTenantUserRealm(tenantId); + + if (userRealm == null) { + throw new ActionExecutionResponseProcessorException( + "User realm is not available for tenant: " + tenantDomain); + } + + UserStoreManager userStoreManager = userRealm.getUserStoreManager(); + if (!(userStoreManager instanceof UniqueIDUserStoreManager)) { + throw new ActionExecutionResponseProcessorException( + "User store manager is not an instance of UniqueIDUserStoreManager for tenant: " + + tenantDomain); + } + + return (UniqueIDUserStoreManager) userStoreManager; + } catch (org.wso2.carbon.user.api.UserStoreException e) { + throw new ActionExecutionResponseProcessorException( + "Error while loading user store manager for tenant: " + tenantDomain, e); + } + } + + private Map getUserClaimValues(String userId, String claimUri, + UniqueIDUserStoreManager userStoreManager) throws + ActionExecutionResponseProcessorException { + + try { + return userStoreManager.getUserClaimValuesWithID(userId, new String[] { claimUri }, + UserCoreConstants.DEFAULT_PROFILE); + } catch (org.wso2.carbon.user.core.UserStoreException e) { + throw new ActionExecutionResponseProcessorException("Failed to retrieve user claims from user store.", e); + } + } + + private List getFilteredModifyingClaimValues(Map userClaimValues, String claimUri, + List claimValue, String separator) { + + List userValues = Optional.ofNullable(userClaimValues.get(claimUri)) + .map(value -> Arrays.asList(value.split(separator))) + .orElse(Collections.emptyList()); + String[] claimValuesList = filterDuplicatedValues(claimValue); + + return Arrays.stream(claimValuesList) + .filter(value -> !userValues.contains(value)) + .collect(Collectors.toList()); + } + + private String[] filterDuplicatedValues(List claimValue) { + + return claimValue.stream() + .map(String::trim) + .filter(s -> !s.isEmpty()) + .distinct() + .toArray(String[]::new); + } + + private void validateImmutableClaims(String claimUri) throws ActionExecutionResponseProcessorException { + + if (claimUri.equals(ID_CLAIM_URI) || claimUri.equals(CREATED_CLAIM_URI) || claimUri.equals(NAME_CLAIM_URI) || + claimUri.equals(FAMILY_NAME_CLAIM_URI) || claimUri.contains(IDENTITY_CLAIM_URI_PREFIX)) { + throw new ActionExecutionResponseProcessorException("Immutable claims cannot be added or modified: " + + claimUri); + } + } + + private void validateFlowInitiatorClaims(String claimUri, Optional localClaim) + throws ActionExecutionResponseProcessorException { + + if (localClaim.get().getFlowInitiator()) { + throw new ActionExecutionResponseProcessorException(claimUri + " is not allowed to be added."); + } + } + + private void validateGroupAndRoleClaims(String claimUri) + throws ActionExecutionResponseProcessorException { + + if (claimUri.equals(GROUP_CLAIM_URI) || claimUri.equals(ROLE_CLAIM_URI)) { + throw new ActionExecutionResponseProcessorException("Groups/Roles are not allowed to be added: " + + claimUri); + } + } } From abb4df56b2bbd626e4ffeecdc18f0d36f66483bb Mon Sep 17 00:00:00 2001 From: Lashen1227 Date: Fri, 22 May 2026 10:22:22 +0530 Subject: [PATCH 02/12] feat: add SCIM level attribute validation in pre-update profile response processing --- .../PreUpdateProfileResponseProcessor.java | 124 +++++++++++++++++- 1 file changed, 119 insertions(+), 5 deletions(-) diff --git a/components/user-mgt/org.wso2.carbon.identity.user.pre.update.profile.action/src/main/java/org/wso2/carbon/identity/user/pre/update/profile/action/internal/execution/PreUpdateProfileResponseProcessor.java b/components/user-mgt/org.wso2.carbon.identity.user.pre.update.profile.action/src/main/java/org/wso2/carbon/identity/user/pre/update/profile/action/internal/execution/PreUpdateProfileResponseProcessor.java index c168532b5817..e8278ec3e63e 100644 --- a/components/user-mgt/org.wso2.carbon.identity.user.pre.update.profile.action/src/main/java/org/wso2/carbon/identity/user/pre/update/profile/action/internal/execution/PreUpdateProfileResponseProcessor.java +++ b/components/user-mgt/org.wso2.carbon.identity.user.pre.update.profile.action/src/main/java/org/wso2/carbon/identity/user/pre/update/profile/action/internal/execution/PreUpdateProfileResponseProcessor.java @@ -32,6 +32,7 @@ import org.wso2.carbon.identity.application.authentication.framework.util.FrameworkUtils; import org.wso2.carbon.identity.claim.metadata.mgt.ClaimMetadataManagementService; import org.wso2.carbon.identity.claim.metadata.mgt.exception.ClaimMetadataException; +import org.wso2.carbon.identity.claim.metadata.mgt.model.Claim; import org.wso2.carbon.identity.claim.metadata.mgt.model.LocalClaim; import org.wso2.carbon.identity.claim.metadata.mgt.util.ClaimConstants; import org.wso2.carbon.identity.core.context.IdentityContext; @@ -62,10 +63,10 @@ public class PreUpdateProfileResponseProcessor implements ActionExecutionRespons private static final String ID_CLAIM_URI = "http://wso2.org/claims/userid"; private static final String NAME_CLAIM_URI = "http://wso2.org/claims/username"; private static final String CREATED_CLAIM_URI = "http://wso2.org/claims/created"; - private static final String FAMILY_NAME_CLAIM_URI = "http://wso2.org/claims/lastname"; private static final String GROUP_CLAIM_URI = "http://wso2.org/claims/groups"; private static final String ROLE_CLAIM_URI = "http://wso2.org/claims/roles"; private static final String IDENTITY_CLAIM_URI_PREFIX = "http://wso2.org/claims/identity/"; + private static final String SCIM_SCHEMA_URI_PREFIX = "urn:ietf:params:scim:schemas"; private static final String URI = "uri"; private static final String VALUE = "value"; private static final String USER_CLAIMS_TO_BE_ADDED = "userClaimsToBeAdded"; @@ -84,8 +85,8 @@ public ActionType getSupportedActionType() { } @Override - public ActionExecutionStatus processSuccessResponse(FlowContext flowContext, - ActionExecutionResponseContext responseContext) + public ActionExecutionStatus processSuccessResponse( + FlowContext flowContext, ActionExecutionResponseContext responseContext) throws ActionExecutionResponseProcessorException { List operationsToPerform = responseContext.getActionInvocationResponse().getOperations(); @@ -162,6 +163,7 @@ private void populateAddOperationResult(PerformableOperation operation, validateGroupAndRoleClaims(claimUri); validateImmutableClaims(claimUri); validateFlowInitiatorClaims(claimUri, localClaim); + validateSCIMLevelAttributes(claimUri, operation.getOp(), operation.getValue()); if (!isMultiValuedClaim(localClaim)) { // Extract single claim value from String directly or from value map. @@ -239,6 +241,7 @@ private void populateModifyOperationResult(PerformableOperation operation, validateGroupAndRoleClaims(claimUri); validateImmutableClaims(claimUri); validateFlowInitiatorClaims(claimUri, localClaim); + validateSCIMLevelAttributes(claimUri, operation.getOp(), operation.getValue()); String separator = FrameworkUtils.getMultiAttributeSeparator(); if (!isMultiValuedClaim(localClaim)) { @@ -286,6 +289,7 @@ private void populateRemoveOperationResult(PerformableOperation operation, Optional localClaim = isLocalClaim(claimUri); validateGroupAndRoleClaims(claimUri); validateImmutableClaims(claimUri); + validateSCIMLevelAttributes(claimUri, operation.getOp(), operation.getValue()); if (!isMultiValuedClaim(localClaim)) { userClaimsToBeRemoved.put(claimUri, ""); @@ -588,8 +592,8 @@ private String[] filterDuplicatedValues(List claimValue) { private void validateImmutableClaims(String claimUri) throws ActionExecutionResponseProcessorException { - if (claimUri.equals(ID_CLAIM_URI) || claimUri.equals(CREATED_CLAIM_URI) || claimUri.equals(NAME_CLAIM_URI) || - claimUri.equals(FAMILY_NAME_CLAIM_URI) || claimUri.contains(IDENTITY_CLAIM_URI_PREFIX)) { + if (claimUri.equals(ID_CLAIM_URI) || claimUri.equals(CREATED_CLAIM_URI) || + claimUri.equals(NAME_CLAIM_URI) || claimUri.contains(IDENTITY_CLAIM_URI_PREFIX)) { throw new ActionExecutionResponseProcessorException("Immutable claims cannot be added or modified: " + claimUri); } @@ -611,4 +615,114 @@ private void validateGroupAndRoleClaims(String claimUri) + claimUri); } } + + private void validateSCIMLevelAttributes(String claimUri, Operation op, Object value) + throws ActionExecutionResponseProcessorException { + + List scimClaims = convertLocalToSCIMDialect(claimUri); + if (scimClaims.isEmpty()) { + return; + } + + boolean isReadOnly = false; + boolean isRequired = false; + boolean isSingleValued = false; + + // multiple scim mapping + for (Claim scimClaim : scimClaims) { + if (Boolean.parseBoolean(scimClaim.getClaimProperty(ClaimConstants.READ_ONLY_PROPERTY))) { + isReadOnly = true; + } + if (Boolean.parseBoolean(scimClaim.getClaimProperty(ClaimConstants.REQUIRED_PROPERTY))) { + isRequired = true; + } + if (!Boolean.parseBoolean(scimClaim.getClaimProperty(ClaimConstants.MULTI_VALUED_PROPERTY))) { + isSingleValued = true; + } + } + + if (isReadOnly) { + throw new ActionExecutionResponseProcessorException( + "Cannot modify read-only SCIM attribute mapped to local claim: " + claimUri); + } + + if (isRequired) { + if (op == Operation.REMOVE) { + throw new ActionExecutionResponseProcessorException( + "Cannot remove required SCIM attribute mapped to local claim: " + claimUri); + } + if (op == Operation.REPLACE || op == Operation.ADD) { + boolean isEmpty = false; + if (value == null) { + isEmpty = true; + } else if (value instanceof String && ((String) value).trim().isEmpty()) { + isEmpty = true; + } else if (value instanceof List && ((List) value).isEmpty()) { + isEmpty = true; + } else if (value instanceof LinkedHashMap) { + Object val = ((LinkedHashMap) value).get(VALUE); + if (val == null || (val instanceof String && ((String) val).trim().isEmpty()) || + (val instanceof List && ((List) val).isEmpty())) { + isEmpty = true; + } + } + if (isEmpty) { + throw new ActionExecutionResponseProcessorException( + "Cannot set empty value to required SCIM attribute mapped to local claim: " + claimUri); + } + } + } + + if (isSingleValued) { + boolean hasMultipleValues = false; + if (value instanceof List && ((List) value).size() > 1) { + hasMultipleValues = true; + } else if (value instanceof LinkedHashMap) { + Object val = ((LinkedHashMap) value).get(VALUE); + if (val instanceof List && ((List) val).size() > 1) { + hasMultipleValues = true; + } + } + if (hasMultipleValues) { + throw new ActionExecutionResponseProcessorException( + "Cannot set multiple values to single-valued SCIM attribute mapped to local claim: " + + claimUri); + } + } + } + + /** + * Converts claims in local WSO2 dialect to SCIM dialect. + * + * @param claimUri The local claim URI. + * @return A list of SCIM dialect claims. + * @throws ActionExecutionResponseProcessorException If an error occurs during the conversion. + */ + private static List convertLocalToSCIMDialect(String claimUri) throws + ActionExecutionResponseProcessorException { + + if (claimUri == null || claimUri.trim().isEmpty()) { + return Collections.emptyList(); + } + + ClaimMetadataManagementService claimMetadataManagementService = PreUpdateProfileActionServiceComponentHolder + .getInstance().getClaimManagementService(); + String tenantDomain = IdentityContext.getThreadLocalIdentityContext().getTenantDomain(); + try { + List mappedExternalClaims = claimMetadataManagementService + .getMappedExternalClaimsForLocalClaim(claimUri, tenantDomain); + if (mappedExternalClaims == null || mappedExternalClaims.isEmpty()) { + return Collections.emptyList(); + } + + return mappedExternalClaims.stream() + .filter(claim -> claim.getClaimDialectURI() != null + && claim.getClaimDialectURI().contains(SCIM_SCHEMA_URI_PREFIX)) + .collect(Collectors.toList()); + + } catch (ClaimMetadataException e) { + throw new ActionExecutionResponseProcessorException( + "Error while retrieving mapped external claims for claim uri: " + claimUri, e); + } + } } From 0d9fd504601d420501ff04db5e43ff92c5603ad4 Mon Sep 17 00:00:00 2001 From: Lashen1227 Date: Fri, 22 May 2026 10:58:54 +0530 Subject: [PATCH 03/12] refactor: update PreUpdateProfileResponseProcessor to use ActionExecutionServiceComponentHolder for realm service --- .../internal/execution/PreUpdateProfileResponseProcessor.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/components/user-mgt/org.wso2.carbon.identity.user.pre.update.profile.action/src/main/java/org/wso2/carbon/identity/user/pre/update/profile/action/internal/execution/PreUpdateProfileResponseProcessor.java b/components/user-mgt/org.wso2.carbon.identity.user.pre.update.profile.action/src/main/java/org/wso2/carbon/identity/user/pre/update/profile/action/internal/execution/PreUpdateProfileResponseProcessor.java index e8278ec3e63e..411577d58235 100644 --- a/components/user-mgt/org.wso2.carbon.identity.user.pre.update.profile.action/src/main/java/org/wso2/carbon/identity/user/pre/update/profile/action/internal/execution/PreUpdateProfileResponseProcessor.java +++ b/components/user-mgt/org.wso2.carbon.identity.user.pre.update.profile.action/src/main/java/org/wso2/carbon/identity/user/pre/update/profile/action/internal/execution/PreUpdateProfileResponseProcessor.java @@ -29,6 +29,7 @@ import org.wso2.carbon.identity.action.execution.api.model.Success; import org.wso2.carbon.identity.action.execution.api.model.SuccessStatus; import org.wso2.carbon.identity.action.execution.api.service.ActionExecutionResponseProcessor; +import org.wso2.carbon.identity.action.execution.internal.component.ActionExecutionServiceComponentHolder; import org.wso2.carbon.identity.application.authentication.framework.util.FrameworkUtils; import org.wso2.carbon.identity.claim.metadata.mgt.ClaimMetadataManagementService; import org.wso2.carbon.identity.claim.metadata.mgt.exception.ClaimMetadataException; @@ -527,7 +528,7 @@ private String getValueToRemoveFromPath(String path) { private UniqueIDUserStoreManager getUserStoreManager() throws ActionExecutionResponseProcessorException { String tenantDomain = IdentityContext.getThreadLocalIdentityContext().getTenantDomain(); - RealmService realmService = PreUpdateProfileActionServiceComponentHolder.getInstance().getRealmService(); + RealmService realmService = ActionExecutionServiceComponentHolder.getInstance().getRealmService(); if (realmService == null) { throw new ActionExecutionResponseProcessorException("Realm service is unavailable."); From 1c303abbcb515220d5440099abad7a02016e9430 Mon Sep 17 00:00:00 2001 From: Lashen1227 Date: Sun, 24 May 2026 18:08:51 +0530 Subject: [PATCH 04/12] refactor: enhance PreUpdateProfileRequestBuilder and PreUpdateProfileResponseProcessor for improved claim handling and user store management --- .../PreUpdateProfileRequestBuilder.java | 74 ++++++++++++++++--- .../PreUpdateProfileResponseProcessor.java | 66 ++++++++++++----- 2 files changed, 111 insertions(+), 29 deletions(-) diff --git a/components/user-mgt/org.wso2.carbon.identity.user.pre.update.profile.action/src/main/java/org/wso2/carbon/identity/user/pre/update/profile/action/internal/execution/PreUpdateProfileRequestBuilder.java b/components/user-mgt/org.wso2.carbon.identity.user.pre.update.profile.action/src/main/java/org/wso2/carbon/identity/user/pre/update/profile/action/internal/execution/PreUpdateProfileRequestBuilder.java index a743f5dbea49..2f27d7cd8355 100644 --- a/components/user-mgt/org.wso2.carbon.identity.user.pre.update.profile.action/src/main/java/org/wso2/carbon/identity/user/pre/update/profile/action/internal/execution/PreUpdateProfileRequestBuilder.java +++ b/components/user-mgt/org.wso2.carbon.identity.user.pre.update.profile.action/src/main/java/org/wso2/carbon/identity/user/pre/update/profile/action/internal/execution/PreUpdateProfileRequestBuilder.java @@ -30,9 +30,10 @@ import org.wso2.carbon.identity.action.execution.api.model.Organization; import org.wso2.carbon.identity.action.execution.api.model.Tenant; import org.wso2.carbon.identity.action.execution.api.model.User; +import org.wso2.carbon.identity.action.execution.api.model.UserClaim; import org.wso2.carbon.identity.action.execution.api.model.UserStore; import org.wso2.carbon.identity.action.execution.api.service.ActionExecutionRequestBuilder; -import org.wso2.carbon.identity.action.execution.api.util.RequestBuilderUtil; +import org.wso2.carbon.identity.action.execution.internal.component.ActionExecutionServiceComponentHolder; import org.wso2.carbon.identity.application.authentication.framework.util.FrameworkUtils; import org.wso2.carbon.identity.claim.metadata.mgt.ClaimMetadataManagementService; import org.wso2.carbon.identity.claim.metadata.mgt.exception.ClaimMetadataException; @@ -48,15 +49,21 @@ import org.wso2.carbon.identity.user.pre.update.profile.action.internal.model.PreUpdateProfileEvent; import org.wso2.carbon.identity.user.pre.update.profile.action.internal.model.PreUpdateProfileRequest; import org.wso2.carbon.identity.user.pre.update.profile.action.internal.model.UpdatingUserClaim; +import org.wso2.carbon.user.api.UserRealm; +import org.wso2.carbon.user.api.UserStoreException; +import org.wso2.carbon.user.api.UserStoreManager; import org.wso2.carbon.user.core.UniqueIDUserStoreManager; import org.wso2.carbon.user.core.UserCoreConstants; +import org.wso2.carbon.user.core.service.RealmService; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.function.Function; import java.util.regex.Pattern; +import java.util.stream.Collectors; /** * Pre Update Profile Action Request Builder. @@ -65,7 +72,7 @@ public class PreUpdateProfileRequestBuilder implements ActionExecutionRequestBui private static final String ROLE_CLAIM_URI = "http://wso2.org/claims/roles"; private static final String GROUP_CLAIM_URI = "http://wso2.org/claims/groups"; - public static final String USER_CLAIMS_PATH_PREFIX = "/user/claims/"; + public static final String USER_CLAIMS_PATH_PREFIX = "/user/claims[uri={clam_uri}]"; @Override public ActionType getSupportedActionType() { @@ -123,10 +130,9 @@ private Event getEvent(UserActionContext userActionContext, PreUpdateProfileActi eventBuilder.action(PreUpdateProfileEvent.Action.UPDATE); eventBuilder.request(getPreUpdateProfileRequest(userActionContext)); eventBuilder.tenant(getTenant()); - eventBuilder.user(getUser(userActionContext, preUpdateProfileAction)); - String tenantDomain = IdentityContext.getThreadLocalIdentityContext().getTenantDomain(); - UniqueIDUserStoreManager userStoreManager = RequestBuilderUtil.getUserStoreManager(tenantDomain); + UniqueIDUserStoreManager userStoreManager = getUserStoreManager(); + eventBuilder.user(getUser(userActionContext, preUpdateProfileAction, userStoreManager)); eventBuilder.userStore(getUserStore(userActionContext.getUserActionRequestDTO(), userStoreManager)); eventBuilder.organization(getOrganization()); @@ -210,8 +216,8 @@ private Organization getOrganization() { .build(); } - private User getUser(UserActionContext userActionContext, PreUpdateProfileAction preUpdateProfileAction) - throws ActionExecutionRequestBuilderException { + private User getUser(UserActionContext userActionContext, PreUpdateProfileAction preUpdateProfileAction, + UniqueIDUserStoreManager userStoreManager) throws ActionExecutionRequestBuilderException { UserActionRequestDTO userActionRequestDTO = userActionContext.getUserActionRequestDTO(); List userClaimsToSetInEvent = preUpdateProfileAction.getAttributes(); @@ -223,9 +229,8 @@ private User getUser(UserActionContext userActionContext, PreUpdateProfileAction return userBuilder.build(); } - String tenantDomain = IdentityContext.getThreadLocalIdentityContext().getTenantDomain(); - Map claimValues = RequestBuilderUtil.getClaimValues(resolveOrgBoundUserId(userActionRequestDTO), - userClaimsToSetInEvent, tenantDomain); + Map claimValues = getClaimValues(resolveOrgBoundUserId(userActionRequestDTO), + userClaimsToSetInEvent, userStoreManager); String multiAttributeSeparator = FrameworkUtils.getMultiAttributeSeparator(); setClaimsInUserBuilder(userBuilder, claimValues, userActionRequestDTO.getClaims(), multiAttributeSeparator); @@ -234,6 +239,23 @@ private User getUser(UserActionContext userActionContext, PreUpdateProfileAction return userBuilder.build(); } + private Map getClaimValues(String userId, List requestedClaims, + UniqueIDUserStoreManager userStoreManager) + throws ActionExecutionRequestBuilderException { + + try { + Map claimValues = userStoreManager.getUserClaimValuesWithID(userId, + requestedClaims.toArray(new String[0]), UserCoreConstants.DEFAULT_PROFILE); + + // Filter out the extra claims that are not requested. + return requestedClaims.stream() + .filter(claimValues::containsKey) + .collect(Collectors.toMap(Function.identity(), claimValues::get)); + } catch (org.wso2.carbon.user.core.UserStoreException e) { + throw new ActionExecutionRequestBuilderException("Failed to retrieve user claims from user store.", e); + } + } + private void setClaimsInUserBuilder(User.Builder userBuilder, Map claimValues, Map updatingUserClaimsInRequest, String multiAttributeSeparator) throws ActionExecutionRequestBuilderException { @@ -347,6 +369,38 @@ private UserStore getUserStore(UserActionRequestDTO userActionRequestDTO, Unique return new UserStore(userStoreDomain); } + private UniqueIDUserStoreManager getUserStoreManager() throws ActionExecutionRequestBuilderException { + + String tenantDomain = IdentityContext.getThreadLocalIdentityContext().getTenantDomain(); + RealmService realmService = ActionExecutionServiceComponentHolder.getInstance().getRealmService(); + + if (realmService == null) { + throw new ActionExecutionRequestBuilderException("Realm service is unavailable."); + } + + try { + int tenantId = realmService.getTenantManager().getTenantId(tenantDomain); + UserRealm userRealm = realmService.getTenantUserRealm(tenantId); + + if (userRealm == null) { + throw new ActionExecutionRequestBuilderException( + "User realm is not available for tenant: " + tenantDomain); + } + + UserStoreManager userStoreManager = userRealm.getUserStoreManager(); + if (!(userStoreManager instanceof UniqueIDUserStoreManager)) { + throw new ActionExecutionRequestBuilderException( + "User store manager is not an instance of UniqueIDUserStoreManager for tenant: " + + tenantDomain); + } + + return (UniqueIDUserStoreManager) userStoreManager; + } catch (UserStoreException e) { + throw new ActionExecutionRequestBuilderException( + "Error while loading user store manager for tenant: " + tenantDomain, e); + } + } + private boolean isMultiValuedClaim(String claimUri) throws ActionExecutionRequestBuilderException { ClaimMetadataManagementService claimMetadataManagementService = diff --git a/components/user-mgt/org.wso2.carbon.identity.user.pre.update.profile.action/src/main/java/org/wso2/carbon/identity/user/pre/update/profile/action/internal/execution/PreUpdateProfileResponseProcessor.java b/components/user-mgt/org.wso2.carbon.identity.user.pre.update.profile.action/src/main/java/org/wso2/carbon/identity/user/pre/update/profile/action/internal/execution/PreUpdateProfileResponseProcessor.java index 411577d58235..7cfdd494eecf 100644 --- a/components/user-mgt/org.wso2.carbon.identity.user.pre.update.profile.action/src/main/java/org/wso2/carbon/identity/user/pre/update/profile/action/internal/execution/PreUpdateProfileResponseProcessor.java +++ b/components/user-mgt/org.wso2.carbon.identity.user.pre.update.profile.action/src/main/java/org/wso2/carbon/identity/user/pre/update/profile/action/internal/execution/PreUpdateProfileResponseProcessor.java @@ -61,12 +61,8 @@ */ public class PreUpdateProfileResponseProcessor implements ActionExecutionResponseProcessor { - private static final String ID_CLAIM_URI = "http://wso2.org/claims/userid"; - private static final String NAME_CLAIM_URI = "http://wso2.org/claims/username"; - private static final String CREATED_CLAIM_URI = "http://wso2.org/claims/created"; private static final String GROUP_CLAIM_URI = "http://wso2.org/claims/groups"; private static final String ROLE_CLAIM_URI = "http://wso2.org/claims/roles"; - private static final String IDENTITY_CLAIM_URI_PREFIX = "http://wso2.org/claims/identity/"; private static final String SCIM_SCHEMA_URI_PREFIX = "urn:ietf:params:scim:schemas"; private static final String URI = "uri"; private static final String VALUE = "value"; @@ -76,6 +72,7 @@ public class PreUpdateProfileResponseProcessor implements ActionExecutionRespons private static final String MULTI_VALUED_CLAIMS_TO_BE_ADDED = "multiValuedClaimsToBeAdded"; private static final String MULTI_VALUED_CLAIMS_TO_BE_REMOVED = "multiValuedClaimsToBeRemoved"; private static final String USER_CLAIMS_PATH_PREFIX = "/user/claims/"; + private static final String USER_CLAIMS_FILTER_PATH_PREFIX = "/user/claims["; private static final String VALUE_PATH_SEGMENT = "/value/"; private static final String ARRAY_APPEND_PATH_SEGMENT = "/-"; @@ -145,8 +142,7 @@ private void populateAddOperationResult(PerformableOperation operation, String path = operation.getPath(); // Determine claim URI: from path (path-based format) or from value map. - if (path != null && path.startsWith(USER_CLAIMS_PATH_PREFIX) && - path.length() > USER_CLAIMS_PATH_PREFIX.length()) { + if (path != null && isClaimPathFormat(path)) { claimUri = getClaimUriFromPath(path); } else if (operation.getValue() instanceof LinkedHashMap) { LinkedHashMap valueMap = (LinkedHashMap) operation.getValue(); @@ -162,7 +158,6 @@ private void populateAddOperationResult(PerformableOperation operation, Optional localClaim = isLocalClaim(claimUri); validateGroupAndRoleClaims(claimUri); - validateImmutableClaims(claimUri); validateFlowInitiatorClaims(claimUri, localClaim); validateSCIMLevelAttributes(claimUri, operation.getOp(), operation.getValue()); @@ -210,7 +205,6 @@ private void populateAddOperationResult(PerformableOperation operation, throw new ActionExecutionResponseProcessorException( "Missing or wrong format for multi valued claim in operation"); } - @SuppressWarnings("unchecked") List typedList = (List) operation.getValue(); claimValue = typedList; } else { @@ -240,7 +234,6 @@ private void populateModifyOperationResult(PerformableOperation operation, Optional localClaim = isLocalClaim(claimUri); validateGroupAndRoleClaims(claimUri); - validateImmutableClaims(claimUri); validateFlowInitiatorClaims(claimUri, localClaim); validateSCIMLevelAttributes(claimUri, operation.getOp(), operation.getValue()); String separator = FrameworkUtils.getMultiAttributeSeparator(); @@ -289,13 +282,16 @@ private void populateRemoveOperationResult(PerformableOperation operation, Optional localClaim = isLocalClaim(claimUri); validateGroupAndRoleClaims(claimUri); - validateImmutableClaims(claimUri); validateSCIMLevelAttributes(claimUri, operation.getOp(), operation.getValue()); if (!isMultiValuedClaim(localClaim)) { userClaimsToBeRemoved.put(claimUri, ""); userClaimsToBeModified.remove(claimUri); } else { + if (operation.getValue() != null) { + throw new ActionExecutionResponseProcessorException( + "Remove specific value from a multivalued claim is not supported."); + } populateMultiValuedClaimsForRemoveOperation(initiatorType, claimUri, valueToRemove, userClaimsToBeRemoved, simpleMultiValuedClaimsToBeRemoved); } @@ -501,6 +497,30 @@ private String getClaimUriFromPath(String path) return remainder; } + if (path.startsWith(USER_CLAIMS_FILTER_PATH_PREFIX)) { + int uriKeyStart = path.indexOf("[uri="); + if (uriKeyStart == -1) { + throw new ActionExecutionResponseProcessorException("Invalid filter path format: " + path); + } + + int valueStart = uriKeyStart + "[uri=".length(); + if (valueStart >= path.length()) { + throw new ActionExecutionResponseProcessorException("Invalid filter path format: " + path); + } + + char quoteChar = path.charAt(valueStart); + if (quoteChar != '\'' && quoteChar != '"') { + throw new ActionExecutionResponseProcessorException("Invalid filter path format: " + path); + } + + int valueEnd = path.indexOf(quoteChar, valueStart + 1); + if (valueEnd == -1 || valueEnd + 1 >= path.length() || path.charAt(valueEnd + 1) != ']') { + throw new ActionExecutionResponseProcessorException("Invalid filter path format: " + path); + } + + return path.substring(valueStart + 1, valueEnd); + } + throw new ActionExecutionResponseProcessorException("Invalid path format: " + path); } @@ -522,9 +542,26 @@ private String getValueToRemoveFromPath(String path) { return remainder.substring(valueIndex + VALUE_PATH_SEGMENT.length()); } } + + if (path.startsWith(USER_CLAIMS_FILTER_PATH_PREFIX)) { + int closingFilterIndex = path.indexOf(']'); + if (closingFilterIndex != -1) { + int valueStartIndex = path.indexOf(VALUE_PATH_SEGMENT, closingFilterIndex); + if (valueStartIndex != -1) { + return path.substring(valueStartIndex + VALUE_PATH_SEGMENT.length()); + } + } + } + return null; } + private boolean isClaimPathFormat(String path) { + + return (path.startsWith(USER_CLAIMS_PATH_PREFIX) && path.length() > USER_CLAIMS_PATH_PREFIX.length()) || + path.startsWith(USER_CLAIMS_FILTER_PATH_PREFIX); + } + private UniqueIDUserStoreManager getUserStoreManager() throws ActionExecutionResponseProcessorException { String tenantDomain = IdentityContext.getThreadLocalIdentityContext().getTenantDomain(); @@ -591,15 +628,6 @@ private String[] filterDuplicatedValues(List claimValue) { .toArray(String[]::new); } - private void validateImmutableClaims(String claimUri) throws ActionExecutionResponseProcessorException { - - if (claimUri.equals(ID_CLAIM_URI) || claimUri.equals(CREATED_CLAIM_URI) || - claimUri.equals(NAME_CLAIM_URI) || claimUri.contains(IDENTITY_CLAIM_URI_PREFIX)) { - throw new ActionExecutionResponseProcessorException("Immutable claims cannot be added or modified: " + - claimUri); - } - } - private void validateFlowInitiatorClaims(String claimUri, Optional localClaim) throws ActionExecutionResponseProcessorException { From bbba6f135fc69966d6738ce20d08d90243e45002 Mon Sep 17 00:00:00 2001 From: Lashen1227 Date: Mon, 25 May 2026 00:10:58 +0530 Subject: [PATCH 05/12] refactor: enhance path matching for user claims with template --- .../internal/util/OperationComparator.java | 24 +++++- .../util/OperationComparatorTest.java | 15 ++++ .../PreUpdateProfileRequestBuilder.java | 16 ++-- .../PreUpdateProfileResponseProcessor.java | 84 ++++--------------- 4 files changed, 61 insertions(+), 78 deletions(-) diff --git a/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/internal/util/OperationComparator.java b/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/internal/util/OperationComparator.java index fecf39430312..11d932aabeed 100644 --- a/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/internal/util/OperationComparator.java +++ b/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/internal/util/OperationComparator.java @@ -55,11 +55,33 @@ public static boolean compare(AllowedOperation allowedOp, PerformableOperation p for (String allowedPath : allowedOp.getPaths()) { if (performableOp.getPath().equals(allowedPath) || performableOperationBasePath.equals(allowedPath) || - performableOp.getPath().startsWith(allowedPath)) { + performableOp.getPath().startsWith(allowedPath) || + matchesPathTemplate(allowedPath, performableOp.getPath())) { return true; } } return false; } + + // Allowed operation paths can contain placeholders (e.g /user/claims[uri={claim_uri}]). + private static boolean matchesPathTemplate(String allowedPathTemplate, String performablePath) { + + if (allowedPathTemplate == null || performablePath == null || !allowedPathTemplate.contains("{")) { + return false; + } + + int templateVariableStart = allowedPathTemplate.indexOf('{'); + int templateVariableEnd = allowedPathTemplate.indexOf('}', templateVariableStart); + if (templateVariableStart == -1 || templateVariableEnd == -1 || templateVariableEnd < templateVariableStart) { + return false; + } + + String prefix = allowedPathTemplate.substring(0, templateVariableStart); + String suffix = allowedPathTemplate.substring(templateVariableEnd + 1); + + return performablePath.startsWith(prefix) && + performablePath.endsWith(suffix) && + performablePath.length() > (prefix.length() + suffix.length()); + } } diff --git a/components/action-mgt/org.wso2.carbon.identity.action.execution/src/test/java/org/wso2/carbon/identity/action/execution/util/OperationComparatorTest.java b/components/action-mgt/org.wso2.carbon.identity.action.execution/src/test/java/org/wso2/carbon/identity/action/execution/util/OperationComparatorTest.java index 2438af9d7e6a..6b14d086e34a 100644 --- a/components/action-mgt/org.wso2.carbon.identity.action.execution/src/test/java/org/wso2/carbon/identity/action/execution/util/OperationComparatorTest.java +++ b/components/action-mgt/org.wso2.carbon.identity.action.execution/src/test/java/org/wso2/carbon/identity/action/execution/util/OperationComparatorTest.java @@ -102,4 +102,19 @@ public void testCompareNonMatchingPath() { assertFalse(OperationComparator.compare(allowedOp, performableOp)); } + + @Test + public void testCompareMatchingPathForTemplateBasedClaimFilterPath() { + + AllowedOperation allowedOp = new AllowedOperation(); + allowedOp.setOp(Operation.ADD); + allowedOp.setPaths(Arrays.asList("/user/claims[uri={claim_uri}]")); + + PerformableOperation performableOp = new PerformableOperation(); + performableOp.setOp(Operation.ADD); + performableOp.setPath("/user/claims[uri='http://wso2.org/claims/country']"); + performableOp.setValue("Sri Lanka"); + + assertTrue(OperationComparator.compare(allowedOp, performableOp)); + } } diff --git a/components/user-mgt/org.wso2.carbon.identity.user.pre.update.profile.action/src/main/java/org/wso2/carbon/identity/user/pre/update/profile/action/internal/execution/PreUpdateProfileRequestBuilder.java b/components/user-mgt/org.wso2.carbon.identity.user.pre.update.profile.action/src/main/java/org/wso2/carbon/identity/user/pre/update/profile/action/internal/execution/PreUpdateProfileRequestBuilder.java index 2f27d7cd8355..8c4d0777e9ec 100644 --- a/components/user-mgt/org.wso2.carbon.identity.user.pre.update.profile.action/src/main/java/org/wso2/carbon/identity/user/pre/update/profile/action/internal/execution/PreUpdateProfileRequestBuilder.java +++ b/components/user-mgt/org.wso2.carbon.identity.user.pre.update.profile.action/src/main/java/org/wso2/carbon/identity/user/pre/update/profile/action/internal/execution/PreUpdateProfileRequestBuilder.java @@ -30,7 +30,6 @@ import org.wso2.carbon.identity.action.execution.api.model.Organization; import org.wso2.carbon.identity.action.execution.api.model.Tenant; import org.wso2.carbon.identity.action.execution.api.model.User; -import org.wso2.carbon.identity.action.execution.api.model.UserClaim; import org.wso2.carbon.identity.action.execution.api.model.UserStore; import org.wso2.carbon.identity.action.execution.api.service.ActionExecutionRequestBuilder; import org.wso2.carbon.identity.action.execution.internal.component.ActionExecutionServiceComponentHolder; @@ -72,7 +71,7 @@ public class PreUpdateProfileRequestBuilder implements ActionExecutionRequestBui private static final String ROLE_CLAIM_URI = "http://wso2.org/claims/roles"; private static final String GROUP_CLAIM_URI = "http://wso2.org/claims/groups"; - public static final String USER_CLAIMS_PATH_PREFIX = "/user/claims[uri={clam_uri}]"; + public static final String USER_CLAIMS_FILTERED_PATH_TEMPLATE = "/user/claims[uri={claim_uri}]"; @Override public ActionType getSupportedActionType() { @@ -99,17 +98,14 @@ public ActionExecutionRequest buildActionExecutionRequest(FlowContext flowContex private List getAllowedOperations(Event event) throws ActionExecutionRequestBuilderException { - List addOrReplacePaths = new ArrayList<>(); - List removePaths = new ArrayList<>(); - - addOrReplacePaths.add(USER_CLAIMS_PATH_PREFIX); - removePaths.add(USER_CLAIMS_PATH_PREFIX); + List allowedPaths = new ArrayList<>(); + allowedPaths.add(USER_CLAIMS_FILTERED_PATH_TEMPLATE); List allowedOperations = new ArrayList<>(); - allowedOperations.add(createAllowedOperation(Operation.ADD, addOrReplacePaths)); - allowedOperations.add(createAllowedOperation(Operation.REMOVE, removePaths)); - allowedOperations.add(createAllowedOperation(Operation.REPLACE, addOrReplacePaths)); + allowedOperations.add(createAllowedOperation(Operation.ADD, allowedPaths)); + allowedOperations.add(createAllowedOperation(Operation.REMOVE, allowedPaths)); + allowedOperations.add(createAllowedOperation(Operation.REPLACE, allowedPaths)); return allowedOperations; } diff --git a/components/user-mgt/org.wso2.carbon.identity.user.pre.update.profile.action/src/main/java/org/wso2/carbon/identity/user/pre/update/profile/action/internal/execution/PreUpdateProfileResponseProcessor.java b/components/user-mgt/org.wso2.carbon.identity.user.pre.update.profile.action/src/main/java/org/wso2/carbon/identity/user/pre/update/profile/action/internal/execution/PreUpdateProfileResponseProcessor.java index 7cfdd494eecf..40d9d6cc9120 100644 --- a/components/user-mgt/org.wso2.carbon.identity.user.pre.update.profile.action/src/main/java/org/wso2/carbon/identity/user/pre/update/profile/action/internal/execution/PreUpdateProfileResponseProcessor.java +++ b/components/user-mgt/org.wso2.carbon.identity.user.pre.update.profile.action/src/main/java/org/wso2/carbon/identity/user/pre/update/profile/action/internal/execution/PreUpdateProfileResponseProcessor.java @@ -71,10 +71,7 @@ public class PreUpdateProfileResponseProcessor implements ActionExecutionRespons private static final String USER_CLAIMS_TO_BE_REMOVED = "userClaimsToBeRemoved"; private static final String MULTI_VALUED_CLAIMS_TO_BE_ADDED = "multiValuedClaimsToBeAdded"; private static final String MULTI_VALUED_CLAIMS_TO_BE_REMOVED = "multiValuedClaimsToBeRemoved"; - private static final String USER_CLAIMS_PATH_PREFIX = "/user/claims/"; private static final String USER_CLAIMS_FILTER_PATH_PREFIX = "/user/claims["; - private static final String VALUE_PATH_SEGMENT = "/value/"; - private static final String ARRAY_APPEND_PATH_SEGMENT = "/-"; @Override public ActionType getSupportedActionType() { @@ -278,7 +275,6 @@ private void populateRemoveOperationResult(PerformableOperation operation, // Extract the claim URI and optionally the specific value to remove for // multivalued claims. String claimUri = getClaimUriFromPath(path); - String valueToRemove = getValueToRemoveFromPath(path); Optional localClaim = isLocalClaim(claimUri); validateGroupAndRoleClaims(claimUri); @@ -292,7 +288,7 @@ private void populateRemoveOperationResult(PerformableOperation operation, throw new ActionExecutionResponseProcessorException( "Remove specific value from a multivalued claim is not supported."); } - populateMultiValuedClaimsForRemoveOperation(initiatorType, claimUri, valueToRemove, userClaimsToBeRemoved, + populateMultiValuedClaimsForRemoveOperation(initiatorType, claimUri, userClaimsToBeRemoved, simpleMultiValuedClaimsToBeRemoved); } } @@ -442,16 +438,10 @@ private void populateMultiValuedClaimsForReplaceOperation(PerformableOperation o private void populateMultiValuedClaimsForRemoveOperation( PreUpdateProfileEvent.FlowInitiatorType initiatorType, String claimUri, - String valueToRemove, Map userClaimsToBeRemoved, Map> simpleMultiValuedClaimsToBeRemoved) throws ActionExecutionResponseProcessorException { - if (valueToRemove != null) { - throw new ActionExecutionResponseProcessorException( - "Removing a specific value from a multivalued claim is not supported."); - } - userClaimsToBeRemoved.put(claimUri, ""); } @@ -481,22 +471,6 @@ private boolean isMultiValuedClaim(Optional localClaim) { private String getClaimUriFromPath(String path) throws ActionExecutionResponseProcessorException { - if (path.startsWith(USER_CLAIMS_PATH_PREFIX)) { - - String remainder = path.substring(USER_CLAIMS_PATH_PREFIX.length()); - // Handle SCIM array append syntax: /claimUri/- - if (remainder.endsWith(ARRAY_APPEND_PATH_SEGMENT)) { - return remainder.substring(0, remainder.length() - 2); - } - - int valueIndex = remainder.indexOf(VALUE_PATH_SEGMENT); - if (valueIndex != -1) { - return remainder.substring(0, valueIndex); - } - - return remainder; - } - if (path.startsWith(USER_CLAIMS_FILTER_PATH_PREFIX)) { int uriKeyStart = path.indexOf("[uri="); if (uriKeyStart == -1) { @@ -509,57 +483,33 @@ private String getClaimUriFromPath(String path) } char quoteChar = path.charAt(valueStart); - if (quoteChar != '\'' && quoteChar != '"') { - throw new ActionExecutionResponseProcessorException("Invalid filter path format: " + path); + if (quoteChar == '\'' || quoteChar == '"') { + int valueEnd = path.indexOf(quoteChar, valueStart + 1); + if (valueEnd == -1 || valueEnd + 1 >= path.length() || path.charAt(valueEnd + 1) != ']') { + throw new ActionExecutionResponseProcessorException("Invalid filter path format: " + path); + } + if (valueEnd + 2 != path.length()) { + throw new ActionExecutionResponseProcessorException("Invalid filter path format: " + path); + } + return path.substring(valueStart + 1, valueEnd); } - int valueEnd = path.indexOf(quoteChar, valueStart + 1); - if (valueEnd == -1 || valueEnd + 1 >= path.length() || path.charAt(valueEnd + 1) != ']') { + int valueEnd = path.indexOf(']', valueStart); + if (valueEnd == -1) { throw new ActionExecutionResponseProcessorException("Invalid filter path format: " + path); } - - return path.substring(valueStart + 1, valueEnd); - } - - throw new ActionExecutionResponseProcessorException("Invalid path format: " + path); - } - - /** - * Extract the specific value to remove from a multivalued claim path. - * - * Path format: /user/claims/{claimURI}/value/{valueToRemove} - * e.g., /user/claims/http://wso2.org/claims/emails/value/bob@example.com - * - * @param path The operation path. - * @return The value to remove, or null if path doesn't contain a value segment. - */ - private String getValueToRemoveFromPath(String path) { - - if (path.startsWith(USER_CLAIMS_PATH_PREFIX)) { - String remainder = path.substring(USER_CLAIMS_PATH_PREFIX.length()); - int valueIndex = remainder.indexOf(VALUE_PATH_SEGMENT); - if (valueIndex != -1) { - return remainder.substring(valueIndex + VALUE_PATH_SEGMENT.length()); - } - } - - if (path.startsWith(USER_CLAIMS_FILTER_PATH_PREFIX)) { - int closingFilterIndex = path.indexOf(']'); - if (closingFilterIndex != -1) { - int valueStartIndex = path.indexOf(VALUE_PATH_SEGMENT, closingFilterIndex); - if (valueStartIndex != -1) { - return path.substring(valueStartIndex + VALUE_PATH_SEGMENT.length()); - } + if (valueEnd + 1 != path.length()) { + throw new ActionExecutionResponseProcessorException("Invalid filter path format: " + path); } + return path.substring(valueStart, valueEnd); } - return null; + throw new ActionExecutionResponseProcessorException("Invalid path format: " + path); } private boolean isClaimPathFormat(String path) { - return (path.startsWith(USER_CLAIMS_PATH_PREFIX) && path.length() > USER_CLAIMS_PATH_PREFIX.length()) || - path.startsWith(USER_CLAIMS_FILTER_PATH_PREFIX); + return path != null && path.startsWith(USER_CLAIMS_FILTER_PATH_PREFIX); } private UniqueIDUserStoreManager getUserStoreManager() throws ActionExecutionResponseProcessorException { From a031911c62e125328ef5c8de4e85078893d72f54 Mon Sep 17 00:00:00 2001 From: Lashen1227 Date: Mon, 25 May 2026 17:23:56 +0530 Subject: [PATCH 06/12] refactor: replace RealmService usage with RequestBuilderUtil for user store management --- .../PreUpdateProfileRequestBuilder.java | 34 ++----------------- .../PreUpdateProfileResponseProcessor.java | 30 ++-------------- 2 files changed, 5 insertions(+), 59 deletions(-) diff --git a/components/user-mgt/org.wso2.carbon.identity.user.pre.update.profile.action/src/main/java/org/wso2/carbon/identity/user/pre/update/profile/action/internal/execution/PreUpdateProfileRequestBuilder.java b/components/user-mgt/org.wso2.carbon.identity.user.pre.update.profile.action/src/main/java/org/wso2/carbon/identity/user/pre/update/profile/action/internal/execution/PreUpdateProfileRequestBuilder.java index 8c4d0777e9ec..146258b2c65c 100644 --- a/components/user-mgt/org.wso2.carbon.identity.user.pre.update.profile.action/src/main/java/org/wso2/carbon/identity/user/pre/update/profile/action/internal/execution/PreUpdateProfileRequestBuilder.java +++ b/components/user-mgt/org.wso2.carbon.identity.user.pre.update.profile.action/src/main/java/org/wso2/carbon/identity/user/pre/update/profile/action/internal/execution/PreUpdateProfileRequestBuilder.java @@ -32,7 +32,7 @@ import org.wso2.carbon.identity.action.execution.api.model.User; import org.wso2.carbon.identity.action.execution.api.model.UserStore; import org.wso2.carbon.identity.action.execution.api.service.ActionExecutionRequestBuilder; -import org.wso2.carbon.identity.action.execution.internal.component.ActionExecutionServiceComponentHolder; +import org.wso2.carbon.identity.action.execution.api.util.RequestBuilderUtil; import org.wso2.carbon.identity.application.authentication.framework.util.FrameworkUtils; import org.wso2.carbon.identity.claim.metadata.mgt.ClaimMetadataManagementService; import org.wso2.carbon.identity.claim.metadata.mgt.exception.ClaimMetadataException; @@ -48,12 +48,8 @@ import org.wso2.carbon.identity.user.pre.update.profile.action.internal.model.PreUpdateProfileEvent; import org.wso2.carbon.identity.user.pre.update.profile.action.internal.model.PreUpdateProfileRequest; import org.wso2.carbon.identity.user.pre.update.profile.action.internal.model.UpdatingUserClaim; -import org.wso2.carbon.user.api.UserRealm; -import org.wso2.carbon.user.api.UserStoreException; -import org.wso2.carbon.user.api.UserStoreManager; import org.wso2.carbon.user.core.UniqueIDUserStoreManager; import org.wso2.carbon.user.core.UserCoreConstants; -import org.wso2.carbon.user.core.service.RealmService; import java.util.ArrayList; import java.util.Arrays; @@ -368,33 +364,7 @@ private UserStore getUserStore(UserActionRequestDTO userActionRequestDTO, Unique private UniqueIDUserStoreManager getUserStoreManager() throws ActionExecutionRequestBuilderException { String tenantDomain = IdentityContext.getThreadLocalIdentityContext().getTenantDomain(); - RealmService realmService = ActionExecutionServiceComponentHolder.getInstance().getRealmService(); - - if (realmService == null) { - throw new ActionExecutionRequestBuilderException("Realm service is unavailable."); - } - - try { - int tenantId = realmService.getTenantManager().getTenantId(tenantDomain); - UserRealm userRealm = realmService.getTenantUserRealm(tenantId); - - if (userRealm == null) { - throw new ActionExecutionRequestBuilderException( - "User realm is not available for tenant: " + tenantDomain); - } - - UserStoreManager userStoreManager = userRealm.getUserStoreManager(); - if (!(userStoreManager instanceof UniqueIDUserStoreManager)) { - throw new ActionExecutionRequestBuilderException( - "User store manager is not an instance of UniqueIDUserStoreManager for tenant: " + - tenantDomain); - } - - return (UniqueIDUserStoreManager) userStoreManager; - } catch (UserStoreException e) { - throw new ActionExecutionRequestBuilderException( - "Error while loading user store manager for tenant: " + tenantDomain, e); - } + return RequestBuilderUtil.getUserStoreManager(tenantDomain); } private boolean isMultiValuedClaim(String claimUri) throws ActionExecutionRequestBuilderException { diff --git a/components/user-mgt/org.wso2.carbon.identity.user.pre.update.profile.action/src/main/java/org/wso2/carbon/identity/user/pre/update/profile/action/internal/execution/PreUpdateProfileResponseProcessor.java b/components/user-mgt/org.wso2.carbon.identity.user.pre.update.profile.action/src/main/java/org/wso2/carbon/identity/user/pre/update/profile/action/internal/execution/PreUpdateProfileResponseProcessor.java index 40d9d6cc9120..7ccf00b0f72e 100644 --- a/components/user-mgt/org.wso2.carbon.identity.user.pre.update.profile.action/src/main/java/org/wso2/carbon/identity/user/pre/update/profile/action/internal/execution/PreUpdateProfileResponseProcessor.java +++ b/components/user-mgt/org.wso2.carbon.identity.user.pre.update.profile.action/src/main/java/org/wso2/carbon/identity/user/pre/update/profile/action/internal/execution/PreUpdateProfileResponseProcessor.java @@ -29,7 +29,7 @@ import org.wso2.carbon.identity.action.execution.api.model.Success; import org.wso2.carbon.identity.action.execution.api.model.SuccessStatus; import org.wso2.carbon.identity.action.execution.api.service.ActionExecutionResponseProcessor; -import org.wso2.carbon.identity.action.execution.internal.component.ActionExecutionServiceComponentHolder; +import org.wso2.carbon.identity.action.execution.api.util.RequestBuilderUtil; import org.wso2.carbon.identity.application.authentication.framework.util.FrameworkUtils; import org.wso2.carbon.identity.claim.metadata.mgt.ClaimMetadataManagementService; import org.wso2.carbon.identity.claim.metadata.mgt.exception.ClaimMetadataException; @@ -39,11 +39,8 @@ import org.wso2.carbon.identity.core.context.IdentityContext; import org.wso2.carbon.identity.user.pre.update.profile.action.internal.component.PreUpdateProfileActionServiceComponentHolder; import org.wso2.carbon.identity.user.pre.update.profile.action.internal.model.PreUpdateProfileEvent; -import org.wso2.carbon.user.api.UserRealm; -import org.wso2.carbon.user.api.UserStoreManager; import org.wso2.carbon.user.core.UniqueIDUserStoreManager; import org.wso2.carbon.user.core.UserCoreConstants; -import org.wso2.carbon.user.core.service.RealmService; import java.util.ArrayList; import java.util.Arrays; @@ -515,30 +512,9 @@ private boolean isClaimPathFormat(String path) { private UniqueIDUserStoreManager getUserStoreManager() throws ActionExecutionResponseProcessorException { String tenantDomain = IdentityContext.getThreadLocalIdentityContext().getTenantDomain(); - RealmService realmService = ActionExecutionServiceComponentHolder.getInstance().getRealmService(); - - if (realmService == null) { - throw new ActionExecutionResponseProcessorException("Realm service is unavailable."); - } - try { - int tenantId = realmService.getTenantManager().getTenantId(tenantDomain); - UserRealm userRealm = realmService.getTenantUserRealm(tenantId); - - if (userRealm == null) { - throw new ActionExecutionResponseProcessorException( - "User realm is not available for tenant: " + tenantDomain); - } - - UserStoreManager userStoreManager = userRealm.getUserStoreManager(); - if (!(userStoreManager instanceof UniqueIDUserStoreManager)) { - throw new ActionExecutionResponseProcessorException( - "User store manager is not an instance of UniqueIDUserStoreManager for tenant: " + - tenantDomain); - } - - return (UniqueIDUserStoreManager) userStoreManager; - } catch (org.wso2.carbon.user.api.UserStoreException e) { + return RequestBuilderUtil.getUserStoreManager(tenantDomain); + } catch (org.wso2.carbon.identity.action.execution.api.exception.ActionExecutionRequestBuilderException e) { throw new ActionExecutionResponseProcessorException( "Error while loading user store manager for tenant: " + tenantDomain, e); } From 0e2bba0952ef609028695ffea9b71963afc253a9 Mon Sep 17 00:00:00 2001 From: Lashen1227 Date: Mon, 25 May 2026 17:40:21 +0530 Subject: [PATCH 07/12] refactor: update claim URI handling to allow unquoted URIs and reject quoted URIs --- .../util/OperationComparatorTest.java | 2 +- .../PreUpdateProfileResponseProcessor.java | 9 +---- ...PreUpdateProfileResponseProcessorTest.java | 34 +++++++++++++++++++ 3 files changed, 36 insertions(+), 9 deletions(-) diff --git a/components/action-mgt/org.wso2.carbon.identity.action.execution/src/test/java/org/wso2/carbon/identity/action/execution/util/OperationComparatorTest.java b/components/action-mgt/org.wso2.carbon.identity.action.execution/src/test/java/org/wso2/carbon/identity/action/execution/util/OperationComparatorTest.java index 6b14d086e34a..29a69ced1b3d 100644 --- a/components/action-mgt/org.wso2.carbon.identity.action.execution/src/test/java/org/wso2/carbon/identity/action/execution/util/OperationComparatorTest.java +++ b/components/action-mgt/org.wso2.carbon.identity.action.execution/src/test/java/org/wso2/carbon/identity/action/execution/util/OperationComparatorTest.java @@ -112,7 +112,7 @@ public void testCompareMatchingPathForTemplateBasedClaimFilterPath() { PerformableOperation performableOp = new PerformableOperation(); performableOp.setOp(Operation.ADD); - performableOp.setPath("/user/claims[uri='http://wso2.org/claims/country']"); + performableOp.setPath("/user/claims[uri=http://wso2.org/claims/country]"); performableOp.setValue("Sri Lanka"); assertTrue(OperationComparator.compare(allowedOp, performableOp)); diff --git a/components/user-mgt/org.wso2.carbon.identity.user.pre.update.profile.action/src/main/java/org/wso2/carbon/identity/user/pre/update/profile/action/internal/execution/PreUpdateProfileResponseProcessor.java b/components/user-mgt/org.wso2.carbon.identity.user.pre.update.profile.action/src/main/java/org/wso2/carbon/identity/user/pre/update/profile/action/internal/execution/PreUpdateProfileResponseProcessor.java index 7ccf00b0f72e..ae778a254449 100644 --- a/components/user-mgt/org.wso2.carbon.identity.user.pre.update.profile.action/src/main/java/org/wso2/carbon/identity/user/pre/update/profile/action/internal/execution/PreUpdateProfileResponseProcessor.java +++ b/components/user-mgt/org.wso2.carbon.identity.user.pre.update.profile.action/src/main/java/org/wso2/carbon/identity/user/pre/update/profile/action/internal/execution/PreUpdateProfileResponseProcessor.java @@ -481,14 +481,7 @@ private String getClaimUriFromPath(String path) char quoteChar = path.charAt(valueStart); if (quoteChar == '\'' || quoteChar == '"') { - int valueEnd = path.indexOf(quoteChar, valueStart + 1); - if (valueEnd == -1 || valueEnd + 1 >= path.length() || path.charAt(valueEnd + 1) != ']') { - throw new ActionExecutionResponseProcessorException("Invalid filter path format: " + path); - } - if (valueEnd + 2 != path.length()) { - throw new ActionExecutionResponseProcessorException("Invalid filter path format: " + path); - } - return path.substring(valueStart + 1, valueEnd); + throw new ActionExecutionResponseProcessorException("Invalid filter path format: " + path); } int valueEnd = path.indexOf(']', valueStart); diff --git a/components/user-mgt/org.wso2.carbon.identity.user.pre.update.profile.action/src/test/java/org/wso2/carbon/identity/user/pre/update/profile/action/execution/PreUpdateProfileResponseProcessorTest.java b/components/user-mgt/org.wso2.carbon.identity.user.pre.update.profile.action/src/test/java/org/wso2/carbon/identity/user/pre/update/profile/action/execution/PreUpdateProfileResponseProcessorTest.java index 7deb1a05fb31..145ba53564de 100644 --- a/components/user-mgt/org.wso2.carbon.identity.user.pre.update.profile.action/src/test/java/org/wso2/carbon/identity/user/pre/update/profile/action/execution/PreUpdateProfileResponseProcessorTest.java +++ b/components/user-mgt/org.wso2.carbon.identity.user.pre.update.profile.action/src/test/java/org/wso2/carbon/identity/user/pre/update/profile/action/execution/PreUpdateProfileResponseProcessorTest.java @@ -37,9 +37,13 @@ import org.wso2.carbon.identity.user.action.api.model.UserActionContext; import org.wso2.carbon.identity.user.pre.update.profile.action.internal.execution.PreUpdateProfileResponseProcessor; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + import static org.mockito.Mockito.mock; import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertTrue; /** * User Pre Update Password Action Response Processor Test. @@ -128,4 +132,34 @@ public void testProcessErrorResponse() throws ActionExecutionResponseProcessorEx assertEquals(resultStatus.getResponse().getErrorMessage(), errorResponse.getErrorMessage()); assertEquals(resultStatus.getResponse().getErrorDescription(), errorResponse.getErrorDescription()); } + + @Test + public void testGetClaimUriFromPathAllowsUnquotedUri() throws Exception { + + String path = "/user/claims[uri=http://wso2.org/claims/country]"; + String claimUri = invokeGetClaimUriFromPath(path); + assertEquals(claimUri, "http://wso2.org/claims/country"); + } + + @Test + public void testGetClaimUriFromPathRejectsQuotedUri() throws Exception { + + String path = "/user/claims[uri='http://wso2.org/claims/country']"; + try { + invokeGetClaimUriFromPath(path); + } catch (InvocationTargetException e) { + assertTrue(e.getCause() instanceof ActionExecutionResponseProcessorException); + assertTrue(e.getCause().getMessage().contains("Invalid filter path format")); + return; + } + throw new AssertionError("Expected ActionExecutionResponseProcessorException for quoted URI path"); + } + + private String invokeGetClaimUriFromPath(String path) throws Exception { + + Method method = PreUpdateProfileResponseProcessor.class.getDeclaredMethod("getClaimUriFromPath", + String.class); + method.setAccessible(true); + return (String) method.invoke(preUpdateProfileResponseProcessor, path); + } } From 04d3fd38416a508ad40a24366f10b7bdfd9f77f9 Mon Sep 17 00:00:00 2001 From: Lashen1227 Date: Tue, 26 May 2026 07:35:47 +0530 Subject: [PATCH 08/12] feat: enhance claim validation in PreUpdateProfileResponseProcessor to prevent modification of immutable claims --- .../pom.xml | 1 + .../PreUpdateProfileResponseProcessor.java | 47 +++++++++++++++++-- 2 files changed, 45 insertions(+), 3 deletions(-) diff --git a/components/user-mgt/org.wso2.carbon.identity.user.pre.update.profile.action/pom.xml b/components/user-mgt/org.wso2.carbon.identity.user.pre.update.profile.action/pom.xml index 7aaea88b401b..1226e68dbe66 100644 --- a/components/user-mgt/org.wso2.carbon.identity.user.pre.update.profile.action/pom.xml +++ b/components/user-mgt/org.wso2.carbon.identity.user.pre.update.profile.action/pom.xml @@ -143,6 +143,7 @@ org.wso2.carbon.identity.rule.evaluation.api.model; version="${carbon.identity.package.import.version.range}", org.wso2.carbon.identity.rule.evaluation.api.exception; version="${carbon.identity.package.import.version.range}", org.wso2.carbon.identity.rule.evaluation.api.provider; version="${carbon.identity.package.import.version.range}", + org.wso2.carbon.identity.central.log.mgt.utils; version="${carbon.identity.package.import.version.range}", org.wso2.carbon.identity.application.authentication.framework.util; version="${carbon.identity.package.import.version.range}" diff --git a/components/user-mgt/org.wso2.carbon.identity.user.pre.update.profile.action/src/main/java/org/wso2/carbon/identity/user/pre/update/profile/action/internal/execution/PreUpdateProfileResponseProcessor.java b/components/user-mgt/org.wso2.carbon.identity.user.pre.update.profile.action/src/main/java/org/wso2/carbon/identity/user/pre/update/profile/action/internal/execution/PreUpdateProfileResponseProcessor.java index ae778a254449..eda61d187c8c 100644 --- a/components/user-mgt/org.wso2.carbon.identity.user.pre.update.profile.action/src/main/java/org/wso2/carbon/identity/user/pre/update/profile/action/internal/execution/PreUpdateProfileResponseProcessor.java +++ b/components/user-mgt/org.wso2.carbon.identity.user.pre.update.profile.action/src/main/java/org/wso2/carbon/identity/user/pre/update/profile/action/internal/execution/PreUpdateProfileResponseProcessor.java @@ -18,6 +18,8 @@ package org.wso2.carbon.identity.user.pre.update.profile.action.internal.execution; +import org.wso2.carbon.identity.action.execution.api.constant.ActionExecutionLogConstants; +import org.wso2.carbon.identity.action.execution.api.exception.ActionExecutionRequestBuilderException; import org.wso2.carbon.identity.action.execution.api.exception.ActionExecutionResponseProcessorException; import org.wso2.carbon.identity.action.execution.api.model.ActionExecutionResponseContext; import org.wso2.carbon.identity.action.execution.api.model.ActionExecutionStatus; @@ -31,6 +33,7 @@ import org.wso2.carbon.identity.action.execution.api.service.ActionExecutionResponseProcessor; import org.wso2.carbon.identity.action.execution.api.util.RequestBuilderUtil; import org.wso2.carbon.identity.application.authentication.framework.util.FrameworkUtils; +import org.wso2.carbon.identity.central.log.mgt.utils.LoggerUtils; import org.wso2.carbon.identity.claim.metadata.mgt.ClaimMetadataManagementService; import org.wso2.carbon.identity.claim.metadata.mgt.exception.ClaimMetadataException; import org.wso2.carbon.identity.claim.metadata.mgt.model.Claim; @@ -41,6 +44,7 @@ import org.wso2.carbon.identity.user.pre.update.profile.action.internal.model.PreUpdateProfileEvent; import org.wso2.carbon.user.core.UniqueIDUserStoreManager; import org.wso2.carbon.user.core.UserCoreConstants; +import org.wso2.carbon.utils.DiagnosticLog; import java.util.ArrayList; import java.util.Arrays; @@ -60,6 +64,8 @@ public class PreUpdateProfileResponseProcessor implements ActionExecutionRespons private static final String GROUP_CLAIM_URI = "http://wso2.org/claims/groups"; private static final String ROLE_CLAIM_URI = "http://wso2.org/claims/roles"; + private static final String USERNAME_CLAIM_URI = "http://wso2.org/claims/username"; + private static final String USERID_CLAIM_URI = "http://wso2.org/claims/userid"; private static final String SCIM_SCHEMA_URI_PREFIX = "urn:ietf:params:scim:schemas"; private static final String URI = "uri"; private static final String VALUE = "value"; @@ -151,6 +157,7 @@ private void populateAddOperationResult(PerformableOperation operation, } Optional localClaim = isLocalClaim(claimUri); + validateImmutableClaims(claimUri); validateGroupAndRoleClaims(claimUri); validateFlowInitiatorClaims(claimUri, localClaim); validateSCIMLevelAttributes(claimUri, operation.getOp(), operation.getValue()); @@ -227,6 +234,7 @@ private void populateModifyOperationResult(PerformableOperation operation, String claimUri = getClaimUriFromPath(path); Optional localClaim = isLocalClaim(claimUri); + validateImmutableClaims(claimUri); validateGroupAndRoleClaims(claimUri); validateFlowInitiatorClaims(claimUri, localClaim); validateSCIMLevelAttributes(claimUri, operation.getOp(), operation.getValue()); @@ -274,6 +282,7 @@ private void populateRemoveOperationResult(PerformableOperation operation, String claimUri = getClaimUriFromPath(path); Optional localClaim = isLocalClaim(claimUri); + validateImmutableClaims(claimUri); validateGroupAndRoleClaims(claimUri); validateSCIMLevelAttributes(claimUri, operation.getOp(), operation.getValue()); @@ -507,7 +516,7 @@ private UniqueIDUserStoreManager getUserStoreManager() throws ActionExecutionRes String tenantDomain = IdentityContext.getThreadLocalIdentityContext().getTenantDomain(); try { return RequestBuilderUtil.getUserStoreManager(tenantDomain); - } catch (org.wso2.carbon.identity.action.execution.api.exception.ActionExecutionRequestBuilderException e) { + } catch (ActionExecutionRequestBuilderException e) { throw new ActionExecutionResponseProcessorException( "Error while loading user store manager for tenant: " + tenantDomain, e); } @@ -551,7 +560,9 @@ private void validateFlowInitiatorClaims(String claimUri, Optional l throws ActionExecutionResponseProcessorException { if (localClaim.get().getFlowInitiator()) { - throw new ActionExecutionResponseProcessorException(claimUri + " is not allowed to be added."); + logRejectedClaimUpdate(claimUri, "flow_initiator", claimUri + + " is not allowed to modified."); + throw new ActionExecutionResponseProcessorException(claimUri + " is not allowed to modified."); } } @@ -559,11 +570,23 @@ private void validateGroupAndRoleClaims(String claimUri) throws ActionExecutionResponseProcessorException { if (claimUri.equals(GROUP_CLAIM_URI) || claimUri.equals(ROLE_CLAIM_URI)) { - throw new ActionExecutionResponseProcessorException("Groups/Roles are not allowed to be added: " + logRejectedClaimUpdate(claimUri, "group_or_role", + "Groups/Roles are not allowed to be modified: " + claimUri); + throw new ActionExecutionResponseProcessorException("Groups/Roles are not allowed to modified: " + claimUri); } } + private void validateImmutableClaims(String claimUri) throws ActionExecutionResponseProcessorException { + + if (USERNAME_CLAIM_URI.equals(claimUri) || USERID_CLAIM_URI.equals(claimUri)) { + logRejectedClaimUpdate(claimUri, "immutable_claim", + "Immutable claim cannot be modified through profile update: " + claimUri); + throw new ActionExecutionResponseProcessorException( + "Immutable claim cannot be modified through profile update: " + claimUri); + } + } + private void validateSCIMLevelAttributes(String claimUri, Operation op, Object value) throws ActionExecutionResponseProcessorException { @@ -673,4 +696,22 @@ private static List convertLocalToSCIMDialect(String claimUri) throws "Error while retrieving mapped external claims for claim uri: " + claimUri, e); } } + + private void logRejectedClaimUpdate(String claimUri, String reason, String resultMessage) { + + if (!LoggerUtils.isDiagnosticLogsEnabled()) { + return; + } + + DiagnosticLog.DiagnosticLogBuilder diagnosticLogBuilder = new DiagnosticLog.DiagnosticLogBuilder( + ActionExecutionLogConstants.ACTION_EXECUTION_COMPONENT_ID, + ActionExecutionLogConstants.ActionIDs.PROCESS_ACTION_RESPONSE) + .resultStatus(DiagnosticLog.ResultStatus.FAILED) + .resultMessage(resultMessage) + .inputParam("actionType", ActionType.PRE_UPDATE_PROFILE.getDisplayName()) + .inputParam("claimUri", claimUri) + .inputParam("reason", reason) + .logDetailLevel(DiagnosticLog.LogDetailLevel.APPLICATION); + LoggerUtils.triggerDiagnosticLogEvent(diagnosticLogBuilder); + } } From 1a28ea79930f5e0a927c533474e690dfe7390b08 Mon Sep 17 00:00:00 2001 From: Lashen1227 Date: Tue, 26 May 2026 18:50:51 +0530 Subject: [PATCH 09/12] feat: implement operation handling in PreUpdateProfileResponseProcessor --- .../PreUpdateProfileResponseProcessor.java | 148 +++++++++++++----- .../ProfileOperationExecutionResult.java | 71 +++++++++ 2 files changed, 181 insertions(+), 38 deletions(-) create mode 100644 components/user-mgt/org.wso2.carbon.identity.user.pre.update.profile.action/src/main/java/org/wso2/carbon/identity/user/pre/update/profile/action/internal/model/ProfileOperationExecutionResult.java diff --git a/components/user-mgt/org.wso2.carbon.identity.user.pre.update.profile.action/src/main/java/org/wso2/carbon/identity/user/pre/update/profile/action/internal/execution/PreUpdateProfileResponseProcessor.java b/components/user-mgt/org.wso2.carbon.identity.user.pre.update.profile.action/src/main/java/org/wso2/carbon/identity/user/pre/update/profile/action/internal/execution/PreUpdateProfileResponseProcessor.java index eda61d187c8c..74d8f7070ab8 100644 --- a/components/user-mgt/org.wso2.carbon.identity.user.pre.update.profile.action/src/main/java/org/wso2/carbon/identity/user/pre/update/profile/action/internal/execution/PreUpdateProfileResponseProcessor.java +++ b/components/user-mgt/org.wso2.carbon.identity.user.pre.update.profile.action/src/main/java/org/wso2/carbon/identity/user/pre/update/profile/action/internal/execution/PreUpdateProfileResponseProcessor.java @@ -18,6 +18,8 @@ package org.wso2.carbon.identity.user.pre.update.profile.action.internal.execution; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; import org.wso2.carbon.identity.action.execution.api.constant.ActionExecutionLogConstants; import org.wso2.carbon.identity.action.execution.api.exception.ActionExecutionRequestBuilderException; import org.wso2.carbon.identity.action.execution.api.exception.ActionExecutionResponseProcessorException; @@ -42,6 +44,7 @@ import org.wso2.carbon.identity.core.context.IdentityContext; import org.wso2.carbon.identity.user.pre.update.profile.action.internal.component.PreUpdateProfileActionServiceComponentHolder; import org.wso2.carbon.identity.user.pre.update.profile.action.internal.model.PreUpdateProfileEvent; +import org.wso2.carbon.identity.user.pre.update.profile.action.internal.model.ProfileOperationExecutionResult; import org.wso2.carbon.user.core.UniqueIDUserStoreManager; import org.wso2.carbon.user.core.UserCoreConstants; import org.wso2.carbon.utils.DiagnosticLog; @@ -62,10 +65,9 @@ */ public class PreUpdateProfileResponseProcessor implements ActionExecutionResponseProcessor { + private static final Log LOG = LogFactory.getLog(PreUpdateProfileResponseProcessor.class); private static final String GROUP_CLAIM_URI = "http://wso2.org/claims/groups"; private static final String ROLE_CLAIM_URI = "http://wso2.org/claims/roles"; - private static final String USERNAME_CLAIM_URI = "http://wso2.org/claims/username"; - private static final String USERID_CLAIM_URI = "http://wso2.org/claims/userid"; private static final String SCIM_SCHEMA_URI_PREFIX = "urn:ietf:params:scim:schemas"; private static final String URI = "uri"; private static final String VALUE = "value"; @@ -93,22 +95,25 @@ public ActionExecutionStatus processSuccessResponse( Map userClaimsToBeRemoved = new HashMap<>(); Map> simpleMultiValuedClaimsToBeAdded = new HashMap<>(); Map> simpleMultiValuedClaimsToBeRemoved = new HashMap<>(); + List operationExecutionResultList = new ArrayList<>(); if (operationsToPerform != null && !operationsToPerform.isEmpty()) { UniqueIDUserStoreManager userStoreManager = getUserStoreManager(); for (PerformableOperation operation : operationsToPerform) { switch (operation.getOp()) { case ADD: - populateAddOperationResult(operation, responseContext, userClaimsToBeAdded, - userClaimsToBeModified, simpleMultiValuedClaimsToBeAdded, userStoreManager); + operationExecutionResultList.add(handleAddOperation(operation, responseContext, + userClaimsToBeAdded, userClaimsToBeModified, simpleMultiValuedClaimsToBeAdded, + userStoreManager)); break; case REPLACE: - populateModifyOperationResult(operation, responseContext, userClaimsToBeModified, - simpleMultiValuedClaimsToBeRemoved, simpleMultiValuedClaimsToBeAdded, userStoreManager); + operationExecutionResultList.add(handleReplaceOperation(operation, responseContext, + userClaimsToBeModified, simpleMultiValuedClaimsToBeRemoved, + simpleMultiValuedClaimsToBeAdded, userStoreManager)); break; case REMOVE: - populateRemoveOperationResult(operation, responseContext, userClaimsToBeModified, - userClaimsToBeRemoved, simpleMultiValuedClaimsToBeRemoved); + operationExecutionResultList.add(handleRemoveOperation(operation, responseContext, + userClaimsToBeModified, userClaimsToBeRemoved, simpleMultiValuedClaimsToBeRemoved)); break; default: break; @@ -116,6 +121,8 @@ public ActionExecutionStatus processSuccessResponse( } } + logOperationExecutionResults(getSupportedActionType(), operationExecutionResultList); + flowContext.add(USER_CLAIMS_TO_BE_ADDED, userClaimsToBeAdded); flowContext.add(USER_CLAIMS_TO_BE_MODIFIED, userClaimsToBeModified); flowContext.add(USER_CLAIMS_TO_BE_REMOVED, userClaimsToBeRemoved); @@ -125,6 +132,62 @@ public ActionExecutionStatus processSuccessResponse( return new SuccessStatus.Builder().setResponseContext(flowContext.getContextData()).build(); } + private ProfileOperationExecutionResult handleAddOperation( + PerformableOperation operation, + ActionExecutionResponseContext responseContext, + Map userClaimsToBeAdded, + Map userClaimsToBeModified, + Map> simpleMultiValuedClaimsToBeAdded, + UniqueIDUserStoreManager userStoreManager) { + + try { + populateAddOperationResult(operation, responseContext, userClaimsToBeAdded, userClaimsToBeModified, + simpleMultiValuedClaimsToBeAdded, userStoreManager); + return new ProfileOperationExecutionResult(operation, ProfileOperationExecutionResult.Status.SUCCESS, + "Operation applied."); + } catch (ActionExecutionResponseProcessorException e) { + return new ProfileOperationExecutionResult(operation, ProfileOperationExecutionResult.Status.FAILURE, + e.getMessage()); + } + } + + private ProfileOperationExecutionResult handleReplaceOperation( + PerformableOperation operation, + ActionExecutionResponseContext responseContext, + Map userClaimsToBeModified, + Map> simpleMultiValuedClaimsToBeRemoved, + Map> simpleMultiValuedClaimsToBeAdded, + UniqueIDUserStoreManager userStoreManager) { + + try { + populateModifyOperationResult(operation, responseContext, userClaimsToBeModified, + simpleMultiValuedClaimsToBeRemoved, simpleMultiValuedClaimsToBeAdded, userStoreManager); + return new ProfileOperationExecutionResult(operation, ProfileOperationExecutionResult.Status.SUCCESS, + "Operation applied."); + } catch (ActionExecutionResponseProcessorException e) { + return new ProfileOperationExecutionResult(operation, ProfileOperationExecutionResult.Status.FAILURE, + e.getMessage()); + } + } + + private ProfileOperationExecutionResult handleRemoveOperation( + PerformableOperation operation, + ActionExecutionResponseContext responseContext, + Map userClaimsToBeModified, + Map userClaimsToBeRemoved, + Map> simpleMultiValuedClaimsToBeRemoved) { + + try { + populateRemoveOperationResult(operation, responseContext, userClaimsToBeModified, userClaimsToBeRemoved, + simpleMultiValuedClaimsToBeRemoved); + return new ProfileOperationExecutionResult(operation, ProfileOperationExecutionResult.Status.SUCCESS, + "Operation applied."); + } catch (ActionExecutionResponseProcessorException e) { + return new ProfileOperationExecutionResult(operation, ProfileOperationExecutionResult.Status.FAILURE, + e.getMessage()); + } + } + private void populateAddOperationResult(PerformableOperation operation, ActionExecutionResponseContext responseContext, @@ -157,7 +220,6 @@ private void populateAddOperationResult(PerformableOperation operation, } Optional localClaim = isLocalClaim(claimUri); - validateImmutableClaims(claimUri); validateGroupAndRoleClaims(claimUri); validateFlowInitiatorClaims(claimUri, localClaim); validateSCIMLevelAttributes(claimUri, operation.getOp(), operation.getValue()); @@ -234,7 +296,6 @@ private void populateModifyOperationResult(PerformableOperation operation, String claimUri = getClaimUriFromPath(path); Optional localClaim = isLocalClaim(claimUri); - validateImmutableClaims(claimUri); validateGroupAndRoleClaims(claimUri); validateFlowInitiatorClaims(claimUri, localClaim); validateSCIMLevelAttributes(claimUri, operation.getOp(), operation.getValue()); @@ -282,7 +343,6 @@ private void populateRemoveOperationResult(PerformableOperation operation, String claimUri = getClaimUriFromPath(path); Optional localClaim = isLocalClaim(claimUri); - validateImmutableClaims(claimUri); validateGroupAndRoleClaims(claimUri); validateSCIMLevelAttributes(claimUri, operation.getOp(), operation.getValue()); @@ -560,8 +620,6 @@ private void validateFlowInitiatorClaims(String claimUri, Optional l throws ActionExecutionResponseProcessorException { if (localClaim.get().getFlowInitiator()) { - logRejectedClaimUpdate(claimUri, "flow_initiator", claimUri + - " is not allowed to modified."); throw new ActionExecutionResponseProcessorException(claimUri + " is not allowed to modified."); } } @@ -570,23 +628,11 @@ private void validateGroupAndRoleClaims(String claimUri) throws ActionExecutionResponseProcessorException { if (claimUri.equals(GROUP_CLAIM_URI) || claimUri.equals(ROLE_CLAIM_URI)) { - logRejectedClaimUpdate(claimUri, "group_or_role", - "Groups/Roles are not allowed to be modified: " + claimUri); throw new ActionExecutionResponseProcessorException("Groups/Roles are not allowed to modified: " + claimUri); } } - private void validateImmutableClaims(String claimUri) throws ActionExecutionResponseProcessorException { - - if (USERNAME_CLAIM_URI.equals(claimUri) || USERID_CLAIM_URI.equals(claimUri)) { - logRejectedClaimUpdate(claimUri, "immutable_claim", - "Immutable claim cannot be modified through profile update: " + claimUri); - throw new ActionExecutionResponseProcessorException( - "Immutable claim cannot be modified through profile update: " + claimUri); - } - } - private void validateSCIMLevelAttributes(String claimUri, Operation op, Object value) throws ActionExecutionResponseProcessorException { @@ -697,21 +743,47 @@ private static List convertLocalToSCIMDialect(String claimUri) throws } } - private void logRejectedClaimUpdate(String claimUri, String reason, String resultMessage) { + private void logOperationExecutionResults(ActionType actionType, + List operationExecutionResultList) { + + if (isDiagnosticLoggingEnabled()) { + List> operationDetailsList = new ArrayList<>(); + operationExecutionResultList.forEach( + performedOperation -> operationDetailsList.add(Map.of( + "operation", performedOperation.getOperation().getOp() + " path: " + + performedOperation.getOperation().getPath(), + "status", performedOperation.getStatus().toString(), + "message", performedOperation.getMessage() + ))); + + DiagnosticLog.DiagnosticLogBuilder diagLogBuilder = new DiagnosticLog.DiagnosticLogBuilder( + ActionExecutionLogConstants.ACTION_EXECUTION_COMPONENT_ID, + ActionExecutionLogConstants.ActionIDs.PROCESS_ACTION_RESPONSE); + diagLogBuilder + .inputParam("executedOperations", + operationDetailsList.isEmpty() ? "empty" : operationDetailsList) + .resultMessage("Allowed operations are executed for " + actionType.getDisplayName() + " action.") + .logDetailLevel(DiagnosticLog.LogDetailLevel.APPLICATION) + .resultStatus(DiagnosticLog.ResultStatus.SUCCESS) + .build(); + LoggerUtils.triggerDiagnosticLogEvent(diagLogBuilder); + } - if (!LoggerUtils.isDiagnosticLogsEnabled()) { - return; + if (LOG.isDebugEnabled()) { + LOG.debug(String.format("Processed response for action type: %s. Results of operations performed: %s", + actionType, operationExecutionResultList)); } + } + + private boolean isDiagnosticLoggingEnabled() { - DiagnosticLog.DiagnosticLogBuilder diagnosticLogBuilder = new DiagnosticLog.DiagnosticLogBuilder( - ActionExecutionLogConstants.ACTION_EXECUTION_COMPONENT_ID, - ActionExecutionLogConstants.ActionIDs.PROCESS_ACTION_RESPONSE) - .resultStatus(DiagnosticLog.ResultStatus.FAILED) - .resultMessage(resultMessage) - .inputParam("actionType", ActionType.PRE_UPDATE_PROFILE.getDisplayName()) - .inputParam("claimUri", claimUri) - .inputParam("reason", reason) - .logDetailLevel(DiagnosticLog.LogDetailLevel.APPLICATION); - LoggerUtils.triggerDiagnosticLogEvent(diagnosticLogBuilder); + try { + return LoggerUtils.isDiagnosticLogsEnabled(); + } catch (Throwable t) { + if (LOG.isDebugEnabled()) { + LOG.debug("Skipping diagnostic log due to runtime context unavailability.", t); + } + return false; + } } } diff --git a/components/user-mgt/org.wso2.carbon.identity.user.pre.update.profile.action/src/main/java/org/wso2/carbon/identity/user/pre/update/profile/action/internal/model/ProfileOperationExecutionResult.java b/components/user-mgt/org.wso2.carbon.identity.user.pre.update.profile.action/src/main/java/org/wso2/carbon/identity/user/pre/update/profile/action/internal/model/ProfileOperationExecutionResult.java new file mode 100644 index 000000000000..be0066cdb2b0 --- /dev/null +++ b/components/user-mgt/org.wso2.carbon.identity.user.pre.update.profile.action/src/main/java/org/wso2/carbon/identity/user/pre/update/profile/action/internal/model/ProfileOperationExecutionResult.java @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2026, WSO2 LLC. (http://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.wso2.carbon.identity.user.pre.update.profile.action.internal.model; + +import org.wso2.carbon.identity.action.execution.api.model.PerformableOperation; + +/** + * This class represents the result of a profile operation execution. + */ +public class ProfileOperationExecutionResult { + + private final PerformableOperation operation; + private final Status status; + private final String message; + + public ProfileOperationExecutionResult(PerformableOperation operation, Status status, String message) { + + this.operation = operation; + this.status = status; + this.message = message; + } + + public PerformableOperation getOperation() { + + return operation; + } + + public Status getStatus() { + + return status; + } + + public String getMessage() { + + return message; + } + + @Override + public String toString() { + + return "ProfileOperationExecutionResult{" + + "operation=" + operation + + ", status=" + status + + ", message='" + message + '\'' + + '}'; + } + + /** + * Enum to represent profile operation execution status. + */ + public enum Status { + SUCCESS, + FAILURE + } +} From e02739abb2068540ff90962ff077ee88e014afde Mon Sep 17 00:00:00 2001 From: Lashen1227 Date: Wed, 27 May 2026 10:59:35 +0530 Subject: [PATCH 10/12] feat: simplify allowed operations handling in PreUpdateProfileRequestBuilder and PreUpdateProfileResponseProcessor --- .../PreUpdateProfileRequestBuilder.java | 4 +-- .../PreUpdateProfileResponseProcessor.java | 33 +++++++------------ 2 files changed, 14 insertions(+), 23 deletions(-) diff --git a/components/user-mgt/org.wso2.carbon.identity.user.pre.update.profile.action/src/main/java/org/wso2/carbon/identity/user/pre/update/profile/action/internal/execution/PreUpdateProfileRequestBuilder.java b/components/user-mgt/org.wso2.carbon.identity.user.pre.update.profile.action/src/main/java/org/wso2/carbon/identity/user/pre/update/profile/action/internal/execution/PreUpdateProfileRequestBuilder.java index 146258b2c65c..4403f11143d5 100644 --- a/components/user-mgt/org.wso2.carbon.identity.user.pre.update.profile.action/src/main/java/org/wso2/carbon/identity/user/pre/update/profile/action/internal/execution/PreUpdateProfileRequestBuilder.java +++ b/components/user-mgt/org.wso2.carbon.identity.user.pre.update.profile.action/src/main/java/org/wso2/carbon/identity/user/pre/update/profile/action/internal/execution/PreUpdateProfileRequestBuilder.java @@ -88,11 +88,11 @@ public ActionExecutionRequest buildActionExecutionRequest(FlowContext flowContex return new ActionExecutionRequest.Builder() .actionType(getSupportedActionType()) .event(event) - .allowedOperations(getAllowedOperations(event)) + .allowedOperations(getAllowedOperations()) .build(); } - private List getAllowedOperations(Event event) throws ActionExecutionRequestBuilderException { + private List getAllowedOperations() { List allowedPaths = new ArrayList<>(); allowedPaths.add(USER_CLAIMS_FILTERED_PATH_TEMPLATE); diff --git a/components/user-mgt/org.wso2.carbon.identity.user.pre.update.profile.action/src/main/java/org/wso2/carbon/identity/user/pre/update/profile/action/internal/execution/PreUpdateProfileResponseProcessor.java b/components/user-mgt/org.wso2.carbon.identity.user.pre.update.profile.action/src/main/java/org/wso2/carbon/identity/user/pre/update/profile/action/internal/execution/PreUpdateProfileResponseProcessor.java index 74d8f7070ab8..8319d603fe10 100644 --- a/components/user-mgt/org.wso2.carbon.identity.user.pre.update.profile.action/src/main/java/org/wso2/carbon/identity/user/pre/update/profile/action/internal/execution/PreUpdateProfileResponseProcessor.java +++ b/components/user-mgt/org.wso2.carbon.identity.user.pre.update.profile.action/src/main/java/org/wso2/carbon/identity/user/pre/update/profile/action/internal/execution/PreUpdateProfileResponseProcessor.java @@ -113,7 +113,7 @@ public ActionExecutionStatus processSuccessResponse( break; case REMOVE: operationExecutionResultList.add(handleRemoveOperation(operation, responseContext, - userClaimsToBeModified, userClaimsToBeRemoved, simpleMultiValuedClaimsToBeRemoved)); + userClaimsToBeModified, userClaimsToBeRemoved)); break; default: break; @@ -174,12 +174,10 @@ private ProfileOperationExecutionResult handleRemoveOperation( PerformableOperation operation, ActionExecutionResponseContext responseContext, Map userClaimsToBeModified, - Map userClaimsToBeRemoved, - Map> simpleMultiValuedClaimsToBeRemoved) { + Map userClaimsToBeRemoved) { try { - populateRemoveOperationResult(operation, responseContext, userClaimsToBeModified, userClaimsToBeRemoved, - simpleMultiValuedClaimsToBeRemoved); + populateRemoveOperationResult(operation, responseContext, userClaimsToBeModified, userClaimsToBeRemoved); return new ProfileOperationExecutionResult(operation, ProfileOperationExecutionResult.Status.SUCCESS, "Operation applied."); } catch (ActionExecutionResponseProcessorException e) { @@ -205,7 +203,7 @@ private void populateAddOperationResult(PerformableOperation operation, String path = operation.getPath(); // Determine claim URI: from path (path-based format) or from value map. - if (path != null && isClaimPathFormat(path)) { + if (isClaimPathFormat(path)) { claimUri = getClaimUriFromPath(path); } else if (operation.getValue() instanceof LinkedHashMap) { LinkedHashMap valueMap = (LinkedHashMap) operation.getValue(); @@ -330,16 +328,14 @@ private void populateRemoveOperationResult(PerformableOperation operation, ActionExecutionResponseContext responseContext, Map userClaimsToBeModified, - Map userClaimsToBeRemoved, - Map> simpleMultiValuedClaimsToBeRemoved) + Map userClaimsToBeRemoved) throws ActionExecutionResponseProcessorException { String path = operation.getPath(); PreUpdateProfileEvent.FlowInitiatorType initiatorType = ((PreUpdateProfileEvent) responseContext .getActionEvent()).getInitiatorType(); - // Extract the claim URI and optionally the specific value to remove for - // multivalued claims. + // Extract the claim URI and optionally the specific value to remove for multivalued claims. String claimUri = getClaimUriFromPath(path); Optional localClaim = isLocalClaim(claimUri); @@ -354,8 +350,7 @@ private void populateRemoveOperationResult(PerformableOperation operation, throw new ActionExecutionResponseProcessorException( "Remove specific value from a multivalued claim is not supported."); } - populateMultiValuedClaimsForRemoveOperation(initiatorType, claimUri, userClaimsToBeRemoved, - simpleMultiValuedClaimsToBeRemoved); + populateMultiValuedClaimsForRemoveOperation(claimUri, userClaimsToBeRemoved); } } @@ -501,12 +496,8 @@ private void populateMultiValuedClaimsForReplaceOperation(PerformableOperation o } } - private void populateMultiValuedClaimsForRemoveOperation( - PreUpdateProfileEvent.FlowInitiatorType initiatorType, - String claimUri, - Map userClaimsToBeRemoved, - Map> simpleMultiValuedClaimsToBeRemoved) - throws ActionExecutionResponseProcessorException { + private void populateMultiValuedClaimsForRemoveOperation(String claimUri, + Map userClaimsToBeRemoved) { userClaimsToBeRemoved.put(claimUri, ""); } @@ -756,17 +747,17 @@ private void logOperationExecutionResults(ActionType actionType, "message", performedOperation.getMessage() ))); - DiagnosticLog.DiagnosticLogBuilder diagLogBuilder = new DiagnosticLog.DiagnosticLogBuilder( + DiagnosticLog.DiagnosticLogBuilder diagnosticLogBuilder = new DiagnosticLog.DiagnosticLogBuilder( ActionExecutionLogConstants.ACTION_EXECUTION_COMPONENT_ID, ActionExecutionLogConstants.ActionIDs.PROCESS_ACTION_RESPONSE); - diagLogBuilder + diagnosticLogBuilder .inputParam("executedOperations", operationDetailsList.isEmpty() ? "empty" : operationDetailsList) .resultMessage("Allowed operations are executed for " + actionType.getDisplayName() + " action.") .logDetailLevel(DiagnosticLog.LogDetailLevel.APPLICATION) .resultStatus(DiagnosticLog.ResultStatus.SUCCESS) .build(); - LoggerUtils.triggerDiagnosticLogEvent(diagLogBuilder); + LoggerUtils.triggerDiagnosticLogEvent(diagnosticLogBuilder); } if (LOG.isDebugEnabled()) { From 8cb1541d0b6f657606c9f30f44871e8d51b5a5bb Mon Sep 17 00:00:00 2001 From: Lashen1227 Date: Wed, 27 May 2026 14:01:09 +0530 Subject: [PATCH 11/12] refactor: remove redundant claim value handling logic in PreUpdateProfileResponseProcessor --- .../PreUpdateProfileResponseProcessor.java | 33 ------------------- 1 file changed, 33 deletions(-) diff --git a/components/user-mgt/org.wso2.carbon.identity.user.pre.update.profile.action/src/main/java/org/wso2/carbon/identity/user/pre/update/profile/action/internal/execution/PreUpdateProfileResponseProcessor.java b/components/user-mgt/org.wso2.carbon.identity.user.pre.update.profile.action/src/main/java/org/wso2/carbon/identity/user/pre/update/profile/action/internal/execution/PreUpdateProfileResponseProcessor.java index 8319d603fe10..fb1ca22ea0a6 100644 --- a/components/user-mgt/org.wso2.carbon.identity.user.pre.update.profile.action/src/main/java/org/wso2/carbon/identity/user/pre/update/profile/action/internal/execution/PreUpdateProfileResponseProcessor.java +++ b/components/user-mgt/org.wso2.carbon.identity.user.pre.update.profile.action/src/main/java/org/wso2/carbon/identity/user/pre/update/profile/action/internal/execution/PreUpdateProfileResponseProcessor.java @@ -460,39 +460,6 @@ private void populateMultiValuedClaimsForReplaceOperation(PerformableOperation o .collect(Collectors.joining(separator)); userClaimsToBeModified.put(claimUri, modifyingClaimValue); } - } else { - // Replacing a specific value in the array - if (initiatorType == PreUpdateProfileEvent.FlowInitiatorType.ADMIN || - initiatorType == PreUpdateProfileEvent.FlowInitiatorType.APPLICATION) { - Map userClaimValues = getUserClaimValues(userId, claimUri, userStoreManager); - List filteredClaims = getFilteredModifyingClaimValues(userClaimValues, claimUri, - claimValue, separator); - - if (!filteredClaims.isEmpty()) { - simpleMultiValuedClaimsToBeRemoved.put(claimUri, Arrays.asList(oldValueName)); - List trimmedFilteredClaims = filteredClaims.stream() - .map(String::trim) - .collect(Collectors.toList()); - simpleMultiValuedClaimsToBeAdded.put(claimUri, trimmedFilteredClaims); - } - } else if (initiatorType == PreUpdateProfileEvent.FlowInitiatorType.USER) { - Map userClaimValues = getUserClaimValues(userId, claimUri, userStoreManager); - List filteredClaims = getFilteredModifyingClaimValues(userClaimValues, claimUri, - claimValue, separator); - - List existingValues = new ArrayList<>(); - if (userClaimValues.get(claimUri) != null && !userClaimValues.get(claimUri).isEmpty()) { - existingValues.addAll(Arrays.asList(userClaimValues.get(claimUri).split(Pattern.quote(separator)))); - } - - existingValues.remove(oldValueName); - existingValues.addAll(filteredClaims); - String modifyingClaimValue = existingValues.stream() - .map(String::trim) - .filter(s -> !s.isEmpty()) - .collect(Collectors.joining(separator)); - userClaimsToBeModified.put(claimUri, modifyingClaimValue); - } } } From 1a17801bd1711f38e6f913b79656b563b59e9f26 Mon Sep 17 00:00:00 2001 From: Lashen1227 Date: Wed, 27 May 2026 23:15:41 +0530 Subject: [PATCH 12/12] test: add unit test for PreUpdateProfileResponseProcessorTest and PreUpdateProfileRequestBuilderTest --- .../pom.xml | 1 - .../PreUpdateProfileRequestBuilderTest.java | 182 ++++ ...PreUpdateProfileResponseProcessorTest.java | 973 +++++++++++++++++- 3 files changed, 1134 insertions(+), 22 deletions(-) diff --git a/components/user-mgt/org.wso2.carbon.identity.user.pre.update.profile.action/pom.xml b/components/user-mgt/org.wso2.carbon.identity.user.pre.update.profile.action/pom.xml index 1226e68dbe66..7aaea88b401b 100644 --- a/components/user-mgt/org.wso2.carbon.identity.user.pre.update.profile.action/pom.xml +++ b/components/user-mgt/org.wso2.carbon.identity.user.pre.update.profile.action/pom.xml @@ -143,7 +143,6 @@ org.wso2.carbon.identity.rule.evaluation.api.model; version="${carbon.identity.package.import.version.range}", org.wso2.carbon.identity.rule.evaluation.api.exception; version="${carbon.identity.package.import.version.range}", org.wso2.carbon.identity.rule.evaluation.api.provider; version="${carbon.identity.package.import.version.range}", - org.wso2.carbon.identity.central.log.mgt.utils; version="${carbon.identity.package.import.version.range}", org.wso2.carbon.identity.application.authentication.framework.util; version="${carbon.identity.package.import.version.range}" diff --git a/components/user-mgt/org.wso2.carbon.identity.user.pre.update.profile.action/src/test/java/org/wso2/carbon/identity/user/pre/update/profile/action/execution/PreUpdateProfileRequestBuilderTest.java b/components/user-mgt/org.wso2.carbon.identity.user.pre.update.profile.action/src/test/java/org/wso2/carbon/identity/user/pre/update/profile/action/execution/PreUpdateProfileRequestBuilderTest.java index 211edb5b1d8b..74d177cc5018 100644 --- a/components/user-mgt/org.wso2.carbon.identity.user.pre.update.profile.action/src/test/java/org/wso2/carbon/identity/user/pre/update/profile/action/execution/PreUpdateProfileRequestBuilderTest.java +++ b/components/user-mgt/org.wso2.carbon.identity.user.pre.update.profile.action/src/test/java/org/wso2/carbon/identity/user/pre/update/profile/action/execution/PreUpdateProfileRequestBuilderTest.java @@ -30,7 +30,9 @@ import org.wso2.carbon.identity.action.execution.api.model.ActionExecutionRequest; import org.wso2.carbon.identity.action.execution.api.model.ActionExecutionRequestContext; import org.wso2.carbon.identity.action.execution.api.model.ActionType; +import org.wso2.carbon.identity.action.execution.api.model.AllowedOperation; import org.wso2.carbon.identity.action.execution.api.model.FlowContext; +import org.wso2.carbon.identity.action.execution.api.model.Operation; import org.wso2.carbon.identity.action.execution.api.model.Organization; import org.wso2.carbon.identity.action.execution.api.model.UserClaim; import org.wso2.carbon.identity.action.execution.internal.component.ActionExecutionServiceComponentHolder; @@ -64,6 +66,8 @@ import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.Base64; +import java.util.HashMap; +import java.util.Map; import java.util.Optional; import static org.mockito.ArgumentMatchers.any; @@ -316,6 +320,163 @@ public void testBuildActionExecutionRequestWhenUpdatingClaimListAndClaimListToSh assertEquals(event.getUser().getRoles().size(), 0); } + @Test + public void testBuildActionExecutionRequestContainsAllowedOperations() throws Exception { + + IdentityContext.getThreadLocalIdentityContext() + .enterFlow(buildMockedFlow(Flow.Name.PROFILE_UPDATE, Flow.InitiatingPersona.USER)); + + FlowContext flowContext = FlowContext.create(); + flowContext.add(UserActionContext.USER_ACTION_CONTEXT_REFERENCE_KEY, + getMockUserActionContextWithoutUpdatingClaims()); + + ActionExecutionRequestContext actionExecutionRequestContext = + ActionExecutionRequestContext.create(getMockPreUpdateProfileActionWithNoClaimsConfiguredToShare()); + + ActionExecutionRequest request = preUpdateProfileRequestBuilder.buildActionExecutionRequest( + flowContext, actionExecutionRequestContext); + + assertNotNull(request.getAllowedOperations()); + assertEquals(request.getAllowedOperations().size(), 3); + + assertAllowedOperationExists(request, Operation.ADD); + assertAllowedOperationExists(request, Operation.REMOVE); + assertAllowedOperationExists(request, Operation.REPLACE); + } + + @Test(expectedExceptions = ActionExecutionRequestBuilderException.class, + expectedExceptionsMessageRegExp = "Unknown flow\\.") + public void testBuildActionExecutionRequestFailureWhenFlowIsNotPresent() throws Exception { + + FlowContext flowContext = FlowContext.create(); + flowContext.add(UserActionContext.USER_ACTION_CONTEXT_REFERENCE_KEY, + getMockUserActionContextWithoutUpdatingClaims()); + + ActionExecutionRequestContext actionExecutionRequestContext = + ActionExecutionRequestContext.create(getMockPreUpdateProfileActionWithNoClaimsConfiguredToShare()); + + preUpdateProfileRequestBuilder.buildActionExecutionRequest(flowContext, actionExecutionRequestContext); + } + + @Test(expectedExceptions = ActionExecutionRequestBuilderException.class, + expectedExceptionsMessageRegExp = "Root organization information is not available in Identity Context\\.") + public void testBuildActionExecutionRequestFailureWhenRootOrganizationIsNotPresent() throws Exception { + + IdentityContext.destroyCurrentContext(); + IdentityContext.getThreadLocalIdentityContext().enterFlow( + buildMockedFlow(Flow.Name.PROFILE_UPDATE, Flow.InitiatingPersona.USER)); + + FlowContext flowContext = FlowContext.create(); + flowContext.add(UserActionContext.USER_ACTION_CONTEXT_REFERENCE_KEY, + getMockUserActionContextWithoutUpdatingClaims()); + + ActionExecutionRequestContext actionExecutionRequestContext = + ActionExecutionRequestContext.create(getMockPreUpdateProfileActionWithNoClaimsConfiguredToShare()); + + preUpdateProfileRequestBuilder.buildActionExecutionRequest(flowContext, actionExecutionRequestContext); + } + + @Test(expectedExceptions = ActionExecutionRequestBuilderException.class, + expectedExceptionsMessageRegExp = "Unknown user claim value format\\. Only String and String\\[] " + + "types expected\\.") + public void testBuildActionExecutionRequestFailureWhenUpdatingClaimHasUnsupportedType() throws Exception { + + IdentityContext.getThreadLocalIdentityContext() + .enterFlow(buildMockedFlow(Flow.Name.PROFILE_UPDATE, Flow.InitiatingPersona.USER)); + + UserActionRequestDTO userActionRequestDTO = mock(UserActionRequestDTO.class); + Map claims = new HashMap<>(); + claims.put(CLAIM1.getClaimURI(), 10); + when(userActionRequestDTO.getUserId()).thenReturn(USER_ID); + when(userActionRequestDTO.getUserStoreDomain()).thenReturn(USER_STORE_DOMAIN); + when(userActionRequestDTO.getClaims()).thenReturn(claims); + when(userActionRequestDTO.getResidentOrganization()).thenReturn(userResidentOrganization); + UserActionContext userActionContext = mock(UserActionContext.class); + when(userActionContext.getUserActionRequestDTO()).thenReturn(userActionRequestDTO); + + FlowContext flowContext = FlowContext.create(); + flowContext.add(UserActionContext.USER_ACTION_CONTEXT_REFERENCE_KEY, userActionContext); + + ActionExecutionRequestContext actionExecutionRequestContext = + ActionExecutionRequestContext.create(getMockPreUpdateProfileActionWithNoClaimsConfiguredToShare()); + + preUpdateProfileRequestBuilder.buildActionExecutionRequest(flowContext, actionExecutionRequestContext); + } + + @Test(expectedExceptions = ActionExecutionRequestBuilderException.class, + expectedExceptionsMessageRegExp = "Invalid claim value format for multi-valued claim: .*") + public void testBuildActionExecutionRequestFailureWhenMultiValuedClaimHasInvalidUpdatingType() throws Exception { + + IdentityContext.getThreadLocalIdentityContext() + .enterFlow(buildMockedFlow(Flow.Name.PROFILE_UPDATE, Flow.InitiatingPersona.USER)); + + FlowContext flowContext = FlowContext.create(); + flowContext.add(UserActionContext.USER_ACTION_CONTEXT_REFERENCE_KEY, new UserActionContext( + new UserActionRequestDTO.Builder() + .userId(USER_ID) + .userStoreDomain(USER_STORE_DOMAIN) + .addClaim(CLAIM2.getClaimURI(), "not-an-array") + .residentOrganization(userResidentOrganization) + .build())); + + ActionExecutionRequestContext actionExecutionRequestContext = + ActionExecutionRequestContext.create(new PreUpdateProfileAction.ResponseBuilder() + .attributes(Arrays.asList(CLAIM2.getClaimURI())) + .build()); + + preUpdateProfileRequestBuilder.buildActionExecutionRequest(flowContext, actionExecutionRequestContext); + } + + @Test(expectedExceptions = ActionExecutionRequestBuilderException.class, + expectedExceptionsMessageRegExp = "Claim not found for claim URI: .*") + public void testBuildActionExecutionRequestFailureWhenClaimMetadataNotFound() throws Exception { + + IdentityContext.getThreadLocalIdentityContext() + .enterFlow(buildMockedFlow(Flow.Name.PROFILE_UPDATE, Flow.InitiatingPersona.USER)); + + String missingClaim = "http://wso2.org/claims/missingClaim"; + when(claimMetadataManagementService.getLocalClaim(missingClaim, TENANT_DOMAIN)) + .thenReturn(Optional.empty()); + + FlowContext flowContext = FlowContext.create(); + flowContext.add(UserActionContext.USER_ACTION_CONTEXT_REFERENCE_KEY, new UserActionContext( + new UserActionRequestDTO.Builder() + .userId(USER_ID) + .userStoreDomain(USER_STORE_DOMAIN) + .addClaim(missingClaim, new String[] {"value1"}) + .residentOrganization(userResidentOrganization) + .build())); + + ActionExecutionRequestContext actionExecutionRequestContext = + ActionExecutionRequestContext.create(new PreUpdateProfileAction.ResponseBuilder() + .attributes(Arrays.asList(missingClaim)) + .build()); + + preUpdateProfileRequestBuilder.buildActionExecutionRequest(flowContext, actionExecutionRequestContext); + } + + @Test + public void testBuildActionExecutionRequestWhenUserStoreDomainNotProvidedInRequest() + throws ActionExecutionRequestBuilderException { + + IdentityContext.getThreadLocalIdentityContext() + .enterFlow(buildMockedFlow(Flow.Name.PROFILE_UPDATE, Flow.InitiatingPersona.USER)); + + FlowContext flowContext = FlowContext.create(); + flowContext.add(UserActionContext.USER_ACTION_CONTEXT_REFERENCE_KEY, + getMockUserActionContextWithoutUserStoreDomain()); + + ActionExecutionRequestContext actionExecutionRequestContext = + ActionExecutionRequestContext.create(getMockPreUpdateProfileActionWithNoClaimsConfiguredToShare()); + + ActionExecutionRequest request = preUpdateProfileRequestBuilder.buildActionExecutionRequest( + flowContext, actionExecutionRequestContext); + + assertNotNull(request); + PreUpdateProfileEvent event = (PreUpdateProfileEvent) request.getEvent(); + assertEquals(event.getUserStore().getName(), USER_STORE_DOMAIN); + } + // Failure tests. // These tests are updates PreUpdateProfileActionServiceComponentHolder with corrupted mock services. // Thus, these tests are always expected to execute after success tests and they are dependent on each other. @@ -528,6 +689,27 @@ private static UserActionContext getMockUserActionContextWithoutUpdatingClaims() .build()); } + private static UserActionContext getMockUserActionContextWithoutUserStoreDomain() { + + return new UserActionContext(new UserActionRequestDTO.Builder() + .userId(USER_ID) + .build()); + } + + private static void assertAllowedOperationExists(ActionExecutionRequest request, Operation expectedOperation) { + + boolean isFound = false; + for (AllowedOperation allowedOperation : request.getAllowedOperations()) { + if (expectedOperation.equals(allowedOperation.getOp())) { + isFound = true; + assertEquals(allowedOperation.getPaths().size(), 1); + assertEquals(allowedOperation.getPaths().get(0), + PreUpdateProfileRequestBuilder.USER_CLAIMS_FILTERED_PATH_TEMPLATE); + } + } + assertTrue(isFound, "Allowed operation not found for: " + expectedOperation); + } + private static PreUpdateProfileAction getMockPreUpdateProfileActionWithClaimsConfiguredToShare() { return new PreUpdateProfileAction.ResponseBuilder() diff --git a/components/user-mgt/org.wso2.carbon.identity.user.pre.update.profile.action/src/test/java/org/wso2/carbon/identity/user/pre/update/profile/action/execution/PreUpdateProfileResponseProcessorTest.java b/components/user-mgt/org.wso2.carbon.identity.user.pre.update.profile.action/src/test/java/org/wso2/carbon/identity/user/pre/update/profile/action/execution/PreUpdateProfileResponseProcessorTest.java index 145ba53564de..b52474e8c050 100644 --- a/components/user-mgt/org.wso2.carbon.identity.user.pre.update.profile.action/src/test/java/org/wso2/carbon/identity/user/pre/update/profile/action/execution/PreUpdateProfileResponseProcessorTest.java +++ b/components/user-mgt/org.wso2.carbon.identity.user.pre.update.profile.action/src/test/java/org/wso2/carbon/identity/user/pre/update/profile/action/execution/PreUpdateProfileResponseProcessorTest.java @@ -18,7 +18,10 @@ package org.wso2.carbon.identity.user.pre.update.profile.action.execution; +import org.mockito.MockedStatic; +import org.testng.annotations.AfterMethod; import org.testng.annotations.BeforeClass; +import org.testng.annotations.BeforeMethod; import org.testng.annotations.Test; import org.wso2.carbon.identity.action.execution.api.exception.ActionExecutionResponseProcessorException; import org.wso2.carbon.identity.action.execution.api.model.ActionExecutionResponseContext; @@ -32,25 +35,62 @@ import org.wso2.carbon.identity.action.execution.api.model.Event; import org.wso2.carbon.identity.action.execution.api.model.Failure; import org.wso2.carbon.identity.action.execution.api.model.FlowContext; +import org.wso2.carbon.identity.action.execution.api.model.Operation; +import org.wso2.carbon.identity.action.execution.api.model.PerformableOperation; import org.wso2.carbon.identity.action.execution.api.model.ResponseData; import org.wso2.carbon.identity.action.execution.api.model.Success; +import org.wso2.carbon.identity.action.execution.api.model.User; +import org.wso2.carbon.identity.action.execution.api.util.RequestBuilderUtil; +import org.wso2.carbon.identity.application.authentication.framework.util.FrameworkUtils; +import org.wso2.carbon.identity.central.log.mgt.utils.LoggerUtils; +import org.wso2.carbon.identity.claim.metadata.mgt.ClaimMetadataManagementService; +import org.wso2.carbon.identity.claim.metadata.mgt.exception.ClaimMetadataException; +import org.wso2.carbon.identity.claim.metadata.mgt.model.Claim; +import org.wso2.carbon.identity.claim.metadata.mgt.model.LocalClaim; +import org.wso2.carbon.identity.claim.metadata.mgt.util.ClaimConstants; +import org.wso2.carbon.identity.common.testng.WithCarbonHome; +import org.wso2.carbon.identity.core.context.IdentityContext; import org.wso2.carbon.identity.user.action.api.model.UserActionContext; +import org.wso2.carbon.identity.user.pre.update.profile.action.internal.component.PreUpdateProfileActionServiceComponentHolder; import org.wso2.carbon.identity.user.pre.update.profile.action.internal.execution.PreUpdateProfileResponseProcessor; +import org.wso2.carbon.identity.user.pre.update.profile.action.internal.model.PreUpdateProfileEvent; +import org.wso2.carbon.user.core.UniqueIDUserStoreManager; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Optional; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.when; import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertNotNull; import static org.testng.Assert.assertTrue; +import static org.testng.Assert.expectThrows; /** * User Pre Update Password Action Response Processor Test. */ +@WithCarbonHome public class PreUpdateProfileResponseProcessorTest { + private static final String USER_ID = "user1"; + private static final String SINGLE_CLAIM_URI = "http://wso2.org/claims/country"; + private static final String MULTI_CLAIM_URI = "http://wso2.org/claims/mobileNumbers"; + private static final String SCIM_SCHEMA_URI_PREFIX = "urn:ietf:params:scim:schemas"; + private PreUpdateProfileResponseProcessor preUpdateProfileResponseProcessor; + private ClaimMetadataManagementService claimMetadataManagementService; + private UniqueIDUserStoreManager userStoreManager; + private MockedStatic requestBuilderUtil; + private MockedStatic frameworkUtils; + private MockedStatic loggerUtils; @BeforeClass void setUp() { @@ -58,6 +98,51 @@ void setUp() { preUpdateProfileResponseProcessor = new PreUpdateProfileResponseProcessor(); } + @BeforeMethod + void setUpMethod() throws Exception { + + IdentityContext.destroyCurrentContext(); + + claimMetadataManagementService = mock(ClaimMetadataManagementService.class); + userStoreManager = mock(UniqueIDUserStoreManager.class); + requestBuilderUtil = mockStatic(RequestBuilderUtil.class); + frameworkUtils = mockStatic(FrameworkUtils.class); + loggerUtils = mockStatic(LoggerUtils.class); + frameworkUtils.when(FrameworkUtils::getMultiAttributeSeparator).thenReturn(","); + loggerUtils.when(LoggerUtils::isDiagnosticLogsEnabled).thenReturn(false); + requestBuilderUtil.when(() -> RequestBuilderUtil.getUserStoreManager(any())) + .thenReturn(userStoreManager); + + when(claimMetadataManagementService.getMappedExternalClaimsForLocalClaim(any(), any())) + .thenReturn(Collections.emptyList()); + when(claimMetadataManagementService.getLocalClaim(any(), any())) + .thenAnswer(invocation -> { + String claimUri = invocation.getArgument(0); + if (MULTI_CLAIM_URI.equals(claimUri)) { + return Optional.of(mockLocalClaim(MULTI_CLAIM_URI, true, false)); + } + return Optional.of(mockLocalClaim(SINGLE_CLAIM_URI, false, false)); + }); + + PreUpdateProfileActionServiceComponentHolder.getInstance() + .setClaimManagementService(claimMetadataManagementService); + } + + @AfterMethod + void tearDownMethod() { + + if (requestBuilderUtil != null) { + requestBuilderUtil.close(); + } + if (frameworkUtils != null) { + frameworkUtils.close(); + } + if (loggerUtils != null) { + loggerUtils.close(); + } + IdentityContext.destroyCurrentContext(); + } + @Test public void testGetSupportedActionType() { @@ -134,32 +219,878 @@ public void testProcessErrorResponse() throws ActionExecutionResponseProcessorEx } @Test - public void testGetClaimUriFromPathAllowsUnquotedUri() throws Exception { + public void testProcessSuccessResponseWhenSingleValuedAddOperation() throws Exception { + + FlowContext flowContext = FlowContext.create(); + flowContext.add(UserActionContext.USER_ACTION_CONTEXT_REFERENCE_KEY, mock(UserActionContext.class)); - String path = "/user/claims[uri=http://wso2.org/claims/country]"; - String claimUri = invokeGetClaimUriFromPath(path); - assertEquals(claimUri, "http://wso2.org/claims/country"); + when(userStoreManager.getUserClaimValuesWithID(anyString(), any(String[].class), anyString())) + .thenReturn(new HashMap<>()); + + PerformableOperation operation = new PerformableOperation(); + operation.setOp(Operation.ADD); + operation.setPath("/user/claims[uri=" + SINGLE_CLAIM_URI + "]"); + operation.setValue("Alex"); + + ActionInvocationSuccessResponse successResponse = new ActionInvocationSuccessResponse.Builder() + .actionStatus(ActionInvocationResponse.Status.SUCCESS) + .operations(Collections.singletonList(operation)) + .responseData(mock(ResponseData.class)) + .build(); + + ActionExecutionStatus resultStatus = preUpdateProfileResponseProcessor.processSuccessResponse( + flowContext, ActionExecutionResponseContext.create(buildEvent(), successResponse)); + + assertNotNull(resultStatus); + Map addedClaims = (Map) resultStatus.getResponseContext() + .get("userClaimsToBeAdded"); + assertEquals(addedClaims.get(SINGLE_CLAIM_URI), "Alex"); } @Test - public void testGetClaimUriFromPathRejectsQuotedUri() throws Exception { + public void testProcessSuccessResponseWhenOperationHasCorrectUriPathFormat() throws Exception { - String path = "/user/claims[uri='http://wso2.org/claims/country']"; - try { - invokeGetClaimUriFromPath(path); - } catch (InvocationTargetException e) { - assertTrue(e.getCause() instanceof ActionExecutionResponseProcessorException); - assertTrue(e.getCause().getMessage().contains("Invalid filter path format")); - return; - } - throw new AssertionError("Expected ActionExecutionResponseProcessorException for quoted URI path"); + FlowContext flowContext = FlowContext.create(); + flowContext.add(UserActionContext.USER_ACTION_CONTEXT_REFERENCE_KEY, mock(UserActionContext.class)); + + when(userStoreManager.getUserClaimValuesWithID(anyString(), any(String[].class), anyString())) + .thenReturn(new HashMap<>()); + + String validPath = "/user/claims[uri=http://wso2.org/claims/country]"; + PerformableOperation operation = new PerformableOperation(); + operation.setOp(Operation.ADD); + operation.setPath(validPath); + operation.setValue("Sri Lanka"); + + ActionInvocationSuccessResponse successResponse = new ActionInvocationSuccessResponse.Builder() + .actionStatus(ActionInvocationResponse.Status.SUCCESS) + .operations(Collections.singletonList(operation)) + .responseData(mock(ResponseData.class)) + .build(); + + ActionExecutionStatus resultStatus = preUpdateProfileResponseProcessor.processSuccessResponse( + flowContext, ActionExecutionResponseContext.create(buildEvent(), successResponse)); + + assertNotNull(resultStatus); + Map addedClaims = (Map) resultStatus.getResponseContext() + .get("userClaimsToBeAdded"); + assertEquals(addedClaims.get(SINGLE_CLAIM_URI), "Sri Lanka"); + } + + @Test + public void testProcessSuccessResponseWhenSingleValuedReplaceOperation() throws Exception { + + FlowContext flowContext = FlowContext.create(); + flowContext.add(UserActionContext.USER_ACTION_CONTEXT_REFERENCE_KEY, mock(UserActionContext.class)); + + when(userStoreManager.getUserClaimValuesWithID(anyString(), any(String[].class), anyString())) + .thenReturn(new HashMap<>()); + + PerformableOperation operation = new PerformableOperation(); + operation.setOp(Operation.REPLACE); + operation.setPath("/user/claims[uri=" + SINGLE_CLAIM_URI + "]"); + operation.setValue("AlexModified"); + + ActionInvocationSuccessResponse successResponse = new ActionInvocationSuccessResponse.Builder() + .actionStatus(ActionInvocationResponse.Status.SUCCESS) + .operations(Collections.singletonList(operation)) + .responseData(mock(ResponseData.class)) + .build(); + + ActionExecutionStatus resultStatus = preUpdateProfileResponseProcessor.processSuccessResponse( + flowContext, ActionExecutionResponseContext.create(buildEvent(), successResponse)); + + assertNotNull(resultStatus); + Map modifiedClaims = (Map) resultStatus.getResponseContext() + .get("userClaimsToBeModified"); + assertEquals(modifiedClaims.get(SINGLE_CLAIM_URI), "AlexModified"); + } + + @Test + public void testProcessSuccessResponseWhenSingleValuedRemoveOperation() throws Exception { + + FlowContext flowContext = FlowContext.create(); + flowContext.add(UserActionContext.USER_ACTION_CONTEXT_REFERENCE_KEY, mock(UserActionContext.class)); + + PerformableOperation operation = new PerformableOperation(); + operation.setOp(Operation.REMOVE); + operation.setPath("/user/claims[uri=" + SINGLE_CLAIM_URI + "]"); + + ActionInvocationSuccessResponse successResponse = new ActionInvocationSuccessResponse.Builder() + .actionStatus(ActionInvocationResponse.Status.SUCCESS) + .operations(Collections.singletonList(operation)) + .responseData(mock(ResponseData.class)) + .build(); + + ActionExecutionStatus resultStatus = preUpdateProfileResponseProcessor.processSuccessResponse( + flowContext, ActionExecutionResponseContext.create(buildEvent(), successResponse)); + + assertNotNull(resultStatus); + Map removedClaims = (Map) resultStatus.getResponseContext() + .get("userClaimsToBeRemoved"); + assertEquals(removedClaims.get(SINGLE_CLAIM_URI), ""); + } + + @Test + public void testProcessSuccessResponseWhenMultiValuedReplaceOperation() throws Exception { + + FlowContext flowContext = FlowContext.create(); + flowContext.add(UserActionContext.USER_ACTION_CONTEXT_REFERENCE_KEY, mock(UserActionContext.class)); + + Map existingClaims = new HashMap<>(); + existingClaims.put(MULTI_CLAIM_URI, "0771234567,0717654329"); + when(userStoreManager.getUserClaimValuesWithID(anyString(), any(String[].class), anyString())) + .thenReturn(existingClaims); + + PerformableOperation operation = new PerformableOperation(); + operation.setOp(Operation.REPLACE); + operation.setPath("/user/claims[uri=" + MULTI_CLAIM_URI + "]"); + operation.setValue(Arrays.asList("0771234567", "0709998888")); + + ActionInvocationSuccessResponse successResponse = new ActionInvocationSuccessResponse.Builder() + .actionStatus(ActionInvocationResponse.Status.SUCCESS) + .operations(Collections.singletonList(operation)) + .responseData(mock(ResponseData.class)) + .build(); + + ActionExecutionStatus resultStatus = preUpdateProfileResponseProcessor.processSuccessResponse( + flowContext, + ActionExecutionResponseContext.create(buildEvent(PreUpdateProfileEvent.FlowInitiatorType.USER), + successResponse)); + + Map modifiedClaims = (Map) resultStatus.getResponseContext() + .get("userClaimsToBeModified"); + assertEquals(modifiedClaims.get(MULTI_CLAIM_URI), "0771234567,0709998888"); + } + + @Test + public void testProcessSuccessResponseWhenMultiValuedReplaceOperationByAdmin() throws Exception { + + FlowContext flowContext = FlowContext.create(); + flowContext.add(UserActionContext.USER_ACTION_CONTEXT_REFERENCE_KEY, mock(UserActionContext.class)); + + Map existingClaims = new HashMap<>(); + existingClaims.put(MULTI_CLAIM_URI, "0771234567,0717654329"); + when(userStoreManager.getUserClaimValuesWithID(anyString(), any(String[].class), anyString())) + .thenReturn(existingClaims); + + PerformableOperation operation = new PerformableOperation(); + operation.setOp(Operation.REPLACE); + operation.setPath("/user/claims[uri=" + MULTI_CLAIM_URI + "]"); + operation.setValue(Arrays.asList("0771234567", "0709998888")); + + ActionInvocationSuccessResponse successResponse = new ActionInvocationSuccessResponse.Builder() + .actionStatus(ActionInvocationResponse.Status.SUCCESS) + .operations(Collections.singletonList(operation)) + .responseData(mock(ResponseData.class)) + .build(); + + ActionExecutionStatus resultStatus = preUpdateProfileResponseProcessor.processSuccessResponse( + flowContext, + ActionExecutionResponseContext.create(buildEvent(PreUpdateProfileEvent.FlowInitiatorType.ADMIN), + successResponse)); + + Map addedClaims = (Map) resultStatus.getResponseContext() + .get("multiValuedClaimsToBeAdded"); + Map removedClaims = (Map) resultStatus.getResponseContext() + .get("multiValuedClaimsToBeRemoved"); + + assertEquals(addedClaims.get(MULTI_CLAIM_URI), Collections.singletonList("0709998888")); + assertEquals(removedClaims.get(MULTI_CLAIM_URI), Collections.singletonList("0717654329")); + } + + @Test + public void testProcessSuccessResponseWhenMultiValuedRemoveOperation() throws Exception { + + FlowContext flowContext = FlowContext.create(); + flowContext.add(UserActionContext.USER_ACTION_CONTEXT_REFERENCE_KEY, mock(UserActionContext.class)); + + PerformableOperation operation = new PerformableOperation(); + operation.setOp(Operation.REMOVE); + operation.setPath("/user/claims[uri=" + MULTI_CLAIM_URI + "]"); + + ActionInvocationSuccessResponse successResponse = new ActionInvocationSuccessResponse.Builder() + .actionStatus(ActionInvocationResponse.Status.SUCCESS) + .operations(Collections.singletonList(operation)) + .responseData(mock(ResponseData.class)) + .build(); + + ActionExecutionStatus resultStatus = preUpdateProfileResponseProcessor.processSuccessResponse( + flowContext, ActionExecutionResponseContext.create(buildEvent(), successResponse)); + + assertNotNull(resultStatus); + Map removedClaims = (Map) resultStatus.getResponseContext() + .get("userClaimsToBeRemoved"); + assertEquals(removedClaims.get(MULTI_CLAIM_URI), ""); + } + + @Test + public void testProcessSuccessResponseWhenMultiValuedRemoveOperationWithSpecificValue() throws Exception { + + FlowContext flowContext = FlowContext.create(); + flowContext.add(UserActionContext.USER_ACTION_CONTEXT_REFERENCE_KEY, mock(UserActionContext.class)); + + PerformableOperation operation = new PerformableOperation(); + operation.setOp(Operation.REMOVE); + operation.setPath("/user/claims[uri=" + MULTI_CLAIM_URI + "]"); + operation.setValue("0717654329"); + + ActionInvocationSuccessResponse successResponse = new ActionInvocationSuccessResponse.Builder() + .actionStatus(ActionInvocationResponse.Status.SUCCESS) + .operations(Collections.singletonList(operation)) + .responseData(mock(ResponseData.class)) + .build(); + + ActionExecutionStatus resultStatus = preUpdateProfileResponseProcessor.processSuccessResponse( + flowContext, ActionExecutionResponseContext.create(buildEvent(), successResponse)); + + assertNotNull(resultStatus); + assertTrue(((Map) resultStatus.getResponseContext().get("userClaimsToBeRemoved")).isEmpty()); + } + + @Test + public void testProcessSuccessResponseWhenInvalidOperationFormat() throws Exception { + + FlowContext flowContext = FlowContext.create(); + flowContext.add(UserActionContext.USER_ACTION_CONTEXT_REFERENCE_KEY, mock(UserActionContext.class)); + + PerformableOperation operation = new PerformableOperation(); + operation.setOp(Operation.ADD); + operation.setPath("/invalid"); + operation.setValue("value"); + + ActionInvocationSuccessResponse successResponse = new ActionInvocationSuccessResponse.Builder() + .actionStatus(ActionInvocationResponse.Status.SUCCESS) + .operations(Collections.singletonList(operation)) + .responseData(mock(ResponseData.class)) + .build(); + + ActionExecutionStatus resultStatus = preUpdateProfileResponseProcessor.processSuccessResponse( + flowContext, ActionExecutionResponseContext.create(buildEvent(), successResponse)); + + assertNotNull(resultStatus); + assertTrue(((Map) resultStatus.getResponseContext().get("userClaimsToBeAdded")).isEmpty()); + assertTrue(((Map) resultStatus.getResponseContext().get("userClaimsToBeModified")).isEmpty()); + assertTrue(((Map) resultStatus.getResponseContext().get("userClaimsToBeRemoved")).isEmpty()); + } + + @Test + public void testProcessSuccessResponseWhenSingleValuedAddOperationWithPathHasQuotedUri() throws Exception { + + FlowContext flowContext = FlowContext.create(); + flowContext.add(UserActionContext.USER_ACTION_CONTEXT_REFERENCE_KEY, mock(UserActionContext.class)); + + PerformableOperation operation = new PerformableOperation(); + operation.setOp(Operation.ADD); + operation.setPath("/user/claims[uri=\"" + SINGLE_CLAIM_URI + "\"]"); + operation.setValue("Alex"); + + ActionInvocationSuccessResponse successResponse = new ActionInvocationSuccessResponse.Builder() + .actionStatus(ActionInvocationResponse.Status.SUCCESS) + .operations(Collections.singletonList(operation)) + .responseData(mock(ResponseData.class)) + .build(); + + ActionExecutionStatus resultStatus = preUpdateProfileResponseProcessor.processSuccessResponse( + flowContext, ActionExecutionResponseContext.create(buildEvent(), successResponse)); + + assertTrue(((Map) resultStatus.getResponseContext().get("userClaimsToBeAdded")).isEmpty()); + } + + @Test + public void testProcessSuccessResponseWhenSingleValuedAddOperationWithPathHasTrailingChars() throws Exception { + + FlowContext flowContext = FlowContext.create(); + flowContext.add(UserActionContext.USER_ACTION_CONTEXT_REFERENCE_KEY, mock(UserActionContext.class)); + + PerformableOperation operation = new PerformableOperation(); + operation.setOp(Operation.ADD); + operation.setPath("/user/claims[uri=" + SINGLE_CLAIM_URI + "]abc"); + operation.setValue("Alex"); + + ActionInvocationSuccessResponse successResponse = new ActionInvocationSuccessResponse.Builder() + .actionStatus(ActionInvocationResponse.Status.SUCCESS) + .operations(Collections.singletonList(operation)) + .responseData(mock(ResponseData.class)) + .build(); + + ActionExecutionStatus resultStatus = preUpdateProfileResponseProcessor.processSuccessResponse( + flowContext, ActionExecutionResponseContext.create(buildEvent(), successResponse)); + + assertTrue(((Map) resultStatus.getResponseContext().get("userClaimsToBeAdded")).isEmpty()); + } + + @Test + public void testProcessSuccessResponseWhenSingleValuedAddOperationWithValueMap() throws Exception { + + FlowContext flowContext = FlowContext.create(); + flowContext.add(UserActionContext.USER_ACTION_CONTEXT_REFERENCE_KEY, mock(UserActionContext.class)); + + when(userStoreManager.getUserClaimValuesWithID(anyString(), any(String[].class), anyString())) + .thenReturn(new HashMap<>()); + + LinkedHashMap valueMap = new LinkedHashMap<>(); + valueMap.put("uri", SINGLE_CLAIM_URI); + valueMap.put("value", "Colombo"); + + PerformableOperation operation = new PerformableOperation(); + operation.setOp(Operation.ADD); + operation.setPath("/invalid"); + operation.setValue(valueMap); + + ActionInvocationSuccessResponse successResponse = new ActionInvocationSuccessResponse.Builder() + .actionStatus(ActionInvocationResponse.Status.SUCCESS) + .operations(Collections.singletonList(operation)) + .responseData(mock(ResponseData.class)) + .build(); + + ActionExecutionStatus resultStatus = preUpdateProfileResponseProcessor.processSuccessResponse( + flowContext, ActionExecutionResponseContext.create(buildEvent(), successResponse)); + + Map addedClaims = (Map) resultStatus.getResponseContext() + .get("userClaimsToBeAdded"); + assertEquals(addedClaims.get(SINGLE_CLAIM_URI), "Colombo"); + } + + @Test + public void testProcessSuccessResponseWhenSingleValuedAddOperationWithValueMapWithoutUri() throws Exception { + + FlowContext flowContext = FlowContext.create(); + flowContext.add(UserActionContext.USER_ACTION_CONTEXT_REFERENCE_KEY, mock(UserActionContext.class)); + + LinkedHashMap valueMap = new LinkedHashMap<>(); + valueMap.put("value", "Colombo"); + + PerformableOperation operation = new PerformableOperation(); + operation.setOp(Operation.ADD); + operation.setPath("/invalid"); + operation.setValue(valueMap); + + ActionInvocationSuccessResponse successResponse = new ActionInvocationSuccessResponse.Builder() + .actionStatus(ActionInvocationResponse.Status.SUCCESS) + .operations(Collections.singletonList(operation)) + .responseData(mock(ResponseData.class)) + .build(); + + ActionExecutionStatus resultStatus = preUpdateProfileResponseProcessor.processSuccessResponse( + flowContext, ActionExecutionResponseContext.create(buildEvent(), successResponse)); + + assertTrue(((Map) resultStatus.getResponseContext().get("userClaimsToBeAdded")).isEmpty()); + } + + @Test + public void testProcessSuccessResponseWhenGroupClaimIsModified() throws Exception { + + FlowContext flowContext = FlowContext.create(); + flowContext.add(UserActionContext.USER_ACTION_CONTEXT_REFERENCE_KEY, mock(UserActionContext.class)); + + String groupClaim = "http://wso2.org/claims/groups"; + LocalClaim localClaim = mockLocalClaim(groupClaim, true, false); + when(claimMetadataManagementService.getLocalClaim(eq(groupClaim), any())) + .thenReturn(Optional.of(localClaim)); + + PerformableOperation operation = new PerformableOperation(); + operation.setOp(Operation.REPLACE); + operation.setPath("/user/claims[uri=" + groupClaim + "]"); + operation.setValue(Arrays.asList("g1", "g2")); + + ActionInvocationSuccessResponse successResponse = new ActionInvocationSuccessResponse.Builder() + .actionStatus(ActionInvocationResponse.Status.SUCCESS) + .operations(Collections.singletonList(operation)) + .responseData(mock(ResponseData.class)) + .build(); + + ActionExecutionStatus resultStatus = preUpdateProfileResponseProcessor.processSuccessResponse( + flowContext, ActionExecutionResponseContext.create(buildEvent(), successResponse)); + + assertTrue(((Map) resultStatus.getResponseContext().get("userClaimsToBeModified")).isEmpty()); + assertTrue(((Map) resultStatus.getResponseContext().get("multiValuedClaimsToBeAdded")).isEmpty()); + } + + @Test + public void testProcessSuccessResponseWhenFlowInitiatorClaimIsModified() throws Exception { + + FlowContext flowContext = FlowContext.create(); + flowContext.add(UserActionContext.USER_ACTION_CONTEXT_REFERENCE_KEY, mock(UserActionContext.class)); + + String flowInitiatorClaim = "http://wso2.org/claims/flowInitiator"; + LocalClaim localClaim = mockLocalClaim(flowInitiatorClaim, false, true); + when(claimMetadataManagementService.getLocalClaim(eq(flowInitiatorClaim), any())) + .thenReturn(Optional.of(localClaim)); + + PerformableOperation operation = new PerformableOperation(); + operation.setOp(Operation.REPLACE); + operation.setPath("/user/claims[uri=" + flowInitiatorClaim + "]"); + operation.setValue("true"); + + ActionInvocationSuccessResponse successResponse = new ActionInvocationSuccessResponse.Builder() + .actionStatus(ActionInvocationResponse.Status.SUCCESS) + .operations(Collections.singletonList(operation)) + .responseData(mock(ResponseData.class)) + .build(); + + ActionExecutionStatus resultStatus = preUpdateProfileResponseProcessor.processSuccessResponse( + flowContext, ActionExecutionResponseContext.create(buildEvent(), successResponse)); + + assertTrue(((Map) resultStatus.getResponseContext().get("userClaimsToBeModified")).isEmpty()); + } + + @Test + public void testProcessSuccessResponseWhenMultiValuedReplaceOperationWithReducedArray() throws Exception { + + FlowContext flowContext = FlowContext.create(); + flowContext.add(UserActionContext.USER_ACTION_CONTEXT_REFERENCE_KEY, mock(UserActionContext.class)); + + Map existingClaims = new HashMap<>(); + existingClaims.put(MULTI_CLAIM_URI, "0771234567,0717654329"); + when(userStoreManager.getUserClaimValuesWithID(anyString(), any(String[].class), anyString())) + .thenReturn(existingClaims); + + PerformableOperation operation = new PerformableOperation(); + operation.setOp(Operation.REPLACE); + operation.setPath("/user/claims[uri=" + MULTI_CLAIM_URI + "]"); + operation.setValue(Collections.singletonList("0771234567")); + + ActionInvocationSuccessResponse successResponse = new ActionInvocationSuccessResponse.Builder() + .actionStatus(ActionInvocationResponse.Status.SUCCESS) + .operations(Collections.singletonList(operation)) + .responseData(mock(ResponseData.class)) + .build(); + + ActionExecutionStatus resultStatus = preUpdateProfileResponseProcessor.processSuccessResponse( + flowContext, + ActionExecutionResponseContext.create(buildEvent(PreUpdateProfileEvent.FlowInitiatorType.ADMIN), + successResponse)); + + assertTrue(((Map) resultStatus.getResponseContext().get("multiValuedClaimsToBeAdded")).isEmpty()); + assertTrue(((Map) resultStatus.getResponseContext().get("multiValuedClaimsToBeRemoved")).isEmpty()); + } + + @Test + public void testProcessSuccessResponseWhenScimReadOnlyAttributeReplaceOperation() throws Exception { + + FlowContext flowContext = FlowContext.create(); + flowContext.add(UserActionContext.USER_ACTION_CONTEXT_REFERENCE_KEY, mock(UserActionContext.class)); + + when(claimMetadataManagementService.getMappedExternalClaimsForLocalClaim(eq(SINGLE_CLAIM_URI), any())) + .thenReturn(Collections.singletonList(mockScimClaim(true, false, false))); + + PerformableOperation operation = new PerformableOperation(); + operation.setOp(Operation.REPLACE); + operation.setPath("/user/claims[uri=" + SINGLE_CLAIM_URI + "]"); + operation.setValue("Updated"); + + ActionInvocationSuccessResponse successResponse = new ActionInvocationSuccessResponse.Builder() + .actionStatus(ActionInvocationResponse.Status.SUCCESS) + .operations(Collections.singletonList(operation)) + .responseData(mock(ResponseData.class)) + .build(); + + ActionExecutionStatus resultStatus = preUpdateProfileResponseProcessor.processSuccessResponse( + flowContext, ActionExecutionResponseContext.create(buildEvent(), successResponse)); + + assertTrue(((Map) resultStatus.getResponseContext().get("userClaimsToBeModified")).isEmpty()); + } + + @Test + public void testProcessSuccessResponseWhenScimRequiredAttributeRemoveOperation() throws Exception { + + FlowContext flowContext = FlowContext.create(); + flowContext.add(UserActionContext.USER_ACTION_CONTEXT_REFERENCE_KEY, mock(UserActionContext.class)); + + when(claimMetadataManagementService.getMappedExternalClaimsForLocalClaim(eq(SINGLE_CLAIM_URI), any())) + .thenReturn(Collections.singletonList(mockScimClaim(false, true, false))); + + PerformableOperation operation = new PerformableOperation(); + operation.setOp(Operation.REMOVE); + operation.setPath("/user/claims[uri=" + SINGLE_CLAIM_URI + "]"); + + ActionInvocationSuccessResponse successResponse = new ActionInvocationSuccessResponse.Builder() + .actionStatus(ActionInvocationResponse.Status.SUCCESS) + .operations(Collections.singletonList(operation)) + .responseData(mock(ResponseData.class)) + .build(); + + ActionExecutionStatus resultStatus = preUpdateProfileResponseProcessor.processSuccessResponse( + flowContext, ActionExecutionResponseContext.create(buildEvent(), successResponse)); + + assertTrue(((Map) resultStatus.getResponseContext().get("userClaimsToBeRemoved")).isEmpty()); + } + + @Test + public void testProcessSuccessResponseWhenScimRequiredAttributeReplaceWithEmptyValue() throws Exception { + + FlowContext flowContext = FlowContext.create(); + flowContext.add(UserActionContext.USER_ACTION_CONTEXT_REFERENCE_KEY, mock(UserActionContext.class)); + + when(claimMetadataManagementService.getMappedExternalClaimsForLocalClaim(eq(SINGLE_CLAIM_URI), any())) + .thenReturn(Collections.singletonList(mockScimClaim(false, true, false))); + + PerformableOperation operation = new PerformableOperation(); + operation.setOp(Operation.REPLACE); + operation.setPath("/user/claims[uri=" + SINGLE_CLAIM_URI + "]"); + operation.setValue(" "); + + ActionInvocationSuccessResponse successResponse = new ActionInvocationSuccessResponse.Builder() + .actionStatus(ActionInvocationResponse.Status.SUCCESS) + .operations(Collections.singletonList(operation)) + .responseData(mock(ResponseData.class)) + .build(); + + ActionExecutionStatus resultStatus = preUpdateProfileResponseProcessor.processSuccessResponse( + flowContext, ActionExecutionResponseContext.create(buildEvent(), successResponse)); + + assertTrue(((Map) resultStatus.getResponseContext().get("userClaimsToBeModified")).isEmpty()); + } + + @Test + public void testProcessSuccessResponseWhenScimSingleValuedAttributeReplaceWithMultipleValues() throws Exception { + + FlowContext flowContext = FlowContext.create(); + flowContext.add(UserActionContext.USER_ACTION_CONTEXT_REFERENCE_KEY, mock(UserActionContext.class)); + + when(claimMetadataManagementService.getMappedExternalClaimsForLocalClaim(eq(SINGLE_CLAIM_URI), any())) + .thenReturn(Collections.singletonList(mockScimClaim(false, false, false))); + + PerformableOperation operation = new PerformableOperation(); + operation.setOp(Operation.REPLACE); + operation.setPath("/user/claims[uri=" + SINGLE_CLAIM_URI + "]"); + operation.setValue(Arrays.asList("value1", "value2")); + + ActionInvocationSuccessResponse successResponse = new ActionInvocationSuccessResponse.Builder() + .actionStatus(ActionInvocationResponse.Status.SUCCESS) + .operations(Collections.singletonList(operation)) + .responseData(mock(ResponseData.class)) + .build(); + + ActionExecutionStatus resultStatus = preUpdateProfileResponseProcessor.processSuccessResponse( + flowContext, ActionExecutionResponseContext.create(buildEvent(), successResponse)); + + assertTrue(((Map) resultStatus.getResponseContext().get("userClaimsToBeModified")).isEmpty()); + } + + @Test + public void testProcessSuccessResponseWhenSingleValuedReplaceOperationWithValueMapWithoutValue() throws Exception { + + FlowContext flowContext = FlowContext.create(); + flowContext.add(UserActionContext.USER_ACTION_CONTEXT_REFERENCE_KEY, mock(UserActionContext.class)); + + LinkedHashMap valueMap = new LinkedHashMap<>(); + valueMap.put("uri", SINGLE_CLAIM_URI); + + PerformableOperation operation = new PerformableOperation(); + operation.setOp(Operation.REPLACE); + operation.setPath("/user/claims[uri=" + SINGLE_CLAIM_URI + "]"); + operation.setValue(valueMap); + + ActionInvocationSuccessResponse successResponse = new ActionInvocationSuccessResponse.Builder() + .actionStatus(ActionInvocationResponse.Status.SUCCESS) + .operations(Collections.singletonList(operation)) + .responseData(mock(ResponseData.class)) + .build(); + + ActionExecutionStatus resultStatus = preUpdateProfileResponseProcessor.processSuccessResponse( + flowContext, ActionExecutionResponseContext.create(buildEvent(), successResponse)); + + assertTrue(((Map) resultStatus.getResponseContext().get("userClaimsToBeModified")).isEmpty()); + } + + @Test + public void testProcessSuccessResponseWhenMultiValuedReplaceOperationWithInvalidListValueType() throws Exception { + + FlowContext flowContext = FlowContext.create(); + flowContext.add(UserActionContext.USER_ACTION_CONTEXT_REFERENCE_KEY, mock(UserActionContext.class)); + + LinkedHashMap valueMap = new LinkedHashMap<>(); + valueMap.put("value", Arrays.asList("0771234567", 123)); + + PerformableOperation operation = new PerformableOperation(); + operation.setOp(Operation.REPLACE); + operation.setPath("/user/claims[uri=" + MULTI_CLAIM_URI + "]"); + operation.setValue(valueMap); + + ActionInvocationSuccessResponse successResponse = new ActionInvocationSuccessResponse.Builder() + .actionStatus(ActionInvocationResponse.Status.SUCCESS) + .operations(Collections.singletonList(operation)) + .responseData(mock(ResponseData.class)) + .build(); + + ActionExecutionStatus resultStatus = preUpdateProfileResponseProcessor.processSuccessResponse( + flowContext, ActionExecutionResponseContext.create(buildEvent(), successResponse)); + + assertTrue(((Map) resultStatus.getResponseContext().get("multiValuedClaimsToBeAdded")).isEmpty()); + assertTrue(((Map) resultStatus.getResponseContext().get("multiValuedClaimsToBeRemoved")).isEmpty()); + } + + @Test + public void testProcessSuccessResponseWhenMultiValuedAddOperationWithInvalidListValueType() throws Exception { + + FlowContext flowContext = FlowContext.create(); + flowContext.add(UserActionContext.USER_ACTION_CONTEXT_REFERENCE_KEY, mock(UserActionContext.class)); + + LinkedHashMap valueMap = new LinkedHashMap<>(); + valueMap.put("value", Arrays.asList("0771234567", 123)); + + PerformableOperation operation = new PerformableOperation(); + operation.setOp(Operation.ADD); + operation.setPath("/user/claims[uri=" + MULTI_CLAIM_URI + "]"); + operation.setValue(valueMap); + + ActionInvocationSuccessResponse successResponse = new ActionInvocationSuccessResponse.Builder() + .actionStatus(ActionInvocationResponse.Status.SUCCESS) + .operations(Collections.singletonList(operation)) + .responseData(mock(ResponseData.class)) + .build(); + + ActionExecutionStatus resultStatus = preUpdateProfileResponseProcessor.processSuccessResponse( + flowContext, ActionExecutionResponseContext.create(buildEvent(), successResponse)); + + assertTrue(((Map) resultStatus.getResponseContext().get("multiValuedClaimsToBeAdded")).isEmpty()); + } + + @Test + public void testProcessSuccessResponseWhenScimRequiredAttributeAddWithEmptyMapValue() throws Exception { + + FlowContext flowContext = FlowContext.create(); + flowContext.add(UserActionContext.USER_ACTION_CONTEXT_REFERENCE_KEY, mock(UserActionContext.class)); + + when(claimMetadataManagementService.getMappedExternalClaimsForLocalClaim(eq(SINGLE_CLAIM_URI), any())) + .thenReturn(Collections.singletonList(mockScimClaim(false, true, false))); + + LinkedHashMap valueMap = new LinkedHashMap<>(); + valueMap.put("value", " "); + + PerformableOperation operation = new PerformableOperation(); + operation.setOp(Operation.ADD); + operation.setPath("/user/claims[uri=" + SINGLE_CLAIM_URI + "]"); + operation.setValue(valueMap); + + ActionInvocationSuccessResponse successResponse = new ActionInvocationSuccessResponse.Builder() + .actionStatus(ActionInvocationResponse.Status.SUCCESS) + .operations(Collections.singletonList(operation)) + .responseData(mock(ResponseData.class)) + .build(); + + ActionExecutionStatus resultStatus = preUpdateProfileResponseProcessor.processSuccessResponse( + flowContext, ActionExecutionResponseContext.create(buildEvent(), successResponse)); + + assertTrue(((Map) resultStatus.getResponseContext().get("userClaimsToBeAdded")).isEmpty()); } - private String invokeGetClaimUriFromPath(String path) throws Exception { + @Test + public void testProcessSuccessResponseWhenScimSingleValuedAttributeReplaceWithMapListValues() throws Exception { + + FlowContext flowContext = FlowContext.create(); + flowContext.add(UserActionContext.USER_ACTION_CONTEXT_REFERENCE_KEY, mock(UserActionContext.class)); + + when(claimMetadataManagementService.getMappedExternalClaimsForLocalClaim(eq(SINGLE_CLAIM_URI), any())) + .thenReturn(Collections.singletonList(mockScimClaim(false, false, false))); + + LinkedHashMap valueMap = new LinkedHashMap<>(); + valueMap.put("value", Arrays.asList("value1", "value2")); + + PerformableOperation operation = new PerformableOperation(); + operation.setOp(Operation.REPLACE); + operation.setPath("/user/claims[uri=" + SINGLE_CLAIM_URI + "]"); + operation.setValue(valueMap); + + ActionInvocationSuccessResponse successResponse = new ActionInvocationSuccessResponse.Builder() + .actionStatus(ActionInvocationResponse.Status.SUCCESS) + .operations(Collections.singletonList(operation)) + .responseData(mock(ResponseData.class)) + .build(); + + ActionExecutionStatus resultStatus = preUpdateProfileResponseProcessor.processSuccessResponse( + flowContext, ActionExecutionResponseContext.create(buildEvent(), successResponse)); + + assertTrue(((Map) resultStatus.getResponseContext().get("userClaimsToBeModified")).isEmpty()); + } + + @Test + public void testProcessSuccessResponseWhenScimMappingsAreNotScimDialect() throws Exception { + + FlowContext flowContext = FlowContext.create(); + flowContext.add(UserActionContext.USER_ACTION_CONTEXT_REFERENCE_KEY, mock(UserActionContext.class)); + + Claim nonScimClaim = new Claim("custom:dialect", SINGLE_CLAIM_URI); + when(claimMetadataManagementService.getMappedExternalClaimsForLocalClaim(eq(SINGLE_CLAIM_URI), any())) + .thenReturn(Collections.singletonList(nonScimClaim)); + when(userStoreManager.getUserClaimValuesWithID(anyString(), any(String[].class), anyString())) + .thenReturn(new HashMap<>()); + + PerformableOperation operation = new PerformableOperation(); + operation.setOp(Operation.ADD); + operation.setPath("/user/claims[uri=" + SINGLE_CLAIM_URI + "]"); + operation.setValue("NoScimValidation"); + + ActionInvocationSuccessResponse successResponse = new ActionInvocationSuccessResponse.Builder() + .actionStatus(ActionInvocationResponse.Status.SUCCESS) + .operations(Collections.singletonList(operation)) + .responseData(mock(ResponseData.class)) + .build(); + + ActionExecutionStatus resultStatus = preUpdateProfileResponseProcessor.processSuccessResponse( + flowContext, ActionExecutionResponseContext.create(buildEvent(), successResponse)); + + Map addedClaims = (Map) resultStatus.getResponseContext() + .get("userClaimsToBeAdded"); + assertEquals(addedClaims.get(SINGLE_CLAIM_URI), "NoScimValidation"); + } + + @Test + public void testProcessSuccessResponseFailureWhenUserStoreManagerLoadingFails() { + + FlowContext flowContext = FlowContext.create(); + flowContext.add(UserActionContext.USER_ACTION_CONTEXT_REFERENCE_KEY, mock(UserActionContext.class)); + + requestBuilderUtil.when(() -> RequestBuilderUtil.getUserStoreManager(any())) + .thenThrow(new org.wso2.carbon.identity.action.execution.api.exception + .ActionExecutionRequestBuilderException("User store manager error")); + + ActionInvocationSuccessResponse successResponse = new ActionInvocationSuccessResponse.Builder() + .actionStatus(ActionInvocationResponse.Status.SUCCESS) + .operations(Collections.singletonList(new PerformableOperation())) + .responseData(mock(ResponseData.class)) + .build(); + + expectThrows(ActionExecutionResponseProcessorException.class, + () -> preUpdateProfileResponseProcessor.processSuccessResponse( + flowContext, ActionExecutionResponseContext.create(buildEvent(), successResponse))); + } + + @Test + public void testProcessSuccessResponseWhenPathMissingClosingBracket() throws Exception { + + FlowContext flowContext = FlowContext.create(); + flowContext.add(UserActionContext.USER_ACTION_CONTEXT_REFERENCE_KEY, mock(UserActionContext.class)); + + PerformableOperation operation = new PerformableOperation(); + operation.setOp(Operation.REPLACE); + operation.setPath("/user/claims[uri=" + SINGLE_CLAIM_URI); + operation.setValue("Alex"); + + ActionInvocationSuccessResponse successResponse = new ActionInvocationSuccessResponse.Builder() + .actionStatus(ActionInvocationResponse.Status.SUCCESS) + .operations(Collections.singletonList(operation)) + .responseData(mock(ResponseData.class)) + .build(); + + ActionExecutionStatus resultStatus = preUpdateProfileResponseProcessor.processSuccessResponse( + flowContext, ActionExecutionResponseContext.create(buildEvent(), successResponse)); + + assertTrue(((Map) resultStatus.getResponseContext().get("userClaimsToBeModified")).isEmpty()); + } + + @Test + public void testProcessSuccessResponseWhenLocalClaimLookupThrowsException() throws Exception { + + FlowContext flowContext = FlowContext.create(); + flowContext.add(UserActionContext.USER_ACTION_CONTEXT_REFERENCE_KEY, mock(UserActionContext.class)); + + when(claimMetadataManagementService.getLocalClaim(eq(SINGLE_CLAIM_URI), any())) + .thenThrow(new ClaimMetadataException("Local claim lookup error")); + + PerformableOperation operation = new PerformableOperation(); + operation.setOp(Operation.ADD); + operation.setPath("/user/claims[uri=" + SINGLE_CLAIM_URI + "]"); + operation.setValue("Alex"); + + ActionInvocationSuccessResponse successResponse = new ActionInvocationSuccessResponse.Builder() + .actionStatus(ActionInvocationResponse.Status.SUCCESS) + .operations(Collections.singletonList(operation)) + .responseData(mock(ResponseData.class)) + .build(); + + ActionExecutionStatus resultStatus = preUpdateProfileResponseProcessor.processSuccessResponse( + flowContext, ActionExecutionResponseContext.create(buildEvent(), successResponse)); + + assertTrue(((Map) resultStatus.getResponseContext().get("userClaimsToBeAdded")).isEmpty()); + } + + @Test + public void testProcessSuccessResponseWhenScimClaimMappingLookupThrowsException() throws Exception { + + FlowContext flowContext = FlowContext.create(); + flowContext.add(UserActionContext.USER_ACTION_CONTEXT_REFERENCE_KEY, mock(UserActionContext.class)); + + when(claimMetadataManagementService.getMappedExternalClaimsForLocalClaim(eq(SINGLE_CLAIM_URI), any())) + .thenThrow(new ClaimMetadataException("SCIM mapping lookup error")); + + PerformableOperation operation = new PerformableOperation(); + operation.setOp(Operation.REPLACE); + operation.setPath("/user/claims[uri=" + SINGLE_CLAIM_URI + "]"); + operation.setValue("Alex"); + + ActionInvocationSuccessResponse successResponse = new ActionInvocationSuccessResponse.Builder() + .actionStatus(ActionInvocationResponse.Status.SUCCESS) + .operations(Collections.singletonList(operation)) + .responseData(mock(ResponseData.class)) + .build(); + + ActionExecutionStatus resultStatus = preUpdateProfileResponseProcessor.processSuccessResponse( + flowContext, ActionExecutionResponseContext.create(buildEvent(), successResponse)); + + assertTrue(((Map) resultStatus.getResponseContext().get("userClaimsToBeModified")).isEmpty()); + } + + @Test + public void testProcessSuccessResponseWhenGetUserClaimValuesThrowsException() throws Exception { + + FlowContext flowContext = FlowContext.create(); + flowContext.add(UserActionContext.USER_ACTION_CONTEXT_REFERENCE_KEY, mock(UserActionContext.class)); + + when(userStoreManager.getUserClaimValuesWithID(anyString(), any(String[].class), anyString())) + .thenThrow(new org.wso2.carbon.user.core.UserStoreException("user store read error")); + + PerformableOperation operation = new PerformableOperation(); + operation.setOp(Operation.ADD); + operation.setPath("/user/claims[uri=" + SINGLE_CLAIM_URI + "]"); + operation.setValue("Alex"); + + ActionInvocationSuccessResponse successResponse = new ActionInvocationSuccessResponse.Builder() + .actionStatus(ActionInvocationResponse.Status.SUCCESS) + .operations(Collections.singletonList(operation)) + .responseData(mock(ResponseData.class)) + .build(); + + ActionExecutionStatus resultStatus = preUpdateProfileResponseProcessor.processSuccessResponse( + flowContext, ActionExecutionResponseContext.create(buildEvent(), successResponse)); + + assertTrue(((Map) resultStatus.getResponseContext().get("userClaimsToBeAdded")).isEmpty()); + } + + private static PreUpdateProfileEvent buildEvent() { + + return buildEvent(PreUpdateProfileEvent.FlowInitiatorType.ADMIN); + } + + private static PreUpdateProfileEvent buildEvent(PreUpdateProfileEvent.FlowInitiatorType initiatorType) { + + return new PreUpdateProfileEvent.Builder() + .initiatorType(initiatorType) + .action(PreUpdateProfileEvent.Action.UPDATE) + .user(new User.Builder(USER_ID).build()) + .build(); + } + + private static LocalClaim mockLocalClaim(String claimUri, boolean isMultiValued, boolean flowInitiatorClaim) { + + LocalClaim localClaim = mock(LocalClaim.class); + when(localClaim.getClaimURI()).thenReturn(claimUri); + when(localClaim.getFlowInitiator()).thenReturn(flowInitiatorClaim); + when(localClaim.getClaimProperty(ClaimConstants.MULTI_VALUED_PROPERTY)) + .thenReturn(String.valueOf(isMultiValued)); + return localClaim; + } + + private static Claim mockScimClaim(boolean readOnly, boolean required, boolean multiValued) { + + Map claimProperties = new HashMap<>(); + claimProperties.put(ClaimConstants.READ_ONLY_PROPERTY, String.valueOf(readOnly)); + claimProperties.put(ClaimConstants.REQUIRED_PROPERTY, String.valueOf(required)); + claimProperties.put(ClaimConstants.MULTI_VALUED_PROPERTY, String.valueOf(multiValued)); - Method method = PreUpdateProfileResponseProcessor.class.getDeclaredMethod("getClaimUriFromPath", - String.class); - method.setAccessible(true); - return (String) method.invoke(preUpdateProfileResponseProcessor, path); + Claim scimClaim = new Claim(SCIM_SCHEMA_URI_PREFIX, SINGLE_CLAIM_URI, claimProperties); + return scimClaim; } }