Skip to content

Enable pre profile update action to allow modifying user claims at profile update#8115

Closed
Lashen1227 wants to merge 14 commits into
wso2:masterfrom
Lashen1227:feat/pre-profile-update-claims
Closed

Enable pre profile update action to allow modifying user claims at profile update#8115
Lashen1227 wants to merge 14 commits into
wso2:masterfrom
Lashen1227:feat/pre-profile-update-claims

Conversation

@Lashen1227

@Lashen1227 Lashen1227 commented May 26, 2026

Copy link
Copy Markdown
Contributor

This pull request enhances support for modifying user claims through user profile update actions and adds comprehensive test coverage for the new functionality.

Supported claim modification operations:

  • ADD
  • REPLACE
  • REMOVE

Sample Response Payload

{
  "actionStatus": "SUCCESS",
  "operations": [
    {
      "op": "add",
      "path": "/user/claims[uri=http://wso2.org/claims/country]",
      "value": "Sri Lanka"
    },
    {
      "op": "replace",
      "path": "/user/claims[uri=http://wso2.org/claims/mobileNumbers]",
      "value": ["0771234567", "0717654321"]
    },
    {
      "op": "remove",
      "path": "/user/claims[uri=http://wso2.org/claims/country]"
    }
  ]
}

Claim Extraction and Validation Improvements

  • Refactored claim extraction logic to directly use the user store manager.
  • Ensured that only requested claims are fetched and included in the event payload.
  • Improved handling of both single-valued and multi-valued claims.
  • Enhanced validation flow for claim modification operations.

Dependency Updates:

  • Updated pom.xml with the required package imports for central log management utilities.

Related PR

Related Issue

@coderabbitai

coderabbitai Bot commented May 26, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: da8bd873-25d3-4f96-8be7-a4cdc94a93e0

📥 Commits

Reviewing files that changed from the base of the PR and between 8cb1541 and 1a17801.

📒 Files selected for processing (2)
  • 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
  • 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

📝 Walkthrough

Walkthrough

This PR implements template-based path matching for action operation permissions and a complete bidirectional claim update flow for pre-update profile actions: the request builder declares allowed operations with filtered-claims paths, and the response processor validates returned operations against claim metadata and SCIM mappings before applying changes.

Changes

Template-Based Operation Matching and Claim Update Flow

