Skip to content

Commit

Permalink
Add base roles to API keys
Browse files Browse the repository at this point in the history
  • Loading branch information
barreiro committed Oct 23, 2024
1 parent bc4eaf2 commit 23f4c78
Show file tree
Hide file tree
Showing 2 changed files with 51 additions and 16 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -76,8 +79,32 @@ public List<UserService.UserData> searchUsers(String query) {

@Override
public List<String> getRoles(String username) {
return keycloak.realm(realm).users().get(findMatchingUser(username).getId()).roles().realmLevel().listAll().stream()
.map(RoleRepresentation::getName).toList();
List<RoleRepresentation> 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<String> roles = new HashSet<>(representations.stream().map(RoleRepresentation::getName).toList());
for (String type : ROLE_TYPES) {
Optional<String> 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<String> getRoleAndComposites(RoleRepresentation representation) {
Set<String> 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
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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));
Expand Down Expand Up @@ -187,7 +214,7 @@ public List<String> 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<UserRepresentation> matchingUsers = keycloak.realm(realm).users().search(username, true);
if (matchingUsers == null || matchingUsers.isEmpty()) {
LOG.warnv("Cannot find user with username {0}", username);
Expand All @@ -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
Expand Down Expand Up @@ -227,10 +254,9 @@ public void updateTeamMembers(String team, Map<String, List<String>> 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
Expand Down Expand Up @@ -267,6 +293,8 @@ public void updateTeamMembers(String team, Map<String, List<String>> 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));
Expand Down Expand Up @@ -376,7 +404,7 @@ public void updateAdministrators(List<String> 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());
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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<String> existingRoles = horreumClient.userService.getRoles();

try (HorreumClient apiClient = new HorreumClient.Builder()
.horreumUrl("http://localhost:".concat(System.getProperty("quarkus.http.test-port")))
Expand All @@ -81,17 +83,22 @@ public void testApiKeys() {

List<String> 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);
}
}
Expand Down

0 comments on commit 23f4c78

Please sign in to comment.