234 lines
9.1 KiB
Java
234 lines
9.1 KiB
Java
package dev.lions.user.manager.client;
|
|
|
|
import com.fasterxml.jackson.core.type.TypeReference;
|
|
import com.fasterxml.jackson.databind.DeserializationFeature;
|
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
import io.quarkus.runtime.Startup;
|
|
import jakarta.annotation.PostConstruct;
|
|
import jakarta.annotation.PreDestroy;
|
|
import jakarta.enterprise.context.ApplicationScoped;
|
|
import jakarta.inject.Inject;
|
|
import lombok.extern.slf4j.Slf4j;
|
|
import org.eclipse.microprofile.config.inject.ConfigProperty;
|
|
import org.eclipse.microprofile.faulttolerance.CircuitBreaker;
|
|
import org.eclipse.microprofile.faulttolerance.Retry;
|
|
import org.eclipse.microprofile.faulttolerance.Timeout;
|
|
import org.keycloak.admin.client.Keycloak;
|
|
import org.keycloak.admin.client.resource.RealmResource;
|
|
import org.keycloak.admin.client.resource.RolesResource;
|
|
import org.keycloak.admin.client.resource.UsersResource;
|
|
|
|
import jakarta.ws.rs.NotFoundException;
|
|
import java.net.URI;
|
|
import java.net.http.HttpClient;
|
|
import java.net.http.HttpRequest;
|
|
import java.net.http.HttpResponse;
|
|
import java.time.temporal.ChronoUnit;
|
|
import java.util.List;
|
|
import java.util.Map;
|
|
import java.util.stream.Collectors;
|
|
|
|
/**
|
|
* Implémentation du client Keycloak Admin
|
|
* Utilise le bean Keycloak géré par Quarkus (quarkus-keycloak-admin-rest-client)
|
|
* qui respecte la configuration Jackson (fail-on-unknown-properties=false)
|
|
* Utilise Circuit Breaker, Retry et Timeout pour la résilience
|
|
*/
|
|
@ApplicationScoped
|
|
@Startup
|
|
@Slf4j
|
|
public class KeycloakAdminClientImpl implements KeycloakAdminClient {
|
|
|
|
@Inject
|
|
Keycloak keycloak;
|
|
|
|
@ConfigProperty(name = "lions.keycloak.server-url")
|
|
String serverUrl;
|
|
|
|
@ConfigProperty(name = "lions.keycloak.admin-realm")
|
|
String adminRealm;
|
|
|
|
@ConfigProperty(name = "lions.keycloak.admin-client-id")
|
|
String adminClientId;
|
|
|
|
@ConfigProperty(name = "lions.keycloak.admin-username")
|
|
String adminUsername;
|
|
|
|
@PostConstruct
|
|
void init() {
|
|
log.info("========================================");
|
|
log.info("Initialisation du client Keycloak Admin");
|
|
log.info("========================================");
|
|
log.info("Server URL: {}", serverUrl);
|
|
log.info("Admin Realm: {}", adminRealm);
|
|
log.info("Admin Client ID: {}", adminClientId);
|
|
log.info("Admin Username: {}", adminUsername);
|
|
log.info("✅ Client Keycloak initialisé via Quarkus CDI (connexion lazy)");
|
|
log.info("La connexion sera établie lors de la première requête API");
|
|
}
|
|
|
|
@Override
|
|
@Retry(maxRetries = 3, delay = 2, delayUnit = ChronoUnit.SECONDS)
|
|
@Timeout(value = 30, unit = ChronoUnit.SECONDS)
|
|
@CircuitBreaker(requestVolumeThreshold = 10, failureRatio = 0.5, delay = 5000)
|
|
public Keycloak getInstance() {
|
|
return keycloak;
|
|
}
|
|
|
|
@Override
|
|
@Retry(maxRetries = 3, delay = 2, delayUnit = ChronoUnit.SECONDS)
|
|
@Timeout(value = 30, unit = ChronoUnit.SECONDS)
|
|
@CircuitBreaker(requestVolumeThreshold = 10, failureRatio = 0.5, delay = 5000)
|
|
public RealmResource getRealm(String realmName) {
|
|
try {
|
|
return keycloak.realm(realmName);
|
|
} catch (Exception e) {
|
|
log.error("Erreur lors de la récupération du realm {}: {}", realmName, e.getMessage());
|
|
throw new RuntimeException("Impossible de récupérer le realm: " + realmName, e);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
@Retry(maxRetries = 3, delay = 2, delayUnit = ChronoUnit.SECONDS)
|
|
@Timeout(value = 30, unit = ChronoUnit.SECONDS)
|
|
@CircuitBreaker(requestVolumeThreshold = 10, failureRatio = 0.5, delay = 5000)
|
|
public UsersResource getUsers(String realmName) {
|
|
return getRealm(realmName).users();
|
|
}
|
|
|
|
@Override
|
|
@Retry(maxRetries = 3, delay = 2, delayUnit = ChronoUnit.SECONDS)
|
|
@Timeout(value = 30, unit = ChronoUnit.SECONDS)
|
|
@CircuitBreaker(requestVolumeThreshold = 10, failureRatio = 0.5, delay = 5000)
|
|
public RolesResource getRoles(String realmName) {
|
|
return getRealm(realmName).roles();
|
|
}
|
|
|
|
@Override
|
|
public boolean isConnected() {
|
|
try {
|
|
// getAccessTokenString() n'implique pas la désérialisation de ServerInfoRepresentation
|
|
// (qui échoue sur le champ inconnu "cpuInfo" avec Keycloak 26+)
|
|
keycloak.tokenManager().getAccessTokenString();
|
|
return true;
|
|
} catch (Exception e) {
|
|
log.warn("Keycloak non connecté: {}", e.getMessage());
|
|
return false;
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public boolean realmExists(String realmName) {
|
|
try {
|
|
getRealm(realmName).roles().list();
|
|
return true;
|
|
} catch (NotFoundException e) {
|
|
log.debug("Realm {} n'existe pas", realmName);
|
|
return false;
|
|
} catch (Exception e) {
|
|
log.debug("Erreur lors de la vérification du realm {} (probablement il existe): {}",
|
|
realmName, e.getMessage());
|
|
return true;
|
|
}
|
|
}
|
|
|
|
@Override
|
|
@Retry(maxRetries = 3, delay = 2, delayUnit = ChronoUnit.SECONDS)
|
|
@Timeout(value = 30, unit = ChronoUnit.SECONDS)
|
|
@CircuitBreaker(requestVolumeThreshold = 10, failureRatio = 0.5, delay = 5000)
|
|
public List<String> getAllRealms() {
|
|
try {
|
|
log.debug("Récupération de tous les realms depuis Keycloak");
|
|
// Appel HTTP direct pour éviter l'erreur de désérialisation de RealmRepresentation
|
|
// (champ bruteForceStrategy inconnu dans la version de la librairie cliente)
|
|
String token = keycloak.tokenManager().getAccessTokenString();
|
|
|
|
HttpRequest request = HttpRequest.newBuilder()
|
|
.uri(URI.create(serverUrl + "/admin/realms"))
|
|
.header("Authorization", "Bearer " + token)
|
|
.header("Accept", "application/json")
|
|
.GET()
|
|
.build();
|
|
|
|
HttpResponse<String> response = HttpClient.newHttpClient()
|
|
.send(request, HttpResponse.BodyHandlers.ofString());
|
|
|
|
if (response.statusCode() != 200) {
|
|
throw new RuntimeException("Keycloak returned HTTP " + response.statusCode());
|
|
}
|
|
|
|
ObjectMapper mapper = new ObjectMapper()
|
|
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
|
|
|
|
List<Map<String, Object>> realmMaps = mapper.readValue(
|
|
response.body(), new TypeReference<>() {});
|
|
|
|
List<String> realms = realmMaps.stream()
|
|
.map(r -> (String) r.get("realm"))
|
|
.filter(r -> r != null)
|
|
.collect(Collectors.toList());
|
|
|
|
log.debug("Realms récupérés: {}", realms);
|
|
return realms;
|
|
} catch (Exception e) {
|
|
log.error("Erreur lors de la récupération de tous les realms: {}", e.getMessage());
|
|
throw new RuntimeException("Impossible de récupérer la liste des realms", e);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
@Retry(maxRetries = 3, delay = 2, delayUnit = ChronoUnit.SECONDS)
|
|
@Timeout(value = 30, unit = ChronoUnit.SECONDS)
|
|
@CircuitBreaker(requestVolumeThreshold = 10, failureRatio = 0.5, delay = 5000)
|
|
public List<String> getRealmClients(String realmName) {
|
|
try {
|
|
log.debug("Récupération des clients du realm {}", realmName);
|
|
String token = keycloak.tokenManager().getAccessTokenString();
|
|
|
|
HttpRequest request = HttpRequest.newBuilder()
|
|
.uri(URI.create(serverUrl + "/admin/realms/" + realmName + "/clients"))
|
|
.header("Authorization", "Bearer " + token)
|
|
.header("Accept", "application/json")
|
|
.GET()
|
|
.build();
|
|
|
|
HttpResponse<String> response = HttpClient.newHttpClient()
|
|
.send(request, HttpResponse.BodyHandlers.ofString());
|
|
|
|
if (response.statusCode() != 200) {
|
|
throw new RuntimeException("Keycloak returned HTTP " + response.statusCode());
|
|
}
|
|
|
|
ObjectMapper mapper = new ObjectMapper()
|
|
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
|
|
|
|
List<Map<String, Object>> clientMaps = mapper.readValue(
|
|
response.body(), new TypeReference<>() {});
|
|
|
|
List<String> clients = clientMaps.stream()
|
|
.map(c -> (String) c.get("clientId"))
|
|
.filter(c -> c != null)
|
|
.collect(Collectors.toList());
|
|
|
|
log.debug("Clients récupérés pour {}: {}", realmName, clients);
|
|
return clients;
|
|
} catch (Exception e) {
|
|
log.error("Erreur lors de la récupération des clients du realm {}: {}", realmName, e.getMessage());
|
|
throw new RuntimeException("Impossible de récupérer les clients du realm: " + realmName, e);
|
|
}
|
|
}
|
|
|
|
@PreDestroy
|
|
@Override
|
|
public void close() {
|
|
log.info("Fermeture de la connexion Keycloak...");
|
|
// Le cycle de vie est géré par Quarkus CDI
|
|
}
|
|
|
|
@Override
|
|
public void reconnect() {
|
|
log.info("Reconnexion à Keycloak... (géré par Quarkus CDI)");
|
|
// Le bean Keycloak est géré par Quarkus, pas de reconnexion manuelle nécessaire
|
|
}
|
|
}
|