Skip to content

Fix NPE when accessing group concurrently - #1

Open
linxia0415 wants to merge 2 commits into
mainfrom
pr-40940
Open

Fix NPE when accessing group concurrently#1
linxia0415 wants to merge 2 commits into
mainfrom
pr-40940

Conversation

@linxia0415

@linxia0415 linxia0415 commented Jun 3, 2026

Copy link
Copy Markdown

Closes #40368

Summary by CodeRabbit

  • Chores
    • Removed automated dependency update configuration
  • Bug Fixes
    • Improved null-safety handling in group count operations to prevent errors with unavailable delegates
    • Enhanced permission access data assignment for group representations
  • Tests
    • Added concurrency test validating group operations stability during simultaneous deletion and retrieval scenarios

vramik and others added 2 commits July 7, 2025 18:53
Closes #40368

Signed-off-by: vramik <vramik@redhat.com>
@coderabbitai

coderabbitai Bot commented Jun 3, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR strengthens group management safety and access control. The cache layer is made defensive against null delegates, group representations now include permission access data, and a concurrency test validates that concurrent delete and read operations do not cause exceptions.

Changes

Group caching and access control

Layer / File(s) Summary
Cache layer nullability safety
model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/GroupAdapter.java, model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/CachedGroup.java
GroupAdapter.getSubGroupsCount() now guards against null delegate by using modelSupplier.get() and returning null if missing. CachedGroup.getRealm() is marked with explicit @Override annotation.
Access control in group representations
services/src/main/java/org/keycloak/utils/GroupUtils.java
GroupUtils.toRepresentation() populates the GroupRepresentation's access field from GroupPermissionEvaluator before returning. Unused groupMatchesSearchOrIsPathElement() helper is removed.
Concurrent delete/read test
tests/base/src/test/java/org/keycloak/tests/admin/group/GroupTest.java
New createMultiDeleteMultiReadMulti() test method bulk-creates 100 groups, spawns a reader thread that repeatedly fetches group briefs while the main thread deletes all groups, and asserts no exceptions occur during concurrent operations. Adds concurrency utilities and assertion imports.

🎯 3 (Moderate) | ⏱️ ~20 minutes


🐰 Cache layers now guard with care,
Access control fills the air,
Threads dance safely, delete and read,
No exceptions to dread!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Description check ❓ Inconclusive The PR description only references the issue number 'Closes #40368' with minimal context; it does not explain the problem, solution, or implementation details needed to understand the fix. Expand the description to explain the concurrent access issue, the root cause of the NPE, and how the changes (null checks and permission access assignment) resolve the problem.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Fix NPE when accessing group concurrently' is clear and specific, accurately summarizing the primary change: addressing a NullPointerException that occurs during concurrent group access.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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 docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch pr-40940

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

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)
tests/base/src/test/java/org/keycloak/tests/admin/group/GroupTest.java (1)

121-121: 💤 Low value

Typo: groupUuuids should be groupUuids.

The variable name has an extra 'u'.

Proposed fix
-    List<String> groupUuuids = new ArrayList<>();
+    List<String> groupUuids = new ArrayList<>();

And update all references on lines 129, 152.

🤖 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 `@tests/base/src/test/java/org/keycloak/tests/admin/group/GroupTest.java` at
line 121, Rename the misspelled variable groupUuuids to groupUuids in
GroupTest.java and update all usages to the corrected name (e.g., where
groupUuuids is added to or accessed later in the test). Ensure you change the
declaration List<String> groupUuuids = new ArrayList<>(); to List<String>
groupUuids = new ArrayList<>(); and fix every reference (adds, gets, assertions,
or method calls) that still uses the old name so the test compiles and runs.
🤖 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
`@model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/GroupAdapter.java`:
- Around line 272-275: The sibling methods getSubGroupsStream(...) in
GroupAdapter still call modelSupplier.get() and dereference its result without
null checks; modify each overload (the getSubGroupsStream methods at/around
lines 256, 262, 268) to mirror getSubGroupsCount() by checking isUpdated()
first, then calling modelSupplier.get() into a local variable and returning an
appropriate empty stream or null-safe value when model is null instead of
dereferencing; ensure updated (via updated.getSubGroupsStream(...)) is used when
isUpdated() is true and otherwise guard modelSupplier.get() with a null check
before calling its getSubGroupsStream methods.

In `@tests/base/src/test/java/org/keycloak/tests/admin/group/GroupTest.java`:
- Around line 139-157: The reader thread may still be running when the assertion
executes; change the anonymous thread creation to store the Thread reference
(e.g., Thread reader = new Thread(() -> { ... })) start it, and after setting
deletedAll.set(true) call reader.join() (handle InterruptedException) before
asserting on caughtExceptions so the background reader has terminated and no
late exceptions are missed; reference the variables deletedAll, caughtExceptions
and the reader thread around the groups() call and group(...).remove() loop.

---

Nitpick comments:
In `@tests/base/src/test/java/org/keycloak/tests/admin/group/GroupTest.java`:
- Line 121: Rename the misspelled variable groupUuuids to groupUuids in
GroupTest.java and update all usages to the corrected name (e.g., where
groupUuuids is added to or accessed later in the test). Ensure you change the
declaration List<String> groupUuuids = new ArrayList<>(); to List<String>
groupUuids = new ArrayList<>(); and fix every reference (adds, gets, assertions,
or method calls) that still uses the old name so the test compiles and runs.
🪄 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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 6bb060f6-e58b-4a8d-8372-4b6c5eb3721b

📥 Commits

Reviewing files that changed from the base of the PR and between d88d207 and 159332f.

