diff --git a/horreum-backend/src/main/java/io/hyperfoil/tools/horreum/svc/user/KeycloakUserBackend.java b/horreum-backend/src/main/java/io/hyperfoil/tools/horreum/svc/user/KeycloakUserBackend.java index d67f1fdbb..12f2489a2 100644 --- a/horreum-backend/src/main/java/io/hyperfoil/tools/horreum/svc/user/KeycloakUserBackend.java +++ b/horreum-backend/src/main/java/io/hyperfoil/tools/horreum/svc/user/KeycloakUserBackend.java @@ -5,9 +5,12 @@ import java.util.ArrayList; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Set; +import java.util.stream.Stream; import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; @@ -76,8 +79,32 @@ public List searchUsers(String query) { @Override public List getRoles(String username) { - return keycloak.realm(realm).users().get(findMatchingUser(username).getId()).roles().realmLevel().listAll().stream() - .map(RoleRepresentation::getName).toList(); + List representations = keycloak.realm(realm).users().get(findMatchingUserId(username)).roles() + .realmLevel().listAll(); + + // the realm level roles does not include the base roles, only the composites, so add them manually + Set roles = new HashSet<>(representations.stream().map(RoleRepresentation::getName).toList()); + for (String type : ROLE_TYPES) { + Optional composite = roles.stream().filter(role -> role.endsWith(type)).findAny(); + if (composite.isPresent()) { + roles.add(type); + roles.add(composite.get().substring(0, composite.get().length() - type.length() - 1) + "-team"); + } + } + return new ArrayList<>(roles); + + // the right way to do this would be something like this (avoided because it does call keycloak a bunch of times) + // return representations.stream().flatMap(this::getRoleAndComposites).toList(); + } + + private Stream getRoleAndComposites(RoleRepresentation representation) { + Set roles = new HashSet<>(); + if (representation.isComposite()) { + keycloak.realm(realm).rolesById().getRealmRoleComposites(representation.getId()).stream() + .flatMap(this::getRoleAndComposites).forEach(roles::add); + } + roles.add(representation.getName()); + return roles.stream(); } @Override @@ -119,7 +146,7 @@ public void createUser(UserService.NewUser user) { try { // assign the provided roles to the realm UsersResource usersResource = keycloak.realm(realm).users(); - String userId = findMatchingUser(rep.getUsername()).getId(); + String userId = findMatchingUserId(rep.getUsername()); if (user.team != null) { String prefix = getTeamPrefix(user.team); @@ -148,7 +175,7 @@ public void createUser(UserService.NewUser user) { @Override public void removeUser(String username) { - try (Response response = keycloak.realm(realm).users().delete(findMatchingUser(username).getId())) { + try (Response response = keycloak.realm(realm).users().delete(findMatchingUserId(username))) { if (response.getStatusInfo().getFamily() != Response.Status.Family.SUCCESSFUL) { LOG.warnv("Got {0} response for removing user {0}", response.getStatusInfo(), username); throw ServiceException.serverError(format("Unable to remove user {0}", username)); @@ -187,7 +214,7 @@ public List getTeams() { // get the "team roles" in the realm } } - private UserRepresentation findMatchingUser(String username) { // find the clientID of a single user + private String findMatchingUserId(String username) { // find the clientID of a single user List matchingUsers = keycloak.realm(realm).users().search(username, true); if (matchingUsers == null || matchingUsers.isEmpty()) { LOG.warnv("Cannot find user with username {0}", username); @@ -197,7 +224,7 @@ private UserRepresentation findMatchingUser(String username) { // find the clien matchingUsers.stream().map(UserRepresentation::getId).collect(joining(" "))); throw ServiceException.serverError(format("More than one user with username {0}", username)); } - return matchingUsers.get(0); + return matchingUsers.get(0).getId(); } @Override @@ -227,10 +254,9 @@ public void updateTeamMembers(String team, Map> roles) { // RoleMappingResource rolesMappingResource; try { // fetch the current roles for the user - String userId = findMatchingUser(entry.getKey()).getId(); + String userId = findMatchingUserId(entry.getKey()); rolesMappingResource = keycloak.realm(realm).users().get(userId).roles(); - existingRoles = rolesMappingResource.getAll().getRealmMappings().stream().map(RoleRepresentation::getName) - .toList(); + existingRoles = rolesMappingResource.realmLevel().listAll().stream().map(RoleRepresentation::getName).toList(); } catch (Throwable t) { LOG.warnv(t, "Failed to retrieve current roles of user {0} from Keycloak", entry.getKey()); throw ServiceException @@ -267,6 +293,8 @@ public void updateTeamMembers(String team, Map> roles) { // } } } + } catch (NotFoundException e) { + throw ServiceException.serverError(format("The team {0} does not exist", team)); } catch (Throwable t) { LOG.warnv(t, "Failed to remove all roles of team {0}", team); throw ServiceException.serverError(format("Failed to remove all roles of team {0}", team)); @@ -376,7 +404,7 @@ public void updateAdministrators(List newAdmins) { // update the list of for (String username : newAdmins) { // add admin role for `newAdmins` not in `oldAdmins` if (oldAdmins.stream().noneMatch(old -> username.equals(old.getUsername()))) { try { - usersResource.get(findMatchingUser(username).getId()).roles().realmLevel().add(List.of(adminRole)); + usersResource.get(findMatchingUserId(username)).roles().realmLevel().add(List.of(adminRole)); LOG.infov("Added administrator role to user {0}", username); } catch (Throwable t) { LOG.warnv("Could not add admin role to user {0} due to {1}", username, t.getMessage()); @@ -398,7 +426,7 @@ public void setPassword(String username, String password) { credentials.setType(CredentialRepresentation.PASSWORD); credentials.setValue(password); - keycloak.realm(realm).users().get(findMatchingUser(username).getId()).resetPassword(credentials); + keycloak.realm(realm).users().get(findMatchingUserId(username)).resetPassword(credentials); } catch (Throwable t) { LOG.warnv(t, "Failed to retrieve current representation of user {0} from Keycloak", username); throw ServiceException diff --git a/horreum-integration-tests/src/test/java/io/hyperfoil/tools/horreum/it/HorreumClientIT.java b/horreum-integration-tests/src/test/java/io/hyperfoil/tools/horreum/it/HorreumClientIT.java index d7dec0d14..c8efb5f2a 100644 --- a/horreum-integration-tests/src/test/java/io/hyperfoil/tools/horreum/it/HorreumClientIT.java +++ b/horreum-integration-tests/src/test/java/io/hyperfoil/tools/horreum/it/HorreumClientIT.java @@ -1,8 +1,10 @@ package io.hyperfoil.tools.horreum.it; import static io.hyperfoil.tools.horreum.api.services.UserService.KeyType.USER; +import static java.time.temporal.ChronoUnit.DAYS; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -13,7 +15,6 @@ import java.io.InputStream; import java.io.InputStreamReader; import java.time.Instant; -import java.time.temporal.ChronoUnit; import java.util.Arrays; import java.util.Collections; import java.util.Comparator; @@ -73,6 +74,7 @@ public class HorreumClientIT implements QuarkusTestBeforeTestExecutionCallback, public void testApiKeys() { String keyName = "Test key"; String theKey = horreumClient.userService.newApiKey(new UserService.ApiKeyRequest(keyName, USER)); + List existingRoles = horreumClient.userService.getRoles(); try (HorreumClient apiClient = new HorreumClient.Builder() .horreumUrl("http://localhost:".concat(System.getProperty("quarkus.http.test-port"))) @@ -81,17 +83,22 @@ public void testApiKeys() { List roles = apiClient.userService.getRoles(); assertFalse(roles.isEmpty()); - assertTrue(roles.contains("dev-" + Roles.TESTER)); + assertTrue(existingRoles.stream().filter(r -> r.startsWith("dev")).allMatch(roles::contains)); + assertTrue(roles.containsAll(List.of(Roles.ADMIN, Roles.MANAGER, Roles.TESTER, Roles.TESTER, Roles.VIEWER))); UserService.ApiKeyResponse apiKey = horreumClient.userService.apiKeys().get(0); + assertEquals(keyName, apiKey.name); assertFalse(apiKey.isRevoked); assertFalse(apiKey.toExpiration < 0); - assertEquals(Instant.now().truncatedTo(ChronoUnit.DAYS), apiKey.creation.truncatedTo(ChronoUnit.DAYS)); - assertEquals(Instant.now().truncatedTo(ChronoUnit.DAYS), apiKey.access.truncatedTo(ChronoUnit.DAYS)); + assertEquals(Instant.now().truncatedTo(DAYS), apiKey.creation.truncatedTo(DAYS)); + assertEquals(Instant.now().truncatedTo(DAYS), apiKey.access.truncatedTo(DAYS)); assertEquals(USER, apiKey.type); - horreumClient.userService.revokeApiKey(apiKey.id); + apiClient.userService.renameApiKey(apiKey.id, "Some new name"); // use key to modify key !! + apiKey = horreumClient.userService.apiKeys().get(0); + assertNotEquals(keyName, apiKey.name); + horreumClient.userService.revokeApiKey(apiKey.id); assertThrows(NotAuthorizedException.class, apiClient.userService::getRoles); } }