From 41075443b63a18c8d4bf81f64265a4b5deb49bf1 Mon Sep 17 00:00:00 2001 From: simonbleher Date: Sat, 25 Jul 2026 16:32:10 +0200 Subject: [PATCH] Fix addManagedMember adding user as unmanaged when user is cache-wrapped UserModel.joinGroup(GroupModel, MembershipMetadata) has a default implementation that drops the metadata and calls the single-arg joinGroup(GroupModel), which defaults the membership to UNMANAGED. The infinispan cache UserAdapter only overrode the single-arg variant, so any UserModel obtained through the standard cached user provider (e.g. session.users().getUserById(...), as used by custom admin resource providers and other real callers of addManagedMember) fell through to that default and silently lost the MANAGED metadata. Add the missing override so the metadata is passed through to the delegate, mirroring the existing joinGroup(GroupModel) override. Closes #43094 Signed-off-by: simonbleher --- .../models/cache/infinispan/UserAdapter.java | 12 ++++++++++ .../cache/OrganizationCacheTest.java | 22 +++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserAdapter.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserAdapter.java index 7dbf6b41b423..70d654f096e8 100755 --- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserAdapter.java +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserAdapter.java @@ -35,6 +35,7 @@ import org.keycloak.models.GroupModel; import org.keycloak.models.GroupModel.Type; import org.keycloak.models.KeycloakSession; +import org.keycloak.models.MembershipMetadata; import org.keycloak.models.RealmModel; import org.keycloak.models.RoleModel; import org.keycloak.models.SubjectCredentialManager; @@ -504,6 +505,17 @@ public void joinGroup(GroupModel group) { } + @Override + public void joinGroup(GroupModel group, MembershipMetadata metadata) { + // Only REALM groups are cached; organization groups always delegate to persistence + if (group.getType() == Type.REALM && updated == null && cached.getGroups(keycloakSession, modelSupplier).contains(group.getId())) { + return; + } + getDelegateForUpdate(); + updated.joinGroup(group, metadata); + + } + @Override public void leaveGroup(GroupModel group) { // Only REALM groups are cached; organization groups always delegate to persistence diff --git a/tests/base/src/test/java/org/keycloak/tests/organization/cache/OrganizationCacheTest.java b/tests/base/src/test/java/org/keycloak/tests/organization/cache/OrganizationCacheTest.java index 0e343c49e0de..7d17b1afe8e0 100644 --- a/tests/base/src/test/java/org/keycloak/tests/organization/cache/OrganizationCacheTest.java +++ b/tests/base/src/test/java/org/keycloak/tests/organization/cache/OrganizationCacheTest.java @@ -130,6 +130,28 @@ public void testGetByDomain() { }); } + @Test + public void testAddManagedMemberWithCachedUser() { + runOnServer.run(session -> { + RealmModel realm = session.getContext().getRealm(); + UserModel member = session.users().addUser(realm, "member"); + member.setEnabled(true); + }); + runOnServer.run(session -> { + OrganizationProvider orgProvider = session.getProvider(OrganizationProvider.class); + OrganizationModel orga = orgProvider.getByDomainName("orga.org"); + RealmModel realm = session.getContext().getRealm(); + // Fetch through the standard cached user provider, as any real caller (e.g. a + // custom AdminRealmResourceProvider, or the registration/broker flows that call + // addManagedMember) would - this returns the infinispan cache-wrapped UserAdapter. + UserModel member = session.users().getUserByUsername(realm, "member"); + boolean added = orgProvider.addManagedMember(orga, member); + assertTrue(added); + assertTrue(orgProvider.isManagedMember(orga, member), + "user added via addManagedMember must be reported as a MANAGED member"); + }); + } + @Test public void testGetByMember() { runOnServer.run(session -> {