📒 Files selected for processing (5)
  • .github/dependabot.yml
  • model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/GroupAdapter.java
  • model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/CachedGroup.java
  • services/src/main/java/org/keycloak/utils/GroupUtils.java
  • tests/base/src/test/java/org/keycloak/tests/admin/group/GroupTest.java
💤 Files with no reviewable changes (2)
  • .github/dependabot.yml
  • services/src/main/java/org/keycloak/utils/GroupUtils.java

Comment on lines 272 to +275
public Long getSubGroupsCount() {
if (isUpdated()) return updated.getSubGroupsCount();
return getGroupModel().getSubGroupsCount();
GroupModel model = modelSupplier.get();
return model == null ? null : model.getSubGroupsCount();

Copy link
Copy Markdown

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

Null-guard is correct here, but the same NPE risk remains in sibling subgroup delegate reads.

Good fix for getSubGroupsCount(). However, modelSupplier.get() can still be null and is dereferenced directly in getSubGroupsStream(...) overloads (Line 256, Line 262, Line 268). Under the same concurrent delete/read condition, those paths can still throw NPE.

Suggested follow-up patch
@@
     `@Override`
     public Stream<GroupModel> getSubGroupsStream(String search, Integer firstResult, Integer maxResults) {
         if (isUpdated()) return updated.getSubGroupsStream(search, firstResult, maxResults);
-        return modelSupplier.get().getSubGroupsStream(search, firstResult, maxResults);
+        GroupModel model = modelSupplier.get();
+        return model == null ? Stream.empty() : model.getSubGroupsStream(search, firstResult, maxResults);
     }
@@
     `@Override`
     public Stream<GroupModel> getSubGroupsStream(Integer firstResult, Integer maxResults) {
         if (isUpdated()) return updated.getSubGroupsStream(firstResult, maxResults);
-        return modelSupplier.get().getSubGroupsStream(firstResult, maxResults);
+        GroupModel model = modelSupplier.get();
+        return model == null ? Stream.empty() : model.getSubGroupsStream(firstResult, maxResults);
     }
@@
     `@Override`
     public Stream<GroupModel> getSubGroupsStream(String search, Boolean exact, Integer firstResult, Integer maxResults) {
         if (isUpdated()) return updated.getSubGroupsStream(search, exact, firstResult, maxResults);
-        return modelSupplier.get().getSubGroupsStream(search, exact, firstResult, maxResults);
+        GroupModel model = modelSupplier.get();
+        return model == null ? Stream.empty() : model.getSubGroupsStream(search, exact, firstResult, maxResults);
     }
🤖 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
`@model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/GroupAdapter.java`
around lines 272 - 275, The sibling methods getSubGroupsStream(...) in
GroupAdapter still call modelSupplier.get() and dereference its result without
null checks; modify each overload (the getSubGroupsStream methods at/around
lines 256, 262, 268) to mirror getSubGroupsCount() by checking isUpdated()
first, then calling modelSupplier.get() into a local variable and returning an
appropriate empty stream or null-safe value when model is null instead of
dereferencing; ensure updated (via updated.getSubGroupsStream(...)) is used when
isUpdated() is true and otherwise guard modelSupplier.get() with a null check
before calling its getSubGroupsStream methods.

Comment on lines +139 to +157
new Thread(() -> {
while (!deletedAll.get()) {
try {
// just loading briefs
managedRealm.admin().groups().groups(null, 0, Integer.MAX_VALUE, true);
} catch (Exception e) {

caughtExceptions.add(e);
}
}
}).start();

// delete groups
groupUuuids.forEach(groupUuid -> {
managedRealm.admin().groups().group(groupUuid).remove();
});
deletedAll.set(true);

assertThat(caughtExceptions, Matchers.empty());

Copy link
Copy Markdown

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

Race condition: reader thread may still be running when assertion executes.

The test sets deletedAll.set(true) and immediately asserts on caughtExceptions, but the reader thread may not have terminated yet. Any exception caught after the assertion runs will be missed, making the test unreliable.

Store the thread reference and join it before asserting.

Proposed fix
     AtomicBoolean deletedAll = new AtomicBoolean(false);
     List<Exception> caughtExceptions = new CopyOnWriteArrayList<>();
     // read groups in a separate thread
-    new Thread(() -> {
+    Thread readerThread = new Thread(() -> {
         while (!deletedAll.get()) {
             try {
                 // just loading briefs
                 managedRealm.admin().groups().groups(null, 0, Integer.MAX_VALUE, true);
             } catch (Exception e) {
-
                 caughtExceptions.add(e);
             }
         }
-    }).start();
+    });
+    readerThread.start();

     // delete groups
     groupUuuids.forEach(groupUuid -> {
         managedRealm.admin().groups().group(groupUuid).remove();
     });
     deletedAll.set(true);

+    try {
+        readerThread.join(5000); // Wait up to 5 seconds for reader to finish
+    } catch (InterruptedException e) {
+        Thread.currentThread().interrupt();
+        fail("Interrupted while waiting for reader thread");
+    }
+
     assertThat(caughtExceptions, Matchers.empty());
 }
🤖 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 `@tests/base/src/test/java/org/keycloak/tests/admin/group/GroupTest.java`
around lines 139 - 157, The reader thread may still be running when the
assertion executes; change the anonymous thread creation to store the Thread
reference (e.g., Thread reader = new Thread(() -> { ... })) start it, and after
setting deletedAll.set(true) call reader.join() (handle InterruptedException)
before asserting on caughtExceptions so the background reader has terminated and
no late exceptions are missed; reference the variables deletedAll,
caughtExceptions and the reader thread around the groups() call and
group(...).remove() loop.

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.

2 participants