Skip to content

Commit d4f2026

Browse files
committed
Add base roles to API keys
1 parent 3f70286 commit d4f2026

File tree

2 files changed

+51
-16
lines changed

2 files changed

+51
-16
lines changed

horreum-backend/src/main/java/io/hyperfoil/tools/horreum/svc/user/KeycloakUserBackend.java

+39-11
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,12 @@
55

66
import java.util.ArrayList;
77
import java.util.HashMap;
8+
import java.util.HashSet;
89
import java.util.List;
910
import java.util.Map;
11+
import java.util.Optional;
1012
import java.util.Set;
13+
import java.util.stream.Stream;
1114

1215
import jakarta.enterprise.context.ApplicationScoped;
1316
import jakarta.inject.Inject;
@@ -76,8 +79,32 @@ public List<UserService.UserData> searchUsers(String query) {
7679

7780
@Override
7881
public List<String> getRoles(String username) {
79-
return keycloak.realm(realm).users().get(findMatchingUser(username).getId()).roles().realmLevel().listAll().stream()
80-
.map(RoleRepresentation::getName).toList();
82+
List<RoleRepresentation> representations = keycloak.realm(realm).users().get(findMatchingUserId(username)).roles()
83+
.realmLevel().listAll();
84+
85+
// the realm level roles does not include the base roles, only the composites, so add them manually
86+
Set<String> roles = new HashSet<>(representations.stream().map(RoleRepresentation::getName).toList());
87+
for (String r : List.of(Roles.MANAGER, Roles.TESTER, Roles.UPLOADER, Roles.VIEWER)) {
88+
Optional<String> composite = roles.stream().filter(role -> role.endsWith(r)).findAny();
89+
if (composite.isPresent()) {
90+
roles.add(r);
91+
roles.add(composite.get().substring(0, composite.get().length() - r.length() - 1) + "-team");
92+
}
93+
}
94+
return new ArrayList<>(roles);
95+
96+
// the right way to do this would be something like this (avoided because it does call keycloak a bunch of times)
97+
// return representations.stream().flatMap(this::getRoleAndComposites).toList();
98+
}
99+
100+
private Stream<String> getRoleAndComposites(RoleRepresentation representation) {
101+
Set<String> roles = new HashSet<>();
102+
if (representation.isComposite()) {
103+
keycloak.realm(realm).rolesById().getRealmRoleComposites(representation.getId()).stream()
104+
.flatMap(this::getRoleAndComposites).forEach(roles::add);
105+
}
106+
roles.add(representation.getName());
107+
return roles.stream();
81108
}
82109

83110
@Override
@@ -119,7 +146,7 @@ public void createUser(UserService.NewUser user) {
119146

120147
try { // assign the provided roles to the realm
121148
UsersResource usersResource = keycloak.realm(realm).users();
122-
String userId = findMatchingUser(rep.getUsername()).getId();
149+
String userId = findMatchingUserId(rep.getUsername());
123150

124151
if (user.team != null) {
125152
String prefix = getTeamPrefix(user.team);
@@ -148,7 +175,7 @@ public void createUser(UserService.NewUser user) {
148175

149176
@Override
150177
public void removeUser(String username) {
151-
try (Response response = keycloak.realm(realm).users().delete(findMatchingUser(username).getId())) {
178+
try (Response response = keycloak.realm(realm).users().delete(findMatchingUserId(username))) {
152179
if (response.getStatusInfo().getFamily() != Response.Status.Family.SUCCESSFUL) {
153180
LOG.warnv("Got {0} response for removing user {0}", response.getStatusInfo(), username);
154181
throw ServiceException.serverError(format("Unable to remove user {0}", username));
@@ -187,7 +214,7 @@ public List<String> getTeams() { // get the "team roles" in the realm
187214
}
188215
}
189216

190-
private UserRepresentation findMatchingUser(String username) { // find the clientID of a single user
217+
private String findMatchingUserId(String username) { // find the clientID of a single user
191218
List<UserRepresentation> matchingUsers = keycloak.realm(realm).users().search(username, true);
192219
if (matchingUsers == null || matchingUsers.isEmpty()) {
193220
LOG.warnv("Cannot find user with username {0}", username);
@@ -197,7 +224,7 @@ private UserRepresentation findMatchingUser(String username) { // find the clien
197224
matchingUsers.stream().map(UserRepresentation::getId).collect(joining(" ")));
198225
throw ServiceException.serverError(format("More than one user with username {0}", username));
199226
}
200-
return matchingUsers.get(0);
227+
return matchingUsers.get(0).getId();
201228
}
202229

203230
@Override
@@ -227,10 +254,9 @@ public void updateTeamMembers(String team, Map<String, List<String>> roles) { //
227254
RoleMappingResource rolesMappingResource;
228255

229256
try { // fetch the current roles for the user
230-
String userId = findMatchingUser(entry.getKey()).getId();
257+
String userId = findMatchingUserId(entry.getKey());
231258
rolesMappingResource = keycloak.realm(realm).users().get(userId).roles();
232-
existingRoles = rolesMappingResource.getAll().getRealmMappings().stream().map(RoleRepresentation::getName)
233-
.toList();
259+
existingRoles = rolesMappingResource.realmLevel().listAll().stream().map(RoleRepresentation::getName).toList();
234260
} catch (Throwable t) {
235261
LOG.warnv(t, "Failed to retrieve current roles of user {0} from Keycloak", entry.getKey());
236262
throw ServiceException
@@ -267,6 +293,8 @@ public void updateTeamMembers(String team, Map<String, List<String>> roles) { //
267293
}
268294
}
269295
}
296+
} catch (NotFoundException e) {
297+
throw ServiceException.serverError(format("The team {0} does not exist", team));
270298
} catch (Throwable t) {
271299
LOG.warnv(t, "Failed to remove all roles of team {0}", team);
272300
throw ServiceException.serverError(format("Failed to remove all roles of team {0}", team));
@@ -376,7 +404,7 @@ public void updateAdministrators(List<String> newAdmins) { // update the list of
376404
for (String username : newAdmins) { // add admin role for `newAdmins` not in `oldAdmins`
377405
if (oldAdmins.stream().noneMatch(old -> username.equals(old.getUsername()))) {
378406
try {
379-
usersResource.get(findMatchingUser(username).getId()).roles().realmLevel().add(List.of(adminRole));
407+
usersResource.get(findMatchingUserId(username)).roles().realmLevel().add(List.of(adminRole));
380408
LOG.infov("Added administrator role to user {0}", username);
381409
} catch (Throwable t) {
382410
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) {
398426
credentials.setType(CredentialRepresentation.PASSWORD);
399427
credentials.setValue(password);
400428

401-
keycloak.realm(realm).users().get(findMatchingUser(username).getId()).resetPassword(credentials);
429+
keycloak.realm(realm).users().get(findMatchingUserId(username)).resetPassword(credentials);
402430
} catch (Throwable t) {
403431
LOG.warnv(t, "Failed to retrieve current representation of user {0} from Keycloak", username);
404432
throw ServiceException

horreum-integration-tests/src/test/java/io/hyperfoil/tools/horreum/it/HorreumClientIT.java

+12-5
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
package io.hyperfoil.tools.horreum.it;
22

33
import static io.hyperfoil.tools.horreum.api.services.UserService.KeyType.USER;
4+
import static java.time.temporal.ChronoUnit.DAYS;
45
import static org.junit.jupiter.api.Assertions.assertEquals;
56
import static org.junit.jupiter.api.Assertions.assertFalse;
7+
import static org.junit.jupiter.api.Assertions.assertNotEquals;
68
import static org.junit.jupiter.api.Assertions.assertNotNull;
79
import static org.junit.jupiter.api.Assertions.assertThrows;
810
import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -13,7 +15,6 @@
1315
import java.io.InputStream;
1416
import java.io.InputStreamReader;
1517
import java.time.Instant;
16-
import java.time.temporal.ChronoUnit;
1718
import java.util.Arrays;
1819
import java.util.Collections;
1920
import java.util.Comparator;
@@ -73,6 +74,7 @@ public class HorreumClientIT implements QuarkusTestBeforeTestExecutionCallback,
7374
public void testApiKeys() {
7475
String keyName = "Test key";
7576
String theKey = horreumClient.userService.newApiKey(new UserService.ApiKeyRequest(keyName, USER));
77+
List<String> existingRoles = horreumClient.userService.getRoles();
7678

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

8284
List<String> roles = apiClient.userService.getRoles();
8385
assertFalse(roles.isEmpty());
84-
assertTrue(roles.contains("dev-" + Roles.TESTER));
86+
assertTrue(existingRoles.stream().filter(r -> r.startsWith("dev")).allMatch(roles::contains));
87+
assertTrue(roles.containsAll(List.of(Roles.ADMIN, Roles.MANAGER, Roles.TESTER, Roles.TESTER, Roles.VIEWER)));
8588

8689
UserService.ApiKeyResponse apiKey = horreumClient.userService.apiKeys().get(0);
90+
assertEquals(keyName, apiKey.name);
8791
assertFalse(apiKey.isRevoked);
8892
assertFalse(apiKey.toExpiration < 0);
89-
assertEquals(Instant.now().truncatedTo(ChronoUnit.DAYS), apiKey.creation.truncatedTo(ChronoUnit.DAYS));
90-
assertEquals(Instant.now().truncatedTo(ChronoUnit.DAYS), apiKey.access.truncatedTo(ChronoUnit.DAYS));
93+
assertEquals(Instant.now().truncatedTo(DAYS), apiKey.creation.truncatedTo(DAYS));
94+
assertEquals(Instant.now().truncatedTo(DAYS), apiKey.access.truncatedTo(DAYS));
9195
assertEquals(USER, apiKey.type);
9296

93-
horreumClient.userService.revokeApiKey(apiKey.id);
97+
apiClient.userService.renameApiKey(apiKey.id, "Some new name"); // use key to modify key !!
98+
apiKey = horreumClient.userService.apiKeys().get(0);
99+
assertNotEquals(keyName, apiKey.name);
94100

101+
horreumClient.userService.revokeApiKey(apiKey.id);
95102
assertThrows(NotAuthorizedException.class, apiClient.userService::getRoles);
96103
}
97104
}

0 commit comments

Comments
 (0)