Layer / File(s) Summary
Template-path comparison and test coverage
components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/internal/util/OperationComparator.java, src/test/java/org/wso2/carbon/identity/action/execution/util/OperationComparatorTest.java
OperationComparator.compare extends path matching to evaluate placeholder-template expressions (e.g., claims[uri={claim_uri}]) by extracting prefix/suffix and validating performable paths start, end, and exceed those boundaries; new test verifies this behavior.
Request builder imports, constant, and model
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, src/main/java/.../internal/model/ProfileOperationExecutionResult.java
Request builder imports action execution model types (AllowedOperation, Operation, Event, FlowContext) and stream utilities; defines USER_CLAIMS_FILTERED_PATH_TEMPLATE constant; introduces ProfileOperationExecutionResult model encapsulating operation status (SUCCESS/FAILURE) and message.
Building allowed operations and threading user store manager
PreUpdateProfileRequestBuilder.java
buildActionExecutionRequest resolves tenant-scoped UniqueIDUserStoreManager, threads it into user building, and includes ADD/REMOVE/REPLACE allowed operations with the filtered-claims template before returning the request.
User store resolution and claim-value retrieval
PreUpdateProfileRequestBuilder.java
getUser(...) now accepts resolved UniqueIDUserStoreManager parameter; getClaimValues(...) fetches requested claims from user store, filters to only requested keys, and wraps UserStoreException as ActionExecutionRequestBuilderException; getUserStoreManager() resolves tenant-scoped manager from IdentityContext.
Claim construction and updating claim validation
PreUpdateProfileRequestBuilder.java
setClaimsInUserBuilder refactored to build multi-/single-valued UpdatingUserClaim entries consistently; validates multi-valued updates from updatingUserClaimsInRequest are String[] format and throws ActionExecutionRequestBuilderException for unsupported types.
Request builder test coverage for allowed operations and failure paths
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
Test imports expanded for allowed operations and utility types; test verifies built ActionExecutionRequest includes exactly three allowed operations; multiple failure-path tests cover unknown flow, missing root organization, unsupported claim types, invalid multi-valued formats, and missing metadata; helper methods for constructing UserActionContext and asserting allowed operations.
Response processor initialization and main flow
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
Imports and constants added for action/claim types and group/role/SCIM URIs; processSuccessResponse replaces no-op implementation by iterating ADD/REPLACE/REMOVE operations, aggregating claim-change intentions into FlowContext, and returning SuccessStatus.
Operation wrappers and per-operation execution results
PreUpdateProfileResponseProcessor.java
Wrapper handlers for ADD/REPLACE/REMOVE catch ActionExecutionResponseProcessorException and return ProfileOperationExecutionResult with SUCCESS/FAILURE status and message for diagnostics.
ADD / REPLACE / REMOVE operation handlers
PreUpdateProfileResponseProcessor.java
populateAddOperationResult extracts claim URI from path filter or value-map uri field; populateModifyOperationResult handles REPLACE; populateRemoveOperationResult handles removal. Each resolves claim metadata, validates group/role/flow-initiator restrictions, enforces SCIM constraints, and populates single/multi-valued change maps based on claim type and initiator role.
Multi-valued claim add / replace / remove computations
PreUpdateProfileResponseProcessor.java
populateMultivaluedClaimsForAddOperation filters/deduplicates/trims candidates and excludes existing values; populateMultiValuedClaimsForReplaceOperation parses list or map forms, computes deltas for admin/application, rejects undersized arrays, and USER-specific concatenation; populateMultiValuedClaimsForRemoveOperation records whole-claim removal only.
Claim metadata resolution and user-claim lookup utilities
PreUpdateProfileResponseProcessor.java
Tenant-aware local-claim resolution via claim metadata service; multi-valued detection from local claim properties; strict /user/claims[uri=...] path parsing via getClaimUriFromPath and isClaimPathFormat; tenant-scoped user-store retrieval; user claim fetching and filtering utilities (trim/deduplicate/exclude empties).
Flow-initiator and SCIM-mapped attribute validation
PreUpdateProfileResponseProcessor.java
Validation helpers reject modifications to flow-initiator-enabled claims and group/role updates; validateSCIMLevelAttributes converts local claims to SCIM dialect, computes read-only/required/single-valued properties, and enforces operation-specific rules; convertLocalToSCIMDialect filters mapped external claims to SCIM-schema URIs.
Diagnostic and debug logging for operation outcomes
PreUpdateProfileResponseProcessor.java
Adds diagnostic logging for aggregated per-operation SUCCESS/FAILURE outcomes with guarded runtime availability check; retains optional debug logging.
Response processor test setup and comprehensive test cases
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
Test imports expanded for Mockito MockedStatic and TestNG lifecycle; setup enhanced with mock-backed instance fields, enhanced lifecycle hooks, and ClaimMetadataManagementService injection. 28+ test cases cover single/multi-valued ADD/REPLACE/REMOVE, valid/malformed URI path formats, value-type validation, SCIM constraints, and error scenarios; utility helpers for building test events and mocking local/SCIM claims.

Possibly related PRs

  • wso2/carbon-identity-framework#8022: Both PRs touch the pre-update profile flow—especially PreUpdateProfileRequestBuilder claim/value retrieval and tenant-scoped user-store manager handling—so the changes likely overlap at the same methods and logic there.

Suggested reviewers

  • ashanthamara
  • RushanNanayakkara
  • piraveena
🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The PR description provides implementation details, a sample payload, and related references, but omits several required template sections (Purpose, Goals, Approach, Release note, Documentation, etc.). Complete the PR description by adding missing sections from the template: Purpose/Goals/Approach, Release note, Documentation status, Training/Certification/Marketing impact, and test environment details.
Docstring Coverage ⚠️ Warning Docstring coverage is 2.04% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main enhancement: enabling user claim modifications through pre-profile-update actions.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@Lashen1227 Lashen1227 changed the title Feat/pre profile update claims Enable pre profile update action to allow modifying user claims at profile update May 26, 2026

@wso2-engineering wso2-engineering Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AI Agent Log Improvement Checklist

