Fix NPE when accessing group concurrently - #1
Conversation
Closes #40368 Signed-off-by: vramik <vramik@redhat.com>
📝 WalkthroughWalkthroughThis 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. ChangesGroup caching and access control
🎯 3 (Moderate) | ⏱️ ~20 minutes
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 inconclusive)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
tests/base/src/test/java/org/keycloak/tests/admin/group/GroupTest.java (1)
121-121: 💤 Low valueTypo:
groupUuuidsshould begroupUuids.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
📒 Files selected for processing (5)
.github/dependabot.ymlmodel/infinispan/src/main/java/org/keycloak/models/cache/infinispan/GroupAdapter.javamodel/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/CachedGroup.javaservices/src/main/java/org/keycloak/utils/GroupUtils.javatests/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
| public Long getSubGroupsCount() { | ||
| if (isUpdated()) return updated.getSubGroupsCount(); | ||
| return getGroupModel().getSubGroupsCount(); | ||
| GroupModel model = modelSupplier.get(); | ||
| return model == null ? null : model.getSubGroupsCount(); |
There was a problem hiding this comment.
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.
| 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()); |
There was a problem hiding this comment.
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.
Closes #40368
Summary by CodeRabbit