Skip to content

Fix SCIM2 /Users filter missing groups when username case differs from DB#4590

Open
PasinduYeshan wants to merge 4 commits intowso2:4.12.xfrom
PasinduYeshan:fix/issue-6807
Open

Fix SCIM2 /Users filter missing groups when username case differs from DB#4590
PasinduYeshan wants to merge 4 commits intowso2:4.12.xfrom
PasinduYeshan:fix/issue-6807

Conversation

@PasinduYeshan
Copy link
Copy Markdown

@PasinduYeshan PasinduYeshan commented Apr 29, 2026

Summary

  • getHybridRoleListOfUsers was silently dropping rows when the input username case
    differed from the DB-stored case (e.g. ["test"] vs DB "TEST"), even though the SQL is
    case-insensitive — the Java userNames.contains(...) check is case-sensitive.
  • getUserIDFromUserName was poisoning the UserIdResolverCache by storing the
    caller-provided username instead of the DB-resolved canonical value. Any case-sensitive
    consumer of the cache (HybridRoleManager, /scim2/Me, etc.) silently misbehaved afterwards.

The combination caused SCIM /Users?filter=… responses to omit groups after a single auth
call with a wrong-case username.

Changes

AbstractUserStoreManager.getUserIDFromUserName — after resolving the userID, re-resolve
the canonical username via doGetUserNameFromUserID(userID) and use that as the cache key.
Applied to both branches (unique-ID enabled, and the doGetUserClaimValues claim fallback).

  • Gated on !isCaseSensitiveUsernameForStore(userStore) — case-sensitive stores can't hit
    this bug, so the extra call is skipped there.
  • Wrapped in try { … } catch (UserStoreException | RuntimeException e) { log.debug(…) } so
    any failure (custom store without the lookup, transient DB error, missing-user race)
    falls back to pre-fix behavior instead of breaking auth.
  • New helper isCaseSensitiveUsernameForStore(UserStore) reads CaseInsensitiveUsername,
    defaulting to case-sensitive — same convention as the 5 existing helpers in the codebase.

HybridRoleManager.getHybridRoleListOfUsers — Java post-filter now mirrors the SQL
semantics:

  • Case-sensitive store → keeps original userNames.contains(dbUserName) (preserves correct
    attribution if two users differ only by case).
  • Case-insensitive store → equalsIgnoreCase stream match, keyed by the input-list casing
    so the existing second loop finds the entry.
  • isCaseSensitiveUsername() hoisted to a single boolean used by both the SQL selection and
    the post-filter.

Related Issues

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 29, 2026

Warning

Rate limit exceeded

@PasinduYeshan has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 47 minutes and 10 seconds before requesting another review.

To keep reviews running without waiting, you can enable usage-based add-on for your organization. This allows additional reviews beyond the hourly cap. Account admins can enable it under billing.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: 3bed869b-fbad-4384-b9b8-4e3bbcc9ca49

📥 Commits

Reviewing files that changed from the base of the PR and between 24223d7 and 8fb980b.

📒 Files selected for processing (1)
  • core/org.wso2.carbon.user.core/src/main/java/org/wso2/carbon/user/core/common/AbstractUserStoreManager.java
📝 Walkthrough

Walkthrough

Implements store-specific case-sensitivity handling for usernames by reading the CASE_INSENSITIVE_USERNAME realm property. When case-insensitive, the system resolves canonical usernames for cache operations, with fallback to the original username if resolution fails.

Changes

Cohort / File(s) Summary
Case-Sensitivity Username Resolution
core/org.wso2.carbon.user.core/src/main/java/org/wso2/carbon/user/core/common/AbstractUserStoreManager.java
Added logic to read CASE_INSENSITIVE_USERNAME realm property and conditionally resolve canonical usernames for cache key operations in getUserIDFromUserName. Includes error handling that logs exceptions and falls back to the original username if canonical resolution fails.
Case-Sensitivity Test Coverage
core/org.wso2.carbon.user.core/src/test/java/org/wso2/carbon/user/core/common/UserIdResolverCacheCacheTest.java
New test class using H2 JDBC-backed user realm to verify that getUserIDFromUserName caches usernames with the casing persisted in the database, and that repeated lookups with different case variants resolve to the same user ID.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The PR description is incomplete. While it provides a clear Summary and Changes section explaining the root cause and implementation details, it is missing most required sections from the template (Purpose with issue links, Goals, Approach, User stories, Release note, Documentation, Training, Certification, Marketing, detailed test coverage, security checks, and test environment information). Add the missing template sections: Purpose (with issue link), Goals, Approach, User stories, Release note, Documentation, Training, Certification, Marketing, detailed Automation tests coverage, Security checks (yes/no answers), and Test environment details.
Docstring Coverage ⚠️ Warning Docstring coverage is 75.00% 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 bug being fixed: SCIM2 /Users filter missing groups when username case differs from DB-stored case.
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
Review rate limit: 0/1 reviews remaining, refill in 47 minutes and 10 seconds.

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