⚠️ Warning: AI-Generated Review Comments

  • The log-related comments and suggestions in this review were generated by an AI tool to assist with identifying potential improvements. Purpose of reviewing the code for log improvements is to improve the troubleshooting capabilities of our products.
  • Please make sure to manually review and validate all suggestions before applying any changes. Not every code suggestion would make sense or add value to our purpose. Therefore, you have the freedom to decide which of the suggestions are helpful.

✅ Before merging this pull request:

  • Review all AI-generated comments for accuracy and relevance.
  • Complete and verify the table below. We need your feedback to measure the accuracy of these suggestions and the value they add. If you are rejecting a certain code suggestion, please mention the reason briefly in the suggestion for us to capture it.
Comment Accepted (Y/N) Reason
#### Log Improvement Suggestion No: 1
#### Log Improvement Suggestion No: 2
#### Log Improvement Suggestion No: 4
#### Log Improvement Suggestion No: 5
#### Log Improvement Suggestion No: 6

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
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 (2)

152-159: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Guard the nullable request-claims map before constructing UpdatingUserClaims.

getPreUpdateProfileRequest() explicitly allows userActionRequestDTO.getClaims() to be null/empty, but this path still passes that map into helpers that call containsKey() unguarded. With configured action attributes and no incoming claims, this blows up with an NPE instead of building the event.

