Skip to content

Commit c65462e

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

File tree

2 files changed

+53
-16
lines changed

2 files changed

+53
-16
lines changed

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

+41-11
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@
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;
1113

1214
import jakarta.enterprise.context.ApplicationScoped;
@@ -76,8 +78,35 @@ public List<UserService.UserData> searchUsers(String query) {
7678

7779
@Override
7880
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();
81+
List<RoleRepresentation> representations = keycloak.realm(realm).users().get(findMatchingUserId(username)).roles()
82+
.realmLevel().listAll();
83+
84+
Set<String> roles = new HashSet<>(representations.stream().map(RoleRepresentation::getName).toList());
85+
86+
// the realm level roles only include the composite roles, not the base ones. add them manually.
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+
95+
// the right way to do this would be something like this (does call keycloak a bunch of times)
96+
// Set<String> roles = new HashSet<>();
97+
// representations.forEach(representation -> roles.addAll(getRoleAndComposites(representation)))
98+
99+
return new ArrayList<>(roles);
100+
}
101+
102+
private Set<String> getRoleAndComposites(RoleRepresentation representation) {
103+
Set<String> roles = new HashSet<>();
104+
if (representation.isComposite()) {
105+
keycloak.realm(realm).rolesById().getRealmRoleComposites(representation.getId()).stream()
106+
.map(this::getRoleAndComposites).forEach(roles::addAll);
107+
}
108+
roles.add(representation.getName());
109+
return roles;
81110
}
82111

83112
@Override
@@ -119,7 +148,7 @@ public void createUser(UserService.NewUser user) {
119148

120149
try { // assign the provided roles to the realm
121150
UsersResource usersResource = keycloak.realm(realm).users();
122-
String userId = findMatchingUser(rep.getUsername()).getId();
151+
String userId = findMatchingUserId(rep.getUsername());
123152

124153
if (user.team != null) {
125154
String prefix = getTeamPrefix(user.team);
@@ -148,7 +177,7 @@ public void createUser(UserService.NewUser user) {
148177

149178
@Override
150179
public void removeUser(String username) {
151-
try (Response response = keycloak.realm(realm).users().delete(findMatchingUser(username).getId())) {
180+
try (Response response = keycloak.realm(realm).users().delete(findMatchingUserId(username))) {
152181
if (response.getStatusInfo().getFamily() != Response.Status.Family.SUCCESSFUL) {
153182
LOG.warnv("Got {0} response for removing user {0}", response.getStatusInfo(), username);
154183
throw ServiceException.serverError(format("Unable to remove user {0}", username));
@@ -187,7 +216,7 @@ public List<String> getTeams() { // get the "team roles" in the realm
187216
}
188217
}
189218

190-
private UserRepresentation findMatchingUser(String username) { // find the clientID of a single user
219+
private String findMatchingUserId(String username) { // find the clientID of a single user
191220
List<UserRepresentation> matchingUsers = keycloak.realm(realm).users().search(username, true);
192221
if (matchingUsers == null || matchingUsers.isEmpty()) {
193222
LOG.warnv("Cannot find user with username {0}", username);
@@ -197,7 +226,7 @@ private UserRepresentation findMatchingUser(String username) { // find the clien
197226
matchingUsers.stream().map(UserRepresentation::getId).collect(joining(" ")));
198227
throw ServiceException.serverError(format("More than one user with username {0}", username));
199228
}
200-
return matchingUsers.get(0);
229+
return matchingUsers.get(0).getId();
201230
}
202231

203232
@Override
@@ -227,10 +256,9 @@ public void updateTeamMembers(String team, Map<String, List<String>> roles) { //
227256
RoleMappingResource rolesMappingResource;
228257

229258
try { // fetch the current roles for the user
230-
String userId = findMatchingUser(entry.getKey()).getId();
259+
String userId = findMatchingUserId(entry.getKey());
231260
rolesMappingResource = keycloak.realm(realm).users().get(userId).roles();
232-
existingRoles = rolesMappingResource.getAll().getRealmMappings().stream().map(RoleRepresentation::getName)
233-
.toList();
261+
existingRoles = rolesMappingResource.realmLevel().listAll().stream().map(RoleRepresentation::getName).toList();
234262
} catch (Throwable t) {
235263
LOG.warnv(t, "Failed to retrieve current roles of user {0} from Keycloak", entry.getKey());
236264
throw ServiceException
@@ -267,6 +295,8 @@ public void updateTeamMembers(String team, Map<String, List<String>> roles) { //
267295
}
268296
}
269297
}
298+
} catch (NotFoundException e) {
299+
throw ServiceException.serverError(format("The team {0} does not exist", team));
270300
} catch (Throwable t) {
271301
LOG.warnv(t, "Failed to remove all roles of team {0}", team);
272302
throw ServiceException.serverError(format("Failed to remove all roles of team {0}", team));
@@ -376,7 +406,7 @@ public void updateAdministrators(List<String> newAdmins) { // update the list of
376406
for (String username : newAdmins) { // add admin role for `newAdmins` not in `oldAdmins`
377407
if (oldAdmins.stream().noneMatch(old -> username.equals(old.getUsername()))) {
378408
try {
379-
usersResource.get(findMatchingUser(username).getId()).roles().realmLevel().add(List.of(adminRole));
409+
usersResource.get(findMatchingUserId(username)).roles().realmLevel().add(List.of(adminRole));
380410
LOG.infov("Added administrator role to user {0}", username);
381411
} catch (Throwable t) {
382412
LOG.warnv("Could not add admin role to user {0} due to {1}", username, t.getMessage());
@@ -398,7 +428,7 @@ public void setPassword(String username, String password) {
398428
credentials.setType(CredentialRepresentation.PASSWORD);
399429
credentials.setValue(password);
400430

401-
keycloak.realm(realm).users().get(findMatchingUser(username).getId()).resetPassword(credentials);
431+
keycloak.realm(realm).users().get(findMatchingUserId(username)).resetPassword(credentials);
402432
} catch (Throwable t) {
403433
LOG.warnv(t, "Failed to retrieve current representation of user {0} from Keycloak", username);
404434
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)