Skip to content

Commit

Permalink
feat: add service user client role mapping (#30)
Browse files Browse the repository at this point in the history
  • Loading branch information
MarcScheib committed Jan 21, 2024
1 parent f9a15f5 commit cea475a
Show file tree
Hide file tree
Showing 4 changed files with 145 additions and 2 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
package com.cycrilabs.keycloak.configurator.commands.configure.boundary;

import java.nio.file.Path;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.ws.rs.core.GenericType;

import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.RoleRepresentation;
import org.keycloak.representations.idm.UserRepresentation;

import com.cycrilabs.keycloak.configurator.commands.configure.entity.ServiceUserClientRoleMappingDTO;
import com.cycrilabs.keycloak.configurator.shared.control.JsonUtil;
import com.cycrilabs.keycloak.configurator.shared.entity.EntityType;

import io.quarkus.logging.Log;

@ApplicationScoped
public class ServiceAccountRoleImporter extends AbstractImporter {
@Override
public EntityType getType() {
return EntityType.SERVICE_ACCOUNT_ROLE;
}

@Override
protected Object importFile(final Path file) {
final String[] fileNameParts = file.toString().split(PATH_SEPARATOR);
final String realmName = fileNameParts[fileNameParts.length - 4];
final String serviceUsername = fileNameParts[fileNameParts.length - 2];

Log.debugf("Importing service account roles '%s' for service user '%s' of realm '%s'.",
file.getFileName(), serviceUsername, realmName);

final UserRepresentation user = loadUserByUsername(realmName, serviceUsername);
if (user == null) {
return null;
}

Log.debugf("Found service user '%s' of realm '%s'.", user.getUsername(), realmName);

importServiceUserClientRoleMappings(file, realmName, user);

return null;
}

private UserRepresentation loadUserByUsername(final String realmName, final String username) {
try {
final List<UserRepresentation> userRepresentations = keycloak.realm(realmName)
.users()
.searchByUsername(username, Boolean.TRUE);
if (userRepresentations.size() == 1) {
return userRepresentations.getFirst();
}

Log.warnf("Found %d users '%s' of realm '%s'. Skipping import.",
Integer.valueOf(userRepresentations.size()), username, realmName);
} catch (final Exception e) {
Log.errorf("Could not find user '%s' of realm '%s': %s", username, realmName,
e.getMessage());
}
return null;
}

private void importServiceUserClientRoleMappings(final Path file, final String realmName,
final UserRepresentation serviceUser) {
final List<ServiceUserClientRoleMappingDTO> serviceUserClientRoleMappings =
JsonUtil.loadEntity(file, new GenericType<List<ServiceUserClientRoleMappingDTO>>() {
}.getType());
for (final ServiceUserClientRoleMappingDTO mapping : serviceUserClientRoleMappings) {
final String clientName = mapping.getClient();
final List<String> roles = mapping.getRoles();

Log.debugf(
"Importing client roles '%s' of client '%s' for service user '%s' of realm '%s'.",
roles.toString(), clientName, serviceUser.getUsername(), realmName);
final List<ClientRepresentation> clients = keycloak.realm(realmName)
.clients()
.findByClientId(clientName);
if (clients.size() != 1) {
Log.warnf("Found %d clients '%s' of realm '%s'. Skipping import.",
Integer.valueOf(clients.size()), clientName, realmName);
return;
}

final ClientRepresentation client = clients.getFirst();
final Map<String, RoleRepresentation> availableClientRoles = keycloak.realm(realmName)
.clients()
.get(client.getId())
.roles()
.list()
.stream()
.collect(Collectors.toMap(RoleRepresentation::getName, Function.identity()));
Log.debugf("Found %d roles of client '%s' of realm '%s'.",
Integer.valueOf(availableClientRoles.size()), clientName, realmName);

keycloak.realm(realmName)
.users()
.get(serviceUser.getId())
.roles()
.clientLevel(client.getId())
.add(roles.stream()
.filter(availableClientRoles::containsKey)
.map(availableClientRoles::get)
.toList());
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.cycrilabs.keycloak.configurator.commands.configure.entity;

import java.util.List;

import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
public class ServiceUserClientRoleMappingDTO {
private String client;
private List<String> roles;
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.cycrilabs.keycloak.configurator.shared.control;

import java.io.IOException;
import java.lang.reflect.Type;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
Expand Down Expand Up @@ -73,4 +74,21 @@ public static <T> T loadEntity(final Path filepath, final Class<T> dtoClass) {
final String json = loadJsonFromPath(filepath);
return fromJson(json, dtoClass);
}

/**
* Loads an entity from the given file path. The file is expected to be a JSON file.
* The JSON is converted to an object of the given type.
*
* @param filepath
* path to the JSON file
* @param dtoType
* type of the object to convert to
* @param <T>
* type of the object to convert to
* @return object of the given type
*/
public static <T> T loadEntity(final Path filepath, final Type dtoType) {
final String json = loadJsonFromPath(filepath);
return JsonbFactory.getJsonb().fromJson(json, dtoType);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@ public enum EntityType {
CLIENT(2, "client", "clients"),
CLIENT_ROLE(3, "client-role", "client-roles"),
REALM_ROLE(4, "realm-role", "realm-roles"),
GROUP(5, "group", "groups"),
USER(6, "user", "users");
SERVICE_ACCOUNT_ROLE(5, "service-account-role", "service-account-roles"),
GROUP(6, "group", "groups"),
USER(7, "user", "users");

private final int priority;
private final String name;
Expand Down

0 comments on commit cea475a

Please sign in to comment.