Suggested fix
 private UpdatingUserClaim constructMultiValuedClaim(Map<String, Object> updatingUserClaimsInRequest,
                                                     String claimKey, String claimValue,
                                                     String multiAttributeSeparator)
         throws ActionExecutionRequestBuilderException {
 
-    if (updatingUserClaimsInRequest.containsKey(claimKey)) {
+    if (updatingUserClaimsInRequest != null && updatingUserClaimsInRequest.containsKey(claimKey)) {
         Object updatingClaimValue = updatingUserClaimsInRequest.get(claimKey);
         if (!(updatingClaimValue instanceof String[])) {
             throw new ActionExecutionRequestBuilderException(
                     "Invalid claim value format for multi-valued claim: " + claimKey +
                             " Only String[] types are expected.");
@@
 private UpdatingUserClaim constructSingleValuedClaim(Map<String, Object> updatingUserClaimsInRequest,
                                                      String claimKey, String claimValue) {
 
-    if (updatingUserClaimsInRequest.containsKey(claimKey)) {
+    if (updatingUserClaimsInRequest != null && updatingUserClaimsInRequest.containsKey(claimKey)) {
         Object updatingClaimValue = updatingUserClaimsInRequest.get(claimKey);
         return new UpdatingUserClaim(claimKey, claimValue, String.valueOf(updatingClaimValue));
     }

Also applies to: 224-228, 264-270

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@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`
around lines 152 - 159, Null-check the claims map returned by
UserActionRequestDTO.getClaims() in getPreUpdateProfileRequest() and any helper
methods that construct UpdatingUserClaim so you never call containsKey() on a
null map: either return early when claims are null/empty (as already done) or
replace a null with Collections.emptyMap() before passing it to methods that
inspect it; update all places where UpdatingUserClaim is built (the blocks
around the helper calls referenced in the comment) to use the guarded map or
empty map to avoid NPEs.

215-225: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Keep user.claims limited to preUpdateProfileAction.getAttributes().

claimValues is already restricted to the action’s requested attributes, but this fallback loop re-adds every incoming request claim that is missing from that map. That lets unconfigured claim URIs leak into the outbound event.

Suggested fix
-        setClaimsInUserBuilder(userBuilder, claimValues, userActionRequestDTO.getClaims(), multiAttributeSeparator);
+        setClaimsInUserBuilder(userBuilder, claimValues, userActionRequestDTO.getClaims(),
+                userClaimsToSetInEvent, multiAttributeSeparator);
@@
     private void setClaimsInUserBuilder(User.Builder userBuilder, Map<String, String> claimValues,
                                         Map<String, Object> updatingUserClaimsInRequest,
+                                        List<String> requestedClaims,
                                         String multiAttributeSeparator) throws ActionExecutionRequestBuilderException {
@@
         if (updatingUserClaimsInRequest != null) {
             for (Map.Entry<String, Object> entry : updatingUserClaimsInRequest.entrySet()) {
                 String claimKey = entry.getKey();
-                if (isRoleOrGroupClaim(claimKey) || claimValues.containsKey(claimKey) &&
-                        StringUtils.isNotBlank(claimValues.get(claimKey))) {
+                if (!requestedClaims.contains(claimKey) ||
+                        isRoleOrGroupClaim(claimKey) ||
+                        (claimValues.containsKey(claimKey) &&
+                                StringUtils.isNotBlank(claimValues.get(claimKey)))) {
                     continue;
                 }

Also applies to: 274-295

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@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`
around lines 215 - 225, The code currently builds claimValues from
getClaimValues(...) but a later fallback loop re-inserts all incoming request
claims into the event, allowing unconfigured claim URIs to leak; update the
fallback logic that merges request claims into user claims (the loop referenced
near lines 274-295) to only consider keys present in
preUpdateProfileAction.getAttributes() (userClaimsToSetInEvent) — i.e., when
iterating userActionRequestDTO.getClaims() only add a claim to claimValues (and
to the User.Builder via userBuilder) if its claim URI is contained in
userClaimsToSetInEvent, leaving other incoming claims out. Ensure this same
restriction is applied wherever the fallback merge occurs so user.claims is
limited to getAttributes().
🧹 Nitpick comments (2)
components/action-mgt/org.wso2.carbon.identity.action.execution/src/test/java/org/wso2/carbon/identity/action/execution/util/OperationComparatorTest.java (1)

106-119: 💤 Low value

Consider adding a negative test case for template non-match.

The happy path test is good. Adding a test where the performable path doesn't match the template pattern (e.g., different prefix like /admin/claims[uri=...] against /user/claims[uri={claim_uri}]) would strengthen coverage.

Example negative test
`@Test`
public void testCompareNonMatchingPathForTemplateBasedClaimFilterPath() {

    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("/admin/claims[uri=http://wso2.org/claims/country]");
    performableOp.setValue("Sri Lanka");

    assertFalse(OperationComparator.compare(allowedOp, performableOp));
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@components/action-mgt/org.wso2.carbon.identity.action.execution/src/test/java/org/wso2/carbon/identity/action/execution/util/OperationComparatorTest.java`
around lines 106 - 119, Add a negative unit test that mirrors
testCompareMatchingPathForTemplateBasedClaimFilterPath but uses a non-matching
performable path (e.g., prefix "/admin/claims[uri=...]" vs allowed
"/user/claims[uri={claim_uri}]") to verify OperationComparator.compare returns
false; create a method like
testCompareNonMatchingPathForTemplateBasedClaimFilterPath that constructs
AllowedOperation (op=ADD, paths=["/user/claims[uri={claim_uri}]"]) and
PerformableOperation (op=ADD,
path="/admin/claims[uri=http://wso2.org/claims/country]", value="Sri Lanka") and
use assertFalse(OperationComparator.compare(allowedOp, performableOp)).
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 (1)

465-467: ⚡ Quick win

Log these handled backend lookup failures before wrapping them.

These catch blocks translate claim/user-store lookup errors into ActionExecutionResponseProcessorException without any log at the handling boundary, which makes production triage harder if the wrapped exception is normalized further up the stack. As per coding guidelines, "Suggest log statements at error handling boundaries (catch blocks, error returns)."

Also applies to: 519-521, 532-534, 694-696

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@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`
around lines 465 - 467, The catch blocks in PreUpdateProfileResponseProcessor
that catch ClaimMetadataException (and similar catch blocks at the other
locations) currently wrap and rethrow as
ActionExecutionResponseProcessorException without logging; update each catch to
log the error before throwing (e.g., use the class logger instance like
log.error or logger.error with a clear message including claimUri or related
identifier and include the caught exception), then rethrow the
ActionExecutionResponseProcessorException with the original exception as the
cause so the failure is both logged and propagated.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@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`:
- Around line 416-422: getFilteredModifyingClaimValues can return an empty
filteredClaims when the replacement value already exists, causing the current
branch to skip both maps and leave the old value unremoved; change
PreUpdateProfileResponseProcessor so that
simpleMultiValuedClaimsToBeRemoved.put(claimUri, Arrays.asList(oldValueName)) is
performed unconditionally (move it out of the if block) and only add
trimmedFilteredClaims into simpleMultiValuedClaimsToBeAdded when filteredClaims
is non-empty; reference the variables simpleMultiValuedClaimsToBeRemoved,
simpleMultiValuedClaimsToBeAdded and the method getFilteredModifyingClaimValues
in your change.
- Around line 540-541: The code in PreUpdateProfileResponseProcessor that builds
userValues from userClaimValues.get(claimUri) uses value.split(separator), which
treats the configured separator as a regex and can mis-split characters like '|'
or '.'. Replace that split call to use a literal separator by quoting it (e.g.,
value.split(Pattern.quote(separator))) or otherwise escaping the separator
before splitting so multivalued claim parsing, duplicate filtering and delta
computation behave consistently with the other branches.
- Around line 284-287: The response update flow currently calls
validateFlowInitiatorClaims for ADD and REPLACE but not for REMOVE, allowing
deletions of flow-initiated-blocked claims; modify the pre-update sequence in
PreUpdateProfileResponseProcessor (around where isLocalClaim,
validateImmutableClaims, validateGroupAndRoleClaims, validateSCIMLevelAttributes
are invoked) to also invoke validateFlowInitiatorClaims for REMOVE operations
(i.e., call validateFlowInitiatorClaims(claimUri, operation.getOp(),
operation.getValue()) for op == REMOVE or include REMOVE with the existing
ADD/REPLACE checks) so flow-initiator validation runs for deletions as well.
- Around line 315-321: In PreUpdateProfileResponseProcessor inside the USER
branch (check on initiatorType), skip mutating userClaimsToBeModified when
filteredValues is empty to avoid adding a trailing separator/empty value; before
computing addingClaimValue for claimUri, add a guard that if
filteredValues.isEmpty() then do not put anything into userClaimsToBeModified
(i.e., return/continue or skip this claim), otherwise build addingClaimValue
using userClaimValues.get(claimUri), separator and String.join as currently
implemented.

---

Outside diff comments:
In
`@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`:
- Around line 152-159: Null-check the claims map returned by
UserActionRequestDTO.getClaims() in getPreUpdateProfileRequest() and any helper
methods that construct UpdatingUserClaim so you never call containsKey() on a
null map: either return early when claims are null/empty (as already done) or
replace a null with Collections.emptyMap() before passing it to methods that
inspect it; update all places where UpdatingUserClaim is built (the blocks
around the helper calls referenced in the comment) to use the guarded map or
empty map to avoid NPEs.
- Around line 215-225: The code currently builds claimValues from
getClaimValues(...) but a later fallback loop re-inserts all incoming request
claims into the event, allowing unconfigured claim URIs to leak; update the
fallback logic that merges request claims into user claims (the loop referenced
near lines 274-295) to only consider keys present in
preUpdateProfileAction.getAttributes() (userClaimsToSetInEvent) — i.e., when
iterating userActionRequestDTO.getClaims() only add a claim to claimValues (and
to the User.Builder via userBuilder) if its claim URI is contained in
userClaimsToSetInEvent, leaving other incoming claims out. Ensure this same
restriction is applied wherever the fallback merge occurs so user.claims is
limited to getAttributes().

---

Nitpick comments:
In
`@components/action-mgt/org.wso2.carbon.identity.action.execution/src/test/java/org/wso2/carbon/identity/action/execution/util/OperationComparatorTest.java`:
- Around line 106-119: Add a negative unit test that mirrors
testCompareMatchingPathForTemplateBasedClaimFilterPath but uses a non-matching
performable path (e.g., prefix "/admin/claims[uri=...]" vs allowed
"/user/claims[uri={claim_uri}]") to verify OperationComparator.compare returns
false; create a method like
testCompareNonMatchingPathForTemplateBasedClaimFilterPath that constructs
AllowedOperation (op=ADD, paths=["/user/claims[uri={claim_uri}]"]) and
PerformableOperation (op=ADD,
path="/admin/claims[uri=http://wso2.org/claims/country]", value="Sri Lanka") and
use assertFalse(OperationComparator.compare(allowedOp, performableOp)).

In
`@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`:
- Around line 465-467: The catch blocks in PreUpdateProfileResponseProcessor
that catch ClaimMetadataException (and similar catch blocks at the other
locations) currently wrap and rethrow as
ActionExecutionResponseProcessorException without logging; update each catch to
log the error before throwing (e.g., use the class logger instance like
log.error or logger.error with a clear message including claimUri or related
identifier and include the caught exception), then rethrow the
ActionExecutionResponseProcessorException with the original exception as the
cause so the failure is both logged and propagated.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: 273809a0-d088-4b80-a0be-29443e769124

📥 Commits

Reviewing files that changed from the base of the PR and between efaa6ec and 04d3fd3.

📒 Files selected for processing (6)
  • components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/internal/util/OperationComparator.java
  • components/action-mgt/org.wso2.carbon.identity.action.execution/src/test/java/org/wso2/carbon/identity/action/execution/util/OperationComparatorTest.java
  • components/user-mgt/org.wso2.carbon.identity.user.pre.update.profile.action/pom.xml
  • 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
  • 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
  • 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

Comment on lines +315 to +321
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);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Skip the user update when no new multivalued entries survive filtering.

If every submitted value is blank or already present, filteredValues is empty and this branch still writes existing + separator, producing a trailing separator / empty mutation for a no-op add.

Suggested fix
         if (initiatorType == PreUpdateProfileEvent.FlowInitiatorType.ADMIN ||
                 initiatorType == PreUpdateProfileEvent.FlowInitiatorType.APPLICATION) {
             simpleMultiValuedClaimsToBeAdded.put(claimUri, filteredValues);
         } else if (initiatorType == PreUpdateProfileEvent.FlowInitiatorType.USER) {
+            if (filteredValues.isEmpty()) {
+                return;
+            }
             String addingClaimValue = (userClaimValues.get(claimUri) == null) ? String.join(separator, filteredValues)
                     : userClaimValues.get(claimUri) + separator + String.join(separator, filteredValues);
             userClaimsToBeModified.put(claimUri, addingClaimValue);
         }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@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`
around lines 315 - 321, In PreUpdateProfileResponseProcessor inside the USER
branch (check on initiatorType), skip mutating userClaimsToBeModified when
filteredValues is empty to avoid adding a trailing separator/empty value; before
computing addingClaimValue for claimUri, add a guard that if
filteredValues.isEmpty() then do not put anything into userClaimsToBeModified
(i.e., return/continue or skip this claim), otherwise build addingClaimValue
using userClaimValues.get(claimUri), separator and String.join as currently
implemented.

Comment on lines +416 to +422
if (!filteredClaims.isEmpty()) {
simpleMultiValuedClaimsToBeRemoved.put(claimUri, Arrays.asList(oldValueName));
List<String> trimmedFilteredClaims = filteredClaims.stream()
.map(String::trim)
.collect(Collectors.toList());
simpleMultiValuedClaimsToBeAdded.put(claimUri, trimmedFilteredClaims);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Specific-value replace becomes a no-op when the replacement value already exists.

getFilteredModifyingClaimValues removes duplicate target values. When oldValue is replaced with a value the user already has, filteredClaims is empty and this branch skips both maps, so the old value is left behind instead of being replaced.

🧰 Tools
🪛 GitHub Check: SonarCloud Code Analysis

[warning] 420-420: Replace this usage of 'Stream.collect(Collectors.toList())' with 'Stream.toList()' and ensure that the list is unmodified.

See more on https://sonarcloud.io/project/issues?id=wso2_carbon-identity-framework&issues=AZ5ioOs3xRHZv9iNmJqx&open=AZ5ioOs3xRHZv9iNmJqx&pullRequest=8115

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@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`
around lines 416 - 422, getFilteredModifyingClaimValues can return an empty
filteredClaims when the replacement value already exists, causing the current
branch to skip both maps and leave the old value unremoved; change
PreUpdateProfileResponseProcessor so that
simpleMultiValuedClaimsToBeRemoved.put(claimUri, Arrays.asList(oldValueName)) is
performed unconditionally (move it out of the if block) and only add
trimmedFilteredClaims into simpleMultiValuedClaimsToBeAdded when filteredClaims
is non-empty; reference the variables simpleMultiValuedClaimsToBeRemoved,
simpleMultiValuedClaimsToBeAdded and the method getFilteredModifyingClaimValues
in your change.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@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`:
- Around line 149-150: The code returns ProfileOperationExecutionResult using
e.getMessage() directly, which can be null and later causes Map.of("message",
...) to throw; in PreUpdateProfileResponseProcessor replace direct
e.getMessage() usage with a normalized non-null string (e.g., String msg =
e.getMessage() != null ? e.getMessage() : ""; or use
Optional.ofNullable(e.getMessage()).orElse("")); then pass msg into new
ProfileOperationExecutionResult(...). Apply the same normalization pattern where
messages are passed into Map.of or ProfileOperationExecutionResult (the other
occurrences around the blocks that create ProfileOperationExecutionResult and
Map.of at the locations you flagged).
- Around line 103-124: The loop currently ignores unknown ops and always lets
processSuccessResponse run even if some handle*Operation produced a rejected
ProfileOperationExecutionResult; update the switch default to create and add a
rejected ProfileOperationExecutionResult for unknown operation types (refer to
operation.getOp() and ProfileOperationExecutionResult), then after the loop
examine operationExecutionResultList for any rejected entries and if any exist
prevent the success path (do not call processSuccessResponse/return
SuccessStatus), instead invoke the failure path (e.g., processFailureResponse or
set FlowContext to a failed status) and ensure
logOperationExecutionResults(getSupportedActionType(),
operationExecutionResultList) still logs before returning; apply the same change
to the corresponding block at lines 135-189 where operations are processed.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: c49bb7a9-07c1-49b3-938a-96fedb4fd793

📥 Commits

Reviewing files that changed from the base of the PR and between 04d3fd3 and 1a28ea7.

📒 Files selected for processing (2)
  • 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
  • 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

Comment on lines +103 to +124
switch (operation.getOp()) {
case ADD:
operationExecutionResultList.add(handleAddOperation(operation, responseContext,
userClaimsToBeAdded, userClaimsToBeModified, simpleMultiValuedClaimsToBeAdded,
userStoreManager));
break;
case REPLACE:
operationExecutionResultList.add(handleReplaceOperation(operation, responseContext,
userClaimsToBeModified, simpleMultiValuedClaimsToBeRemoved,
simpleMultiValuedClaimsToBeAdded, userStoreManager));
break;
case REMOVE:
operationExecutionResultList.add(handleRemoveOperation(operation, responseContext,
userClaimsToBeModified, userClaimsToBeRemoved, simpleMultiValuedClaimsToBeRemoved));
break;
default:
break;
}
}
}

logOperationExecutionResults(getSupportedActionType(), operationExecutionResultList);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Fail the response when any operation is rejected.

Unknown ops are silently ignored in the default branch, and the handle*Operation wrappers convert validation failures into ProfileOperationExecutionResult entries but still let processSuccessResponse populate FlowContext and return SuccessStatus. That turns malformed/forbidden action responses into partial claim updates instead of rejecting the response.

💡 Suggested fix
             for (PerformableOperation operation : operationsToPerform) {
                 switch (operation.getOp()) {
                     case ADD:
                         operationExecutionResultList.add(handleAddOperation(operation, responseContext,
                                 userClaimsToBeAdded, userClaimsToBeModified, simpleMultiValuedClaimsToBeAdded,
                                 userStoreManager));
                         break;
                     case REPLACE:
                         operationExecutionResultList.add(handleReplaceOperation(operation, responseContext,
                                 userClaimsToBeModified, simpleMultiValuedClaimsToBeRemoved,
                                 simpleMultiValuedClaimsToBeAdded, userStoreManager));
                         break;
                     case REMOVE:
                         operationExecutionResultList.add(handleRemoveOperation(operation, responseContext,
                                 userClaimsToBeModified, userClaimsToBeRemoved, simpleMultiValuedClaimsToBeRemoved));
                         break;
                     default:
-                        break;
+                        throw new ActionExecutionResponseProcessorException(
+                                "Unsupported operation type in pre-update profile response: " + operation.getOp());
                 }
             }
         }
 
         logOperationExecutionResults(getSupportedActionType(), operationExecutionResultList);
+
+        for (ProfileOperationExecutionResult result : operationExecutionResultList) {
+            if (result.getStatus() == ProfileOperationExecutionResult.Status.FAILURE) {
+                throw new ActionExecutionResponseProcessorException(result.getMessage());
+            }
+        }

Also applies to: 135-189

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@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`
around lines 103 - 124, The loop currently ignores unknown ops and always lets
processSuccessResponse run even if some handle*Operation produced a rejected
ProfileOperationExecutionResult; update the switch default to create and add a
rejected ProfileOperationExecutionResult for unknown operation types (refer to
operation.getOp() and ProfileOperationExecutionResult), then after the loop
examine operationExecutionResultList for any rejected entries and if any exist
prevent the success path (do not call processSuccessResponse/return
SuccessStatus), instead invoke the failure path (e.g., processFailureResponse or
set FlowContext to a failed status) and ensure
logOperationExecutionResults(getSupportedActionType(),
operationExecutionResultList) still logs before returning; apply the same change
to the corresponding block at lines 135-189 where operations are processed.

Comment on lines +149 to +150
return new ProfileOperationExecutionResult(operation, ProfileOperationExecutionResult.Status.FAILURE,
e.getMessage());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Normalize failure messages before adding them to Map.of(...).

e.getMessage() can be null. When that happens, Map.of("message", performedOperation.getMessage()) throws, so a handled operation failure can crash response processing as soon as diagnostic logging is enabled.

💡 Suggested fix
             operationExecutionResultList.forEach(
                     performedOperation -> operationDetailsList.add(Map.of(
                             "operation", performedOperation.getOperation().getOp() + " path: "
                                     + performedOperation.getOperation().getPath(),
                             "status", performedOperation.getStatus().toString(),
-                            "message", performedOperation.getMessage()
+                            "message", Optional.ofNullable(performedOperation.getMessage())
+                                    .orElse("Operation failed.")
                     )));

As per coding guidelines, "avoid NPE risks" in added logs.

Also applies to: 168-169, 186-187, 751-757

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@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`
around lines 149 - 150, The code returns ProfileOperationExecutionResult using
e.getMessage() directly, which can be null and later causes Map.of("message",
...) to throw; in PreUpdateProfileResponseProcessor replace direct
e.getMessage() usage with a normalized non-null string (e.g., String msg =
e.getMessage() != null ? e.getMessage() : ""; or use
Optional.ofNullable(e.getMessage()).orElse("")); then pass msg into new
ProfileOperationExecutionResult(...). Apply the same normalization pattern where
messages are passed into Map.of or ProfileOperationExecutionResult (the other
occurrences around the blocks that create ProfileOperationExecutionResult and
Map.of at the locations you flagged).

@sonarqubecloud

Copy link
Copy Markdown

@codecov

codecov Bot commented May 27, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 65.92040% with 137 lines in your changes missing coverage. Please review.
✅ Project coverage is 53.10%. Comparing base (11cf7cf) to head (1a17801).
⚠️ Report is 70 commits behind head on master.

Files with missing lines Patch % Lines
...l/execution/PreUpdateProfileResponseProcessor.java 64.86% 74 Missing and 43 partials ⚠️
...rnal/execution/PreUpdateProfileRequestBuilder.java 76.74% 8 Missing and 2 partials ⚠️
...n/execution/internal/util/OperationComparator.java 50.00% 1 Missing and 6 partials ⚠️
...nternal/model/ProfileOperationExecutionResult.java 75.00% 3 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##             master    #8115      +/-   ##
============================================
+ Coverage     52.80%   53.10%   +0.30%     
- Complexity    20837    21022     +185     
============================================
  Files          2151     2152       +1     
  Lines        128129   129223    +1094     
  Branches      18744    18909     +165     
============================================
+ Hits          67654    68629     +975     
- Misses        52155    52184      +29     
- Partials       8320     8410      +90     
Flag Coverage Δ
unit 37.95% <65.92%> (+0.22%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@Lashen1227

Copy link
Copy Markdown
Contributor Author

Addressed on #8124

@Lashen1227 Lashen1227 closed this May 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant