From 7e565e86fedf187cb19a82fa9383d681b4b5642b Mon Sep 17 00:00:00 2001 From: mposolda Date: Thu, 9 Apr 2026 10:34:42 +0200 Subject: [PATCH] Not possible to sync latest keycloak-admin-client to keycloak-client closes #47889 Signed-off-by: mposolda --- .../org/keycloak/admin/client/Keycloak.java | 20 ++++- .../admin/client/v2/ClientApiV2Test.java | 81 ++++++++++--------- 2 files changed, 60 insertions(+), 41 deletions(-) diff --git a/integration/admin-client/src/main/java/org/keycloak/admin/client/Keycloak.java b/integration/admin-client/src/main/java/org/keycloak/admin/client/Keycloak.java index 7eb7c0217d67..f95b12bbd967 100644 --- a/integration/admin-client/src/main/java/org/keycloak/admin/client/Keycloak.java +++ b/integration/admin-client/src/main/java/org/keycloak/admin/client/Keycloak.java @@ -16,6 +16,8 @@ */ package org.keycloak.admin.client; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; import java.net.URI; import java.util.Iterator; import java.util.ServiceLoader; @@ -32,7 +34,6 @@ import org.keycloak.admin.client.resource.ServerInfoResource; import org.keycloak.admin.client.spi.ResteasyClientProvider; import org.keycloak.admin.client.token.TokenManager; -import org.keycloak.admin.client.wrapper.Clients; import static org.keycloak.OAuth2Constants.PASSWORD; @@ -242,7 +243,20 @@ public boolean isClosed() { return closed; } - public Clients clients(String realmName) { - return new Clients(CLIENT_PROVIDER, target, realmName); + /** + * Method to return org.keycloak.admin.client.wrapper.Clients instance. Note that this class + * may not be available in some cases and hence reflection is used to instantiate that class + * + * @param realmName realm name + * @param clientsClass Typically class org.keycloak.admin.client.wrapper.Clients . This argument is present just to avoid casting. + * @return Instance of org.keycloak.admin.client.wrapper.Clients + */ + public C clients(String realmName, Class clientsClass) { + try { + Constructor constructor = clientsClass.getDeclaredConstructor(ResteasyClientProvider.class, WebTarget.class, String.class); + return constructor.newInstance(CLIENT_PROVIDER, target, realmName); + } catch (NoSuchMethodException | InstantiationException | IllegalAccessException | InvocationTargetException e) { + throw new RuntimeException(e); + } } } diff --git a/rest/admin-v2/tests/src/test/java/org/keycloak/tests/admin/client/v2/ClientApiV2Test.java b/rest/admin-v2/tests/src/test/java/org/keycloak/tests/admin/client/v2/ClientApiV2Test.java index a99753221d40..ab28450db6a2 100644 --- a/rest/admin-v2/tests/src/test/java/org/keycloak/tests/admin/client/v2/ClientApiV2Test.java +++ b/rest/admin-v2/tests/src/test/java/org/keycloak/tests/admin/client/v2/ClientApiV2Test.java @@ -27,6 +27,7 @@ import jakarta.ws.rs.core.MediaType; import org.keycloak.admin.client.Keycloak; +import org.keycloak.admin.client.wrapper.Clients; import org.keycloak.authentication.authenticators.client.ClientIdAndSecretAuthenticator; import org.keycloak.authentication.authenticators.client.JWTClientAuthenticator; import org.keycloak.authentication.authenticators.client.JWTClientSecretAuthenticator; @@ -98,10 +99,14 @@ public class ClientApiV2Test extends AbstractClientApiV2Test{ @Test public void getClient() { - var client = adminClient.clients(testRealm.getName()).v2().client("account").getClient(); + var client = clients(testRealm.getName()).v2().client("account").getClient(); assertEquals("account", client.getClientId()); } + private Clients clients(String realmName) { + return adminClient.clients(realmName, Clients.class); + } + @Test public void jsonPatchClient() throws Exception { HttpPatch request = new HttpPatch(getClientApiUrl(testClient.getClientId())); @@ -117,7 +122,7 @@ public void jsonPatchClient() throws Exception { public void jsonMergePatchClient() { OIDCClientRepresentation patch = new OIDCClientRepresentation(); patch.setDescription("I'm also a description"); - BaseClientRepresentation baseRep = adminClient.clients(masterRealm.getName()).v2().client(testClient.getClientId()).patchClient(mapper.valueToTree(patch)); + BaseClientRepresentation baseRep = clients(masterRealm.getName()).v2().client(testClient.getClientId()).patchClient(mapper.valueToTree(patch)); assertEquals("I'm also a description", baseRep.getDescription()); } @@ -155,7 +160,7 @@ public void putFailsWithDifferentClientId() { OIDCClientRepresentation rep = new OIDCClientRepresentation(); rep.setClientId("other"); - try (var response = adminClient.clients(testRealm.getName()).v2().client("account").createOrUpdateClient(rep)) { + try (var response = clients(testRealm.getName()).v2().client("account").createOrUpdateClient(rep)) { assertEquals(400, response.getStatus()); } } @@ -168,7 +173,7 @@ public void putCreateOrUpdates() { rep.setClientId(clientId); rep.setDescription("I'm new"); - try (var response = adminClient.clients(testRealm.getName()).v2().client(clientId).createOrUpdateClient(rep)) { + try (var response = clients(testRealm.getName()).v2().client(clientId).createOrUpdateClient(rep)) { assertEquals(201, response.getStatus()); OIDCClientRepresentation client = response.readEntity(OIDCClientRepresentation.class); assertEquals("I'm new", client.getDescription()); @@ -177,7 +182,7 @@ public void putCreateOrUpdates() { rep.setDescription("I'm updated"); - try (var response = adminClient.clients(testRealm.getName()).v2().client(clientId).createOrUpdateClient(rep)) { + try (var response = clients(testRealm.getName()).v2().client(clientId).createOrUpdateClient(rep)) { assertEquals(200, response.getStatus()); OIDCClientRepresentation client = response.readEntity(OIDCClientRepresentation.class); assertEquals("I'm updated", client.getDescription()); @@ -193,7 +198,7 @@ public void createClient() { rep.setClientId(clientId); rep.setDescription("I'm new"); - try (var response = adminClient.clients(testRealm.getName()).v2().createClient(rep)) { + try (var response = clients(testRealm.getName()).v2().createClient(rep)) { assertThat(response.getStatus(), is(201)); OIDCClientRepresentation client = response.readEntity(OIDCClientRepresentation.class); assertThat(client.getEnabled(), is(true)); @@ -201,7 +206,7 @@ public void createClient() { assertThat(client.getDescription(), is("I'm new")); } - try (var response = adminClient.clients(testRealm.getName()).v2().createClient(rep)) { + try (var response = clients(testRealm.getName()).v2().createClient(rep)) { assertThat(response.getStatus(), is(409)); } } @@ -213,21 +218,21 @@ public void deleteClient() { rep.setClientId(clientIdToDelete); rep.setEnabled(true); - try (var response = adminClient.clients(testRealm.getName()).v2().createClient(rep)) { + try (var response = clients(testRealm.getName()).v2().createClient(rep)) { assertEquals(201, response.getStatus()); } - var baseClientRepresentation = adminClient.clients(testRealm.getName()).v2().client(clientIdToDelete).getClient(); + var baseClientRepresentation = clients(testRealm.getName()).v2().client(clientIdToDelete).getClient(); assertEquals(clientIdToDelete, baseClientRepresentation.getClientId()); - try (var response = adminClient.clients(testRealm.getName()).v2().client(clientIdToDelete).deleteClient()) { + try (var response = clients(testRealm.getName()).v2().client(clientIdToDelete).deleteClient()) { assertEquals(204, response.getStatus()); } NotFoundException exception = assertThrows( NotFoundException.class, - () -> adminClient.clients(testRealm.getName()).v2().client(clientIdToDelete).getClient()); + () -> clients(testRealm.getName()).v2().client(clientIdToDelete).getClient()); assertTrue(exception.getMessage().contains("HTTP 404 Not Found")); } @@ -243,7 +248,7 @@ public void getClientsMixedProtocols() throws JsonProcessingException { oidcRep.setLoginFlows(Set.of(OIDCClientRepresentation.Flow.STANDARD, OIDCClientRepresentation.Flow.DIRECT_GRANT)); oidcRep.setWebOrigins(Set.of("http://localhost:3000", "http://localhost:4000")); - try (var response = adminClient.clients(testRealm.getName()).v2().createClient(oidcRep)) { + try (var response = clients(testRealm.getName()).v2().createClient(oidcRep)) { assertEquals(201, response.getStatus()); OIDCClientRepresentation created = response.readEntity(OIDCClientRepresentation.class); assertThat(created, notNullValue()); @@ -262,7 +267,7 @@ public void getClientsMixedProtocols() throws JsonProcessingException { samlRep.setForcePostBinding(true); samlRep.setFrontChannelLogout(false); - try (var response = adminClient.clients(testRealm.getName()).v2().createClient(samlRep)) { + try (var response = clients(testRealm.getName()).v2().createClient(samlRep)) { assertEquals(201, response.getStatus()); SAMLClientRepresentation created = response.readEntity(SAMLClientRepresentation.class); assertThat(created, notNullValue()); @@ -270,7 +275,7 @@ public void getClientsMixedProtocols() throws JsonProcessingException { } // Get all clients - this should work with mixed protocols - try (Stream baseClientRepresentationStream = adminClient.clients(testRealm.getName()).v2().getClients()) { + try (Stream baseClientRepresentationStream = clients(testRealm.getName()).v2().getClients()) { List clients = baseClientRepresentationStream.toList(); // Verify OIDC client with protocol-specific fields @@ -301,13 +306,13 @@ public void getClientsMixedProtocols() throws JsonProcessingException { // Get individual OIDC client and verify OIDC-specific fields - OIDCClientRepresentation oidcClient = (OIDCClientRepresentation) adminClient.clients(testRealm.getName()).v2().client(oidcRep.getClientId()).getClient(); + OIDCClientRepresentation oidcClient = (OIDCClientRepresentation) clients(testRealm.getName()).v2().client(oidcRep.getClientId()).getClient(); assertEquals("mixed-test-oidc", oidcClient.getClientId()); assertThat(oidcClient.getLoginFlows(), is(Set.of(OIDCClientRepresentation.Flow.STANDARD, OIDCClientRepresentation.Flow.DIRECT_GRANT))); assertThat(oidcClient.getWebOrigins(), is(Set.of("http://localhost:3000", "http://localhost:4000"))); // Get individual SAML client and verify SAML-specific fields - SAMLClientRepresentation samlClient = (SAMLClientRepresentation) adminClient.clients(testRealm.getName()).v2().client(samlRep.getClientId()).getClient(); + SAMLClientRepresentation samlClient = (SAMLClientRepresentation) clients(testRealm.getName()).v2().client(samlRep.getClientId()).getClient(); assertEquals("mixed-test-saml", samlClient.getClientId()); assertEquals("SAML client for mixed protocol test", samlClient.getDescription()); assertThat(samlClient.getNameIdFormat(), is("email")); @@ -323,7 +328,7 @@ public void OIDCClientRepresentationValidation() { oidcRep.setDisplayName("something"); oidcRep.setAppUrl("notUrl"); - try (var response = adminClient.clients(testRealm.getName()).v2().createClient(oidcRep)) { + try (var response = clients(testRealm.getName()).v2().createClient(oidcRep)) { assertThat(response, notNullValue()); assertThat(response.getStatus(), is(400)); @@ -343,7 +348,7 @@ public void OIDCClientRepresentationValidation() { auth.setMethod("missing-enabled"); oidcRep.setAuth(auth); - try (var response = adminClient.clients(testRealm.getName()).v2().createClient(oidcRep)) { + try (var response = clients(testRealm.getName()).v2().createClient(oidcRep)) { assertThat(response, notNullValue()); assertThat(response.getStatus(), is(400)); var body = response.readEntity(ViolationExceptionResponse.class); @@ -367,7 +372,7 @@ public void authenticationRequired() throws Exception { public void createFullClient() { OIDCClientRepresentation rep = getTestingFullClientRep(); - try (var response = adminClient.clients(testRealm.getName()).v2().createClient(rep);) { + try (var response = clients(testRealm.getName()).v2().createClient(rep);) { assertEquals(201, response.getStatus()); OIDCClientRepresentation client = response.readEntity(OIDCClientRepresentation.class); rep.setUuid(client.getUuid()); // needed for the use of equals() @@ -380,7 +385,7 @@ public void createFullClientWrongServiceAccountRoles() { OIDCClientRepresentation rep = getTestingFullClientRep(); rep.setServiceAccountRoles(Set.of("non-existing", "bad-role")); - try (var response = adminClient.clients(testRealm.getName()).v2().createClient(rep);) { + try (var response = clients(testRealm.getName()).v2().createClient(rep);) { assertEquals(400, response.getStatus()); assertThat(response.readEntity(String.class), containsString("Cannot assign role to the service account (field 'serviceAccount.roles') as it does not exist")); } @@ -394,7 +399,7 @@ public void declarativeRoleManagement() { rep.setEnabled(true); rep.setRoles(Set.of("role1", "role2", "role3")); - try (var response = adminClient.clients(testRealm.getName()).v2().createClient(rep)) { + try (var response = clients(testRealm.getName()).v2().createClient(rep)) { assertEquals(201, response.getStatus()); OIDCClientRepresentation created = response.readEntity(OIDCClientRepresentation.class); assertThat(created.getRoles(), is(Set.of("role1", "role2", "role3"))); @@ -402,7 +407,7 @@ public void declarativeRoleManagement() { // 2. Update with completely new roles - should remove old ones and add new ones rep.setRoles(Set.of("new-role1", "new-role2")); - try (var response = adminClient.clients(testRealm.getName()).v2().client("declarative-role-test").createOrUpdateClient(rep)) { + try (var response = clients(testRealm.getName()).v2().client("declarative-role-test").createOrUpdateClient(rep)) { assertEquals(200, response.getStatus()); OIDCClientRepresentation updated = response.readEntity(OIDCClientRepresentation.class); assertThat(updated.getRoles(), is(Set.of("new-role1", "new-role2"))); @@ -410,7 +415,7 @@ public void declarativeRoleManagement() { // 3. Update with partial overlap - keep some, add some, remove some rep.setRoles(Set.of("new-role1", "add-role3", "add-role4")); - try (var response = adminClient.clients(testRealm.getName()).v2().client("declarative-role-test").createOrUpdateClient(rep)) { + try (var response = clients(testRealm.getName()).v2().client("declarative-role-test").createOrUpdateClient(rep)) { assertEquals(200, response.getStatus()); OIDCClientRepresentation updated = response.readEntity(OIDCClientRepresentation.class); assertThat(updated.getRoles(), is(Set.of("new-role1", "add-role3", "add-role4"))); @@ -418,7 +423,7 @@ public void declarativeRoleManagement() { // 4. Update with same roles - should be idempotent rep.setRoles(Set.of("new-role1", "add-role3", "add-role4")); - try (var response = adminClient.clients(testRealm.getName()).v2().client("declarative-role-test").createOrUpdateClient(rep)) { + try (var response = clients(testRealm.getName()).v2().client("declarative-role-test").createOrUpdateClient(rep)) { assertEquals(200, response.getStatus()); OIDCClientRepresentation updated = response.readEntity(OIDCClientRepresentation.class); assertThat(updated.getRoles(), is(Set.of("new-role1", "add-role3", "add-role4"))); @@ -426,7 +431,7 @@ public void declarativeRoleManagement() { // 5. Update with empty set - should remove all roles rep.setRoles(Set.of()); - try (var response = adminClient.clients(testRealm.getName()).v2().client("declarative-role-test").createOrUpdateClient(rep)) { + try (var response = clients(testRealm.getName()).v2().client("declarative-role-test").createOrUpdateClient(rep)) { assertEquals(200, response.getStatus()); OIDCClientRepresentation updated = response.readEntity(OIDCClientRepresentation.class); assertThat(updated.getRoles(), is(Set.of())); @@ -444,7 +449,7 @@ public void declarativeServiceAccountRoleManagement() { rep.setLoginFlows(Set.of(OIDCClientRepresentation.Flow.SERVICE_ACCOUNT)); rep.setServiceAccountRoles(Set.of(defaultRealmRoles, "offline_access")); - try (var response = adminClient.clients(testRealm.getName()).v2().createClient(rep)) { + try (var response = clients(testRealm.getName()).v2().createClient(rep)) { assertEquals(201, response.getStatus()); OIDCClientRepresentation created = response.readEntity(OIDCClientRepresentation.class); assertThat(created.getServiceAccountRoles(), is(Set.of(defaultRealmRoles, "offline_access"))); @@ -452,7 +457,7 @@ public void declarativeServiceAccountRoleManagement() { // 2. Update with completely new roles - should remove old ones and add new ones rep.setServiceAccountRoles(Set.of("uma_authorization", "offline_access")); - try (var response = adminClient.clients(testRealm.getName()).v2().client("sa-declarative-test").createOrUpdateClient(rep)) { + try (var response = clients(testRealm.getName()).v2().client("sa-declarative-test").createOrUpdateClient(rep)) { assertEquals(200, response.getStatus()); OIDCClientRepresentation updated = response.readEntity(OIDCClientRepresentation.class); assertThat(updated.getServiceAccountRoles(), is(Set.of("uma_authorization", "offline_access"))); @@ -460,7 +465,7 @@ public void declarativeServiceAccountRoleManagement() { // 3. Update with partial overlap - keep some, add some, remove some rep.setServiceAccountRoles(Set.of("offline_access", defaultRealmRoles)); - try (var response = adminClient.clients(testRealm.getName()).v2().client("sa-declarative-test").createOrUpdateClient(rep)) { + try (var response = clients(testRealm.getName()).v2().client("sa-declarative-test").createOrUpdateClient(rep)) { assertEquals(200, response.getStatus()); OIDCClientRepresentation updated = response.readEntity(OIDCClientRepresentation.class); assertThat(updated.getServiceAccountRoles(), is(Set.of("offline_access", defaultRealmRoles))); @@ -468,7 +473,7 @@ public void declarativeServiceAccountRoleManagement() { // 4. Update with same roles - should be idempotent rep.setServiceAccountRoles(Set.of("offline_access", defaultRealmRoles)); - try (var response = adminClient.clients(testRealm.getName()).v2().client("sa-declarative-test").createOrUpdateClient(rep)) { + try (var response = clients(testRealm.getName()).v2().client("sa-declarative-test").createOrUpdateClient(rep)) { assertEquals(200, response.getStatus()); OIDCClientRepresentation updated = response.readEntity(OIDCClientRepresentation.class); assertThat(updated.getServiceAccountRoles(), is(Set.of("offline_access", defaultRealmRoles))); @@ -476,7 +481,7 @@ public void declarativeServiceAccountRoleManagement() { // 5. Update with empty set - should remove all roles rep.setServiceAccountRoles(Set.of()); - try (var response = adminClient.clients(testRealm.getName()).v2().client("sa-declarative-test").createOrUpdateClient(rep)) { + try (var response = clients(testRealm.getName()).v2().client("sa-declarative-test").createOrUpdateClient(rep)) { assertEquals(200, response.getStatus()); OIDCClientRepresentation updated = response.readEntity(OIDCClientRepresentation.class); assertThat(updated.getServiceAccountRoles(), is(Set.of())); @@ -657,14 +662,14 @@ void createClientWithPostAndGeneratedSecret(String authenticationMethod) { auth.setSecret(null); client.setAuth(auth); - try (var response = adminClient.clients(testRealm.getName()).v2().createClient(client)) { + try (var response = clients(testRealm.getName()).v2().createClient(client)) { var createdClient = response.readEntity(OIDCClientRepresentation.class); assertThat(createdClient.getAuth(), notNullValue()); assertThat(createdClient.getAuth().getSecret(), Matchers.not(emptyOrNullString())); } // make sure that the created model was persisted and GET method returns the newly generated secret - var persistedClient = (OIDCClientRepresentation) adminClient.clients(testRealm.getName()).v2().client(clientId).getClient(); + var persistedClient = (OIDCClientRepresentation) clients(testRealm.getName()).v2().client(clientId).getClient(); assertEquals(clientId, persistedClient.getClientId()); assertThat(persistedClient.getAuth().getSecret(), Matchers.not(emptyOrNullString())); } @@ -863,7 +868,7 @@ void usePutToTurnConfidentialClientToPublicOne(String authenticationMethod) thro private OIDCClientRepresentation.Auth getResultingAuthConfigPost(OIDCClientRepresentation.Auth auth, String clientId, String... additionalFields) throws IllegalArgumentException { var rep = getResultingClientRep(auth, clientId, additionalFields); - try (var response = adminClient.clients(testRealm.getName()).v2().createClient(rep)) { + try (var response = clients(testRealm.getName()).v2().createClient(rep)) { assertThat(response.getStatus(), Matchers.anyOf(is(201), is(200))); OIDCClientRepresentation createdClient = response.readEntity(OIDCClientRepresentation.class); return assertClientEnabledIdDescriptionAndAuth(rep, createdClient); @@ -872,7 +877,7 @@ private OIDCClientRepresentation.Auth getResultingAuthConfigPost(OIDCClientRepre private OIDCClientRepresentation.Auth getResultingAuthConfigPut(OIDCClientRepresentation.Auth auth, String clientId, String... additionalFields) throws IllegalArgumentException { var rep = getResultingClientRep(auth, clientId, additionalFields); - try (var response = adminClient.clients(testRealm.getName()).v2().client(clientId).createOrUpdateClient(rep)) { + try (var response = clients(testRealm.getName()).v2().client(clientId).createOrUpdateClient(rep)) { assertThat(response.getStatus(), Matchers.anyOf(is(201), is(200))); OIDCClientRepresentation createdClient = response.readEntity(OIDCClientRepresentation.class); return assertClientEnabledIdDescriptionAndAuth(rep, createdClient); @@ -881,7 +886,7 @@ private OIDCClientRepresentation.Auth getResultingAuthConfigPut(OIDCClientRepres private OIDCClientRepresentation.Auth getResultingAuthConfigPatch(OIDCClientRepresentation.Auth auth, String clientId, String... additionalFields) throws IllegalArgumentException { var rep = getResultingClientRep(auth, clientId, additionalFields); - OIDCClientRepresentation createdClient = (OIDCClientRepresentation) adminClient.clients(testRealm.getName()).v2().client(clientId).patchClient(mapper.valueToTree(rep)); + OIDCClientRepresentation createdClient = (OIDCClientRepresentation) clients(testRealm.getName()).v2().client(clientId).patchClient(mapper.valueToTree(rep)); return assertClientEnabledIdDescriptionAndAuth(rep, createdClient); } @@ -920,7 +925,7 @@ private OIDCClientRepresentation.Auth assertClientEnabledIdDescriptionAndAuth(OI * This verifies that ValidationUtil.validateClient is called after the full model is populated. */ private void assertClientCreationFailsWithError(BaseClientRepresentation rep, String expectedErrorMessage) throws Exception { - try (var response = adminClient.clients(testRealm.getName()).v2().createClient(rep)) { + try (var response = clients(testRealm.getName()).v2().createClient(rep)) { assertThat(response.getStatus(), is(400)); String body = response.readEntity(String.class); assertThat(body, containsString(expectedErrorMessage)); @@ -944,7 +949,7 @@ private void assertClientUpdateFailsWithError(BaseClientRepresentation rep, Stri validRep.setClientId(clientId); validRep.setEnabled(true); - try (var response = adminClient.clients(testRealm.getName()).v2().createClient(validRep)) { + try (var response = clients(testRealm.getName()).v2().createClient(validRep)) { assertThat(response.getStatus(), is(201)); BaseClientRepresentation created = response.readEntity(BaseClientRepresentation.class); assertThat(created, notNullValue()); @@ -952,7 +957,7 @@ private void assertClientUpdateFailsWithError(BaseClientRepresentation rep, Stri } // Now try to update with invalid data - try (var response = adminClient.clients(testRealm.getName()).v2().client(clientId).createOrUpdateClient(rep)) { + try (var response = clients(testRealm.getName()).v2().client(clientId).createOrUpdateClient(rep)) { assertThat(response.getStatus(), is(400)); String body = response.readEntity(String.class); assertThat(body, containsString(expectedErrorMessage));