Comment on lines 13930 to +13933
}
if (!isCaseSensitiveUsernameForStore(userStore)) {
try {
String resolvedUserName = doGetUserNameFromUserID(userID);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Log Improvement Suggestion No: 2

Suggested change
}
if (!isCaseSensitiveUsernameForStore(userStore)) {
try {
String resolvedUserName = doGetUserNameFromUserID(userID);
}
if (!isCaseSensitiveUsernameForStore(userStore)) {
log.debug("Username is case-insensitive, resolving canonical username for userID: " + userID);
try {

Copy link
Copy Markdown

@wso2-engineering wso2-engineering Bot left a comment

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: 3
#### Log Improvement Suggestion No: 4

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
core/org.wso2.carbon.user.core/src/main/java/org/wso2/carbon/user/core/common/AbstractUserStoreManager.java (1)

13931-13943: Extract canonical-username resolution into one helper.

The same try/catch resolution block appears twice, which increases drift risk for future fixes. A private helper returning (resolvedUserName, shouldWriteCache) (or equivalent) would keep behavior consistent across both branches.

Also applies to: 13954-13966

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@core/org.wso2.carbon.user.core/src/main/java/org/wso2/carbon/user/core/common/AbstractUserStoreManager.java`
around lines 13931 - 13943, Extract the duplicated try/catch that resolves a
canonical username into a private helper in AbstractUserStoreManager (e.g.,
resolveCanonicalUsername(userID, userStore) returning an Optional<String> or a
pair like (resolvedUserName, shouldWriteCache)); replace both occurrences around
isCaseSensitiveUsernameForStore(...) that call doGetUserNameFromUserID(userID)
and update userName only when the helper returns a non-empty result, and
preserve existing debug logging by moving the log.debug(...) into the helper's
catch handling so behavior remains identical in both branches.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@core/org.wso2.carbon.user.core/src/main/java/org/wso2/carbon/user/core/common/AbstractUserStoreManager.java`:
- Around line 13931-13945: When resolving canonical usernames for
case-insensitive stores, avoid writing cache entries if canonical resolution
fails; modify the block that calls isCaseSensitiveUsernameForStore and
doGetUserNameFromUserID so that addToUserIDCacheOnRead and
addToUserNameCacheOnRead are only invoked when doGetUserNameFromUserID returns a
non-empty resolvedUserName (i.e., successful canonical lookup). Apply the same
guarding change to the other analogous block (lines referenced around
addToUserIDCacheOnRead/addToUserNameCacheOnRead) so both places check the
successful resolution before calling
addToUserIDCacheOnRead/addToUserNameCacheOnRead; use the existing methods
isCaseSensitiveUsernameForStore, doGetUserNameFromUserID,
addToUserIDCacheOnRead, and addToUserNameCacheOnRead to locate and update the
logic.

In
`@core/org.wso2.carbon.user.core/src/main/java/org/wso2/carbon/user/core/hybrid/HybridRoleManager.java`:
- Around line 845-858: The code currently uses findFirst() on userNames to set
matchedUserName, which assigns the DB role row to only one caller key and
ignores other case-variants; change this to fan out the role to all
case-insensitive matches by replacing the findFirst() usage with a search that
collects all names matching equalsIgnoreCase(dbUserName) (e.g.,
userNames.stream().filter(...).collect(...)) and for each matched name add the
role to hybridRoleListOfUsers (create the List if null) instead of only updating
the first matchedUserName; alternatively, you may dedupe userNames up front, but
ensure the logic in the block that reads roleName and updates
hybridRoleListOfUsers applies to every matching key, not just matchedUserName.

---

Nitpick comments:
In
`@core/org.wso2.carbon.user.core/src/main/java/org/wso2/carbon/user/core/common/AbstractUserStoreManager.java`:
- Around line 13931-13943: Extract the duplicated try/catch that resolves a
canonical username into a private helper in AbstractUserStoreManager (e.g.,
resolveCanonicalUsername(userID, userStore) returning an Optional<String> or a
pair like (resolvedUserName, shouldWriteCache)); replace both occurrences around
isCaseSensitiveUsernameForStore(...) that call doGetUserNameFromUserID(userID)
and update userName only when the helper returns a non-empty result, and
preserve existing debug logging by moving the log.debug(...) into the helper's
catch handling so behavior remains identical in both branches.
🪄 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: 1ea5b9a7-b38a-401d-8e59-d4754f227c04

📥 Commits

Reviewing files that changed from the base of the PR and between 1dac8ae and c4c5156.

📒 Files selected for processing (2)
  • core/org.wso2.carbon.user.core/src/main/java/org/wso2/carbon/user/core/common/AbstractUserStoreManager.java
  • core/org.wso2.carbon.user.core/src/main/java/org/wso2/carbon/user/core/hybrid/HybridRoleManager.java

Comment on lines 13931 to 13945
if (!isCaseSensitiveUsernameForStore(userStore)) {
try {
String resolvedUserName = doGetUserNameFromUserID(userID);
if (StringUtils.isNotEmpty(resolvedUserName)) {
userName = resolvedUserName;
}
} catch (UserStoreException | RuntimeException e) {
if (log.isDebugEnabled()) {
log.debug("Could not resolve canonical username for userID; falling back "
+ "to caller-provided username for cache key.", e);
}
}
}
addToUserIDCacheOnRead(userID, userName, userStore);
addToUserNameCacheOnRead(userID, userName, userStore);
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot Apr 29, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Avoid writing cache entries when canonical username resolution fails.

At Line 13944/Line 13945 and Line 13967/Line 13968, cache writes still happen after fallback to caller-provided casing. In case-insensitive stores, that can reintroduce the same cache-key poisoning this PR is fixing whenever canonical lookup fails.

Proposed fix
-                    if (!isCaseSensitiveUsernameForStore(userStore)) {
+                    boolean shouldWriteCache = true;
+                    if (!isCaseSensitiveUsernameForStore(userStore)) {
                         try {
                             String resolvedUserName = doGetUserNameFromUserID(userID);
                             if (StringUtils.isNotEmpty(resolvedUserName)) {
                                 userName = resolvedUserName;
+                            } else {
+                                shouldWriteCache = false;
                             }
                         } catch (UserStoreException | RuntimeException e) {
+                            shouldWriteCache = false;
                             if (log.isDebugEnabled()) {
                                 log.debug("Could not resolve canonical username for userID; falling back "
                                         + "to caller-provided username for cache key.", e);
                             }
                         }
                     }
-                    addToUserIDCacheOnRead(userID, userName, userStore);
-                    addToUserNameCacheOnRead(userID, userName, userStore);
+                    if (shouldWriteCache) {
+                        addToUserIDCacheOnRead(userID, userName, userStore);
+                        addToUserNameCacheOnRead(userID, userName, userStore);
+                    }
                     return userID;

Also applies to: 13954-13968

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@core/org.wso2.carbon.user.core/src/main/java/org/wso2/carbon/user/core/common/AbstractUserStoreManager.java`
around lines 13931 - 13945, When resolving canonical usernames for
case-insensitive stores, avoid writing cache entries if canonical resolution
fails; modify the block that calls isCaseSensitiveUsernameForStore and
doGetUserNameFromUserID so that addToUserIDCacheOnRead and
addToUserNameCacheOnRead are only invoked when doGetUserNameFromUserID returns a
non-empty resolvedUserName (i.e., successful canonical lookup). Apply the same
guarding change to the other analogous block (lines referenced around
addToUserIDCacheOnRead/addToUserNameCacheOnRead) so both places check the
successful resolution before calling
addToUserIDCacheOnRead/addToUserNameCacheOnRead; use the existing methods
isCaseSensitiveUsernameForStore, doGetUserNameFromUserID,
addToUserIDCacheOnRead, and addToUserNameCacheOnRead to locate and update the
logic.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

The fallback path matches pre-PR behavior, so no regression. The catch only fires on NotImplementedException from custom stores, transient DB errors, or a delete-race. Skipping the cache write would mean a custom store that always fails resolution gets no cache benefit at all.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@PasinduYeshan, thanks for the detailed explanation — I should mention I'm replying only to your latest comment, as the earlier conversation was too long to be provided to me.

Your reasoning is well-founded:

  1. No regression on fallback — using the caller-provided userName matches the pre-PR cache-write behavior exactly, so any code path that worked before still works.
  2. Narrow catch surface — the three failure modes you describe (NotImplementedException from custom stores, transient DB errors, delete-race) are all cases where the resolution attempt was "best-effort" anyway, and silently swallowing them (with a debug log) is appropriate.
  3. Cache utility for non-implementing stores — skipping addToUserIDCacheOnRead / addToUserNameCacheOnRead entirely on failure would permanently deprive custom stores (that never implement doGetUserNameFromUserID) of any caching benefit, which is a worse trade-off than a slightly imprecise cache key.

The design is a reasonable "try to improve, but do no harm" pattern. The concern is acknowledged and understood.


✏️ Learnings added
Learnt from: PasinduYeshan
Repo: wso2/carbon-kernel PR: 4590
File: core/org.wso2.carbon.user.core/src/main/java/org/wso2/carbon/user/core/common/AbstractUserStoreManager.java:13931-13945
Timestamp: 2026-04-29T11:19:11.292Z
Learning: In `AbstractUserStoreManager.getUserIDFromUserName` (file: `core/org.wso2.carbon.user.core/src/main/java/org/wso2/carbon/user/core/common/AbstractUserStoreManager.java`), the catch block around `doGetUserNameFromUserID` for case-insensitive stores is intentional. The fallback to the caller-provided username matches pre-PR behavior (no regression). The catch fires only for edge cases: `NotImplementedException` from custom stores, transient DB errors, or delete-race conditions. Skipping the cache write on failure is intentionally avoided because custom stores that never implement `doGetUserNameFromUserID` would lose all caching benefit.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

@jenkins-is-staging
Copy link
Copy Markdown

PR builder started
Link: https://github.com/wso2/product-is/actions/runs/25150542493

Comment on lines +840 to +852
String dbUserName = resultSet.getString(1);
String matchedUserName;
if (userNames.contains(dbUserName)) {
matchedUserName = dbUserName;
} else if (!caseSensitive) {
matchedUserName = userNames.stream()
.filter(name -> name != null && name.equalsIgnoreCase(dbUserName))
.findFirst()
.orElse(null);
} else {
matchedUserName = null;
}
if (matchedUserName == null) {
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

As discussed offline with @sadilchamishka , this secondary fix will be removed since the cache fix already addresses the reported issue. Callers are expected to provide the correct usernames.

…tion

The cache canonicalisation in AbstractUserStoreManager.getUserIDFromUserName
addresses the root cause; the HybridRoleManager post-filter change is no
longer needed for the SCIM2 /Users filter scenario. Restoring the original
case-sensitive contains() and adding the abstract test for the cache fix.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@core/org.wso2.carbon.user.core/src/test/java/org/wso2/carbon/user/core/common/UserIdResolverCacheCaseTest.java`:
- Around line 58-63: The test leaves an InputStream open (created around line
~77) and only closes the DB pool on the happy path; modify the test so resources
are always cleaned up: ensure the InputStream used by
initRealmStuff()/doTestCacheCaseCorrectness is opened with try-with-resources
(or explicitly closed in a finally) and move
DatabaseUtil.closeDatabasePoolConnection() into a finally block or an `@After`
teardown method so it runs even when setup or assertions throw; update
testGetUserIDFromUserNameCachesDBStoredCase, initRealmStuff, or the helper that
opens the stream to reference the stream variable and guarantee closure, and
ensure DatabaseUtil.closeDatabasePoolConnection is invoked unconditionally.
🪄 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: ecddd178-5fb6-4107-9daf-2fd71cbbf833

📥 Commits

Reviewing files that changed from the base of the PR and between 02eef04 and 24223d7.

📒 Files selected for processing (1)
  • core/org.wso2.carbon.user.core/src/test/java/org/wso2/carbon/user/core/common/UserIdResolverCacheCaseTest.java

@jenkins-is-staging
Copy link
Copy Markdown

PR builder completed
Link: https://github.com/wso2/product-is/actions/runs/25150542493
Status: success

Copy link
Copy Markdown

@jenkins-is-staging jenkins-is-staging left a comment

Choose a reason for hiding this comment

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

Approving the pull request based on the successful pr build https://github.com/wso2/product-is/actions/runs/25150542493

Renames the helper to isCaseInsensitiveUsernameForStore and inverts the
return value so call sites no longer need a negation. Also removes the
UserIdResolverCacheCaseTest as agreed.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@jenkins-is-staging
Copy link
Copy Markdown

PR builder started
Link: https://github.com/wso2/product-is/actions/runs/25152686742

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.

3 participants