From f1ee29590002672e53fafcd4c18a8ab86bbcad5e Mon Sep 17 00:00:00 2001 From: SirSkizo Date: Mon, 20 Jun 2022 13:14:27 +0200 Subject: [PATCH 1/4] feat: Enhacements in the pagination when fecthing several items from the Keycloak DB Modify methods: listAllUsers, getGroupMembers --- .../client/bl/KeycloakClientLogic.java | 78 +++++++++---------- .../KeycloakAuthAdminResource.java | 3 +- .../keycloak/client/LogicCRUDTest.java | 2 +- .../keycloak/client/LogicGroupTest.java | 54 +++++++------ 4 files changed, 71 insertions(+), 66 deletions(-) diff --git a/src/main/java/com/trikorasolutions/keycloak/client/bl/KeycloakClientLogic.java b/src/main/java/com/trikorasolutions/keycloak/client/bl/KeycloakClientLogic.java index f813515..d7cf479 100644 --- a/src/main/java/com/trikorasolutions/keycloak/client/bl/KeycloakClientLogic.java +++ b/src/main/java/com/trikorasolutions/keycloak/client/bl/KeycloakClientLogic.java @@ -236,41 +236,22 @@ public Uni deleteUser(final String realm, final String token, * @param realm the realm name in which the users are going to be queried. * @param token access token provided by the keycloak SecurityIdentity. * @param keycloakClientId id of the client (service name). + * @param bufferSize size of the sample to be queried. + * @param max maximum number of elements to be fetched from Keycloak, if you need + * all, set this parameter to -1. * @return a JsonArray of Keycloak UserRepresentations. */ public Uni> listAllUsers(final String realm, final String token, - final String keycloakClientId) { - return this.listAllUsers(realm, token, keycloakClientId, Integer.MAX_VALUE); - // Keycloak di not allow to have more than (Int32) users, so Integer.MAX_VALUE would not - // imitate the search - } - - /** - * This method return a list with all the users in the client provided as argument. It makes use - * of the first and the max params, in order to paginate the search. Making recursion with the - * mutiny, will subscribe the events sequentially. - * - * @param realm the realm name in which the users are going to be queried. - * @param token access token provided by the keycloak SecurityIdentity. - * @param keycloakClientId id of the client (service name). - * @param maxUsers maximum number of users that are desired to be queried. It has to be a - * multiple of bufferSize that ATTOW is 100. - * @return a JsonArray of Keycloak UserRepresentations. - */ - public Uni> listAllUsers(final String realm, final String token, - final String keycloakClientId, Integer maxUsers) { - int bufferSize = 100, cursor = 0; - LOGGER.info("#listAllUsers()..."); - return this.listAllUsersRec(realm, token, keycloakClientId, cursor, bufferSize, - new ArrayList<>(), maxUsers); + final String keycloakClientId, final Integer bufferSize, final Integer max) { + return this.listAllUsersRec(realm, token, keycloakClientId, 0, bufferSize, new ArrayList<>(), + (max < 0) ? Integer.MAX_VALUE : max); } private Uni> listAllUsersRec(final String realm, final String token, final String keycloakClientId, int cursor, int bufferSize, List res, Integer maxUsers) { - LOGGER.info("#listAllUsersRec(cursor,bufferSize,usersFetched)...{}-{}-{}", cursor, bufferSize, - res.size()); + LOGGER.info("#listAllUsersRec(cursor,usersFetched)...{}-{}", cursor, res.size()); return keycloakClient.listAllUsers(BEARER + token, realm, GRANT_TYPE, keycloakClientId, cursor, bufferSize) .map(KeycloakUserRepresentation::allFrom) @@ -332,7 +313,7 @@ public Uni getGroupInfo(final String realm, final String to return getGroupInfoNoEnrich(realm, token, keycloakClientId, groupName) .flatMap(group -> this.getGroupRolesById(realm, token, keycloakClientId, group.id) .map(group::addRoles)) - .flatMap(group -> this.getUsersForGroupById(realm, token, keycloakClientId, group.id) + .flatMap(group -> this.getGroupMembers(realm, token, keycloakClientId, group.name, 100, -1) .map(group::addMembers) ); } @@ -353,22 +334,41 @@ public Uni getGroupInfoNoEnrich(final String realm, final S * @param token access token provided by the keycloak SecurityIdentity. * @param keycloakClientId id of the client (service name). * @param groupName name of the group that is going to be queried. - * @return a JsonArray of UserRepresentation. + * @param bufferSize size of the sample to be queried. + * @param max maximum number of elements to be fetched from Keycloak, if you need + * all, set this parameter to -1. + * @return a JsonArray of GroupRepresentation. */ - public Uni> getUsersForGroup(final String realm, - final String token, final String keycloakClientId, final String groupName) { + public Uni> getGroupMembers(final String realm, + final String token, final String keycloakClientId, final String groupName, + final Integer bufferSize, Integer max) { + LOGGER.info("#getGroupMembers()..."); return this.getGroupInfoNoEnrich(realm, token, keycloakClientId, groupName) .map(GroupRepresentation::getId) - .flatMap(userId -> keycloakClient.getGroupUsers(BEARER + token, realm, GRANT_TYPE, - keycloakClientId, userId)) - .map(KeycloakUserRepresentation::allFrom); + .flatMap(groupId -> this.getGroupMembersRec(realm, token, keycloakClientId, groupId, 0, + bufferSize, new ArrayList<>(), (max < 0) ? Integer.MAX_VALUE : max)) + .invoke(x->LOGGER.info("#getGroupMembers()...{}",x.size())); + } - private Uni> getUsersForGroupById(final String realm, - final String token, final String keycloakClientId, final String id) { - return keycloakClient.getGroupUsers(BEARER + token, realm, GRANT_TYPE, - keycloakClientId, id) - .map(KeycloakUserRepresentation::allFrom); + private Uni> getGroupMembersRec(final String realm, + final String token, + final String keycloakClientId, final String groupId, int cursor, int bufferSize, + List res, Integer max) { + LOGGER.info("#getGroupMembersRec(cursor, usersFetched)...{}-{}", cursor, res.size()); + return keycloakClient.getGroupUsers(BEARER + token, realm, GRANT_TYPE, keycloakClientId, + groupId, cursor, bufferSize) + .map(KeycloakUserRepresentation::allFrom) + .flatMap(currentSelection -> { + res.addAll(currentSelection); + if (currentSelection.size() < bufferSize || currentSelection.size() >= max) { + return Uni.createFrom().item(res); // Recursion Base case + } else { + return this.getGroupMembersRec(realm, token, keycloakClientId, groupId, + cursor + bufferSize, + bufferSize, res, max); + } + }); } /** @@ -548,7 +548,7 @@ public Uni> getAllUserInEffectiveRole(final Stri .combinedWith((users, groups) -> { Builder> builder = Uni.join().builder(); for (GroupRepresentation group : groups) { - builder.add(this.getUsersForGroup(realm, token, keycloakClientId, group.name)); + builder.add(this.getGroupMembers(realm, token, keycloakClientId, group.name,100,-1)); } return builder.joinAll().andCollectFailures() .map(listOfList -> listOfList.stream() diff --git a/src/main/java/com/trikorasolutions/keycloak/client/clientresource/KeycloakAuthAdminResource.java b/src/main/java/com/trikorasolutions/keycloak/client/clientresource/KeycloakAuthAdminResource.java index 6579797..282e94d 100644 --- a/src/main/java/com/trikorasolutions/keycloak/client/clientresource/KeycloakAuthAdminResource.java +++ b/src/main/java/com/trikorasolutions/keycloak/client/clientresource/KeycloakAuthAdminResource.java @@ -145,7 +145,8 @@ Uni getGroupInfo(@HeaderParam("Authorization") String bearerToken, @Produces(MediaType.APPLICATION_JSON) Uni getGroupUsers(@HeaderParam("Authorization") String bearerToken, @PathParam("realm") String realm, @QueryParam("grant_type") String grantType, - @QueryParam("client_id") String clientId, @PathParam("id") String id); + @QueryParam("client_id") String clientId, @PathParam("id") String id, + @QueryParam("first") Integer first, @QueryParam("max") Integer max); /** * Return all the groups of a given user. diff --git a/src/test/java/com/trikorasolutions/keycloak/client/LogicCRUDTest.java b/src/test/java/com/trikorasolutions/keycloak/client/LogicCRUDTest.java index 169524f..e210b50 100644 --- a/src/test/java/com/trikorasolutions/keycloak/client/LogicCRUDTest.java +++ b/src/test/java/com/trikorasolutions/keycloak/client/LogicCRUDTest.java @@ -306,7 +306,7 @@ public void testListKeycloakUsers() { String accessToken = tkrKcCli.getAccessToken(ADM); List logicResponse = keycloakClientLogic.listAllUsers( - tkrKcCli.getRealmName(), accessToken, tkrKcCli.getClientId()) + tkrKcCli.getRealmName(), accessToken, tkrKcCli.getClientId(), 100,-1) .await().indefinitely(); List usernameList = logicResponse.stream() diff --git a/src/test/java/com/trikorasolutions/keycloak/client/LogicGroupTest.java b/src/test/java/com/trikorasolutions/keycloak/client/LogicGroupTest.java index 7c3919e..345cd34 100644 --- a/src/test/java/com/trikorasolutions/keycloak/client/LogicGroupTest.java +++ b/src/test/java/com/trikorasolutions/keycloak/client/LogicGroupTest.java @@ -8,19 +8,18 @@ import org.junit.jupiter.api.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; - import javax.inject.Inject; - import java.util.List; import java.util.stream.Collectors; import static com.trikorasolutions.keycloak.client.TrikoraKeycloakClientInfo.ADM; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.*; -import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; @QuarkusTest public class LogicGroupTest { + private static final Logger LOGGER = LoggerFactory.getLogger(LogicGroupTest.class); @Inject @@ -34,11 +33,12 @@ public void testGroupInfoOk() { String accessToken = tkrKcCli.getAccessToken(ADM); GroupRepresentation logicResponse; - logicResponse = keycloakClientLogic.getGroupInfo(tkrKcCli.getRealmName(), accessToken, tkrKcCli.getClientId(), - "TENANT_TEST").await().indefinitely(); + logicResponse = keycloakClientLogic.getGroupInfo(tkrKcCli.getRealmName(), accessToken, + tkrKcCli.getClientId(), + "TENANT_TEST").await().indefinitely(); assertThat(logicResponse.getName(), is("TENANT_TEST")); - LOGGER.info("test{}",logicResponse); + LOGGER.info("test{}", logicResponse); } @Test @@ -47,11 +47,10 @@ public void testGroupInfoErr() { try { keycloakClientLogic.getGroupInfo(tkrKcCli.getRealmName(), accessToken, tkrKcCli.getClientId(), - "unknown").onFailure(NoSuchGroupException.class).transform(x -> { + "unknown").onFailure(NoSuchGroupException.class).transform(x -> { throw (NoSuchGroupException) x; }).await().indefinitely(); - - assertTrue(false); + fail(); } catch (NoSuchGroupException ex) { assertThat(ex.getClass(), is(NoSuchGroupException.class)); assertThat(ex.getMessage(), containsString("unknown")); @@ -63,12 +62,13 @@ public void testGroupListUsers() { String accessToken = tkrKcCli.getAccessToken(ADM); List logicResponse; - logicResponse = keycloakClientLogic.getUsersForGroup(tkrKcCli.getRealmName(), accessToken, tkrKcCli.getClientId(), - "TENANT_TEST").await().indefinitely(); + logicResponse = keycloakClientLogic.getGroupMembers(tkrKcCli.getRealmName(), accessToken, + tkrKcCli.getClientId(), + "TENANT_TEST", 100, -1).await().indefinitely(); List userRepresentation = logicResponse.stream() - .map(user -> user.username) - .collect(Collectors.toList()); + .map(user -> user.username) + .collect(Collectors.toList()); assertThat(userRepresentation.size(), greaterThanOrEqualTo(1)); assertThat(userRepresentation, hasItem(ADM)); } @@ -80,8 +80,9 @@ public void testPutAndRemoveUserInGroup() { List logicResponse2; // Put a new user in the group - logicResponse = keycloakClientLogic.putUserInGroup(tkrKcCli.getRealmName(), accessToken, tkrKcCli.getClientId(), - "mrsquare", "TENANT_TEST").await().indefinitely(); + logicResponse = keycloakClientLogic.putUserInGroup(tkrKcCli.getRealmName(), accessToken, + tkrKcCli.getClientId(), + "mrsquare", "TENANT_TEST").await().indefinitely(); assertThat(logicResponse.username, is("mrsquare")); assertThat(logicResponse.groups.stream() @@ -89,25 +90,28 @@ public void testPutAndRemoveUserInGroup() { .collect(Collectors.toList()), hasItem("TENANT_TEST")); // Check if the change has been persisted in keycloak - logicResponse2 = keycloakClientLogic.getUsersForGroup(tkrKcCli.getRealmName(), accessToken, tkrKcCli.getClientId(), - "TENANT_TEST").await().indefinitely(); + logicResponse2 = keycloakClientLogic.getGroupMembers(tkrKcCli.getRealmName(), accessToken, + tkrKcCli.getClientId(), + "TENANT_TEST", 3, -1).await().indefinitely(); List userRepresentation = logicResponse2.stream() - .map(user -> user.username) - .collect(Collectors.toList()); + .map(user -> user.username) + .collect(Collectors.toList()); assertThat(userRepresentation.size(), greaterThanOrEqualTo(1)); assertThat(userRepresentation, hasItem("mrsquare")); // Kick the user out of the group - logicResponse = keycloakClientLogic.deleteUserFromGroup(tkrKcCli.getRealmName(), accessToken, tkrKcCli.getClientId(), - "mrsquare", "TENANT_TEST").await().indefinitely(); + logicResponse = keycloakClientLogic.deleteUserFromGroup(tkrKcCli.getRealmName(), accessToken, + tkrKcCli.getClientId(), + "mrsquare", "TENANT_TEST").await().indefinitely(); assertThat(logicResponse.username, is("mrsquare")); // Check if the change has been persisted in keycloak - logicResponse2 = keycloakClientLogic.getUsersForGroup(tkrKcCli.getRealmName(), accessToken, tkrKcCli.getClientId(), - "TENANT_TEST").await().indefinitely(); + logicResponse2 = keycloakClientLogic.getGroupMembers(tkrKcCli.getRealmName(), accessToken, + tkrKcCli.getClientId(), + "TENANT_TEST", 100, -1).await().indefinitely(); userRepresentation = logicResponse2.stream() - .map(user -> user.username) - .collect(Collectors.toList()); + .map(user -> user.username) + .collect(Collectors.toList()); assertThat(userRepresentation.size(), greaterThanOrEqualTo(0)); assertThat(userRepresentation, not(hasItem("mrsquare"))); } From 8de7fadc7bd0021c9f1386d4cd61767d00e3db09 Mon Sep 17 00:00:00 2001 From: SirSkizo Date: Mon, 20 Jun 2022 13:16:45 +0200 Subject: [PATCH 2/4] chore: code refractor --- .../keycloak/client/bl/KeycloakClientLogic.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/java/com/trikorasolutions/keycloak/client/bl/KeycloakClientLogic.java b/src/main/java/com/trikorasolutions/keycloak/client/bl/KeycloakClientLogic.java index d7cf479..fffc07b 100644 --- a/src/main/java/com/trikorasolutions/keycloak/client/bl/KeycloakClientLogic.java +++ b/src/main/java/com/trikorasolutions/keycloak/client/bl/KeycloakClientLogic.java @@ -346,8 +346,7 @@ public Uni> getGroupMembers(final String realm, return this.getGroupInfoNoEnrich(realm, token, keycloakClientId, groupName) .map(GroupRepresentation::getId) .flatMap(groupId -> this.getGroupMembersRec(realm, token, keycloakClientId, groupId, 0, - bufferSize, new ArrayList<>(), (max < 0) ? Integer.MAX_VALUE : max)) - .invoke(x->LOGGER.info("#getGroupMembers()...{}",x.size())); + bufferSize, new ArrayList<>(), (max < 0) ? Integer.MAX_VALUE : max)); } From 30f8732caa6694ad62d08011f0afa5c948d32ce2 Mon Sep 17 00:00:00 2001 From: SirSkizo Date: Wed, 22 Jun 2022 11:10:39 +0200 Subject: [PATCH 3/4] feat: pagination on listing functions --- .../client/bl/KeycloakClientLogic.java | 106 ++++++++++++------ src/main/resources/application.properties | 10 +- .../keycloak/client/LogicCRUDTest.java | 23 +++- .../keycloak/client/LogicGroupTest.java | 10 +- .../client/TrikoraKeycloakClientInfo.java | 2 +- 5 files changed, 103 insertions(+), 48 deletions(-) diff --git a/src/main/java/com/trikorasolutions/keycloak/client/bl/KeycloakClientLogic.java b/src/main/java/com/trikorasolutions/keycloak/client/bl/KeycloakClientLogic.java index fffc07b..5375d5e 100644 --- a/src/main/java/com/trikorasolutions/keycloak/client/bl/KeycloakClientLogic.java +++ b/src/main/java/com/trikorasolutions/keycloak/client/bl/KeycloakClientLogic.java @@ -10,12 +10,14 @@ import io.restassured.RestAssured; import io.smallrye.mutiny.Uni; import io.smallrye.mutiny.groups.UniJoin.Builder; +import org.eclipse.microprofile.config.inject.ConfigProperty; import org.eclipse.microprofile.rest.client.inject.RestClient; import org.jboss.resteasy.reactive.ClientWebApplicationException; import org.keycloak.representations.AccessTokenResponse; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.enterprise.context.ApplicationScoped; +import javax.inject.Inject; import java.util.ArrayList; import java.util.LinkedHashSet; import java.util.List; @@ -34,6 +36,9 @@ public class KeycloakClientLogic { private static final String GRANT_TYPE = "implicit"; private static final String GRANT_TYPE_PS = "password"; + @ConfigProperty(name = "trikora.keycloak.buffer-size") + private Integer KC_BUFFER_SIZE; + @RestClient KeycloakAuthAdminResource keycloakClient; @@ -62,7 +67,7 @@ public Uni getTokenForUser(final String realm, final String keycloakClie .as(AccessTokenResponse.class) .getToken(); return Uni.createFrom().item(tok); - + // return keycloakUserClient.getToken(realm, "client_credentials", keycloakClientId, secret) // .onFailure().invoke(ex->LOGGER.warn("ERR KC: {}." + ex)) // .map(jsonArray -> (jsonArray.size() != 1) ? null : jsonArray.get(0).asJsonObject()) @@ -230,38 +235,54 @@ public Uni deleteUser(final String realm, final String token, /** * This method return a list with all the users in the client provided as argument. It makes use - * of the first and the max params, in order to paginate the search. Making recursion with the - * mutiny, will subscribe the events sequentially. + * of the Keycloak first and the max params, in order to paginate the search. Making recursion + * with the mutiny, will subscribe the events sequentially. + * + * @param realm the realm name in which the users are going to be queried. + * @param token access token provided by the keycloak SecurityIdentity. + * @param keycloakClientId id of the client (service name). + * @return a JsonArray of Keycloak UserRepresentations. + */ + public Uni> listAllUsers(final String realm, final String token, + final String keycloakClientId) { + return this.listAllUsersRec(realm, token, keycloakClientId, 0, Integer.MAX_VALUE, + new ArrayList<>()); + } + + /** + * This method return a list with the users in the client provided as argument. It makes use of + * the Keycloak first and the max params, in order to paginate the search. This method is useful + * to paginate the users * * @param realm the realm name in which the users are going to be queried. * @param token access token provided by the keycloak SecurityIdentity. * @param keycloakClientId id of the client (service name). - * @param bufferSize size of the sample to be queried. - * @param max maximum number of elements to be fetched from Keycloak, if you need - * all, set this parameter to -1. + * @param first first user to be fetched + * @param recCount number of users to be fetched from the first one * @return a JsonArray of Keycloak UserRepresentations. */ public Uni> listAllUsers(final String realm, final String token, - final String keycloakClientId, final Integer bufferSize, final Integer max) { - return this.listAllUsersRec(realm, token, keycloakClientId, 0, bufferSize, new ArrayList<>(), - (max < 0) ? Integer.MAX_VALUE : max); + final String keycloakClientId, Integer first, Integer recCount) { + + return this.listAllUsersRec(realm, token, keycloakClientId, first, recCount, + new ArrayList<>()); } private Uni> listAllUsersRec(final String realm, final String token, - final String keycloakClientId, int cursor, int bufferSize, - List res, Integer maxUsers) { - LOGGER.info("#listAllUsersRec(cursor,usersFetched)...{}-{}", cursor, res.size()); - return keycloakClient.listAllUsers(BEARER + token, realm, GRANT_TYPE, keycloakClientId, cursor, - bufferSize) + final String keycloakClientId, Integer first, Integer recCount, + List res) { + LOGGER.debug("#listAllUsersRec(first, usersFetched)...{}-{}", first, res.size()); + return keycloakClient.listAllUsers(BEARER + token, realm, GRANT_TYPE, keycloakClientId, first, + (KC_BUFFER_SIZE < (recCount - first) ? KC_BUFFER_SIZE : recCount - first)) .map(KeycloakUserRepresentation::allFrom) - .flatMap(hundredUsers -> { - res.addAll(hundredUsers); - if (hundredUsers.size() < bufferSize || hundredUsers.size() >= maxUsers) { + .flatMap(currentSelection -> { + res.addAll(currentSelection); + if (currentSelection.size() < KC_BUFFER_SIZE || res.size() >= recCount) { return Uni.createFrom().item(res); // Recursion Base case } else { - return this.listAllUsersRec(realm, token, keycloakClientId, cursor + bufferSize, - bufferSize, res, maxUsers); + return this.listAllUsersRec(realm, token, keycloakClientId, first + KC_BUFFER_SIZE, recCount, + res); } }); } @@ -313,7 +334,7 @@ public Uni getGroupInfo(final String realm, final String to return getGroupInfoNoEnrich(realm, token, keycloakClientId, groupName) .flatMap(group -> this.getGroupRolesById(realm, token, keycloakClientId, group.id) .map(group::addRoles)) - .flatMap(group -> this.getGroupMembers(realm, token, keycloakClientId, group.name, 100, -1) + .flatMap(group -> this.getGroupMembers(realm, token, keycloakClientId, group.name) .map(group::addMembers) ); } @@ -334,38 +355,53 @@ public Uni getGroupInfoNoEnrich(final String realm, final S * @param token access token provided by the keycloak SecurityIdentity. * @param keycloakClientId id of the client (service name). * @param groupName name of the group that is going to be queried. - * @param bufferSize size of the sample to be queried. - * @param max maximum number of elements to be fetched from Keycloak, if you need - * all, set this parameter to -1. * @return a JsonArray of GroupRepresentation. */ public Uni> getGroupMembers(final String realm, - final String token, final String keycloakClientId, final String groupName, - final Integer bufferSize, Integer max) { - LOGGER.info("#getGroupMembers()..."); + final String token, final String keycloakClientId, final String groupName) { return this.getGroupInfoNoEnrich(realm, token, keycloakClientId, groupName) .map(GroupRepresentation::getId) .flatMap(groupId -> this.getGroupMembersRec(realm, token, keycloakClientId, groupId, 0, - bufferSize, new ArrayList<>(), (max < 0) ? Integer.MAX_VALUE : max)); + Integer.MAX_VALUE, new ArrayList<>())); + + } + + /** + * Gets all the users that belongs to a concrete group. It can throw NoSuchGroupException. + * + * @param realm the realm name in which the users are going to be queried. + * @param token access token provided by the keycloak SecurityIdentity. + * @param keycloakClientId id of the client (service name). + * @param groupName name of the group that is going to be queried. + * @param first first user to be fetched within the members of the group + * @param recCount number of users to be fetched from the first one + * @return a JsonArray of GroupRepresentation. + */ + public Uni> getGroupMembers(final String realm, + final String token, final String keycloakClientId, final String groupName, Integer first, + Integer recCount) { + return this.getGroupInfoNoEnrich(realm, token, keycloakClientId, groupName) + .map(GroupRepresentation::getId) + .flatMap(groupId -> this.getGroupMembersRec(realm, token, keycloakClientId, groupId, first, + recCount, new ArrayList<>())); } private Uni> getGroupMembersRec(final String realm, final String token, - final String keycloakClientId, final String groupId, int cursor, int bufferSize, - List res, Integer max) { - LOGGER.info("#getGroupMembersRec(cursor, usersFetched)...{}-{}", cursor, res.size()); + final String keycloakClientId, final String groupId, Integer first, Integer recCount, + List res) { + LOGGER.debug("#getGroupMembersRec(cursor, usersFetched)...{}-{}", first, res.size()); return keycloakClient.getGroupUsers(BEARER + token, realm, GRANT_TYPE, keycloakClientId, - groupId, cursor, bufferSize) + groupId, first, (KC_BUFFER_SIZE < (recCount - first) ? KC_BUFFER_SIZE : recCount - first)) .map(KeycloakUserRepresentation::allFrom) .flatMap(currentSelection -> { res.addAll(currentSelection); - if (currentSelection.size() < bufferSize || currentSelection.size() >= max) { + if (currentSelection.size() < KC_BUFFER_SIZE || res.size() >= recCount) { return Uni.createFrom().item(res); // Recursion Base case } else { return this.getGroupMembersRec(realm, token, keycloakClientId, groupId, - cursor + bufferSize, - bufferSize, res, max); + first + KC_BUFFER_SIZE, recCount, res); } }); } @@ -547,7 +583,7 @@ public Uni> getAllUserInEffectiveRole(final Stri .combinedWith((users, groups) -> { Builder> builder = Uni.join().builder(); for (GroupRepresentation group : groups) { - builder.add(this.getGroupMembers(realm, token, keycloakClientId, group.name,100,-1)); + builder.add(this.getGroupMembers(realm, token, keycloakClientId, group.name)); } return builder.joinAll().andCollectFailures() .map(listOfList -> listOfList.stream() diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 0e76212..a14859c 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -11,7 +11,8 @@ quarkus.http.cors=true quarkus.keycloak.policy-enforcer.enable=true quarkus.keycloak.policy-enforcer.lazy-load-paths=false -trikora.realm-name=trikorasolutions +trikora.keycloak.realm-name=trikorasolutions +trikora.keycloak.buffer-size=100 # REST CLIENT #keycloak-api/mp-rest/url=https://localhost:8543/ @@ -27,6 +28,9 @@ keycloak-api/mp-rest/scope=javax.inject.Singleton # LOG # ####### -%test.quarkus.log.level=INFO +quarkus.log.level=WARN + %dev.quarkus.log.level=INFO -%test.quarkus.log.category."org.jboss.resteasy".level=INFO \ No newline at end of file + +%test.quarkus.log.level=INFO +%test.quarkus.log.category."org.jboss.resteasy".level=INFO diff --git a/src/test/java/com/trikorasolutions/keycloak/client/LogicCRUDTest.java b/src/test/java/com/trikorasolutions/keycloak/client/LogicCRUDTest.java index e210b50..8454e9c 100644 --- a/src/test/java/com/trikorasolutions/keycloak/client/LogicCRUDTest.java +++ b/src/test/java/com/trikorasolutions/keycloak/client/LogicCRUDTest.java @@ -306,14 +306,31 @@ public void testListKeycloakUsers() { String accessToken = tkrKcCli.getAccessToken(ADM); List logicResponse = keycloakClientLogic.listAllUsers( - tkrKcCli.getRealmName(), accessToken, tkrKcCli.getClientId(), 100,-1) + tkrKcCli.getRealmName(), accessToken, tkrKcCli.getClientId()) .await().indefinitely(); List usernameList = logicResponse.stream() .map(tuple -> tuple.username).collect(Collectors.toList()); - assertThat(usernameList, hasItems("jdoe", ADM, "mrsquare", "mrtriangle")); - LOGGER.info("TOTAL USERS IN REALM LIST: {}", logicResponse.size()); + LOGGER.info("TOTAL USERS IN REALM LIST: {}{}", logicResponse.size(), logicResponse.get(1)); + + // Test base case of recursion + int f = 50, m = 75; + logicResponse = keycloakClientLogic.listAllUsers( + tkrKcCli.getRealmName(), accessToken, tkrKcCli.getClientId(), f, m) + .await().indefinitely(); + assertThat(logicResponse.size(), is(m - f)); + m = 275; + logicResponse = keycloakClientLogic.listAllUsers( + tkrKcCli.getRealmName(), accessToken, tkrKcCli.getClientId(), f, m) + .await().indefinitely(); + assertThat(logicResponse.size(), is(m - f)); + f = 0; + m = 300; + logicResponse = keycloakClientLogic.listAllUsers( + tkrKcCli.getRealmName(), accessToken, tkrKcCli.getClientId(), f, m) + .await().indefinitely(); + assertThat(logicResponse.size(), is(m - f)); } @Test diff --git a/src/test/java/com/trikorasolutions/keycloak/client/LogicGroupTest.java b/src/test/java/com/trikorasolutions/keycloak/client/LogicGroupTest.java index 345cd34..19e538f 100644 --- a/src/test/java/com/trikorasolutions/keycloak/client/LogicGroupTest.java +++ b/src/test/java/com/trikorasolutions/keycloak/client/LogicGroupTest.java @@ -8,6 +8,7 @@ import org.junit.jupiter.api.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; + import javax.inject.Inject; import java.util.List; import java.util.stream.Collectors; @@ -63,8 +64,7 @@ public void testGroupListUsers() { List logicResponse; logicResponse = keycloakClientLogic.getGroupMembers(tkrKcCli.getRealmName(), accessToken, - tkrKcCli.getClientId(), - "TENANT_TEST", 100, -1).await().indefinitely(); + tkrKcCli.getClientId(), "TENANT_TEST").await().indefinitely(); List userRepresentation = logicResponse.stream() .map(user -> user.username) @@ -91,8 +91,7 @@ public void testPutAndRemoveUserInGroup() { // Check if the change has been persisted in keycloak logicResponse2 = keycloakClientLogic.getGroupMembers(tkrKcCli.getRealmName(), accessToken, - tkrKcCli.getClientId(), - "TENANT_TEST", 3, -1).await().indefinitely(); + tkrKcCli.getClientId(), "TENANT_TEST").await().indefinitely(); List userRepresentation = logicResponse2.stream() .map(user -> user.username) .collect(Collectors.toList()); @@ -107,8 +106,7 @@ public void testPutAndRemoveUserInGroup() { // Check if the change has been persisted in keycloak logicResponse2 = keycloakClientLogic.getGroupMembers(tkrKcCli.getRealmName(), accessToken, - tkrKcCli.getClientId(), - "TENANT_TEST", 100, -1).await().indefinitely(); + tkrKcCli.getClientId(), "TENANT_TEST").await().indefinitely(); userRepresentation = logicResponse2.stream() .map(user -> user.username) .collect(Collectors.toList()); diff --git a/src/test/java/com/trikorasolutions/keycloak/client/TrikoraKeycloakClientInfo.java b/src/test/java/com/trikorasolutions/keycloak/client/TrikoraKeycloakClientInfo.java index fc2bd18..8ff4408 100644 --- a/src/test/java/com/trikorasolutions/keycloak/client/TrikoraKeycloakClientInfo.java +++ b/src/test/java/com/trikorasolutions/keycloak/client/TrikoraKeycloakClientInfo.java @@ -31,7 +31,7 @@ public class TrikoraKeycloakClientInfo { @ConfigProperty(name = "quarkus.oidc.auth-server-url") protected String clientServerUrl; - @ConfigProperty(name = "trikora.realm-name") + @ConfigProperty(name = "trikora.keycloak.realm-name") protected String realmName; public String getAccessToken(String userName) { From 79928f8aef8b946e5904ad2639de36fc82202fd0 Mon Sep 17 00:00:00 2001 From: SirSkizo Date: Fri, 24 Jun 2022 11:38:25 +0200 Subject: [PATCH 4/4] feat: Add new functionality for groups create, delete, add and remove roles --- README.adoc | 2 + .../client/bl/KeycloakClientLogic.java | 90 +++++++++++++++++-- .../KeycloakAuthAdminResource.java | 80 ++++++++++++++++- .../client/dto/GroupRepresentation.java | 18 ++-- .../keycloak/client/LogicGroupTest.java | 19 ++++ 5 files changed, 191 insertions(+), 18 deletions(-) diff --git a/README.adoc b/README.adoc index c5a2a36..0320118 100644 --- a/README.adoc +++ b/README.adoc @@ -659,6 +659,8 @@ curl -s -X GET 'http://localhost:8090/auth/admin/realms/trikorasolutions/users/c } } ---- +Create realm role +curl -X POST "http://localhost:8090/auth/admin/realms/trikorasolutions/roles" -H "Content-Type: application/json" -H "Authorization: Bearer ${TKN}" -d '{"name": "test-role2"}' | jq . == Tokens (example with different credential types) diff --git a/src/main/java/com/trikorasolutions/keycloak/client/bl/KeycloakClientLogic.java b/src/main/java/com/trikorasolutions/keycloak/client/bl/KeycloakClientLogic.java index 5375d5e..7a3bd62 100644 --- a/src/main/java/com/trikorasolutions/keycloak/client/bl/KeycloakClientLogic.java +++ b/src/main/java/com/trikorasolutions/keycloak/client/bl/KeycloakClientLogic.java @@ -17,7 +17,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.enterprise.context.ApplicationScoped; -import javax.inject.Inject; import java.util.ArrayList; import java.util.LinkedHashSet; import java.util.List; @@ -56,7 +55,7 @@ public class KeycloakClientLogic { */ public Uni getTokenForUser(final String realm, final String keycloakClientId, final String secret) { - LOGGER.info("Getting token with params [realm: {}, client_id: {}]", realm, keycloakClientId); + LOGGER.debug("Getting token with params [realm: {}, client_id: {}]", realm, keycloakClientId); final String tok = RestAssured.given() .param("grant_type", "client_credentials") .param("client_id", keycloakClientId) @@ -67,7 +66,7 @@ public Uni getTokenForUser(final String realm, final String keycloakClie .as(AccessTokenResponse.class) .getToken(); return Uni.createFrom().item(tok); - + // return keycloakUserClient.getToken(realm, "client_credentials", keycloakClientId, secret) // .onFailure().invoke(ex->LOGGER.warn("ERR KC: {}." + ex)) // .map(jsonArray -> (jsonArray.size() != 1) ? null : jsonArray.get(0).asJsonObject()) @@ -258,7 +257,7 @@ public Uni> listAllUsers(final String realm, fi * @param token access token provided by the keycloak SecurityIdentity. * @param keycloakClientId id of the client (service name). * @param first first user to be fetched - * @param recCount number of users to be fetched from the first one + * @param recCount number of users to be fetched from the first one * @return a JsonArray of Keycloak UserRepresentations. */ public Uni> listAllUsers(final String realm, final String token, @@ -281,7 +280,8 @@ private Uni> listAllUsersRec(final String realm if (currentSelection.size() < KC_BUFFER_SIZE || res.size() >= recCount) { return Uni.createFrom().item(res); // Recursion Base case } else { - return this.listAllUsersRec(realm, token, keycloakClientId, first + KC_BUFFER_SIZE, recCount, + return this.listAllUsersRec(realm, token, keycloakClientId, first + KC_BUFFER_SIZE, + recCount, res); } }); @@ -318,6 +318,22 @@ public Uni> listAllGroups(final String realm, final St .map(GroupRepresentation::allFrom); } + /** + * Creates a group in Keycloak + * + * @param realm the realm name in which the users are going to be queried. + * @param token access token provided by the keycloak SecurityIdentity. + * @param keycloakClientId id of the client (service name). + * @param newGroup group that is going to be created into the Keycloak database. + * @return a GroupRepresentation of the new group. + */ + public Uni createGroup(final String realm, final String token, + final String keycloakClientId, final GroupRepresentation newGroup) { + return keycloakClient.createGroup(BEARER + token, realm, GRANT_TYPE, keycloakClientId, + "{\"name\": \"" + newGroup.name + "\"}") + .replaceWith(this.getGroupInfoNoEnrich(realm, token, keycloakClientId, newGroup.name)); + } + /** * Return information of one group. And enrich it with its members. It can throw * NoSuchGroupException. @@ -348,6 +364,66 @@ public Uni getGroupInfoNoEnrich(final String realm, final S .map(GroupRepresentation::from); } + /** + * Deletes a group in Keycloak + * + * @param realm the realm name in which the users are going to be queried. + * @param token access token provided by the keycloak SecurityIdentity. + * @param keycloakClientId id of the client (service name). + * @param groupName name of the group that is desired to be deleted. + * @return True if the groups has been removed from the DB, FALSE otherwise. + */ + public Uni deleteGroup(final String realm, final String token, + final String keycloakClientId, final String groupName) { + return this.getGroupInfoNoEnrich(realm, token, keycloakClientId, groupName) + .map(GroupRepresentation::getId) + .flatMap(groupId -> keycloakClient.deleteGroup(BEARER + token, realm, GRANT_TYPE, + keycloakClientId, groupId)) + .replaceWith(this.getGroupInfoNoEnrich(realm, token, keycloakClientId, groupName) + .map(x -> Boolean.FALSE) + .onFailure(NoSuchGroupException.class).recoverWithNull().replaceWith(Boolean.TRUE)); + + } + + /** + * Add the given roles to the given group in Keycloak. + * + * @param realm the realm name in which the users are going to be queried. + * @param token access token provided by the keycloak SecurityIdentity. + * @param keycloakClientId id of the client (service name). + * @param groupName name of the group that is desired to be updated. + * @param roles an array of the roles that are going to be added to the group. + * @return True if the groups has been removed from the DB, FALSE otherwise. + */ + public Uni addRolesToGroup(final String realm, final String token, + final String keycloakClientId, final String groupName, RoleRepresentation[] roles) { + + return this.getGroupInfoNoEnrich(realm, token, keycloakClientId, groupName) + .map(GroupRepresentation::getId) + .flatMap(groupId -> keycloakClient.addRolesToGroup(BEARER + token, realm, GRANT_TYPE, + keycloakClientId, groupId, roles)) + .replaceWith(this.getGroupInfo(realm, token, keycloakClientId, groupName)); + } + + /** + * Removes the given roles from the given group in Keycloak. + * + * @param realm the realm name in which the users are going to be queried. + * @param token access token provided by the keycloak SecurityIdentity. + * @param keycloakClientId id of the client (service name). + * @param groupName name of the group that is desired to be updated. + * @param roles an array of the roles that are going to be removed from the group. + * @return True if the groups has been removed from the DB, FALSE otherwise. + */ + public Uni removeRolesFromGroup(final String realm, final String token, + final String keycloakClientId, final String groupName, RoleRepresentation[] roles) { + return this.getGroupInfoNoEnrich(realm, token, keycloakClientId, groupName) + .map(GroupRepresentation::getId) + .flatMap(groupId -> keycloakClient.removeRolesFromGroup(BEARER + token, realm, GRANT_TYPE, + keycloakClientId, groupId, roles)) + .replaceWith(this.getGroupInfo(realm, token, keycloakClientId, groupName)); + } + /** * Gets all the users that belongs to a concrete group. It can throw NoSuchGroupException. * @@ -374,7 +450,7 @@ public Uni> getGroupMembers(final String realm, * @param keycloakClientId id of the client (service name). * @param groupName name of the group that is going to be queried. * @param first first user to be fetched within the members of the group - * @param recCount number of users to be fetched from the first one + * @param recCount number of users to be fetched from the first one * @return a JsonArray of GroupRepresentation. */ public Uni> getGroupMembers(final String realm, @@ -458,6 +534,8 @@ public Uni deleteUserFromGroup(final String realm, f } /******************************* ROLE FUNCTIONS *******************************/ + + /** * Return a List of RoleRepresentation with all the roles to the User. * diff --git a/src/main/java/com/trikorasolutions/keycloak/client/clientresource/KeycloakAuthAdminResource.java b/src/main/java/com/trikorasolutions/keycloak/client/clientresource/KeycloakAuthAdminResource.java index 282e94d..4612a20 100644 --- a/src/main/java/com/trikorasolutions/keycloak/client/clientresource/KeycloakAuthAdminResource.java +++ b/src/main/java/com/trikorasolutions/keycloak/client/clientresource/KeycloakAuthAdminResource.java @@ -1,12 +1,23 @@ package com.trikorasolutions.keycloak.client.clientresource; +import com.trikorasolutions.keycloak.client.dto.GroupRepresentation; +import com.trikorasolutions.keycloak.client.dto.RoleRepresentation; import com.trikorasolutions.keycloak.client.dto.UserRepresentation; import com.trikorasolutions.keycloak.client.dto.UserRepresentation.UserDtoCredential; import io.smallrye.mutiny.Uni; -import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; import javax.json.JsonArray; -import javax.ws.rs.*; +import javax.ws.rs.Consumes; +import javax.ws.rs.DELETE; +import javax.ws.rs.GET; +import javax.ws.rs.HeaderParam; +import javax.ws.rs.POST; +import javax.ws.rs.PUT; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; import javax.ws.rs.core.MediaType; +import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; /** * Common arguments to all the methods: @@ -55,7 +66,8 @@ Uni updateUser(@HeaderParam("Authorization") String bearerToken, * Updated a user password in Keycloak. * * @param id Id of the user that is going to be updated. - * @param body Raw string containing the new user password in the CredentialRepresentation format. + * @param body Raw string containing the new user password in the CredentialRepresentation + * format. * @return -. */ @PUT @@ -121,6 +133,20 @@ Uni listAllGroups(@HeaderParam("Authorization") String bearerToken, @PathParam("realm") String realm, @QueryParam("grant_type") String grantType, @QueryParam("client_id") String clientId); + /** + * This will update the group and set the parent if it exists. Create it and set the parent if the + * group doesn't exist. + * + * @param group that is going to be created in the Keycloak database. + * @return a GroupRepresentation of the new group. + */ + @POST + @Path("/realms/{realm}/groups") + @Produces(MediaType.APPLICATION_JSON) + Uni createGroup(@HeaderParam("Authorization") String bearerToken, + @PathParam("realm") String realm, @QueryParam("grant_type") String grantType, + @QueryParam("client_id") String clientId, String group); + /** * Return information of one group. * @@ -134,6 +160,20 @@ Uni getGroupInfo(@HeaderParam("Authorization") String bearerToken, @PathParam("realm") String realm, @QueryParam("grant_type") String grantType, @QueryParam("client_id") String clientId, @QueryParam("search") String groupName); + + /** + * This method deletes a group. + * + * @param id that is going to be deleted from the Keycloak database. + * @return - + */ + @DELETE + @Path("/realms/{realm}/groups/{id}") + @Produces(MediaType.APPLICATION_JSON) + Uni deleteGroup(@HeaderParam("Authorization") String bearerToken, + @PathParam("realm") String realm, @QueryParam("grant_type") String grantType, + @QueryParam("client_id") String clientId, @PathParam("id") String id); + /** * Gets all the users that belongs to a concrete group. * @@ -230,6 +270,38 @@ Uni getUserRoles(@HeaderParam("Authorization") String bearerToken, @PathParam("realm") String realm, @QueryParam("grant_type") String grantType, @QueryParam("client_id") String clientId, @PathParam("id") String userId); + /** + * Add the given role mappings to a group + * + * @param groupId id of the group to be upgraded. + * @param roles array containing the roles to be added, both id and name of the roles need to be + * provided. + * @return JsonArray with the roles + */ + @POST + @Path("/realms/{realm}/groups/{id}/role-mappings/realm") + @Produces(MediaType.APPLICATION_JSON) + Uni addRolesToGroup(@HeaderParam("Authorization") String bearerToken, + @PathParam("realm") String realm, @QueryParam("grant_type") String grantType, + @QueryParam("client_id") String clientId, @PathParam("id") String groupId, + RoleRepresentation[] roles); + + /** + * Remove the given role mappings from a group + * + * @param groupId id of the group to be upgraded. + * @param roles array containing the roles to be added, both id and name of the roles need to be + * provided. + * @return JsonArray with the roles + */ + @DELETE + @Path("/realms/{realm}/groups/{id}/role-mappings/realm") + @Produces(MediaType.APPLICATION_JSON) + Uni removeRolesFromGroup(@HeaderParam("Authorization") String bearerToken, + @PathParam("realm") String realm, @QueryParam("grant_type") String grantType, + @QueryParam("client_id") String clientId, @PathParam("id") String groupId, + RoleRepresentation[] roles); + /** * Return ALL the roles of one group * @@ -242,4 +314,6 @@ Uni getUserRoles(@HeaderParam("Authorization") String bearerToken, Uni getGroupRoles(@HeaderParam("Authorization") String bearerToken, @PathParam("realm") String realm, @QueryParam("grant_type") String grantType, @QueryParam("client_id") String clientId, @PathParam("id") String groupId); + + } diff --git a/src/main/java/com/trikorasolutions/keycloak/client/dto/GroupRepresentation.java b/src/main/java/com/trikorasolutions/keycloak/client/dto/GroupRepresentation.java index c181243..39814a6 100644 --- a/src/main/java/com/trikorasolutions/keycloak/client/dto/GroupRepresentation.java +++ b/src/main/java/com/trikorasolutions/keycloak/client/dto/GroupRepresentation.java @@ -29,12 +29,17 @@ public class GroupRepresentation { @JsonProperty("members") public Set members; - public GroupRepresentation(String id) { + public GroupRepresentation(String id, String name) { this.id = id; + this.name = name; this.roles = new LinkedHashSet<>(); this.members = new LinkedHashSet<>(); } + public GroupRepresentation(String name) { + this.name = name; + } + public GroupRepresentation(String id, String name, String path) { this.id = id; this.name = name; @@ -52,16 +57,11 @@ public static GroupRepresentation from(JsonObject from) { // Create the DTO only with the mandatory fields GroupRepresentation parsedResponse = new GroupRepresentation( - from.getString("id")); + from.getString("id"), from.getString("name")); - // Then add only the available optional fields - Iterator iterator = from.keySet().iterator(); - while (iterator.hasNext()) { - String key = iterator.next(); + // Then add only the available optional fields (more fields will be added in future releases) + for (String key : from.keySet()) { switch (key) { - case "name": - parsedResponse.setName(from.getString(key)); - break; case "path": parsedResponse.setPath(from.getString(key)); break; diff --git a/src/test/java/com/trikorasolutions/keycloak/client/LogicGroupTest.java b/src/test/java/com/trikorasolutions/keycloak/client/LogicGroupTest.java index 19e538f..407e621 100644 --- a/src/test/java/com/trikorasolutions/keycloak/client/LogicGroupTest.java +++ b/src/test/java/com/trikorasolutions/keycloak/client/LogicGroupTest.java @@ -29,6 +29,25 @@ public class LogicGroupTest { @Inject TrikoraKeycloakClientInfo tkrKcCli; + + @Test + public void testCreateGroupOk() { + String accessToken = tkrKcCli.getAccessToken(ADM); + GroupRepresentation newGroup = new GroupRepresentation("TEST_CREATE"); + LOGGER.info("test{}", newGroup); + GroupRepresentation logicResponse; + boolean logicResponse2; + + logicResponse = keycloakClientLogic.createGroup(tkrKcCli.getRealmName(), accessToken, + tkrKcCli.getClientId(), newGroup).await().indefinitely(); + assertThat(logicResponse.getName(), is(newGroup.name)); + + logicResponse2= keycloakClientLogic.deleteGroup(tkrKcCli.getRealmName(), accessToken, + tkrKcCli.getClientId(), logicResponse.name).await().indefinitely(); + assertThat(logicResponse2, is(true)); + + } + @Test public void testGroupInfoOk() { String accessToken = tkrKcCli.getAccessToken(ADM);