chore(quarkus-327): bump to Quarkus 3.27.3 LTS, make pom autonomous, fix UserServiceImpl tests (search → searchByUsername), rename deprecated config keys
Some checks failed
CI/CD Pipeline / pipeline (push) Failing after 1m57s
Some checks failed
CI/CD Pipeline / pipeline (push) Failing after 1m57s
This commit is contained in:
@@ -1,76 +1,76 @@
|
||||
package dev.lions.user.manager.client;
|
||||
|
||||
import org.keycloak.admin.client.Keycloak;
|
||||
import org.keycloak.admin.client.resource.RealmResource;
|
||||
import org.keycloak.admin.client.resource.UsersResource;
|
||||
import org.keycloak.admin.client.resource.RolesResource;
|
||||
|
||||
/**
|
||||
* Interface pour le client Keycloak Admin
|
||||
* Abstraction pour faciliter les tests et la gestion du cycle de vie
|
||||
*/
|
||||
public interface KeycloakAdminClient {
|
||||
|
||||
/**
|
||||
* Récupère l'instance Keycloak
|
||||
* @return instance Keycloak
|
||||
*/
|
||||
Keycloak getInstance();
|
||||
|
||||
/**
|
||||
* Récupère une ressource Realm
|
||||
* @param realmName nom du realm
|
||||
* @return RealmResource
|
||||
*/
|
||||
RealmResource getRealm(String realmName);
|
||||
|
||||
/**
|
||||
* Récupère la ressource Users d'un realm
|
||||
* @param realmName nom du realm
|
||||
* @return UsersResource
|
||||
*/
|
||||
UsersResource getUsers(String realmName);
|
||||
|
||||
/**
|
||||
* Récupère la ressource Roles d'un realm
|
||||
* @param realmName nom du realm
|
||||
* @return RolesResource
|
||||
*/
|
||||
RolesResource getRoles(String realmName);
|
||||
|
||||
/**
|
||||
* Vérifie si la connexion à Keycloak est active
|
||||
* @return true si connecté
|
||||
*/
|
||||
boolean isConnected();
|
||||
|
||||
/**
|
||||
* Vérifie si un realm existe
|
||||
* @param realmName nom du realm
|
||||
* @return true si le realm existe
|
||||
*/
|
||||
boolean realmExists(String realmName);
|
||||
|
||||
/**
|
||||
* Récupère la liste de tous les realms
|
||||
* @return Liste des noms de realms
|
||||
*/
|
||||
java.util.List<String> getAllRealms();
|
||||
|
||||
/**
|
||||
* Récupère la liste des clientId d'un realm
|
||||
* @param realmName nom du realm
|
||||
* @return Liste des clientId
|
||||
*/
|
||||
java.util.List<String> getRealmClients(String realmName);
|
||||
|
||||
/**
|
||||
* Ferme la connexion Keycloak
|
||||
*/
|
||||
void close();
|
||||
|
||||
/**
|
||||
* Force la reconnexion
|
||||
*/
|
||||
void reconnect();
|
||||
}
|
||||
package dev.lions.user.manager.client;
|
||||
|
||||
import org.keycloak.admin.client.Keycloak;
|
||||
import org.keycloak.admin.client.resource.RealmResource;
|
||||
import org.keycloak.admin.client.resource.UsersResource;
|
||||
import org.keycloak.admin.client.resource.RolesResource;
|
||||
|
||||
/**
|
||||
* Interface pour le client Keycloak Admin
|
||||
* Abstraction pour faciliter les tests et la gestion du cycle de vie
|
||||
*/
|
||||
public interface KeycloakAdminClient {
|
||||
|
||||
/**
|
||||
* Récupère l'instance Keycloak
|
||||
* @return instance Keycloak
|
||||
*/
|
||||
Keycloak getInstance();
|
||||
|
||||
/**
|
||||
* Récupère une ressource Realm
|
||||
* @param realmName nom du realm
|
||||
* @return RealmResource
|
||||
*/
|
||||
RealmResource getRealm(String realmName);
|
||||
|
||||
/**
|
||||
* Récupère la ressource Users d'un realm
|
||||
* @param realmName nom du realm
|
||||
* @return UsersResource
|
||||
*/
|
||||
UsersResource getUsers(String realmName);
|
||||
|
||||
/**
|
||||
* Récupère la ressource Roles d'un realm
|
||||
* @param realmName nom du realm
|
||||
* @return RolesResource
|
||||
*/
|
||||
RolesResource getRoles(String realmName);
|
||||
|
||||
/**
|
||||
* Vérifie si la connexion à Keycloak est active
|
||||
* @return true si connecté
|
||||
*/
|
||||
boolean isConnected();
|
||||
|
||||
/**
|
||||
* Vérifie si un realm existe
|
||||
* @param realmName nom du realm
|
||||
* @return true si le realm existe
|
||||
*/
|
||||
boolean realmExists(String realmName);
|
||||
|
||||
/**
|
||||
* Récupère la liste de tous les realms
|
||||
* @return Liste des noms de realms
|
||||
*/
|
||||
java.util.List<String> getAllRealms();
|
||||
|
||||
/**
|
||||
* Récupère la liste des clientId d'un realm
|
||||
* @param realmName nom du realm
|
||||
* @return Liste des clientId
|
||||
*/
|
||||
java.util.List<String> getRealmClients(String realmName);
|
||||
|
||||
/**
|
||||
* Ferme la connexion Keycloak
|
||||
*/
|
||||
void close();
|
||||
|
||||
/**
|
||||
* Force la reconnexion
|
||||
*/
|
||||
void reconnect();
|
||||
}
|
||||
|
||||
@@ -1,233 +1,233 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
package dev.lions.user.manager.config;
|
||||
|
||||
import com.fasterxml.jackson.databind.DeserializationFeature;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import io.quarkus.jackson.ObjectMapperCustomizer;
|
||||
import jakarta.inject.Singleton;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* Configure Jackson globally to ignore unknown JSON properties.
|
||||
* This is required for forward compatibility with newer Keycloak versions (e.g. cpuInfo field).
|
||||
*/
|
||||
@Singleton
|
||||
@Slf4j
|
||||
public class JacksonConfig implements ObjectMapperCustomizer {
|
||||
|
||||
@Override
|
||||
public void customize(ObjectMapper objectMapper) {
|
||||
log.info("### LIONS: Applying Jackson configuration for Keycloak compatibility ###");
|
||||
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
|
||||
}
|
||||
}
|
||||
package dev.lions.user.manager.config;
|
||||
|
||||
import com.fasterxml.jackson.databind.DeserializationFeature;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import io.quarkus.jackson.ObjectMapperCustomizer;
|
||||
import jakarta.inject.Singleton;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* Configure Jackson globally to ignore unknown JSON properties.
|
||||
* This is required for forward compatibility with newer Keycloak versions (e.g. cpuInfo field).
|
||||
*/
|
||||
@Singleton
|
||||
@Slf4j
|
||||
public class JacksonConfig implements ObjectMapperCustomizer {
|
||||
|
||||
@Override
|
||||
public void customize(ObjectMapper objectMapper) {
|
||||
log.info("### LIONS: Applying Jackson configuration for Keycloak compatibility ###");
|
||||
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,33 +1,33 @@
|
||||
package dev.lions.user.manager.config;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import io.quarkus.jackson.ObjectMapperCustomizer;
|
||||
import jakarta.inject.Singleton;
|
||||
import org.keycloak.representations.idm.RealmRepresentation;
|
||||
import org.keycloak.representations.idm.UserRepresentation;
|
||||
import org.keycloak.representations.idm.RoleRepresentation;
|
||||
|
||||
/**
|
||||
* Customizer pour Jackson afin d'ignorer les propriétés inconnues dans les
|
||||
* représentations Keycloak.
|
||||
* Cela évite les erreurs de désérialisation (comme bruteForceStrategy) lorsque
|
||||
* le serveur Keycloak
|
||||
* est plus récent que les bibliothèques clients.
|
||||
*/
|
||||
@Singleton
|
||||
public class KeycloakJacksonCustomizer implements ObjectMapperCustomizer {
|
||||
|
||||
@Override
|
||||
public void customize(ObjectMapper objectMapper) {
|
||||
// En plus de la configuration globale, on force les Mix-ins pour les classes
|
||||
// Keycloak critiques
|
||||
objectMapper.addMixIn(RealmRepresentation.class, IgnoreUnknownMixin.class);
|
||||
objectMapper.addMixIn(UserRepresentation.class, IgnoreUnknownMixin.class);
|
||||
objectMapper.addMixIn(RoleRepresentation.class, IgnoreUnknownMixin.class);
|
||||
}
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
abstract static class IgnoreUnknownMixin {
|
||||
}
|
||||
}
|
||||
package dev.lions.user.manager.config;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import io.quarkus.jackson.ObjectMapperCustomizer;
|
||||
import jakarta.inject.Singleton;
|
||||
import org.keycloak.representations.idm.RealmRepresentation;
|
||||
import org.keycloak.representations.idm.UserRepresentation;
|
||||
import org.keycloak.representations.idm.RoleRepresentation;
|
||||
|
||||
/**
|
||||
* Customizer pour Jackson afin d'ignorer les propriétés inconnues dans les
|
||||
* représentations Keycloak.
|
||||
* Cela évite les erreurs de désérialisation (comme bruteForceStrategy) lorsque
|
||||
* le serveur Keycloak
|
||||
* est plus récent que les bibliothèques clients.
|
||||
*/
|
||||
@Singleton
|
||||
public class KeycloakJacksonCustomizer implements ObjectMapperCustomizer {
|
||||
|
||||
@Override
|
||||
public void customize(ObjectMapper objectMapper) {
|
||||
// En plus de la configuration globale, on force les Mix-ins pour les classes
|
||||
// Keycloak critiques
|
||||
objectMapper.addMixIn(RealmRepresentation.class, IgnoreUnknownMixin.class);
|
||||
objectMapper.addMixIn(UserRepresentation.class, IgnoreUnknownMixin.class);
|
||||
objectMapper.addMixIn(RoleRepresentation.class, IgnoreUnknownMixin.class);
|
||||
}
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
abstract static class IgnoreUnknownMixin {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,281 +1,281 @@
|
||||
package dev.lions.user.manager.config;
|
||||
|
||||
import io.quarkus.arc.profile.IfBuildProfile;
|
||||
import io.quarkus.runtime.StartupEvent;
|
||||
import jakarta.enterprise.event.Observes;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.inject.Singleton;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.eclipse.microprofile.config.inject.ConfigProperty;
|
||||
import org.keycloak.admin.client.Keycloak;
|
||||
import org.keycloak.admin.client.KeycloakBuilder;
|
||||
import org.keycloak.representations.idm.CredentialRepresentation;
|
||||
import org.keycloak.representations.idm.ProtocolMapperRepresentation;
|
||||
import org.keycloak.representations.idm.RealmRepresentation;
|
||||
import org.keycloak.representations.idm.RoleRepresentation;
|
||||
import org.keycloak.representations.idm.UserRepresentation;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* Configuration automatique de Keycloak pour l'utilisateur de test
|
||||
* S'exécute au démarrage de l'application en mode dev
|
||||
*/
|
||||
@Singleton
|
||||
@IfBuildProfile("dev")
|
||||
@Slf4j
|
||||
public class KeycloakTestUserConfig {
|
||||
|
||||
@Inject
|
||||
@ConfigProperty(name = "quarkus.profile", defaultValue = "prod")
|
||||
String profile;
|
||||
|
||||
@Inject
|
||||
@ConfigProperty(name = "lions.keycloak.server-url")
|
||||
String keycloakServerUrl;
|
||||
|
||||
@Inject
|
||||
@ConfigProperty(name = "lions.keycloak.admin-realm", defaultValue = "master")
|
||||
String adminRealm;
|
||||
|
||||
@Inject
|
||||
@ConfigProperty(name = "lions.keycloak.admin-username", defaultValue = "admin")
|
||||
String adminUsername;
|
||||
|
||||
@Inject
|
||||
@ConfigProperty(name = "lions.keycloak.admin-password", defaultValue = "admin")
|
||||
String adminPassword;
|
||||
|
||||
@Inject
|
||||
@ConfigProperty(name = "lions.keycloak.authorized-realms")
|
||||
String authorizedRealms;
|
||||
|
||||
private static final String TEST_REALM = "lions-user-manager";
|
||||
private static final String TEST_USER = "test-user";
|
||||
private static final String TEST_PASSWORD = "test123";
|
||||
private static final String TEST_EMAIL = "test@lions.dev";
|
||||
private static final String CLIENT_ID = "lions-user-manager-client";
|
||||
|
||||
private static final List<String> REQUIRED_ROLES = Arrays.asList(
|
||||
"admin", "user_manager", "user_viewer",
|
||||
"role_manager", "role_viewer", "auditor", "sync_manager"
|
||||
);
|
||||
|
||||
void onStart(@Observes StartupEvent ev) {
|
||||
// DÉSACTIVÉ: Configuration manuelle via script create-roles-and-assign.sh
|
||||
// Cette configuration automatique cause des erreurs de compatibilité Keycloak
|
||||
// (bruteForceStrategy, cpuInfo non reconnus par la version Keycloak client)
|
||||
log.info("Configuration automatique de Keycloak DÉSACTIVÉE");
|
||||
log.info("Utiliser le script create-roles-and-assign.sh pour configurer Keycloak manuellement");
|
||||
return;
|
||||
|
||||
/* ANCIEN CODE DÉSACTIVÉ
|
||||
// Ne s'exécuter qu'en mode dev
|
||||
if (!"dev".equals(profile) && !"development".equals(profile)) {
|
||||
log.debug("Mode non-dev détecté ({}), configuration Keycloak ignorée", profile);
|
||||
return;
|
||||
}
|
||||
|
||||
log.info("Configuration automatique de Keycloak pour l'utilisateur de test...");
|
||||
|
||||
Keycloak adminClient = null;
|
||||
try {
|
||||
// Connexion en tant qu'admin
|
||||
adminClient = KeycloakBuilder.builder()
|
||||
.serverUrl(keycloakServerUrl)
|
||||
.realm(adminRealm)
|
||||
.username(adminUsername)
|
||||
.password(adminPassword)
|
||||
.clientId("admin-cli")
|
||||
.build();
|
||||
|
||||
// 1. Vérifier/Créer le realm
|
||||
ensureRealmExists(adminClient);
|
||||
|
||||
// 2. Créer les rôles
|
||||
ensureRolesExist(adminClient);
|
||||
|
||||
// 3. Créer l'utilisateur de test
|
||||
String userId = ensureTestUserExists(adminClient);
|
||||
|
||||
// 4. Assigner les rôles
|
||||
assignRolesToUser(adminClient, userId);
|
||||
|
||||
// 5. Vérifier/Créer le client et le mapper
|
||||
ensureClientAndMapper(adminClient);
|
||||
|
||||
log.info("✓ Configuration Keycloak terminée avec succès");
|
||||
log.info(" Utilisateur de test: {} / {}", TEST_USER, TEST_PASSWORD);
|
||||
log.info(" Rôles assignés: {}", String.join(", ", REQUIRED_ROLES));
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("Erreur lors de la configuration Keycloak: {}", e.getMessage(), e);
|
||||
} finally {
|
||||
if (adminClient != null) {
|
||||
adminClient.close();
|
||||
}
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
private void ensureRealmExists(Keycloak adminClient) {
|
||||
try {
|
||||
adminClient.realms().realm(TEST_REALM).toRepresentation();
|
||||
log.debug("Realm '{}' existe déjà", TEST_REALM);
|
||||
} catch (jakarta.ws.rs.NotFoundException e) {
|
||||
log.info("Création du realm '{}'...", TEST_REALM);
|
||||
RealmRepresentation realm = new RealmRepresentation();
|
||||
realm.setRealm(TEST_REALM);
|
||||
realm.setEnabled(true);
|
||||
adminClient.realms().create(realm);
|
||||
log.info("✓ Realm '{}' créé", TEST_REALM);
|
||||
}
|
||||
}
|
||||
|
||||
private void ensureRolesExist(Keycloak adminClient) {
|
||||
var rolesResource = adminClient.realms().realm(TEST_REALM).roles();
|
||||
|
||||
for (String roleName : REQUIRED_ROLES) {
|
||||
try {
|
||||
rolesResource.get(roleName).toRepresentation();
|
||||
log.debug("Rôle '{}' existe déjà", roleName);
|
||||
} catch (jakarta.ws.rs.NotFoundException e) {
|
||||
log.info("Création du rôle '{}'...", roleName);
|
||||
RoleRepresentation role = new RoleRepresentation();
|
||||
role.setName(roleName);
|
||||
role.setDescription("Rôle " + roleName + " pour lions-user-manager");
|
||||
rolesResource.create(role);
|
||||
log.info("✓ Rôle '{}' créé", roleName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private String ensureTestUserExists(Keycloak adminClient) {
|
||||
var usersResource = adminClient.realms().realm(TEST_REALM).users();
|
||||
|
||||
// Chercher l'utilisateur
|
||||
List<UserRepresentation> users = usersResource.search(TEST_USER, true);
|
||||
|
||||
String userId;
|
||||
if (users != null && !users.isEmpty()) {
|
||||
userId = users.get(0).getId();
|
||||
log.debug("Utilisateur '{}' existe déjà (ID: {})", TEST_USER, userId);
|
||||
} else {
|
||||
log.info("Création de l'utilisateur '{}'...", TEST_USER);
|
||||
UserRepresentation user = new UserRepresentation();
|
||||
user.setUsername(TEST_USER);
|
||||
user.setEmail(TEST_EMAIL);
|
||||
user.setFirstName("Test");
|
||||
user.setLastName("User");
|
||||
user.setEnabled(true);
|
||||
user.setEmailVerified(true);
|
||||
|
||||
jakarta.ws.rs.core.Response response = usersResource.create(user);
|
||||
userId = getCreatedId(response);
|
||||
|
||||
// Définir le mot de passe
|
||||
CredentialRepresentation credential = new CredentialRepresentation();
|
||||
credential.setType(CredentialRepresentation.PASSWORD);
|
||||
credential.setValue(TEST_PASSWORD);
|
||||
credential.setTemporary(false);
|
||||
usersResource.get(userId).resetPassword(credential);
|
||||
|
||||
log.info("✓ Utilisateur '{}' créé (ID: {})", TEST_USER, userId);
|
||||
}
|
||||
|
||||
return userId;
|
||||
}
|
||||
|
||||
private void assignRolesToUser(Keycloak adminClient, String userId) {
|
||||
var usersResource = adminClient.realms().realm(TEST_REALM).users();
|
||||
var rolesResource = adminClient.realms().realm(TEST_REALM).roles();
|
||||
|
||||
List<RoleRepresentation> rolesToAssign = new ArrayList<>();
|
||||
for (String roleName : REQUIRED_ROLES) {
|
||||
RoleRepresentation role = rolesResource.get(roleName).toRepresentation();
|
||||
rolesToAssign.add(role);
|
||||
}
|
||||
|
||||
usersResource.get(userId).roles().realmLevel().add(rolesToAssign);
|
||||
log.info("✓ {} rôles assignés à l'utilisateur", rolesToAssign.size());
|
||||
}
|
||||
|
||||
private void ensureClientAndMapper(Keycloak adminClient) {
|
||||
try {
|
||||
var clientsResource = adminClient.realms().realm(TEST_REALM).clients();
|
||||
var clients = clientsResource.findByClientId(CLIENT_ID);
|
||||
|
||||
String clientId;
|
||||
if (clients == null || clients.isEmpty()) {
|
||||
log.info("Création du client '{}'...", CLIENT_ID);
|
||||
org.keycloak.representations.idm.ClientRepresentation client = new org.keycloak.representations.idm.ClientRepresentation();
|
||||
client.setClientId(CLIENT_ID);
|
||||
client.setName(CLIENT_ID);
|
||||
client.setDescription("Client OIDC pour lions-user-manager");
|
||||
client.setEnabled(true);
|
||||
client.setPublicClient(false);
|
||||
client.setStandardFlowEnabled(true);
|
||||
client.setDirectAccessGrantsEnabled(true);
|
||||
client.setFullScopeAllowed(true); // IMPORTANT: Permet d'inclure tous les rôles dans le token
|
||||
client.setRedirectUris(java.util.Arrays.asList(
|
||||
"http://localhost:8080/*",
|
||||
"http://localhost:8080/auth/callback"
|
||||
));
|
||||
client.setWebOrigins(java.util.Arrays.asList("http://localhost:8080"));
|
||||
client.setSecret("NTuaQpk5E6qiMqAWTFrCOcIkOABzZzKO");
|
||||
|
||||
jakarta.ws.rs.core.Response response = clientsResource.create(client);
|
||||
clientId = getCreatedId(response);
|
||||
log.info("✓ Client '{}' créé (ID: {})", CLIENT_ID, clientId);
|
||||
} else {
|
||||
clientId = clients.get(0).getId();
|
||||
log.debug("Client '{}' existe déjà (ID: {})", CLIENT_ID, clientId);
|
||||
}
|
||||
|
||||
// Ajouter le scope "roles" par défaut au client
|
||||
try {
|
||||
var clientScopesResource = adminClient.realms().realm(TEST_REALM).clientScopes();
|
||||
var defaultClientScopes = clientScopesResource.findAll();
|
||||
var rolesScope = defaultClientScopes.stream()
|
||||
.filter(s -> "roles".equals(s.getName()))
|
||||
.findFirst();
|
||||
|
||||
if (rolesScope.isPresent()) {
|
||||
var clientResource = clientsResource.get(clientId);
|
||||
var defaultScopes = clientResource.getDefaultClientScopes();
|
||||
boolean hasRolesScope = defaultScopes.stream()
|
||||
.anyMatch(s -> "roles".equals(s.getName()));
|
||||
|
||||
if (!hasRolesScope) {
|
||||
log.info("Ajout du scope 'roles' au client...");
|
||||
clientResource.addDefaultClientScope(rolesScope.get().getId());
|
||||
log.info("✓ Scope 'roles' ajouté au client");
|
||||
} else {
|
||||
log.debug("Scope 'roles' déjà présent sur le client");
|
||||
}
|
||||
} else {
|
||||
log.warn("Scope 'roles' non trouvé dans les scopes par défaut du realm");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("Erreur lors de l'ajout du scope 'roles': {}", e.getMessage());
|
||||
}
|
||||
|
||||
// Le scope "roles" de Keycloak crée automatiquement realm_access.roles
|
||||
// Pas besoin de mapper personnalisé si on utilise realm_access.roles
|
||||
// Le mapper personnalisé peut créer des conflits (comme dans unionflow)
|
||||
log.debug("Le scope 'roles' est utilisé pour créer realm_access.roles automatiquement");
|
||||
} catch (Exception e) {
|
||||
log.warn("Erreur lors de la vérification/création du client: {}", e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
private String getCreatedId(jakarta.ws.rs.core.Response response) {
|
||||
jakarta.ws.rs.core.Response.StatusType statusInfo = response.getStatusInfo();
|
||||
if (statusInfo.equals(jakarta.ws.rs.core.Response.Status.CREATED)) {
|
||||
String location = response.getLocation().getPath();
|
||||
return location.substring(location.lastIndexOf('/') + 1);
|
||||
}
|
||||
throw new RuntimeException("Erreur lors de la création: " + statusInfo.getStatusCode());
|
||||
}
|
||||
}
|
||||
|
||||
package dev.lions.user.manager.config;
|
||||
|
||||
import io.quarkus.arc.profile.IfBuildProfile;
|
||||
import io.quarkus.runtime.StartupEvent;
|
||||
import jakarta.enterprise.event.Observes;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.inject.Singleton;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.eclipse.microprofile.config.inject.ConfigProperty;
|
||||
import org.keycloak.admin.client.Keycloak;
|
||||
import org.keycloak.admin.client.KeycloakBuilder;
|
||||
import org.keycloak.representations.idm.CredentialRepresentation;
|
||||
import org.keycloak.representations.idm.ProtocolMapperRepresentation;
|
||||
import org.keycloak.representations.idm.RealmRepresentation;
|
||||
import org.keycloak.representations.idm.RoleRepresentation;
|
||||
import org.keycloak.representations.idm.UserRepresentation;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* Configuration automatique de Keycloak pour l'utilisateur de test
|
||||
* S'exécute au démarrage de l'application en mode dev
|
||||
*/
|
||||
@Singleton
|
||||
@IfBuildProfile("dev")
|
||||
@Slf4j
|
||||
public class KeycloakTestUserConfig {
|
||||
|
||||
@Inject
|
||||
@ConfigProperty(name = "quarkus.profile", defaultValue = "prod")
|
||||
String profile;
|
||||
|
||||
@Inject
|
||||
@ConfigProperty(name = "lions.keycloak.server-url")
|
||||
String keycloakServerUrl;
|
||||
|
||||
@Inject
|
||||
@ConfigProperty(name = "lions.keycloak.admin-realm", defaultValue = "master")
|
||||
String adminRealm;
|
||||
|
||||
@Inject
|
||||
@ConfigProperty(name = "lions.keycloak.admin-username", defaultValue = "admin")
|
||||
String adminUsername;
|
||||
|
||||
@Inject
|
||||
@ConfigProperty(name = "lions.keycloak.admin-password", defaultValue = "admin")
|
||||
String adminPassword;
|
||||
|
||||
@Inject
|
||||
@ConfigProperty(name = "lions.keycloak.authorized-realms")
|
||||
String authorizedRealms;
|
||||
|
||||
private static final String TEST_REALM = "lions-user-manager";
|
||||
private static final String TEST_USER = "test-user";
|
||||
private static final String TEST_PASSWORD = "test123";
|
||||
private static final String TEST_EMAIL = "test@lions.dev";
|
||||
private static final String CLIENT_ID = "lions-user-manager-client";
|
||||
|
||||
private static final List<String> REQUIRED_ROLES = Arrays.asList(
|
||||
"admin", "user_manager", "user_viewer",
|
||||
"role_manager", "role_viewer", "auditor", "sync_manager"
|
||||
);
|
||||
|
||||
void onStart(@Observes StartupEvent ev) {
|
||||
// DÉSACTIVÉ: Configuration manuelle via script create-roles-and-assign.sh
|
||||
// Cette configuration automatique cause des erreurs de compatibilité Keycloak
|
||||
// (bruteForceStrategy, cpuInfo non reconnus par la version Keycloak client)
|
||||
log.info("Configuration automatique de Keycloak DÉSACTIVÉE");
|
||||
log.info("Utiliser le script create-roles-and-assign.sh pour configurer Keycloak manuellement");
|
||||
return;
|
||||
|
||||
/* ANCIEN CODE DÉSACTIVÉ
|
||||
// Ne s'exécuter qu'en mode dev
|
||||
if (!"dev".equals(profile) && !"development".equals(profile)) {
|
||||
log.debug("Mode non-dev détecté ({}), configuration Keycloak ignorée", profile);
|
||||
return;
|
||||
}
|
||||
|
||||
log.info("Configuration automatique de Keycloak pour l'utilisateur de test...");
|
||||
|
||||
Keycloak adminClient = null;
|
||||
try {
|
||||
// Connexion en tant qu'admin
|
||||
adminClient = KeycloakBuilder.builder()
|
||||
.serverUrl(keycloakServerUrl)
|
||||
.realm(adminRealm)
|
||||
.username(adminUsername)
|
||||
.password(adminPassword)
|
||||
.clientId("admin-cli")
|
||||
.build();
|
||||
|
||||
// 1. Vérifier/Créer le realm
|
||||
ensureRealmExists(adminClient);
|
||||
|
||||
// 2. Créer les rôles
|
||||
ensureRolesExist(adminClient);
|
||||
|
||||
// 3. Créer l'utilisateur de test
|
||||
String userId = ensureTestUserExists(adminClient);
|
||||
|
||||
// 4. Assigner les rôles
|
||||
assignRolesToUser(adminClient, userId);
|
||||
|
||||
// 5. Vérifier/Créer le client et le mapper
|
||||
ensureClientAndMapper(adminClient);
|
||||
|
||||
log.info("✓ Configuration Keycloak terminée avec succès");
|
||||
log.info(" Utilisateur de test: {} / {}", TEST_USER, TEST_PASSWORD);
|
||||
log.info(" Rôles assignés: {}", String.join(", ", REQUIRED_ROLES));
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("Erreur lors de la configuration Keycloak: {}", e.getMessage(), e);
|
||||
} finally {
|
||||
if (adminClient != null) {
|
||||
adminClient.close();
|
||||
}
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
private void ensureRealmExists(Keycloak adminClient) {
|
||||
try {
|
||||
adminClient.realms().realm(TEST_REALM).toRepresentation();
|
||||
log.debug("Realm '{}' existe déjà", TEST_REALM);
|
||||
} catch (jakarta.ws.rs.NotFoundException e) {
|
||||
log.info("Création du realm '{}'...", TEST_REALM);
|
||||
RealmRepresentation realm = new RealmRepresentation();
|
||||
realm.setRealm(TEST_REALM);
|
||||
realm.setEnabled(true);
|
||||
adminClient.realms().create(realm);
|
||||
log.info("✓ Realm '{}' créé", TEST_REALM);
|
||||
}
|
||||
}
|
||||
|
||||
private void ensureRolesExist(Keycloak adminClient) {
|
||||
var rolesResource = adminClient.realms().realm(TEST_REALM).roles();
|
||||
|
||||
for (String roleName : REQUIRED_ROLES) {
|
||||
try {
|
||||
rolesResource.get(roleName).toRepresentation();
|
||||
log.debug("Rôle '{}' existe déjà", roleName);
|
||||
} catch (jakarta.ws.rs.NotFoundException e) {
|
||||
log.info("Création du rôle '{}'...", roleName);
|
||||
RoleRepresentation role = new RoleRepresentation();
|
||||
role.setName(roleName);
|
||||
role.setDescription("Rôle " + roleName + " pour lions-user-manager");
|
||||
rolesResource.create(role);
|
||||
log.info("✓ Rôle '{}' créé", roleName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private String ensureTestUserExists(Keycloak adminClient) {
|
||||
var usersResource = adminClient.realms().realm(TEST_REALM).users();
|
||||
|
||||
// Chercher l'utilisateur
|
||||
List<UserRepresentation> users = usersResource.search(TEST_USER, true);
|
||||
|
||||
String userId;
|
||||
if (users != null && !users.isEmpty()) {
|
||||
userId = users.get(0).getId();
|
||||
log.debug("Utilisateur '{}' existe déjà (ID: {})", TEST_USER, userId);
|
||||
} else {
|
||||
log.info("Création de l'utilisateur '{}'...", TEST_USER);
|
||||
UserRepresentation user = new UserRepresentation();
|
||||
user.setUsername(TEST_USER);
|
||||
user.setEmail(TEST_EMAIL);
|
||||
user.setFirstName("Test");
|
||||
user.setLastName("User");
|
||||
user.setEnabled(true);
|
||||
user.setEmailVerified(true);
|
||||
|
||||
jakarta.ws.rs.core.Response response = usersResource.create(user);
|
||||
userId = getCreatedId(response);
|
||||
|
||||
// Définir le mot de passe
|
||||
CredentialRepresentation credential = new CredentialRepresentation();
|
||||
credential.setType(CredentialRepresentation.PASSWORD);
|
||||
credential.setValue(TEST_PASSWORD);
|
||||
credential.setTemporary(false);
|
||||
usersResource.get(userId).resetPassword(credential);
|
||||
|
||||
log.info("✓ Utilisateur '{}' créé (ID: {})", TEST_USER, userId);
|
||||
}
|
||||
|
||||
return userId;
|
||||
}
|
||||
|
||||
private void assignRolesToUser(Keycloak adminClient, String userId) {
|
||||
var usersResource = adminClient.realms().realm(TEST_REALM).users();
|
||||
var rolesResource = adminClient.realms().realm(TEST_REALM).roles();
|
||||
|
||||
List<RoleRepresentation> rolesToAssign = new ArrayList<>();
|
||||
for (String roleName : REQUIRED_ROLES) {
|
||||
RoleRepresentation role = rolesResource.get(roleName).toRepresentation();
|
||||
rolesToAssign.add(role);
|
||||
}
|
||||
|
||||
usersResource.get(userId).roles().realmLevel().add(rolesToAssign);
|
||||
log.info("✓ {} rôles assignés à l'utilisateur", rolesToAssign.size());
|
||||
}
|
||||
|
||||
private void ensureClientAndMapper(Keycloak adminClient) {
|
||||
try {
|
||||
var clientsResource = adminClient.realms().realm(TEST_REALM).clients();
|
||||
var clients = clientsResource.findByClientId(CLIENT_ID);
|
||||
|
||||
String clientId;
|
||||
if (clients == null || clients.isEmpty()) {
|
||||
log.info("Création du client '{}'...", CLIENT_ID);
|
||||
org.keycloak.representations.idm.ClientRepresentation client = new org.keycloak.representations.idm.ClientRepresentation();
|
||||
client.setClientId(CLIENT_ID);
|
||||
client.setName(CLIENT_ID);
|
||||
client.setDescription("Client OIDC pour lions-user-manager");
|
||||
client.setEnabled(true);
|
||||
client.setPublicClient(false);
|
||||
client.setStandardFlowEnabled(true);
|
||||
client.setDirectAccessGrantsEnabled(true);
|
||||
client.setFullScopeAllowed(true); // IMPORTANT: Permet d'inclure tous les rôles dans le token
|
||||
client.setRedirectUris(java.util.Arrays.asList(
|
||||
"http://localhost:8080/*",
|
||||
"http://localhost:8080/auth/callback"
|
||||
));
|
||||
client.setWebOrigins(java.util.Arrays.asList("http://localhost:8080"));
|
||||
client.setSecret("NTuaQpk5E6qiMqAWTFrCOcIkOABzZzKO");
|
||||
|
||||
jakarta.ws.rs.core.Response response = clientsResource.create(client);
|
||||
clientId = getCreatedId(response);
|
||||
log.info("✓ Client '{}' créé (ID: {})", CLIENT_ID, clientId);
|
||||
} else {
|
||||
clientId = clients.get(0).getId();
|
||||
log.debug("Client '{}' existe déjà (ID: {})", CLIENT_ID, clientId);
|
||||
}
|
||||
|
||||
// Ajouter le scope "roles" par défaut au client
|
||||
try {
|
||||
var clientScopesResource = adminClient.realms().realm(TEST_REALM).clientScopes();
|
||||
var defaultClientScopes = clientScopesResource.findAll();
|
||||
var rolesScope = defaultClientScopes.stream()
|
||||
.filter(s -> "roles".equals(s.getName()))
|
||||
.findFirst();
|
||||
|
||||
if (rolesScope.isPresent()) {
|
||||
var clientResource = clientsResource.get(clientId);
|
||||
var defaultScopes = clientResource.getDefaultClientScopes();
|
||||
boolean hasRolesScope = defaultScopes.stream()
|
||||
.anyMatch(s -> "roles".equals(s.getName()));
|
||||
|
||||
if (!hasRolesScope) {
|
||||
log.info("Ajout du scope 'roles' au client...");
|
||||
clientResource.addDefaultClientScope(rolesScope.get().getId());
|
||||
log.info("✓ Scope 'roles' ajouté au client");
|
||||
} else {
|
||||
log.debug("Scope 'roles' déjà présent sur le client");
|
||||
}
|
||||
} else {
|
||||
log.warn("Scope 'roles' non trouvé dans les scopes par défaut du realm");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("Erreur lors de l'ajout du scope 'roles': {}", e.getMessage());
|
||||
}
|
||||
|
||||
// Le scope "roles" de Keycloak crée automatiquement realm_access.roles
|
||||
// Pas besoin de mapper personnalisé si on utilise realm_access.roles
|
||||
// Le mapper personnalisé peut créer des conflits (comme dans unionflow)
|
||||
log.debug("Le scope 'roles' est utilisé pour créer realm_access.roles automatiquement");
|
||||
} catch (Exception e) {
|
||||
log.warn("Erreur lors de la vérification/création du client: {}", e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
private String getCreatedId(jakarta.ws.rs.core.Response response) {
|
||||
jakarta.ws.rs.core.Response.StatusType statusInfo = response.getStatusInfo();
|
||||
if (statusInfo.equals(jakarta.ws.rs.core.Response.Status.CREATED)) {
|
||||
String location = response.getLocation().getPath();
|
||||
return location.substring(location.lastIndexOf('/') + 1);
|
||||
}
|
||||
throw new RuntimeException("Erreur lors de la création: " + statusInfo.getStatusCode());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,76 +1,76 @@
|
||||
package dev.lions.user.manager.mapper;
|
||||
|
||||
import dev.lions.user.manager.dto.role.RoleDTO;
|
||||
import dev.lions.user.manager.enums.role.TypeRole;
|
||||
import org.keycloak.representations.idm.RoleRepresentation;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Mapper pour convertir entre RoleDTO et Keycloak RoleRepresentation
|
||||
*/
|
||||
public class RoleMapper {
|
||||
|
||||
/**
|
||||
* Convertit une RoleRepresentation Keycloak en RoleDTO
|
||||
*/
|
||||
public static RoleDTO toDTO(RoleRepresentation roleRep, String realmName, TypeRole typeRole) {
|
||||
if (roleRep == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return RoleDTO.builder()
|
||||
.id(roleRep.getId())
|
||||
.name(roleRep.getName())
|
||||
.description(roleRep.getDescription())
|
||||
.typeRole(typeRole)
|
||||
.realmName(realmName)
|
||||
.composite(roleRep.isComposite())
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Convertit un RoleDTO en RoleRepresentation Keycloak
|
||||
*/
|
||||
public static RoleRepresentation toRepresentation(RoleDTO roleDTO) {
|
||||
if (roleDTO == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
RoleRepresentation roleRep = new RoleRepresentation();
|
||||
roleRep.setId(roleDTO.getId());
|
||||
roleRep.setName(roleDTO.getName());
|
||||
roleRep.setDescription(roleDTO.getDescription());
|
||||
roleRep.setComposite(roleDTO.isComposite());
|
||||
roleRep.setClientRole(roleDTO.getTypeRole() == TypeRole.CLIENT_ROLE);
|
||||
|
||||
return roleRep;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convertit une liste de RoleRepresentation en liste de RoleDTO
|
||||
*/
|
||||
public static List<RoleDTO> toDTOList(List<RoleRepresentation> roleReps, String realmName, TypeRole typeRole) {
|
||||
if (roleReps == null) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
return roleReps.stream()
|
||||
.map(roleRep -> toDTO(roleRep, realmName, typeRole))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* Convertit une liste de RoleDTO en liste de RoleRepresentation
|
||||
*/
|
||||
public static List<RoleRepresentation> toRepresentationList(List<RoleDTO> roleDTOs) {
|
||||
if (roleDTOs == null) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
return roleDTOs.stream()
|
||||
.map(RoleMapper::toRepresentation)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
}
|
||||
package dev.lions.user.manager.mapper;
|
||||
|
||||
import dev.lions.user.manager.dto.role.RoleDTO;
|
||||
import dev.lions.user.manager.enums.role.TypeRole;
|
||||
import org.keycloak.representations.idm.RoleRepresentation;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Mapper pour convertir entre RoleDTO et Keycloak RoleRepresentation
|
||||
*/
|
||||
public class RoleMapper {
|
||||
|
||||
/**
|
||||
* Convertit une RoleRepresentation Keycloak en RoleDTO
|
||||
*/
|
||||
public static RoleDTO toDTO(RoleRepresentation roleRep, String realmName, TypeRole typeRole) {
|
||||
if (roleRep == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return RoleDTO.builder()
|
||||
.id(roleRep.getId())
|
||||
.name(roleRep.getName())
|
||||
.description(roleRep.getDescription())
|
||||
.typeRole(typeRole)
|
||||
.realmName(realmName)
|
||||
.composite(roleRep.isComposite())
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Convertit un RoleDTO en RoleRepresentation Keycloak
|
||||
*/
|
||||
public static RoleRepresentation toRepresentation(RoleDTO roleDTO) {
|
||||
if (roleDTO == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
RoleRepresentation roleRep = new RoleRepresentation();
|
||||
roleRep.setId(roleDTO.getId());
|
||||
roleRep.setName(roleDTO.getName());
|
||||
roleRep.setDescription(roleDTO.getDescription());
|
||||
roleRep.setComposite(roleDTO.isComposite());
|
||||
roleRep.setClientRole(roleDTO.getTypeRole() == TypeRole.CLIENT_ROLE);
|
||||
|
||||
return roleRep;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convertit une liste de RoleRepresentation en liste de RoleDTO
|
||||
*/
|
||||
public static List<RoleDTO> toDTOList(List<RoleRepresentation> roleReps, String realmName, TypeRole typeRole) {
|
||||
if (roleReps == null) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
return roleReps.stream()
|
||||
.map(roleRep -> toDTO(roleRep, realmName, typeRole))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* Convertit une liste de RoleDTO en liste de RoleRepresentation
|
||||
*/
|
||||
public static List<RoleRepresentation> toRepresentationList(List<RoleDTO> roleDTOs) {
|
||||
if (roleDTOs == null) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
return roleDTOs.stream()
|
||||
.map(RoleMapper::toRepresentation)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,173 +1,173 @@
|
||||
package dev.lions.user.manager.mapper;
|
||||
|
||||
import dev.lions.user.manager.dto.user.UserDTO;
|
||||
import dev.lions.user.manager.enums.user.StatutUser;
|
||||
import org.keycloak.representations.idm.UserRepresentation;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Mapper pour convertir UserRepresentation (Keycloak) -> UserDTO
|
||||
* Utilisé pour transformer les objets de l'API Keycloak vers nos DTOs
|
||||
*/
|
||||
public class UserMapper {
|
||||
|
||||
private UserMapper() {
|
||||
// Classe utilitaire
|
||||
}
|
||||
|
||||
/**
|
||||
* Convertit UserRepresentation vers UserDTO
|
||||
* @param userRep UserRepresentation de Keycloak
|
||||
* @param realmName nom du realm
|
||||
* @return UserDTO
|
||||
*/
|
||||
public static UserDTO toDTO(UserRepresentation userRep, String realmName) {
|
||||
if (userRep == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return UserDTO.builder()
|
||||
.id(userRep.getId())
|
||||
.username(userRep.getUsername())
|
||||
.email(userRep.getEmail())
|
||||
.emailVerified(userRep.isEmailVerified())
|
||||
.prenom(userRep.getFirstName())
|
||||
.nom(userRep.getLastName())
|
||||
.statut(StatutUser.fromEnabled(userRep.isEnabled()))
|
||||
.enabled(userRep.isEnabled())
|
||||
.realmName(realmName)
|
||||
.attributes(userRep.getAttributes())
|
||||
.requiredActions(userRep.getRequiredActions())
|
||||
.dateCreation(convertTimestamp(userRep.getCreatedTimestamp()))
|
||||
.telephone(getAttributeValue(userRep, "phone_number"))
|
||||
.organisation(getAttributeValue(userRep, "organization"))
|
||||
.departement(getAttributeValue(userRep, "department"))
|
||||
.fonction(getAttributeValue(userRep, "job_title"))
|
||||
.pays(getAttributeValue(userRep, "country"))
|
||||
.ville(getAttributeValue(userRep, "city"))
|
||||
.langue(getAttributeValue(userRep, "locale"))
|
||||
.timezone(getAttributeValue(userRep, "timezone"))
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Convertit UserDTO vers UserRepresentation
|
||||
* @param userDTO UserDTO
|
||||
* @return UserRepresentation
|
||||
*/
|
||||
public static UserRepresentation toRepresentation(UserDTO userDTO) {
|
||||
if (userDTO == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
UserRepresentation userRep = new UserRepresentation();
|
||||
userRep.setId(userDTO.getId());
|
||||
userRep.setUsername(userDTO.getUsername());
|
||||
userRep.setEmail(userDTO.getEmail());
|
||||
userRep.setEmailVerified(userDTO.getEmailVerified());
|
||||
userRep.setFirstName(userDTO.getPrenom());
|
||||
userRep.setLastName(userDTO.getNom());
|
||||
userRep.setEnabled(userDTO.getEnabled() != null ? userDTO.getEnabled() : true);
|
||||
|
||||
// Attributs personnalisés
|
||||
Map<String, List<String>> attributes = new HashMap<>();
|
||||
|
||||
if (userDTO.getTelephone() != null) {
|
||||
attributes.put("phone_number", List.of(userDTO.getTelephone()));
|
||||
}
|
||||
if (userDTO.getOrganisation() != null) {
|
||||
attributes.put("organization", List.of(userDTO.getOrganisation()));
|
||||
}
|
||||
if (userDTO.getDepartement() != null) {
|
||||
attributes.put("department", List.of(userDTO.getDepartement()));
|
||||
}
|
||||
if (userDTO.getFonction() != null) {
|
||||
attributes.put("job_title", List.of(userDTO.getFonction()));
|
||||
}
|
||||
if (userDTO.getPays() != null) {
|
||||
attributes.put("country", List.of(userDTO.getPays()));
|
||||
}
|
||||
if (userDTO.getVille() != null) {
|
||||
attributes.put("city", List.of(userDTO.getVille()));
|
||||
}
|
||||
if (userDTO.getLangue() != null) {
|
||||
attributes.put("locale", List.of(userDTO.getLangue()));
|
||||
}
|
||||
if (userDTO.getTimezone() != null) {
|
||||
attributes.put("timezone", List.of(userDTO.getTimezone()));
|
||||
}
|
||||
|
||||
// Ajouter les attributs existants du DTO
|
||||
if (userDTO.getAttributes() != null) {
|
||||
attributes.putAll(userDTO.getAttributes());
|
||||
}
|
||||
|
||||
userRep.setAttributes(attributes);
|
||||
|
||||
// Actions requises
|
||||
if (userDTO.getRequiredActions() != null) {
|
||||
userRep.setRequiredActions(userDTO.getRequiredActions());
|
||||
}
|
||||
|
||||
return userRep;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convertit une liste de UserRepresentation vers UserDTO
|
||||
* @param userReps liste de UserRepresentation
|
||||
* @param realmName nom du realm
|
||||
* @return liste de UserDTO
|
||||
*/
|
||||
public static List<UserDTO> toDTOList(List<UserRepresentation> userReps, String realmName) {
|
||||
if (userReps == null) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
return userReps.stream()
|
||||
.map(userRep -> toDTO(userRep, realmName))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère la valeur d'un attribut Keycloak
|
||||
* @param userRep UserRepresentation
|
||||
* @param attributeName nom de l'attribut
|
||||
* @return valeur de l'attribut ou null
|
||||
*/
|
||||
private static String getAttributeValue(UserRepresentation userRep, String attributeName) {
|
||||
if (userRep.getAttributes() == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
List<String> values = userRep.getAttributes().get(attributeName);
|
||||
if (values == null || values.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return values.get(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convertit un timestamp (millisecondes) vers LocalDateTime
|
||||
* @param timestamp timestamp en millisecondes
|
||||
* @return LocalDateTime ou null
|
||||
*/
|
||||
private static LocalDateTime convertTimestamp(Long timestamp) {
|
||||
if (timestamp == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return LocalDateTime.ofInstant(
|
||||
Instant.ofEpochMilli(timestamp),
|
||||
ZoneId.systemDefault()
|
||||
);
|
||||
}
|
||||
}
|
||||
package dev.lions.user.manager.mapper;
|
||||
|
||||
import dev.lions.user.manager.dto.user.UserDTO;
|
||||
import dev.lions.user.manager.enums.user.StatutUser;
|
||||
import org.keycloak.representations.idm.UserRepresentation;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Mapper pour convertir UserRepresentation (Keycloak) -> UserDTO
|
||||
* Utilisé pour transformer les objets de l'API Keycloak vers nos DTOs
|
||||
*/
|
||||
public class UserMapper {
|
||||
|
||||
private UserMapper() {
|
||||
// Classe utilitaire
|
||||
}
|
||||
|
||||
/**
|
||||
* Convertit UserRepresentation vers UserDTO
|
||||
* @param userRep UserRepresentation de Keycloak
|
||||
* @param realmName nom du realm
|
||||
* @return UserDTO
|
||||
*/
|
||||
public static UserDTO toDTO(UserRepresentation userRep, String realmName) {
|
||||
if (userRep == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return UserDTO.builder()
|
||||
.id(userRep.getId())
|
||||
.username(userRep.getUsername())
|
||||
.email(userRep.getEmail())
|
||||
.emailVerified(userRep.isEmailVerified())
|
||||
.prenom(userRep.getFirstName())
|
||||
.nom(userRep.getLastName())
|
||||
.statut(StatutUser.fromEnabled(userRep.isEnabled()))
|
||||
.enabled(userRep.isEnabled())
|
||||
.realmName(realmName)
|
||||
.attributes(userRep.getAttributes())
|
||||
.requiredActions(userRep.getRequiredActions())
|
||||
.dateCreation(convertTimestamp(userRep.getCreatedTimestamp()))
|
||||
.telephone(getAttributeValue(userRep, "phone_number"))
|
||||
.organisation(getAttributeValue(userRep, "organization"))
|
||||
.departement(getAttributeValue(userRep, "department"))
|
||||
.fonction(getAttributeValue(userRep, "job_title"))
|
||||
.pays(getAttributeValue(userRep, "country"))
|
||||
.ville(getAttributeValue(userRep, "city"))
|
||||
.langue(getAttributeValue(userRep, "locale"))
|
||||
.timezone(getAttributeValue(userRep, "timezone"))
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Convertit UserDTO vers UserRepresentation
|
||||
* @param userDTO UserDTO
|
||||
* @return UserRepresentation
|
||||
*/
|
||||
public static UserRepresentation toRepresentation(UserDTO userDTO) {
|
||||
if (userDTO == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
UserRepresentation userRep = new UserRepresentation();
|
||||
userRep.setId(userDTO.getId());
|
||||
userRep.setUsername(userDTO.getUsername());
|
||||
userRep.setEmail(userDTO.getEmail());
|
||||
userRep.setEmailVerified(userDTO.getEmailVerified());
|
||||
userRep.setFirstName(userDTO.getPrenom());
|
||||
userRep.setLastName(userDTO.getNom());
|
||||
userRep.setEnabled(userDTO.getEnabled() != null ? userDTO.getEnabled() : true);
|
||||
|
||||
// Attributs personnalisés
|
||||
Map<String, List<String>> attributes = new HashMap<>();
|
||||
|
||||
if (userDTO.getTelephone() != null) {
|
||||
attributes.put("phone_number", List.of(userDTO.getTelephone()));
|
||||
}
|
||||
if (userDTO.getOrganisation() != null) {
|
||||
attributes.put("organization", List.of(userDTO.getOrganisation()));
|
||||
}
|
||||
if (userDTO.getDepartement() != null) {
|
||||
attributes.put("department", List.of(userDTO.getDepartement()));
|
||||
}
|
||||
if (userDTO.getFonction() != null) {
|
||||
attributes.put("job_title", List.of(userDTO.getFonction()));
|
||||
}
|
||||
if (userDTO.getPays() != null) {
|
||||
attributes.put("country", List.of(userDTO.getPays()));
|
||||
}
|
||||
if (userDTO.getVille() != null) {
|
||||
attributes.put("city", List.of(userDTO.getVille()));
|
||||
}
|
||||
if (userDTO.getLangue() != null) {
|
||||
attributes.put("locale", List.of(userDTO.getLangue()));
|
||||
}
|
||||
if (userDTO.getTimezone() != null) {
|
||||
attributes.put("timezone", List.of(userDTO.getTimezone()));
|
||||
}
|
||||
|
||||
// Ajouter les attributs existants du DTO
|
||||
if (userDTO.getAttributes() != null) {
|
||||
attributes.putAll(userDTO.getAttributes());
|
||||
}
|
||||
|
||||
userRep.setAttributes(attributes);
|
||||
|
||||
// Actions requises
|
||||
if (userDTO.getRequiredActions() != null) {
|
||||
userRep.setRequiredActions(userDTO.getRequiredActions());
|
||||
}
|
||||
|
||||
return userRep;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convertit une liste de UserRepresentation vers UserDTO
|
||||
* @param userReps liste de UserRepresentation
|
||||
* @param realmName nom du realm
|
||||
* @return liste de UserDTO
|
||||
*/
|
||||
public static List<UserDTO> toDTOList(List<UserRepresentation> userReps, String realmName) {
|
||||
if (userReps == null) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
return userReps.stream()
|
||||
.map(userRep -> toDTO(userRep, realmName))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère la valeur d'un attribut Keycloak
|
||||
* @param userRep UserRepresentation
|
||||
* @param attributeName nom de l'attribut
|
||||
* @return valeur de l'attribut ou null
|
||||
*/
|
||||
private static String getAttributeValue(UserRepresentation userRep, String attributeName) {
|
||||
if (userRep.getAttributes() == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
List<String> values = userRep.getAttributes().get(attributeName);
|
||||
if (values == null || values.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return values.get(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convertit un timestamp (millisecondes) vers LocalDateTime
|
||||
* @param timestamp timestamp en millisecondes
|
||||
* @return LocalDateTime ou null
|
||||
*/
|
||||
private static LocalDateTime convertTimestamp(Long timestamp) {
|
||||
if (timestamp == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return LocalDateTime.ofInstant(
|
||||
Instant.ofEpochMilli(timestamp),
|
||||
ZoneId.systemDefault()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,171 +1,171 @@
|
||||
package dev.lions.user.manager.resource;
|
||||
|
||||
import dev.lions.user.manager.api.AuditResourceApi;
|
||||
import dev.lions.user.manager.dto.audit.AuditLogDTO;
|
||||
import dev.lions.user.manager.dto.common.CountDTO;
|
||||
import dev.lions.user.manager.enums.audit.TypeActionAudit;
|
||||
import dev.lions.user.manager.service.AuditService;
|
||||
import jakarta.annotation.security.RolesAllowed;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import org.eclipse.microprofile.config.inject.ConfigProperty;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* REST Resource pour l'audit et la consultation des logs
|
||||
* Implémente l'interface API commune.
|
||||
*/
|
||||
@Slf4j
|
||||
@jakarta.enterprise.context.ApplicationScoped
|
||||
@jakarta.ws.rs.Path("/api/audit")
|
||||
public class AuditResource implements AuditResourceApi {
|
||||
|
||||
private static final String DEFAULT_REALM_VALUE = "master";
|
||||
|
||||
@Inject
|
||||
AuditService auditService;
|
||||
|
||||
@ConfigProperty(name = "lions.keycloak.admin-realm", defaultValue = DEFAULT_REALM_VALUE)
|
||||
String defaultRealm;
|
||||
|
||||
@Override
|
||||
@RolesAllowed({ "admin", "auditor" })
|
||||
public List<AuditLogDTO> searchLogs(
|
||||
String acteurUsername,
|
||||
String dateDebutStr,
|
||||
String dateFinStr,
|
||||
TypeActionAudit typeAction,
|
||||
String ressourceType,
|
||||
Boolean succes,
|
||||
int page,
|
||||
int pageSize) {
|
||||
log.info("POST /api/audit/search - Recherche de logs");
|
||||
|
||||
LocalDateTime dateDebut = dateDebutStr != null ? LocalDateTime.parse(dateDebutStr) : null;
|
||||
LocalDateTime dateFin = dateFinStr != null ? LocalDateTime.parse(dateFinStr) : null;
|
||||
|
||||
// Utiliser findByActeur si acteurUsername est fourni, sinon findByRealm
|
||||
List<AuditLogDTO> logs;
|
||||
if (acteurUsername != null && !acteurUsername.isBlank()) {
|
||||
logs = auditService.findByActeur(acteurUsername, dateDebut, dateFin, page, pageSize);
|
||||
} else {
|
||||
// Pour une recherche générale, utiliser findByRealm (on utilise defaultRealm par
|
||||
// défaut)
|
||||
logs = auditService.findByRealm(defaultRealm, dateDebut, dateFin, page, pageSize);
|
||||
}
|
||||
|
||||
// Filtrer par typeAction, ressourceType et succes si fournis
|
||||
if (typeAction != null || ressourceType != null || succes != null) {
|
||||
logs = logs.stream()
|
||||
.filter(log -> typeAction == null || typeAction.equals(log.getTypeAction()))
|
||||
.filter(log -> ressourceType == null || ressourceType.equals(log.getRessourceType()))
|
||||
.filter(log -> succes == null || succes == log.isSuccessful())
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
return logs;
|
||||
}
|
||||
|
||||
@Override
|
||||
@RolesAllowed({ "admin", "auditor" })
|
||||
public List<AuditLogDTO> getLogsByActor(String acteurUsername, int limit) {
|
||||
log.info("GET /api/audit/actor/{} - Limite: {}", acteurUsername, limit);
|
||||
return auditService.findByActeur(acteurUsername, null, null, 0, limit);
|
||||
}
|
||||
|
||||
@Override
|
||||
@RolesAllowed({ "admin", "auditor" })
|
||||
public List<AuditLogDTO> getLogsByResource(String ressourceType, String ressourceId, int limit) {
|
||||
log.info("GET /api/audit/resource/{}/{} - Limite: {}", ressourceType, ressourceId, limit);
|
||||
return auditService.findByRessource(ressourceType, ressourceId, null, null, 0, limit);
|
||||
}
|
||||
|
||||
@Override
|
||||
@RolesAllowed({ "admin", "auditor" })
|
||||
public List<AuditLogDTO> getLogsByAction(TypeActionAudit typeAction, String dateDebutStr, String dateFinStr,
|
||||
int limit) {
|
||||
log.info("GET /api/audit/action/{} - Limite: {}", typeAction, limit);
|
||||
LocalDateTime dateDebut = dateDebutStr != null ? LocalDateTime.parse(dateDebutStr) : null;
|
||||
LocalDateTime dateFin = dateFinStr != null ? LocalDateTime.parse(dateFinStr) : null;
|
||||
|
||||
return auditService.findByTypeAction(typeAction, defaultRealm, dateDebut, dateFin, 0, limit);
|
||||
}
|
||||
|
||||
@Override
|
||||
@RolesAllowed({ "admin", "auditor" })
|
||||
public Map<TypeActionAudit, Long> getActionStatistics(String dateDebutStr, String dateFinStr) {
|
||||
log.info("GET /api/audit/stats/actions - Période: {} à {}", dateDebutStr, dateFinStr);
|
||||
LocalDateTime dateDebut = dateDebutStr != null ? LocalDateTime.parse(dateDebutStr) : null;
|
||||
LocalDateTime dateFin = dateFinStr != null ? LocalDateTime.parse(dateFinStr) : null;
|
||||
|
||||
return auditService.countByActionType(defaultRealm, dateDebut, dateFin);
|
||||
}
|
||||
|
||||
@Override
|
||||
@RolesAllowed({ "admin", "auditor" })
|
||||
public Map<String, Long> getUserActivityStatistics(String dateDebutStr, String dateFinStr) {
|
||||
log.info("GET /api/audit/stats/users - Période: {} à {}", dateDebutStr, dateFinStr);
|
||||
LocalDateTime dateDebut = dateDebutStr != null ? LocalDateTime.parse(dateDebutStr) : null;
|
||||
LocalDateTime dateFin = dateFinStr != null ? LocalDateTime.parse(dateFinStr) : null;
|
||||
|
||||
return auditService.countByActeur(defaultRealm, dateDebut, dateFin);
|
||||
}
|
||||
|
||||
@Override
|
||||
@RolesAllowed({ "admin", "auditor" })
|
||||
public CountDTO getFailureCount(String dateDebutStr, String dateFinStr) {
|
||||
log.info("GET /api/audit/stats/failures - Période: {} à {}", dateDebutStr, dateFinStr);
|
||||
LocalDateTime dateDebut = dateDebutStr != null ? LocalDateTime.parse(dateDebutStr) : null;
|
||||
LocalDateTime dateFin = dateFinStr != null ? LocalDateTime.parse(dateFinStr) : null;
|
||||
|
||||
Map<String, Long> successVsFailure = auditService.countSuccessVsFailure(defaultRealm, dateDebut, dateFin);
|
||||
long count = successVsFailure.getOrDefault("failure", 0L);
|
||||
return new CountDTO(count);
|
||||
}
|
||||
|
||||
@Override
|
||||
@RolesAllowed({ "admin", "auditor" })
|
||||
public CountDTO getSuccessCount(String dateDebutStr, String dateFinStr) {
|
||||
log.info("GET /api/audit/stats/success - Période: {} à {}", dateDebutStr, dateFinStr);
|
||||
LocalDateTime dateDebut = dateDebutStr != null ? LocalDateTime.parse(dateDebutStr) : null;
|
||||
LocalDateTime dateFin = dateFinStr != null ? LocalDateTime.parse(dateFinStr) : null;
|
||||
|
||||
Map<String, Long> successVsFailure = auditService.countSuccessVsFailure(defaultRealm, dateDebut, dateFin);
|
||||
long count = successVsFailure.getOrDefault("success", 0L);
|
||||
return new CountDTO(count);
|
||||
}
|
||||
|
||||
@Override
|
||||
@RolesAllowed({ "admin", "auditor" })
|
||||
public Response exportLogsToCSV(String dateDebutStr, String dateFinStr) {
|
||||
log.info("GET /api/audit/export/csv - Période: {} à {}", dateDebutStr, dateFinStr);
|
||||
|
||||
try {
|
||||
LocalDateTime dateDebut = dateDebutStr != null ? LocalDateTime.parse(dateDebutStr) : null;
|
||||
LocalDateTime dateFin = dateFinStr != null ? LocalDateTime.parse(dateFinStr) : null;
|
||||
|
||||
String csvContent = auditService.exportToCSV(defaultRealm, dateDebut, dateFin);
|
||||
|
||||
return Response.ok(csvContent)
|
||||
.header("Content-Disposition", "attachment; filename=\"audit-logs-" +
|
||||
LocalDateTime.now().toString().replace(":", "-") + ".csv\"")
|
||||
.build();
|
||||
} catch (Exception e) {
|
||||
log.error("Erreur lors de l'export CSV des logs", e);
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@RolesAllowed({ "admin" })
|
||||
public void purgeOldLogs(int joursAnciennete) {
|
||||
log.info("DELETE /api/audit/purge - Suppression des logs de plus de {} jours", joursAnciennete);
|
||||
LocalDateTime dateLimite = LocalDateTime.now().minusDays(joursAnciennete);
|
||||
auditService.purgeOldLogs(dateLimite);
|
||||
}
|
||||
}
|
||||
package dev.lions.user.manager.resource;
|
||||
|
||||
import dev.lions.user.manager.api.AuditResourceApi;
|
||||
import dev.lions.user.manager.dto.audit.AuditLogDTO;
|
||||
import dev.lions.user.manager.dto.common.CountDTO;
|
||||
import dev.lions.user.manager.enums.audit.TypeActionAudit;
|
||||
import dev.lions.user.manager.service.AuditService;
|
||||
import jakarta.annotation.security.RolesAllowed;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import org.eclipse.microprofile.config.inject.ConfigProperty;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* REST Resource pour l'audit et la consultation des logs
|
||||
* Implémente l'interface API commune.
|
||||
*/
|
||||
@Slf4j
|
||||
@jakarta.enterprise.context.ApplicationScoped
|
||||
@jakarta.ws.rs.Path("/api/audit")
|
||||
public class AuditResource implements AuditResourceApi {
|
||||
|
||||
private static final String DEFAULT_REALM_VALUE = "master";
|
||||
|
||||
@Inject
|
||||
AuditService auditService;
|
||||
|
||||
@ConfigProperty(name = "lions.keycloak.admin-realm", defaultValue = DEFAULT_REALM_VALUE)
|
||||
String defaultRealm;
|
||||
|
||||
@Override
|
||||
@RolesAllowed({ "admin", "auditor" })
|
||||
public List<AuditLogDTO> searchLogs(
|
||||
String acteurUsername,
|
||||
String dateDebutStr,
|
||||
String dateFinStr,
|
||||
TypeActionAudit typeAction,
|
||||
String ressourceType,
|
||||
Boolean succes,
|
||||
int page,
|
||||
int pageSize) {
|
||||
log.info("POST /api/audit/search - Recherche de logs");
|
||||
|
||||
LocalDateTime dateDebut = dateDebutStr != null ? LocalDateTime.parse(dateDebutStr) : null;
|
||||
LocalDateTime dateFin = dateFinStr != null ? LocalDateTime.parse(dateFinStr) : null;
|
||||
|
||||
// Utiliser findByActeur si acteurUsername est fourni, sinon findByRealm
|
||||
List<AuditLogDTO> logs;
|
||||
if (acteurUsername != null && !acteurUsername.isBlank()) {
|
||||
logs = auditService.findByActeur(acteurUsername, dateDebut, dateFin, page, pageSize);
|
||||
} else {
|
||||
// Pour une recherche générale, utiliser findByRealm (on utilise defaultRealm par
|
||||
// défaut)
|
||||
logs = auditService.findByRealm(defaultRealm, dateDebut, dateFin, page, pageSize);
|
||||
}
|
||||
|
||||
// Filtrer par typeAction, ressourceType et succes si fournis
|
||||
if (typeAction != null || ressourceType != null || succes != null) {
|
||||
logs = logs.stream()
|
||||
.filter(log -> typeAction == null || typeAction.equals(log.getTypeAction()))
|
||||
.filter(log -> ressourceType == null || ressourceType.equals(log.getRessourceType()))
|
||||
.filter(log -> succes == null || succes == log.isSuccessful())
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
return logs;
|
||||
}
|
||||
|
||||
@Override
|
||||
@RolesAllowed({ "admin", "auditor" })
|
||||
public List<AuditLogDTO> getLogsByActor(String acteurUsername, int limit) {
|
||||
log.info("GET /api/audit/actor/{} - Limite: {}", acteurUsername, limit);
|
||||
return auditService.findByActeur(acteurUsername, null, null, 0, limit);
|
||||
}
|
||||
|
||||
@Override
|
||||
@RolesAllowed({ "admin", "auditor" })
|
||||
public List<AuditLogDTO> getLogsByResource(String ressourceType, String ressourceId, int limit) {
|
||||
log.info("GET /api/audit/resource/{}/{} - Limite: {}", ressourceType, ressourceId, limit);
|
||||
return auditService.findByRessource(ressourceType, ressourceId, null, null, 0, limit);
|
||||
}
|
||||
|
||||
@Override
|
||||
@RolesAllowed({ "admin", "auditor" })
|
||||
public List<AuditLogDTO> getLogsByAction(TypeActionAudit typeAction, String dateDebutStr, String dateFinStr,
|
||||
int limit) {
|
||||
log.info("GET /api/audit/action/{} - Limite: {}", typeAction, limit);
|
||||
LocalDateTime dateDebut = dateDebutStr != null ? LocalDateTime.parse(dateDebutStr) : null;
|
||||
LocalDateTime dateFin = dateFinStr != null ? LocalDateTime.parse(dateFinStr) : null;
|
||||
|
||||
return auditService.findByTypeAction(typeAction, defaultRealm, dateDebut, dateFin, 0, limit);
|
||||
}
|
||||
|
||||
@Override
|
||||
@RolesAllowed({ "admin", "auditor" })
|
||||
public Map<TypeActionAudit, Long> getActionStatistics(String dateDebutStr, String dateFinStr) {
|
||||
log.info("GET /api/audit/stats/actions - Période: {} à {}", dateDebutStr, dateFinStr);
|
||||
LocalDateTime dateDebut = dateDebutStr != null ? LocalDateTime.parse(dateDebutStr) : null;
|
||||
LocalDateTime dateFin = dateFinStr != null ? LocalDateTime.parse(dateFinStr) : null;
|
||||
|
||||
return auditService.countByActionType(defaultRealm, dateDebut, dateFin);
|
||||
}
|
||||
|
||||
@Override
|
||||
@RolesAllowed({ "admin", "auditor" })
|
||||
public Map<String, Long> getUserActivityStatistics(String dateDebutStr, String dateFinStr) {
|
||||
log.info("GET /api/audit/stats/users - Période: {} à {}", dateDebutStr, dateFinStr);
|
||||
LocalDateTime dateDebut = dateDebutStr != null ? LocalDateTime.parse(dateDebutStr) : null;
|
||||
LocalDateTime dateFin = dateFinStr != null ? LocalDateTime.parse(dateFinStr) : null;
|
||||
|
||||
return auditService.countByActeur(defaultRealm, dateDebut, dateFin);
|
||||
}
|
||||
|
||||
@Override
|
||||
@RolesAllowed({ "admin", "auditor" })
|
||||
public CountDTO getFailureCount(String dateDebutStr, String dateFinStr) {
|
||||
log.info("GET /api/audit/stats/failures - Période: {} à {}", dateDebutStr, dateFinStr);
|
||||
LocalDateTime dateDebut = dateDebutStr != null ? LocalDateTime.parse(dateDebutStr) : null;
|
||||
LocalDateTime dateFin = dateFinStr != null ? LocalDateTime.parse(dateFinStr) : null;
|
||||
|
||||
Map<String, Long> successVsFailure = auditService.countSuccessVsFailure(defaultRealm, dateDebut, dateFin);
|
||||
long count = successVsFailure.getOrDefault("failure", 0L);
|
||||
return new CountDTO(count);
|
||||
}
|
||||
|
||||
@Override
|
||||
@RolesAllowed({ "admin", "auditor" })
|
||||
public CountDTO getSuccessCount(String dateDebutStr, String dateFinStr) {
|
||||
log.info("GET /api/audit/stats/success - Période: {} à {}", dateDebutStr, dateFinStr);
|
||||
LocalDateTime dateDebut = dateDebutStr != null ? LocalDateTime.parse(dateDebutStr) : null;
|
||||
LocalDateTime dateFin = dateFinStr != null ? LocalDateTime.parse(dateFinStr) : null;
|
||||
|
||||
Map<String, Long> successVsFailure = auditService.countSuccessVsFailure(defaultRealm, dateDebut, dateFin);
|
||||
long count = successVsFailure.getOrDefault("success", 0L);
|
||||
return new CountDTO(count);
|
||||
}
|
||||
|
||||
@Override
|
||||
@RolesAllowed({ "admin", "auditor" })
|
||||
public Response exportLogsToCSV(String dateDebutStr, String dateFinStr) {
|
||||
log.info("GET /api/audit/export/csv - Période: {} à {}", dateDebutStr, dateFinStr);
|
||||
|
||||
try {
|
||||
LocalDateTime dateDebut = dateDebutStr != null ? LocalDateTime.parse(dateDebutStr) : null;
|
||||
LocalDateTime dateFin = dateFinStr != null ? LocalDateTime.parse(dateFinStr) : null;
|
||||
|
||||
String csvContent = auditService.exportToCSV(defaultRealm, dateDebut, dateFin);
|
||||
|
||||
return Response.ok(csvContent)
|
||||
.header("Content-Disposition", "attachment; filename=\"audit-logs-" +
|
||||
LocalDateTime.now().toString().replace(":", "-") + ".csv\"")
|
||||
.build();
|
||||
} catch (Exception e) {
|
||||
log.error("Erreur lors de l'export CSV des logs", e);
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@RolesAllowed({ "admin" })
|
||||
public void purgeOldLogs(int joursAnciennete) {
|
||||
log.info("DELETE /api/audit/purge - Suppression des logs de plus de {} jours", joursAnciennete);
|
||||
LocalDateTime dateLimite = LocalDateTime.now().minusDays(joursAnciennete);
|
||||
auditService.purgeOldLogs(dateLimite);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,68 +1,68 @@
|
||||
package dev.lions.user.manager.resource;
|
||||
|
||||
import dev.lions.user.manager.client.KeycloakAdminClient;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.ws.rs.GET;
|
||||
import jakarta.ws.rs.Path;
|
||||
import jakarta.ws.rs.Produces;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Resource REST pour health et readiness
|
||||
*/
|
||||
@Path("/api/health")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Slf4j
|
||||
public class HealthResourceEndpoint {
|
||||
|
||||
@Inject
|
||||
KeycloakAdminClient keycloakAdminClient;
|
||||
|
||||
@GET
|
||||
@Path("/keycloak")
|
||||
public Map<String, Object> getKeycloakHealth() {
|
||||
Map<String, Object> health = new HashMap<>();
|
||||
|
||||
try {
|
||||
// Vérifier simplement que le client est initialisé (pas d'appel réel à Keycloak)
|
||||
boolean initialized = keycloakAdminClient.getInstance() != null;
|
||||
health.put("status", initialized ? "UP" : "DOWN");
|
||||
health.put("connected", initialized);
|
||||
health.put("message", initialized ? "Client Keycloak initialisé" : "Client non initialisé");
|
||||
health.put("timestamp", System.currentTimeMillis());
|
||||
} catch (Exception e) {
|
||||
log.error("Erreur health check Keycloak", e);
|
||||
health.put("status", "ERROR");
|
||||
health.put("connected", false);
|
||||
health.put("error", e.getMessage());
|
||||
health.put("timestamp", System.currentTimeMillis());
|
||||
}
|
||||
|
||||
return health;
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/status")
|
||||
public Map<String, Object> getServiceStatus() {
|
||||
Map<String, Object> status = new HashMap<>();
|
||||
status.put("service", "lions-user-manager-server");
|
||||
status.put("version", "1.0.0");
|
||||
status.put("status", "UP");
|
||||
status.put("timestamp", System.currentTimeMillis());
|
||||
|
||||
// Health Keycloak
|
||||
try {
|
||||
boolean keycloakConnected = keycloakAdminClient.isConnected();
|
||||
status.put("keycloak", keycloakConnected ? "CONNECTED" : "DISCONNECTED");
|
||||
} catch (Exception e) {
|
||||
status.put("keycloak", "ERROR");
|
||||
status.put("keycloakError", e.getMessage());
|
||||
}
|
||||
|
||||
return status;
|
||||
}
|
||||
}
|
||||
package dev.lions.user.manager.resource;
|
||||
|
||||
import dev.lions.user.manager.client.KeycloakAdminClient;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.ws.rs.GET;
|
||||
import jakarta.ws.rs.Path;
|
||||
import jakarta.ws.rs.Produces;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Resource REST pour health et readiness
|
||||
*/
|
||||
@Path("/api/health")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Slf4j
|
||||
public class HealthResourceEndpoint {
|
||||
|
||||
@Inject
|
||||
KeycloakAdminClient keycloakAdminClient;
|
||||
|
||||
@GET
|
||||
@Path("/keycloak")
|
||||
public Map<String, Object> getKeycloakHealth() {
|
||||
Map<String, Object> health = new HashMap<>();
|
||||
|
||||
try {
|
||||
// Vérifier simplement que le client est initialisé (pas d'appel réel à Keycloak)
|
||||
boolean initialized = keycloakAdminClient.getInstance() != null;
|
||||
health.put("status", initialized ? "UP" : "DOWN");
|
||||
health.put("connected", initialized);
|
||||
health.put("message", initialized ? "Client Keycloak initialisé" : "Client non initialisé");
|
||||
health.put("timestamp", System.currentTimeMillis());
|
||||
} catch (Exception e) {
|
||||
log.error("Erreur health check Keycloak", e);
|
||||
health.put("status", "ERROR");
|
||||
health.put("connected", false);
|
||||
health.put("error", e.getMessage());
|
||||
health.put("timestamp", System.currentTimeMillis());
|
||||
}
|
||||
|
||||
return health;
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/status")
|
||||
public Map<String, Object> getServiceStatus() {
|
||||
Map<String, Object> status = new HashMap<>();
|
||||
status.put("service", "lions-user-manager-server");
|
||||
status.put("version", "1.0.0");
|
||||
status.put("status", "UP");
|
||||
status.put("timestamp", System.currentTimeMillis());
|
||||
|
||||
// Health Keycloak
|
||||
try {
|
||||
boolean keycloakConnected = keycloakAdminClient.isConnected();
|
||||
status.put("keycloak", keycloakConnected ? "CONNECTED" : "DISCONNECTED");
|
||||
} catch (Exception e) {
|
||||
status.put("keycloak", "ERROR");
|
||||
status.put("keycloakError", e.getMessage());
|
||||
}
|
||||
|
||||
return status;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,50 +1,50 @@
|
||||
package dev.lions.user.manager.resource;
|
||||
|
||||
import dev.lions.user.manager.client.KeycloakAdminClient;
|
||||
import jakarta.inject.Inject;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.eclipse.microprofile.health.HealthCheck;
|
||||
import org.eclipse.microprofile.health.HealthCheckResponse;
|
||||
import org.eclipse.microprofile.health.Readiness;
|
||||
|
||||
/**
|
||||
* Health check pour Keycloak
|
||||
*/
|
||||
@Readiness
|
||||
@Slf4j
|
||||
public class KeycloakHealthCheck implements HealthCheck {
|
||||
|
||||
@Inject
|
||||
KeycloakAdminClient keycloakAdminClient;
|
||||
|
||||
@Override
|
||||
public HealthCheckResponse call() {
|
||||
try {
|
||||
boolean connected = keycloakAdminClient.isConnected();
|
||||
|
||||
if (connected) {
|
||||
return HealthCheckResponse.builder()
|
||||
.name("keycloak-connection")
|
||||
.up()
|
||||
.withData("status", "connected")
|
||||
.withData("message", "Keycloak est disponible")
|
||||
.build();
|
||||
} else {
|
||||
return HealthCheckResponse.builder()
|
||||
.name("keycloak-connection")
|
||||
.down()
|
||||
.withData("status", "disconnected")
|
||||
.withData("message", "Keycloak n'est pas disponible")
|
||||
.build();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("Erreur lors du health check Keycloak", e);
|
||||
return HealthCheckResponse.builder()
|
||||
.name("keycloak-connection")
|
||||
.down()
|
||||
.withData("status", "error")
|
||||
.withData("message", e.getMessage())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
}
|
||||
package dev.lions.user.manager.resource;
|
||||
|
||||
import dev.lions.user.manager.client.KeycloakAdminClient;
|
||||
import jakarta.inject.Inject;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.eclipse.microprofile.health.HealthCheck;
|
||||
import org.eclipse.microprofile.health.HealthCheckResponse;
|
||||
import org.eclipse.microprofile.health.Readiness;
|
||||
|
||||
/**
|
||||
* Health check pour Keycloak
|
||||
*/
|
||||
@Readiness
|
||||
@Slf4j
|
||||
public class KeycloakHealthCheck implements HealthCheck {
|
||||
|
||||
@Inject
|
||||
KeycloakAdminClient keycloakAdminClient;
|
||||
|
||||
@Override
|
||||
public HealthCheckResponse call() {
|
||||
try {
|
||||
boolean connected = keycloakAdminClient.isConnected();
|
||||
|
||||
if (connected) {
|
||||
return HealthCheckResponse.builder()
|
||||
.name("keycloak-connection")
|
||||
.up()
|
||||
.withData("status", "connected")
|
||||
.withData("message", "Keycloak est disponible")
|
||||
.build();
|
||||
} else {
|
||||
return HealthCheckResponse.builder()
|
||||
.name("keycloak-connection")
|
||||
.down()
|
||||
.withData("status", "disconnected")
|
||||
.withData("message", "Keycloak n'est pas disponible")
|
||||
.build();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("Erreur lors du health check Keycloak", e);
|
||||
return HealthCheckResponse.builder()
|
||||
.name("keycloak-connection")
|
||||
.down()
|
||||
.withData("status", "error")
|
||||
.withData("message", e.getMessage())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,141 +1,141 @@
|
||||
package dev.lions.user.manager.resource;
|
||||
|
||||
import dev.lions.user.manager.api.RealmAssignmentResourceApi;
|
||||
import dev.lions.user.manager.dto.realm.AuthorizedRealmsDTO;
|
||||
import dev.lions.user.manager.dto.realm.RealmAccessCheckDTO;
|
||||
import dev.lions.user.manager.dto.realm.RealmAssignmentDTO;
|
||||
import dev.lions.user.manager.service.RealmAuthorizationService;
|
||||
import jakarta.annotation.security.RolesAllowed;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.ws.rs.core.Context;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import jakarta.ws.rs.core.SecurityContext;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* REST Resource pour la gestion des affectations de realms aux utilisateurs
|
||||
* Implémente l'interface API commune.
|
||||
*/
|
||||
@Slf4j
|
||||
@jakarta.enterprise.context.ApplicationScoped
|
||||
@jakarta.ws.rs.Path("/api/realm-assignments")
|
||||
public class RealmAssignmentResource implements RealmAssignmentResourceApi {
|
||||
|
||||
@Inject
|
||||
RealmAuthorizationService realmAuthorizationService;
|
||||
|
||||
@Context
|
||||
SecurityContext securityContext;
|
||||
|
||||
@Override
|
||||
@RolesAllowed({ "admin" })
|
||||
public List<RealmAssignmentDTO> getAllAssignments() {
|
||||
log.info("GET /api/realm-assignments - Récupération de toutes les affectations");
|
||||
return realmAuthorizationService.getAllAssignments();
|
||||
}
|
||||
|
||||
@Override
|
||||
@RolesAllowed({ "admin", "user_manager" })
|
||||
public List<RealmAssignmentDTO> getAssignmentsByUser(String userId) {
|
||||
log.info("GET /api/realm-assignments/user/{}", userId);
|
||||
return realmAuthorizationService.getAssignmentsByUser(userId);
|
||||
}
|
||||
|
||||
@Override
|
||||
@RolesAllowed({ "admin" })
|
||||
public List<RealmAssignmentDTO> getAssignmentsByRealm(String realmName) {
|
||||
log.info("GET /api/realm-assignments/realm/{}", realmName);
|
||||
return realmAuthorizationService.getAssignmentsByRealm(realmName);
|
||||
}
|
||||
|
||||
@Override
|
||||
@RolesAllowed({ "admin" })
|
||||
public RealmAssignmentDTO getAssignmentById(String assignmentId) {
|
||||
log.info("GET /api/realm-assignments/{}", assignmentId);
|
||||
return realmAuthorizationService.getAssignmentById(assignmentId)
|
||||
.orElseThrow(() -> new RuntimeException("Affectation non trouvée")); // ExceptionMapper should
|
||||
// handle/map to 404
|
||||
}
|
||||
|
||||
@Override
|
||||
@RolesAllowed({ "admin", "user_manager" })
|
||||
public RealmAccessCheckDTO canManageRealm(String userId, String realmName) {
|
||||
log.info("GET /api/realm-assignments/check - userId: {}, realmName: {}", userId, realmName);
|
||||
boolean canManage = realmAuthorizationService.canManageRealm(userId, realmName);
|
||||
return new RealmAccessCheckDTO(canManage, userId, realmName);
|
||||
}
|
||||
|
||||
@Override
|
||||
@RolesAllowed({ "admin", "user_manager" })
|
||||
public AuthorizedRealmsDTO getAuthorizedRealms(String userId) {
|
||||
log.info("GET /api/realm-assignments/authorized-realms/{}", userId);
|
||||
List<String> realms = realmAuthorizationService.getAuthorizedRealms(userId);
|
||||
boolean isSuperAdmin = realmAuthorizationService.isSuperAdmin(userId);
|
||||
return new AuthorizedRealmsDTO(realms, isSuperAdmin);
|
||||
}
|
||||
|
||||
@Override
|
||||
@RolesAllowed({ "admin" })
|
||||
public Response assignRealmToUser(@Valid @NotNull RealmAssignmentDTO assignment) {
|
||||
log.info("POST /api/realm-assignments - Assignation du realm {} à l'utilisateur {}",
|
||||
assignment.getRealmName(), assignment.getUserId());
|
||||
|
||||
try {
|
||||
// Ajouter l'utilisateur qui fait l'assignation
|
||||
if (securityContext.getUserPrincipal() != null) {
|
||||
assignment.setAssignedBy(securityContext.getUserPrincipal().getName());
|
||||
}
|
||||
|
||||
RealmAssignmentDTO createdAssignment = realmAuthorizationService.assignRealmToUser(assignment);
|
||||
return Response.status(Response.Status.CREATED).entity(createdAssignment).build();
|
||||
} catch (IllegalArgumentException e) {
|
||||
log.warn("Données invalides lors de l'assignation: {}", e.getMessage());
|
||||
// Need to return 409 or 400 manually since this method returns Response
|
||||
return Response.status(Response.Status.CONFLICT)
|
||||
.entity(new dev.lions.user.manager.dto.common.ApiErrorDTO(e.getMessage()))
|
||||
.build();
|
||||
} catch (Exception e) {
|
||||
log.error("Erreur lors de l'assignation du realm", e);
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@RolesAllowed({ "admin" })
|
||||
public void revokeRealmFromUser(String userId, String realmName) {
|
||||
log.info("DELETE /api/realm-assignments/user/{}/realm/{}", userId, realmName);
|
||||
realmAuthorizationService.revokeRealmFromUser(userId, realmName);
|
||||
}
|
||||
|
||||
@Override
|
||||
@RolesAllowed({ "admin" })
|
||||
public void revokeAllRealmsFromUser(String userId) {
|
||||
log.info("DELETE /api/realm-assignments/user/{}", userId);
|
||||
realmAuthorizationService.revokeAllRealmsFromUser(userId);
|
||||
}
|
||||
|
||||
@Override
|
||||
@RolesAllowed({ "admin" })
|
||||
public void deactivateAssignment(String assignmentId) {
|
||||
log.info("PUT /api/realm-assignments/{}/deactivate", assignmentId);
|
||||
realmAuthorizationService.deactivateAssignment(assignmentId);
|
||||
}
|
||||
|
||||
@Override
|
||||
@RolesAllowed({ "admin" })
|
||||
public void activateAssignment(String assignmentId) {
|
||||
log.info("PUT /api/realm-assignments/{}/activate", assignmentId);
|
||||
realmAuthorizationService.activateAssignment(assignmentId);
|
||||
}
|
||||
|
||||
@Override
|
||||
@RolesAllowed({ "admin" })
|
||||
public void setSuperAdmin(String userId, @NotNull Boolean superAdmin) {
|
||||
log.info("PUT /api/realm-assignments/super-admin/{} - superAdmin: {}", userId, superAdmin);
|
||||
realmAuthorizationService.setSuperAdmin(userId, superAdmin);
|
||||
}
|
||||
}
|
||||
package dev.lions.user.manager.resource;
|
||||
|
||||
import dev.lions.user.manager.api.RealmAssignmentResourceApi;
|
||||
import dev.lions.user.manager.dto.realm.AuthorizedRealmsDTO;
|
||||
import dev.lions.user.manager.dto.realm.RealmAccessCheckDTO;
|
||||
import dev.lions.user.manager.dto.realm.RealmAssignmentDTO;
|
||||
import dev.lions.user.manager.service.RealmAuthorizationService;
|
||||
import jakarta.annotation.security.RolesAllowed;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.ws.rs.core.Context;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import jakarta.ws.rs.core.SecurityContext;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* REST Resource pour la gestion des affectations de realms aux utilisateurs
|
||||
* Implémente l'interface API commune.
|
||||
*/
|
||||
@Slf4j
|
||||
@jakarta.enterprise.context.ApplicationScoped
|
||||
@jakarta.ws.rs.Path("/api/realm-assignments")
|
||||
public class RealmAssignmentResource implements RealmAssignmentResourceApi {
|
||||
|
||||
@Inject
|
||||
RealmAuthorizationService realmAuthorizationService;
|
||||
|
||||
@Context
|
||||
SecurityContext securityContext;
|
||||
|
||||
@Override
|
||||
@RolesAllowed({ "admin" })
|
||||
public List<RealmAssignmentDTO> getAllAssignments() {
|
||||
log.info("GET /api/realm-assignments - Récupération de toutes les affectations");
|
||||
return realmAuthorizationService.getAllAssignments();
|
||||
}
|
||||
|
||||
@Override
|
||||
@RolesAllowed({ "admin", "user_manager" })
|
||||
public List<RealmAssignmentDTO> getAssignmentsByUser(String userId) {
|
||||
log.info("GET /api/realm-assignments/user/{}", userId);
|
||||
return realmAuthorizationService.getAssignmentsByUser(userId);
|
||||
}
|
||||
|
||||
@Override
|
||||
@RolesAllowed({ "admin" })
|
||||
public List<RealmAssignmentDTO> getAssignmentsByRealm(String realmName) {
|
||||
log.info("GET /api/realm-assignments/realm/{}", realmName);
|
||||
return realmAuthorizationService.getAssignmentsByRealm(realmName);
|
||||
}
|
||||
|
||||
@Override
|
||||
@RolesAllowed({ "admin" })
|
||||
public RealmAssignmentDTO getAssignmentById(String assignmentId) {
|
||||
log.info("GET /api/realm-assignments/{}", assignmentId);
|
||||
return realmAuthorizationService.getAssignmentById(assignmentId)
|
||||
.orElseThrow(() -> new RuntimeException("Affectation non trouvée")); // ExceptionMapper should
|
||||
// handle/map to 404
|
||||
}
|
||||
|
||||
@Override
|
||||
@RolesAllowed({ "admin", "user_manager" })
|
||||
public RealmAccessCheckDTO canManageRealm(String userId, String realmName) {
|
||||
log.info("GET /api/realm-assignments/check - userId: {}, realmName: {}", userId, realmName);
|
||||
boolean canManage = realmAuthorizationService.canManageRealm(userId, realmName);
|
||||
return new RealmAccessCheckDTO(canManage, userId, realmName);
|
||||
}
|
||||
|
||||
@Override
|
||||
@RolesAllowed({ "admin", "user_manager" })
|
||||
public AuthorizedRealmsDTO getAuthorizedRealms(String userId) {
|
||||
log.info("GET /api/realm-assignments/authorized-realms/{}", userId);
|
||||
List<String> realms = realmAuthorizationService.getAuthorizedRealms(userId);
|
||||
boolean isSuperAdmin = realmAuthorizationService.isSuperAdmin(userId);
|
||||
return new AuthorizedRealmsDTO(realms, isSuperAdmin);
|
||||
}
|
||||
|
||||
@Override
|
||||
@RolesAllowed({ "admin" })
|
||||
public Response assignRealmToUser(@Valid @NotNull RealmAssignmentDTO assignment) {
|
||||
log.info("POST /api/realm-assignments - Assignation du realm {} à l'utilisateur {}",
|
||||
assignment.getRealmName(), assignment.getUserId());
|
||||
|
||||
try {
|
||||
// Ajouter l'utilisateur qui fait l'assignation
|
||||
if (securityContext.getUserPrincipal() != null) {
|
||||
assignment.setAssignedBy(securityContext.getUserPrincipal().getName());
|
||||
}
|
||||
|
||||
RealmAssignmentDTO createdAssignment = realmAuthorizationService.assignRealmToUser(assignment);
|
||||
return Response.status(Response.Status.CREATED).entity(createdAssignment).build();
|
||||
} catch (IllegalArgumentException e) {
|
||||
log.warn("Données invalides lors de l'assignation: {}", e.getMessage());
|
||||
// Need to return 409 or 400 manually since this method returns Response
|
||||
return Response.status(Response.Status.CONFLICT)
|
||||
.entity(new dev.lions.user.manager.dto.common.ApiErrorDTO(e.getMessage()))
|
||||
.build();
|
||||
} catch (Exception e) {
|
||||
log.error("Erreur lors de l'assignation du realm", e);
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@RolesAllowed({ "admin" })
|
||||
public void revokeRealmFromUser(String userId, String realmName) {
|
||||
log.info("DELETE /api/realm-assignments/user/{}/realm/{}", userId, realmName);
|
||||
realmAuthorizationService.revokeRealmFromUser(userId, realmName);
|
||||
}
|
||||
|
||||
@Override
|
||||
@RolesAllowed({ "admin" })
|
||||
public void revokeAllRealmsFromUser(String userId) {
|
||||
log.info("DELETE /api/realm-assignments/user/{}", userId);
|
||||
realmAuthorizationService.revokeAllRealmsFromUser(userId);
|
||||
}
|
||||
|
||||
@Override
|
||||
@RolesAllowed({ "admin" })
|
||||
public void deactivateAssignment(String assignmentId) {
|
||||
log.info("PUT /api/realm-assignments/{}/deactivate", assignmentId);
|
||||
realmAuthorizationService.deactivateAssignment(assignmentId);
|
||||
}
|
||||
|
||||
@Override
|
||||
@RolesAllowed({ "admin" })
|
||||
public void activateAssignment(String assignmentId) {
|
||||
log.info("PUT /api/realm-assignments/{}/activate", assignmentId);
|
||||
realmAuthorizationService.activateAssignment(assignmentId);
|
||||
}
|
||||
|
||||
@Override
|
||||
@RolesAllowed({ "admin" })
|
||||
public void setSuperAdmin(String userId, @NotNull Boolean superAdmin) {
|
||||
log.info("PUT /api/realm-assignments/super-admin/{} - superAdmin: {}", userId, superAdmin);
|
||||
realmAuthorizationService.setSuperAdmin(userId, superAdmin);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,56 +1,56 @@
|
||||
package dev.lions.user.manager.resource;
|
||||
|
||||
import dev.lions.user.manager.api.RealmResourceApi;
|
||||
import dev.lions.user.manager.client.KeycloakAdminClient;
|
||||
import io.quarkus.security.identity.SecurityIdentity;
|
||||
import jakarta.annotation.security.RolesAllowed;
|
||||
import jakarta.inject.Inject;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Ressource REST pour la gestion des realms Keycloak
|
||||
* Implémente l'interface API commune.
|
||||
*/
|
||||
@Slf4j
|
||||
@jakarta.enterprise.context.ApplicationScoped
|
||||
@jakarta.ws.rs.Path("/api/realms")
|
||||
public class RealmResource implements RealmResourceApi {
|
||||
|
||||
@Inject
|
||||
KeycloakAdminClient keycloakAdminClient;
|
||||
|
||||
@Inject
|
||||
SecurityIdentity securityIdentity;
|
||||
|
||||
@Override
|
||||
@RolesAllowed({ "admin", "user_manager", "user_viewer", "role_manager", "role_viewer" })
|
||||
public List<String> getAllRealms() {
|
||||
log.info("GET /api/realms/list");
|
||||
|
||||
try {
|
||||
List<String> realms = keycloakAdminClient.getAllRealms();
|
||||
log.info("Récupération réussie: {} realms trouvés", realms.size());
|
||||
return realms;
|
||||
} catch (Exception e) {
|
||||
log.error("Erreur lors de la récupération des realms", e);
|
||||
throw new RuntimeException("Erreur lors de la récupération des realms: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@RolesAllowed({ "admin", "user_manager", "role_manager", "role_viewer" })
|
||||
public List<String> getRealmClients(String realmName) {
|
||||
log.info("GET /api/realms/{}/clients", realmName);
|
||||
|
||||
try {
|
||||
List<String> clients = keycloakAdminClient.getRealmClients(realmName);
|
||||
log.info("Récupération réussie: {} clients trouvés pour le realm {}", clients.size(), realmName);
|
||||
return clients;
|
||||
} catch (Exception e) {
|
||||
log.error("Erreur lors de la récupération des clients du realm {}", realmName, e);
|
||||
throw new RuntimeException("Erreur lors de la récupération des clients: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
package dev.lions.user.manager.resource;
|
||||
|
||||
import dev.lions.user.manager.api.RealmResourceApi;
|
||||
import dev.lions.user.manager.client.KeycloakAdminClient;
|
||||
import io.quarkus.security.identity.SecurityIdentity;
|
||||
import jakarta.annotation.security.RolesAllowed;
|
||||
import jakarta.inject.Inject;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Ressource REST pour la gestion des realms Keycloak
|
||||
* Implémente l'interface API commune.
|
||||
*/
|
||||
@Slf4j
|
||||
@jakarta.enterprise.context.ApplicationScoped
|
||||
@jakarta.ws.rs.Path("/api/realms")
|
||||
public class RealmResource implements RealmResourceApi {
|
||||
|
||||
@Inject
|
||||
KeycloakAdminClient keycloakAdminClient;
|
||||
|
||||
@Inject
|
||||
SecurityIdentity securityIdentity;
|
||||
|
||||
@Override
|
||||
@RolesAllowed({ "admin", "user_manager", "user_viewer", "role_manager", "role_viewer" })
|
||||
public List<String> getAllRealms() {
|
||||
log.info("GET /api/realms/list");
|
||||
|
||||
try {
|
||||
List<String> realms = keycloakAdminClient.getAllRealms();
|
||||
log.info("Récupération réussie: {} realms trouvés", realms.size());
|
||||
return realms;
|
||||
} catch (Exception e) {
|
||||
log.error("Erreur lors de la récupération des realms", e);
|
||||
throw new RuntimeException("Erreur lors de la récupération des realms: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@RolesAllowed({ "admin", "user_manager", "role_manager", "role_viewer" })
|
||||
public List<String> getRealmClients(String realmName) {
|
||||
log.info("GET /api/realms/{}/clients", realmName);
|
||||
|
||||
try {
|
||||
List<String> clients = keycloakAdminClient.getRealmClients(realmName);
|
||||
log.info("Récupération réussie: {} clients trouvés pour le realm {}", clients.size(), realmName);
|
||||
return clients;
|
||||
} catch (Exception e) {
|
||||
log.error("Erreur lors de la récupération des clients du realm {}", realmName, e);
|
||||
throw new RuntimeException("Erreur lors de la récupération des clients: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,290 +1,290 @@
|
||||
package dev.lions.user.manager.resource;
|
||||
|
||||
import dev.lions.user.manager.api.RoleResourceApi;
|
||||
import dev.lions.user.manager.dto.common.ApiErrorDTO;
|
||||
import dev.lions.user.manager.dto.role.RoleAssignmentDTO;
|
||||
import dev.lions.user.manager.dto.role.RoleAssignmentRequestDTO;
|
||||
import dev.lions.user.manager.dto.role.RoleDTO;
|
||||
import dev.lions.user.manager.enums.role.TypeRole;
|
||||
import dev.lions.user.manager.service.RoleService;
|
||||
import jakarta.annotation.security.RolesAllowed;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.ws.rs.*;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* REST Resource pour la gestion des rôles Keycloak
|
||||
* Implémente l'interface API commune.
|
||||
* Annotation explicite des méthodes pour éviter les problèmes d'héritage JAX-RS
|
||||
* dans Quarkus.
|
||||
*/
|
||||
@Slf4j
|
||||
@jakarta.enterprise.context.ApplicationScoped
|
||||
@Path("/api/roles")
|
||||
public class RoleResource implements RoleResourceApi {
|
||||
|
||||
@Inject
|
||||
RoleService roleService;
|
||||
|
||||
// ==================== Endpoints Realm Roles ====================
|
||||
|
||||
@Override
|
||||
@POST
|
||||
@Path("/realm")
|
||||
@RolesAllowed({ "admin", "role_manager", "ADMIN", "SUPER_ADMIN" })
|
||||
public Response createRealmRole(
|
||||
@Valid @NotNull RoleDTO roleDTO,
|
||||
@QueryParam("realm") String realmName) {
|
||||
log.info("POST /api/roles/realm - Création du rôle realm: {} dans le realm: {}",
|
||||
roleDTO.getName(), realmName);
|
||||
|
||||
try {
|
||||
RoleDTO createdRole = roleService.createRealmRole(roleDTO, realmName);
|
||||
return Response.status(Response.Status.CREATED).entity(createdRole).build();
|
||||
} catch (IllegalArgumentException e) {
|
||||
log.warn("Données invalides lors de la création du rôle: {}", e.getMessage());
|
||||
return Response.status(Response.Status.CONFLICT)
|
||||
.entity(new ApiErrorDTO(e.getMessage()))
|
||||
.build();
|
||||
} catch (Exception e) {
|
||||
log.error("Erreur lors de la création du rôle realm", e);
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@GET
|
||||
@Path("/realm/{roleName}")
|
||||
@RolesAllowed({ "admin", "role_manager", "role_viewer", "ADMIN", "SUPER_ADMIN", "USER" })
|
||||
public RoleDTO getRealmRole(@PathParam("roleName") String roleName, @QueryParam("realm") String realmName) {
|
||||
log.info("GET /api/roles/realm/{} - realm: {}", roleName, realmName);
|
||||
return roleService.getRoleByName(roleName, realmName, TypeRole.REALM_ROLE, null)
|
||||
.orElseThrow(() -> new RuntimeException("Rôle non trouvé"));
|
||||
}
|
||||
|
||||
@Override
|
||||
@GET
|
||||
@Path("/realm")
|
||||
@RolesAllowed({ "admin", "role_manager", "role_viewer", "ADMIN", "SUPER_ADMIN", "USER" })
|
||||
public List<RoleDTO> getAllRealmRoles(@QueryParam("realm") String realmName) {
|
||||
log.info("GET /api/roles/realm - realm: {}", realmName);
|
||||
return roleService.getAllRealmRoles(realmName);
|
||||
}
|
||||
|
||||
@Override
|
||||
@PUT
|
||||
@Path("/realm/{roleName}")
|
||||
@RolesAllowed({ "admin", "role_manager", "ADMIN", "SUPER_ADMIN" })
|
||||
public RoleDTO updateRealmRole(@PathParam("roleName") String roleName, @Valid @NotNull RoleDTO roleDTO,
|
||||
@QueryParam("realm") String realmName) {
|
||||
log.info("PUT /api/roles/realm/{} - realm: {}", roleName, realmName);
|
||||
|
||||
Optional<RoleDTO> existingRole = roleService.getRoleByName(roleName, realmName, TypeRole.REALM_ROLE, null);
|
||||
if (existingRole.isEmpty()) {
|
||||
throw new RuntimeException("Rôle non trouvé");
|
||||
}
|
||||
|
||||
return roleService.updateRole(existingRole.get().getId(), roleDTO, realmName, TypeRole.REALM_ROLE, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
@DELETE
|
||||
@Path("/realm/{roleName}")
|
||||
@RolesAllowed({ "admin" })
|
||||
public void deleteRealmRole(@PathParam("roleName") String roleName, @QueryParam("realm") String realmName) {
|
||||
log.info("DELETE /api/roles/realm/{} - realm: {}", roleName, realmName);
|
||||
|
||||
Optional<RoleDTO> existingRole = roleService.getRoleByName(roleName, realmName, TypeRole.REALM_ROLE, null);
|
||||
if (existingRole.isEmpty()) {
|
||||
throw new RuntimeException("Rôle non trouvé");
|
||||
}
|
||||
|
||||
roleService.deleteRole(existingRole.get().getId(), realmName, TypeRole.REALM_ROLE, null);
|
||||
}
|
||||
|
||||
// ==================== Endpoints Client Roles ====================
|
||||
|
||||
@Override
|
||||
@POST
|
||||
@Path("/client/{clientId}")
|
||||
@RolesAllowed({ "admin", "role_manager", "ADMIN", "SUPER_ADMIN" })
|
||||
public Response createClientRole(@PathParam("clientId") String clientId, @Valid @NotNull RoleDTO roleDTO,
|
||||
@QueryParam("realm") String realmName) {
|
||||
log.info("POST /api/roles/client/{} - Création du rôle client dans le realm: {}",
|
||||
clientId, realmName);
|
||||
|
||||
try {
|
||||
RoleDTO createdRole = roleService.createClientRole(roleDTO, clientId, realmName);
|
||||
return Response.status(Response.Status.CREATED).entity(createdRole).build();
|
||||
} catch (IllegalArgumentException e) {
|
||||
log.warn("Données invalides lors de la création du rôle client: {}", e.getMessage());
|
||||
return Response.status(Response.Status.CONFLICT)
|
||||
.entity(new ApiErrorDTO(e.getMessage()))
|
||||
.build();
|
||||
} catch (Exception e) {
|
||||
log.error("Erreur lors de la création du rôle client", e);
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@GET
|
||||
@Path("/client/{clientId}/{roleName}")
|
||||
@RolesAllowed({ "admin", "role_manager", "role_viewer", "ADMIN", "SUPER_ADMIN", "USER" })
|
||||
public RoleDTO getClientRole(@PathParam("clientId") String clientId, @PathParam("roleName") String roleName,
|
||||
@QueryParam("realm") String realmName) {
|
||||
log.info("GET /api/roles/client/{}/{} - realm: {}", clientId, roleName, realmName);
|
||||
return roleService.getRoleByName(roleName, realmName, TypeRole.CLIENT_ROLE, clientId)
|
||||
.orElseThrow(() -> new RuntimeException("Rôle client non trouvé"));
|
||||
}
|
||||
|
||||
@Override
|
||||
@GET
|
||||
@Path("/client/{clientId}")
|
||||
@RolesAllowed({ "admin", "role_manager", "role_viewer", "ADMIN", "SUPER_ADMIN", "USER" })
|
||||
public List<RoleDTO> getAllClientRoles(@PathParam("clientId") String clientId,
|
||||
@QueryParam("realm") String realmName) {
|
||||
log.info("GET /api/roles/client/{} - realm: {}", clientId, realmName);
|
||||
return roleService.getAllClientRoles(realmName, clientId);
|
||||
}
|
||||
|
||||
@Override
|
||||
@DELETE
|
||||
@Path("/client/{clientId}/{roleName}")
|
||||
@RolesAllowed({ "admin" })
|
||||
public void deleteClientRole(@PathParam("clientId") String clientId, @PathParam("roleName") String roleName,
|
||||
@QueryParam("realm") String realmName) {
|
||||
log.info("DELETE /api/roles/client/{}/{} - realm: {}", clientId, roleName, realmName);
|
||||
|
||||
Optional<RoleDTO> existingRole = roleService.getRoleByName(roleName, realmName, TypeRole.CLIENT_ROLE, clientId);
|
||||
if (existingRole.isEmpty()) {
|
||||
throw new RuntimeException("Rôle client non trouvé");
|
||||
}
|
||||
|
||||
roleService.deleteRole(existingRole.get().getId(), realmName, TypeRole.CLIENT_ROLE, clientId);
|
||||
}
|
||||
|
||||
// ==================== Endpoints Attribution de rôles ====================
|
||||
|
||||
@Override
|
||||
@POST
|
||||
@Path("/assign/realm/{userId}")
|
||||
@RolesAllowed({ "admin", "role_manager", "user_manager", "ADMIN", "SUPER_ADMIN" })
|
||||
public void assignRealmRoles(@PathParam("userId") String userId, @QueryParam("realm") String realmName,
|
||||
@NotNull RoleAssignmentRequestDTO request) {
|
||||
log.info("POST /api/roles/assign/realm/{} - Attribution de {} rôles", userId, request.getRoleNames().size());
|
||||
|
||||
RoleAssignmentDTO assignment = RoleAssignmentDTO.builder()
|
||||
.userId(userId)
|
||||
.roleNames(request.getRoleNames())
|
||||
.typeRole(TypeRole.REALM_ROLE)
|
||||
.realmName(realmName)
|
||||
.build();
|
||||
roleService.assignRolesToUser(assignment);
|
||||
}
|
||||
|
||||
@Override
|
||||
@POST
|
||||
@Path("/revoke/realm/{userId}")
|
||||
@RolesAllowed({ "admin", "role_manager", "user_manager", "ADMIN", "SUPER_ADMIN" })
|
||||
public void revokeRealmRoles(@PathParam("userId") String userId, @QueryParam("realm") String realmName,
|
||||
@NotNull RoleAssignmentRequestDTO request) {
|
||||
log.info("POST /api/roles/revoke/realm/{} - Révocation de {} rôles", userId, request.getRoleNames().size());
|
||||
|
||||
RoleAssignmentDTO assignment = RoleAssignmentDTO.builder()
|
||||
.userId(userId)
|
||||
.roleNames(request.getRoleNames())
|
||||
.typeRole(TypeRole.REALM_ROLE)
|
||||
.realmName(realmName)
|
||||
.build();
|
||||
roleService.revokeRolesFromUser(assignment);
|
||||
}
|
||||
|
||||
@Override
|
||||
@POST
|
||||
@Path("/assign/client/{clientId}/{userId}")
|
||||
@RolesAllowed({ "admin", "role_manager", "ADMIN", "SUPER_ADMIN" })
|
||||
public void assignClientRoles(@PathParam("clientId") String clientId, @PathParam("userId") String userId,
|
||||
@QueryParam("realm") String realmName,
|
||||
@NotNull RoleAssignmentRequestDTO request) {
|
||||
log.info("POST /api/roles/assign/client/{}/{} - Attribution de {} rôles client",
|
||||
clientId, userId, request.getRoleNames().size());
|
||||
|
||||
RoleAssignmentDTO assignment = RoleAssignmentDTO.builder()
|
||||
.userId(userId)
|
||||
.roleNames(request.getRoleNames())
|
||||
.typeRole(TypeRole.CLIENT_ROLE)
|
||||
.realmName(realmName)
|
||||
.clientName(clientId)
|
||||
.build();
|
||||
roleService.assignRolesToUser(assignment);
|
||||
}
|
||||
|
||||
@Override
|
||||
@GET
|
||||
@Path("/user/realm/{userId}")
|
||||
@RolesAllowed({ "admin", "role_manager", "role_viewer", "ADMIN", "SUPER_ADMIN", "USER" })
|
||||
public List<RoleDTO> getUserRealmRoles(@PathParam("userId") String userId, @QueryParam("realm") String realmName) {
|
||||
log.info("GET /api/roles/user/realm/{} - realm: {}", userId, realmName);
|
||||
return roleService.getUserRealmRoles(userId, realmName);
|
||||
}
|
||||
|
||||
@Override
|
||||
@GET
|
||||
@Path("/user/client/{clientId}/{userId}")
|
||||
@RolesAllowed({ "admin", "role_manager", "role_viewer", "ADMIN", "SUPER_ADMIN", "USER" })
|
||||
public List<RoleDTO> getUserClientRoles(@PathParam("clientId") String clientId, @PathParam("userId") String userId,
|
||||
@QueryParam("realm") String realmName) {
|
||||
log.info("GET /api/roles/user/client/{}/{} - realm: {}", clientId, userId, realmName);
|
||||
return roleService.getUserClientRoles(userId, clientId, realmName);
|
||||
}
|
||||
|
||||
// ==================== Endpoints Rôles composites ====================
|
||||
|
||||
@Override
|
||||
@POST
|
||||
@Path("/composite/{roleName}/add")
|
||||
@RolesAllowed({ "admin", "role_manager", "ADMIN", "SUPER_ADMIN" })
|
||||
public void addComposites(@PathParam("roleName") String roleName, @QueryParam("realm") String realmName,
|
||||
@NotNull RoleAssignmentRequestDTO request) {
|
||||
log.info("POST /api/roles/composite/{}/add - Ajout de {} composites", roleName, request.getRoleNames().size());
|
||||
|
||||
Optional<RoleDTO> parentRole = roleService.getRoleByName(roleName, realmName, TypeRole.REALM_ROLE, null);
|
||||
if (parentRole.isEmpty()) {
|
||||
throw new RuntimeException("Rôle parent non trouvé");
|
||||
}
|
||||
|
||||
List<String> childRoleIds = request.getRoleNames().stream()
|
||||
.map(name -> {
|
||||
Optional<RoleDTO> role = roleService.getRoleByName(name, realmName, TypeRole.REALM_ROLE, null);
|
||||
return role.map(RoleDTO::getId).orElse(null);
|
||||
})
|
||||
.filter(id -> id != null)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
roleService.addCompositeRoles(parentRole.get().getId(), childRoleIds, realmName, TypeRole.REALM_ROLE, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
@GET
|
||||
@Path("/composite/{roleName}")
|
||||
@RolesAllowed({ "admin", "role_manager", "role_viewer", "ADMIN", "SUPER_ADMIN", "USER" })
|
||||
public List<RoleDTO> getComposites(@PathParam("roleName") String roleName, @QueryParam("realm") String realmName) {
|
||||
log.info("GET /api/roles/composite/{} - realm: {}", roleName, realmName);
|
||||
|
||||
Optional<RoleDTO> role = roleService.getRoleByName(roleName, realmName, TypeRole.REALM_ROLE, null);
|
||||
if (role.isEmpty()) {
|
||||
throw new RuntimeException("Rôle non trouvé");
|
||||
}
|
||||
|
||||
return roleService.getCompositeRoles(role.get().getId(), realmName, TypeRole.REALM_ROLE, null);
|
||||
}
|
||||
}
|
||||
package dev.lions.user.manager.resource;
|
||||
|
||||
import dev.lions.user.manager.api.RoleResourceApi;
|
||||
import dev.lions.user.manager.dto.common.ApiErrorDTO;
|
||||
import dev.lions.user.manager.dto.role.RoleAssignmentDTO;
|
||||
import dev.lions.user.manager.dto.role.RoleAssignmentRequestDTO;
|
||||
import dev.lions.user.manager.dto.role.RoleDTO;
|
||||
import dev.lions.user.manager.enums.role.TypeRole;
|
||||
import dev.lions.user.manager.service.RoleService;
|
||||
import jakarta.annotation.security.RolesAllowed;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.ws.rs.*;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* REST Resource pour la gestion des rôles Keycloak
|
||||
* Implémente l'interface API commune.
|
||||
* Annotation explicite des méthodes pour éviter les problèmes d'héritage JAX-RS
|
||||
* dans Quarkus.
|
||||
*/
|
||||
@Slf4j
|
||||
@jakarta.enterprise.context.ApplicationScoped
|
||||
@Path("/api/roles")
|
||||
public class RoleResource implements RoleResourceApi {
|
||||
|
||||
@Inject
|
||||
RoleService roleService;
|
||||
|
||||
// ==================== Endpoints Realm Roles ====================
|
||||
|
||||
@Override
|
||||
@POST
|
||||
@Path("/realm")
|
||||
@RolesAllowed({ "admin", "role_manager", "ADMIN", "SUPER_ADMIN" })
|
||||
public Response createRealmRole(
|
||||
@Valid @NotNull RoleDTO roleDTO,
|
||||
@QueryParam("realm") String realmName) {
|
||||
log.info("POST /api/roles/realm - Création du rôle realm: {} dans le realm: {}",
|
||||
roleDTO.getName(), realmName);
|
||||
|
||||
try {
|
||||
RoleDTO createdRole = roleService.createRealmRole(roleDTO, realmName);
|
||||
return Response.status(Response.Status.CREATED).entity(createdRole).build();
|
||||
} catch (IllegalArgumentException e) {
|
||||
log.warn("Données invalides lors de la création du rôle: {}", e.getMessage());
|
||||
return Response.status(Response.Status.CONFLICT)
|
||||
.entity(new ApiErrorDTO(e.getMessage()))
|
||||
.build();
|
||||
} catch (Exception e) {
|
||||
log.error("Erreur lors de la création du rôle realm", e);
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@GET
|
||||
@Path("/realm/{roleName}")
|
||||
@RolesAllowed({ "admin", "role_manager", "role_viewer", "ADMIN", "SUPER_ADMIN", "USER" })
|
||||
public RoleDTO getRealmRole(@PathParam("roleName") String roleName, @QueryParam("realm") String realmName) {
|
||||
log.info("GET /api/roles/realm/{} - realm: {}", roleName, realmName);
|
||||
return roleService.getRoleByName(roleName, realmName, TypeRole.REALM_ROLE, null)
|
||||
.orElseThrow(() -> new RuntimeException("Rôle non trouvé"));
|
||||
}
|
||||
|
||||
@Override
|
||||
@GET
|
||||
@Path("/realm")
|
||||
@RolesAllowed({ "admin", "role_manager", "role_viewer", "ADMIN", "SUPER_ADMIN", "USER" })
|
||||
public List<RoleDTO> getAllRealmRoles(@QueryParam("realm") String realmName) {
|
||||
log.info("GET /api/roles/realm - realm: {}", realmName);
|
||||
return roleService.getAllRealmRoles(realmName);
|
||||
}
|
||||
|
||||
@Override
|
||||
@PUT
|
||||
@Path("/realm/{roleName}")
|
||||
@RolesAllowed({ "admin", "role_manager", "ADMIN", "SUPER_ADMIN" })
|
||||
public RoleDTO updateRealmRole(@PathParam("roleName") String roleName, @Valid @NotNull RoleDTO roleDTO,
|
||||
@QueryParam("realm") String realmName) {
|
||||
log.info("PUT /api/roles/realm/{} - realm: {}", roleName, realmName);
|
||||
|
||||
Optional<RoleDTO> existingRole = roleService.getRoleByName(roleName, realmName, TypeRole.REALM_ROLE, null);
|
||||
if (existingRole.isEmpty()) {
|
||||
throw new RuntimeException("Rôle non trouvé");
|
||||
}
|
||||
|
||||
return roleService.updateRole(existingRole.get().getId(), roleDTO, realmName, TypeRole.REALM_ROLE, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
@DELETE
|
||||
@Path("/realm/{roleName}")
|
||||
@RolesAllowed({ "admin" })
|
||||
public void deleteRealmRole(@PathParam("roleName") String roleName, @QueryParam("realm") String realmName) {
|
||||
log.info("DELETE /api/roles/realm/{} - realm: {}", roleName, realmName);
|
||||
|
||||
Optional<RoleDTO> existingRole = roleService.getRoleByName(roleName, realmName, TypeRole.REALM_ROLE, null);
|
||||
if (existingRole.isEmpty()) {
|
||||
throw new RuntimeException("Rôle non trouvé");
|
||||
}
|
||||
|
||||
roleService.deleteRole(existingRole.get().getId(), realmName, TypeRole.REALM_ROLE, null);
|
||||
}
|
||||
|
||||
// ==================== Endpoints Client Roles ====================
|
||||
|
||||
@Override
|
||||
@POST
|
||||
@Path("/client/{clientId}")
|
||||
@RolesAllowed({ "admin", "role_manager", "ADMIN", "SUPER_ADMIN" })
|
||||
public Response createClientRole(@PathParam("clientId") String clientId, @Valid @NotNull RoleDTO roleDTO,
|
||||
@QueryParam("realm") String realmName) {
|
||||
log.info("POST /api/roles/client/{} - Création du rôle client dans le realm: {}",
|
||||
clientId, realmName);
|
||||
|
||||
try {
|
||||
RoleDTO createdRole = roleService.createClientRole(roleDTO, clientId, realmName);
|
||||
return Response.status(Response.Status.CREATED).entity(createdRole).build();
|
||||
} catch (IllegalArgumentException e) {
|
||||
log.warn("Données invalides lors de la création du rôle client: {}", e.getMessage());
|
||||
return Response.status(Response.Status.CONFLICT)
|
||||
.entity(new ApiErrorDTO(e.getMessage()))
|
||||
.build();
|
||||
} catch (Exception e) {
|
||||
log.error("Erreur lors de la création du rôle client", e);
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@GET
|
||||
@Path("/client/{clientId}/{roleName}")
|
||||
@RolesAllowed({ "admin", "role_manager", "role_viewer", "ADMIN", "SUPER_ADMIN", "USER" })
|
||||
public RoleDTO getClientRole(@PathParam("clientId") String clientId, @PathParam("roleName") String roleName,
|
||||
@QueryParam("realm") String realmName) {
|
||||
log.info("GET /api/roles/client/{}/{} - realm: {}", clientId, roleName, realmName);
|
||||
return roleService.getRoleByName(roleName, realmName, TypeRole.CLIENT_ROLE, clientId)
|
||||
.orElseThrow(() -> new RuntimeException("Rôle client non trouvé"));
|
||||
}
|
||||
|
||||
@Override
|
||||
@GET
|
||||
@Path("/client/{clientId}")
|
||||
@RolesAllowed({ "admin", "role_manager", "role_viewer", "ADMIN", "SUPER_ADMIN", "USER" })
|
||||
public List<RoleDTO> getAllClientRoles(@PathParam("clientId") String clientId,
|
||||
@QueryParam("realm") String realmName) {
|
||||
log.info("GET /api/roles/client/{} - realm: {}", clientId, realmName);
|
||||
return roleService.getAllClientRoles(realmName, clientId);
|
||||
}
|
||||
|
||||
@Override
|
||||
@DELETE
|
||||
@Path("/client/{clientId}/{roleName}")
|
||||
@RolesAllowed({ "admin" })
|
||||
public void deleteClientRole(@PathParam("clientId") String clientId, @PathParam("roleName") String roleName,
|
||||
@QueryParam("realm") String realmName) {
|
||||
log.info("DELETE /api/roles/client/{}/{} - realm: {}", clientId, roleName, realmName);
|
||||
|
||||
Optional<RoleDTO> existingRole = roleService.getRoleByName(roleName, realmName, TypeRole.CLIENT_ROLE, clientId);
|
||||
if (existingRole.isEmpty()) {
|
||||
throw new RuntimeException("Rôle client non trouvé");
|
||||
}
|
||||
|
||||
roleService.deleteRole(existingRole.get().getId(), realmName, TypeRole.CLIENT_ROLE, clientId);
|
||||
}
|
||||
|
||||
// ==================== Endpoints Attribution de rôles ====================
|
||||
|
||||
@Override
|
||||
@POST
|
||||
@Path("/assign/realm/{userId}")
|
||||
@RolesAllowed({ "admin", "role_manager", "user_manager", "ADMIN", "SUPER_ADMIN" })
|
||||
public void assignRealmRoles(@PathParam("userId") String userId, @QueryParam("realm") String realmName,
|
||||
@NotNull RoleAssignmentRequestDTO request) {
|
||||
log.info("POST /api/roles/assign/realm/{} - Attribution de {} rôles", userId, request.getRoleNames().size());
|
||||
|
||||
RoleAssignmentDTO assignment = RoleAssignmentDTO.builder()
|
||||
.userId(userId)
|
||||
.roleNames(request.getRoleNames())
|
||||
.typeRole(TypeRole.REALM_ROLE)
|
||||
.realmName(realmName)
|
||||
.build();
|
||||
roleService.assignRolesToUser(assignment);
|
||||
}
|
||||
|
||||
@Override
|
||||
@POST
|
||||
@Path("/revoke/realm/{userId}")
|
||||
@RolesAllowed({ "admin", "role_manager", "user_manager", "ADMIN", "SUPER_ADMIN" })
|
||||
public void revokeRealmRoles(@PathParam("userId") String userId, @QueryParam("realm") String realmName,
|
||||
@NotNull RoleAssignmentRequestDTO request) {
|
||||
log.info("POST /api/roles/revoke/realm/{} - Révocation de {} rôles", userId, request.getRoleNames().size());
|
||||
|
||||
RoleAssignmentDTO assignment = RoleAssignmentDTO.builder()
|
||||
.userId(userId)
|
||||
.roleNames(request.getRoleNames())
|
||||
.typeRole(TypeRole.REALM_ROLE)
|
||||
.realmName(realmName)
|
||||
.build();
|
||||
roleService.revokeRolesFromUser(assignment);
|
||||
}
|
||||
|
||||
@Override
|
||||
@POST
|
||||
@Path("/assign/client/{clientId}/{userId}")
|
||||
@RolesAllowed({ "admin", "role_manager", "ADMIN", "SUPER_ADMIN" })
|
||||
public void assignClientRoles(@PathParam("clientId") String clientId, @PathParam("userId") String userId,
|
||||
@QueryParam("realm") String realmName,
|
||||
@NotNull RoleAssignmentRequestDTO request) {
|
||||
log.info("POST /api/roles/assign/client/{}/{} - Attribution de {} rôles client",
|
||||
clientId, userId, request.getRoleNames().size());
|
||||
|
||||
RoleAssignmentDTO assignment = RoleAssignmentDTO.builder()
|
||||
.userId(userId)
|
||||
.roleNames(request.getRoleNames())
|
||||
.typeRole(TypeRole.CLIENT_ROLE)
|
||||
.realmName(realmName)
|
||||
.clientName(clientId)
|
||||
.build();
|
||||
roleService.assignRolesToUser(assignment);
|
||||
}
|
||||
|
||||
@Override
|
||||
@GET
|
||||
@Path("/user/realm/{userId}")
|
||||
@RolesAllowed({ "admin", "role_manager", "role_viewer", "ADMIN", "SUPER_ADMIN", "USER" })
|
||||
public List<RoleDTO> getUserRealmRoles(@PathParam("userId") String userId, @QueryParam("realm") String realmName) {
|
||||
log.info("GET /api/roles/user/realm/{} - realm: {}", userId, realmName);
|
||||
return roleService.getUserRealmRoles(userId, realmName);
|
||||
}
|
||||
|
||||
@Override
|
||||
@GET
|
||||
@Path("/user/client/{clientId}/{userId}")
|
||||
@RolesAllowed({ "admin", "role_manager", "role_viewer", "ADMIN", "SUPER_ADMIN", "USER" })
|
||||
public List<RoleDTO> getUserClientRoles(@PathParam("clientId") String clientId, @PathParam("userId") String userId,
|
||||
@QueryParam("realm") String realmName) {
|
||||
log.info("GET /api/roles/user/client/{}/{} - realm: {}", clientId, userId, realmName);
|
||||
return roleService.getUserClientRoles(userId, clientId, realmName);
|
||||
}
|
||||
|
||||
// ==================== Endpoints Rôles composites ====================
|
||||
|
||||
@Override
|
||||
@POST
|
||||
@Path("/composite/{roleName}/add")
|
||||
@RolesAllowed({ "admin", "role_manager", "ADMIN", "SUPER_ADMIN" })
|
||||
public void addComposites(@PathParam("roleName") String roleName, @QueryParam("realm") String realmName,
|
||||
@NotNull RoleAssignmentRequestDTO request) {
|
||||
log.info("POST /api/roles/composite/{}/add - Ajout de {} composites", roleName, request.getRoleNames().size());
|
||||
|
||||
Optional<RoleDTO> parentRole = roleService.getRoleByName(roleName, realmName, TypeRole.REALM_ROLE, null);
|
||||
if (parentRole.isEmpty()) {
|
||||
throw new RuntimeException("Rôle parent non trouvé");
|
||||
}
|
||||
|
||||
List<String> childRoleIds = request.getRoleNames().stream()
|
||||
.map(name -> {
|
||||
Optional<RoleDTO> role = roleService.getRoleByName(name, realmName, TypeRole.REALM_ROLE, null);
|
||||
return role.map(RoleDTO::getId).orElse(null);
|
||||
})
|
||||
.filter(id -> id != null)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
roleService.addCompositeRoles(parentRole.get().getId(), childRoleIds, realmName, TypeRole.REALM_ROLE, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
@GET
|
||||
@Path("/composite/{roleName}")
|
||||
@RolesAllowed({ "admin", "role_manager", "role_viewer", "ADMIN", "SUPER_ADMIN", "USER" })
|
||||
public List<RoleDTO> getComposites(@PathParam("roleName") String roleName, @QueryParam("realm") String realmName) {
|
||||
log.info("GET /api/roles/composite/{} - realm: {}", roleName, realmName);
|
||||
|
||||
Optional<RoleDTO> role = roleService.getRoleByName(roleName, realmName, TypeRole.REALM_ROLE, null);
|
||||
if (role.isEmpty()) {
|
||||
throw new RuntimeException("Rôle non trouvé");
|
||||
}
|
||||
|
||||
return roleService.getCompositeRoles(role.get().getId(), realmName, TypeRole.REALM_ROLE, null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,166 +1,166 @@
|
||||
package dev.lions.user.manager.resource;
|
||||
|
||||
import dev.lions.user.manager.api.SyncResourceApi;
|
||||
import dev.lions.user.manager.dto.sync.HealthStatusDTO;
|
||||
import dev.lions.user.manager.dto.sync.SyncConsistencyDTO;
|
||||
import dev.lions.user.manager.dto.sync.SyncHistoryDTO;
|
||||
import dev.lions.user.manager.dto.sync.SyncResultDTO;
|
||||
import dev.lions.user.manager.service.SyncService;
|
||||
import jakarta.annotation.security.PermitAll;
|
||||
import jakarta.annotation.security.RolesAllowed;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.ws.rs.GET;
|
||||
import jakarta.ws.rs.Path;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* REST Resource pour la synchronisation avec Keycloak.
|
||||
* Suit le même pattern que AuditResource : les annotations JAX-RS des méthodes
|
||||
* héritées de l'interface ne sont PAS répétées ici (conformité RESTEasy Reactive).
|
||||
*/
|
||||
@Slf4j
|
||||
@jakarta.enterprise.context.ApplicationScoped
|
||||
@jakarta.ws.rs.Path("/api/sync")
|
||||
@jakarta.ws.rs.Produces(jakarta.ws.rs.core.MediaType.APPLICATION_JSON)
|
||||
@jakarta.ws.rs.Consumes(jakarta.ws.rs.core.MediaType.APPLICATION_JSON)
|
||||
public class SyncResource implements SyncResourceApi {
|
||||
|
||||
@Inject
|
||||
SyncService syncService;
|
||||
|
||||
@GET
|
||||
@Path("/ping")
|
||||
@PermitAll
|
||||
public String ping() {
|
||||
return "{\"status\":\"pong\",\"resource\":\"SyncResource\"}";
|
||||
}
|
||||
|
||||
@Override
|
||||
@PermitAll
|
||||
public HealthStatusDTO checkKeycloakHealth() {
|
||||
log.info("REST: checkKeycloakHealth sur /api/sync/health/keycloak");
|
||||
try {
|
||||
boolean available = syncService.isKeycloakAvailable();
|
||||
Map<String, Object> details = syncService.getKeycloakHealthInfo();
|
||||
return HealthStatusDTO.builder()
|
||||
.keycloakAccessible(available)
|
||||
.overallHealthy(available)
|
||||
.keycloakVersion((String) details.getOrDefault("version", "Unknown"))
|
||||
.timestamp(System.currentTimeMillis())
|
||||
.build();
|
||||
} catch (Exception e) {
|
||||
log.error("Erreur lors du check health keycloak", e);
|
||||
return HealthStatusDTO.builder()
|
||||
.overallHealthy(false)
|
||||
.errorMessage("Erreur: " + e.getMessage())
|
||||
.timestamp(System.currentTimeMillis())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@RolesAllowed({ "admin", "sync_manager" })
|
||||
public SyncResultDTO syncUsers(String realmName) {
|
||||
log.info("REST: syncUsers pour le realm: {}", realmName);
|
||||
long start = System.currentTimeMillis();
|
||||
try {
|
||||
int count = syncService.syncUsersFromRealm(realmName);
|
||||
return SyncResultDTO.builder()
|
||||
.success(true)
|
||||
.usersCount(count)
|
||||
.realmName(realmName)
|
||||
.startTime(start)
|
||||
.endTime(System.currentTimeMillis())
|
||||
.build();
|
||||
} catch (Exception e) {
|
||||
log.error("Erreur lors de la synchro users realm {}", realmName, e);
|
||||
return SyncResultDTO.builder()
|
||||
.success(false)
|
||||
.errorMessage(e.getMessage())
|
||||
.realmName(realmName)
|
||||
.startTime(start)
|
||||
.endTime(System.currentTimeMillis())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@RolesAllowed({ "admin", "sync_manager" })
|
||||
public SyncResultDTO syncRoles(String realmName, String clientName) {
|
||||
log.info("REST: syncRoles pour le realm: {}, client: {}", realmName, clientName);
|
||||
long start = System.currentTimeMillis();
|
||||
try {
|
||||
int count = syncService.syncRolesFromRealm(realmName);
|
||||
return SyncResultDTO.builder()
|
||||
.success(true)
|
||||
.realmRolesCount(count)
|
||||
.realmName(realmName)
|
||||
.startTime(start)
|
||||
.endTime(System.currentTimeMillis())
|
||||
.build();
|
||||
} catch (Exception e) {
|
||||
log.error("Erreur lors de la synchro roles realm {}", realmName, e);
|
||||
return SyncResultDTO.builder()
|
||||
.success(false)
|
||||
.errorMessage(e.getMessage())
|
||||
.realmName(realmName)
|
||||
.startTime(start)
|
||||
.endTime(System.currentTimeMillis())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@RolesAllowed({ "admin", "sync_manager" })
|
||||
public SyncConsistencyDTO checkDataConsistency(String realmName) {
|
||||
log.info("REST: checkDataConsistency pour realm: {}", realmName);
|
||||
try {
|
||||
Map<String, Object> report = syncService.checkDataConsistency(realmName);
|
||||
return SyncConsistencyDTO.builder()
|
||||
.realmName((String) report.get("realmName"))
|
||||
.status((String) report.get("status"))
|
||||
.usersKeycloakCount((Integer) report.get("usersKeycloakCount"))
|
||||
.usersLocalCount((Integer) report.get("usersLocalCount"))
|
||||
.error((String) report.get("error"))
|
||||
.build();
|
||||
} catch (Exception e) {
|
||||
log.error("Erreur checkDataConsistency realm {}", realmName, e);
|
||||
return SyncConsistencyDTO.builder()
|
||||
.realmName(realmName)
|
||||
.status("ERROR")
|
||||
.error(e.getMessage())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@RolesAllowed({ "admin", "sync_manager", "user_viewer" })
|
||||
public SyncHistoryDTO getLastSyncStatus(String realmName) {
|
||||
log.info("REST: getLastSyncStatus pour realm: {}", realmName);
|
||||
return SyncHistoryDTO.builder()
|
||||
.realmName(realmName)
|
||||
.status("NEVER_SYNCED")
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
@RolesAllowed({ "admin", "sync_manager" })
|
||||
public SyncHistoryDTO forceSyncRealm(String realmName) {
|
||||
log.info("REST: forceSyncRealm pour realm: {}", realmName);
|
||||
try {
|
||||
syncService.forceSyncRealm(realmName);
|
||||
return SyncHistoryDTO.builder()
|
||||
.realmName(realmName)
|
||||
.status("SUCCESS")
|
||||
.build();
|
||||
} catch (Exception e) {
|
||||
log.error("Erreur forceSyncRealm realm {}", realmName, e);
|
||||
return SyncHistoryDTO.builder()
|
||||
.realmName(realmName)
|
||||
.status("FAILED")
|
||||
.build();
|
||||
}
|
||||
}
|
||||
}
|
||||
package dev.lions.user.manager.resource;
|
||||
|
||||
import dev.lions.user.manager.api.SyncResourceApi;
|
||||
import dev.lions.user.manager.dto.sync.HealthStatusDTO;
|
||||
import dev.lions.user.manager.dto.sync.SyncConsistencyDTO;
|
||||
import dev.lions.user.manager.dto.sync.SyncHistoryDTO;
|
||||
import dev.lions.user.manager.dto.sync.SyncResultDTO;
|
||||
import dev.lions.user.manager.service.SyncService;
|
||||
import jakarta.annotation.security.PermitAll;
|
||||
import jakarta.annotation.security.RolesAllowed;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.ws.rs.GET;
|
||||
import jakarta.ws.rs.Path;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* REST Resource pour la synchronisation avec Keycloak.
|
||||
* Suit le même pattern que AuditResource : les annotations JAX-RS des méthodes
|
||||
* héritées de l'interface ne sont PAS répétées ici (conformité RESTEasy Reactive).
|
||||
*/
|
||||
@Slf4j
|
||||
@jakarta.enterprise.context.ApplicationScoped
|
||||
@jakarta.ws.rs.Path("/api/sync")
|
||||
@jakarta.ws.rs.Produces(jakarta.ws.rs.core.MediaType.APPLICATION_JSON)
|
||||
@jakarta.ws.rs.Consumes(jakarta.ws.rs.core.MediaType.APPLICATION_JSON)
|
||||
public class SyncResource implements SyncResourceApi {
|
||||
|
||||
@Inject
|
||||
SyncService syncService;
|
||||
|
||||
@GET
|
||||
@Path("/ping")
|
||||
@PermitAll
|
||||
public String ping() {
|
||||
return "{\"status\":\"pong\",\"resource\":\"SyncResource\"}";
|
||||
}
|
||||
|
||||
@Override
|
||||
@PermitAll
|
||||
public HealthStatusDTO checkKeycloakHealth() {
|
||||
log.info("REST: checkKeycloakHealth sur /api/sync/health/keycloak");
|
||||
try {
|
||||
boolean available = syncService.isKeycloakAvailable();
|
||||
Map<String, Object> details = syncService.getKeycloakHealthInfo();
|
||||
return HealthStatusDTO.builder()
|
||||
.keycloakAccessible(available)
|
||||
.overallHealthy(available)
|
||||
.keycloakVersion((String) details.getOrDefault("version", "Unknown"))
|
||||
.timestamp(System.currentTimeMillis())
|
||||
.build();
|
||||
} catch (Exception e) {
|
||||
log.error("Erreur lors du check health keycloak", e);
|
||||
return HealthStatusDTO.builder()
|
||||
.overallHealthy(false)
|
||||
.errorMessage("Erreur: " + e.getMessage())
|
||||
.timestamp(System.currentTimeMillis())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@RolesAllowed({ "admin", "sync_manager" })
|
||||
public SyncResultDTO syncUsers(String realmName) {
|
||||
log.info("REST: syncUsers pour le realm: {}", realmName);
|
||||
long start = System.currentTimeMillis();
|
||||
try {
|
||||
int count = syncService.syncUsersFromRealm(realmName);
|
||||
return SyncResultDTO.builder()
|
||||
.success(true)
|
||||
.usersCount(count)
|
||||
.realmName(realmName)
|
||||
.startTime(start)
|
||||
.endTime(System.currentTimeMillis())
|
||||
.build();
|
||||
} catch (Exception e) {
|
||||
log.error("Erreur lors de la synchro users realm {}", realmName, e);
|
||||
return SyncResultDTO.builder()
|
||||
.success(false)
|
||||
.errorMessage(e.getMessage())
|
||||
.realmName(realmName)
|
||||
.startTime(start)
|
||||
.endTime(System.currentTimeMillis())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@RolesAllowed({ "admin", "sync_manager" })
|
||||
public SyncResultDTO syncRoles(String realmName, String clientName) {
|
||||
log.info("REST: syncRoles pour le realm: {}, client: {}", realmName, clientName);
|
||||
long start = System.currentTimeMillis();
|
||||
try {
|
||||
int count = syncService.syncRolesFromRealm(realmName);
|
||||
return SyncResultDTO.builder()
|
||||
.success(true)
|
||||
.realmRolesCount(count)
|
||||
.realmName(realmName)
|
||||
.startTime(start)
|
||||
.endTime(System.currentTimeMillis())
|
||||
.build();
|
||||
} catch (Exception e) {
|
||||
log.error("Erreur lors de la synchro roles realm {}", realmName, e);
|
||||
return SyncResultDTO.builder()
|
||||
.success(false)
|
||||
.errorMessage(e.getMessage())
|
||||
.realmName(realmName)
|
||||
.startTime(start)
|
||||
.endTime(System.currentTimeMillis())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@RolesAllowed({ "admin", "sync_manager" })
|
||||
public SyncConsistencyDTO checkDataConsistency(String realmName) {
|
||||
log.info("REST: checkDataConsistency pour realm: {}", realmName);
|
||||
try {
|
||||
Map<String, Object> report = syncService.checkDataConsistency(realmName);
|
||||
return SyncConsistencyDTO.builder()
|
||||
.realmName((String) report.get("realmName"))
|
||||
.status((String) report.get("status"))
|
||||
.usersKeycloakCount((Integer) report.get("usersKeycloakCount"))
|
||||
.usersLocalCount((Integer) report.get("usersLocalCount"))
|
||||
.error((String) report.get("error"))
|
||||
.build();
|
||||
} catch (Exception e) {
|
||||
log.error("Erreur checkDataConsistency realm {}", realmName, e);
|
||||
return SyncConsistencyDTO.builder()
|
||||
.realmName(realmName)
|
||||
.status("ERROR")
|
||||
.error(e.getMessage())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@RolesAllowed({ "admin", "sync_manager", "user_viewer" })
|
||||
public SyncHistoryDTO getLastSyncStatus(String realmName) {
|
||||
log.info("REST: getLastSyncStatus pour realm: {}", realmName);
|
||||
return SyncHistoryDTO.builder()
|
||||
.realmName(realmName)
|
||||
.status("NEVER_SYNCED")
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
@RolesAllowed({ "admin", "sync_manager" })
|
||||
public SyncHistoryDTO forceSyncRealm(String realmName) {
|
||||
log.info("REST: forceSyncRealm pour realm: {}", realmName);
|
||||
try {
|
||||
syncService.forceSyncRealm(realmName);
|
||||
return SyncHistoryDTO.builder()
|
||||
.realmName(realmName)
|
||||
.status("SUCCESS")
|
||||
.build();
|
||||
} catch (Exception e) {
|
||||
log.error("Erreur forceSyncRealm realm {}", realmName, e);
|
||||
return SyncHistoryDTO.builder()
|
||||
.realmName(realmName)
|
||||
.status("FAILED")
|
||||
.build();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,73 +1,73 @@
|
||||
package dev.lions.user.manager.resource;
|
||||
|
||||
import dev.lions.user.manager.api.UserMetricsResourceApi;
|
||||
import dev.lions.user.manager.client.KeycloakAdminClient;
|
||||
import dev.lions.user.manager.dto.common.UserSessionStatsDTO;
|
||||
import jakarta.annotation.security.RolesAllowed;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.ws.rs.Path;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.keycloak.admin.client.resource.RealmResource;
|
||||
import org.keycloak.admin.client.resource.UserResource;
|
||||
import org.keycloak.admin.client.resource.UsersResource;
|
||||
import org.keycloak.representations.idm.UserRepresentation;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Ressource REST fournissant des métriques agrégées sur les utilisateurs.
|
||||
* Implémente l'interface API commune.
|
||||
*
|
||||
* Toutes les valeurs sont calculées en temps réel à partir de Keycloak
|
||||
* (aucune approximation ni cache local).
|
||||
*/
|
||||
@Slf4j
|
||||
@ApplicationScoped
|
||||
@Path("/api/metrics/users")
|
||||
public class UserMetricsResource implements UserMetricsResourceApi {
|
||||
|
||||
@Inject
|
||||
KeycloakAdminClient keycloakAdminClient;
|
||||
|
||||
@Override
|
||||
@RolesAllowed({ "admin", "user_manager", "auditor" })
|
||||
public UserSessionStatsDTO getUserSessionStats(String realmName) {
|
||||
String effectiveRealm = (realmName == null || realmName.isBlank()) ? "master" : realmName;
|
||||
log.info("GET /api/metrics/users/sessions - realm={}", effectiveRealm);
|
||||
|
||||
try {
|
||||
RealmResource realm = keycloakAdminClient.getRealm(effectiveRealm);
|
||||
UsersResource usersResource = realm.users();
|
||||
|
||||
// Liste complète des utilisateurs du realm (source de vérité Keycloak)
|
||||
List<UserRepresentation> users = usersResource.list();
|
||||
long totalUsers = users.size();
|
||||
|
||||
long activeSessions = 0L;
|
||||
long onlineUsers = 0L;
|
||||
|
||||
for (UserRepresentation user : users) {
|
||||
UserResource userResource = usersResource.get(user.getId());
|
||||
int sessionsForUser = userResource.getUserSessions().size();
|
||||
|
||||
activeSessions += sessionsForUser;
|
||||
if (sessionsForUser > 0) {
|
||||
onlineUsers++;
|
||||
}
|
||||
}
|
||||
|
||||
return UserSessionStatsDTO.builder()
|
||||
.realmName(effectiveRealm)
|
||||
.totalUsers(totalUsers)
|
||||
.activeSessions(activeSessions)
|
||||
.onlineUsers(onlineUsers)
|
||||
.build();
|
||||
} catch (Exception e) {
|
||||
log.error("Erreur lors du calcul des statistiques de sessions pour le realm {}", effectiveRealm, e);
|
||||
// On laisse l'exception remonter pour signaler une vraie erreur (pas de valeur approximative)
|
||||
throw new RuntimeException("Impossible de calculer les statistiques de sessions en temps réel", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
package dev.lions.user.manager.resource;
|
||||
|
||||
import dev.lions.user.manager.api.UserMetricsResourceApi;
|
||||
import dev.lions.user.manager.client.KeycloakAdminClient;
|
||||
import dev.lions.user.manager.dto.common.UserSessionStatsDTO;
|
||||
import jakarta.annotation.security.RolesAllowed;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.ws.rs.Path;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.keycloak.admin.client.resource.RealmResource;
|
||||
import org.keycloak.admin.client.resource.UserResource;
|
||||
import org.keycloak.admin.client.resource.UsersResource;
|
||||
import org.keycloak.representations.idm.UserRepresentation;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Ressource REST fournissant des métriques agrégées sur les utilisateurs.
|
||||
* Implémente l'interface API commune.
|
||||
*
|
||||
* Toutes les valeurs sont calculées en temps réel à partir de Keycloak
|
||||
* (aucune approximation ni cache local).
|
||||
*/
|
||||
@Slf4j
|
||||
@ApplicationScoped
|
||||
@Path("/api/metrics/users")
|
||||
public class UserMetricsResource implements UserMetricsResourceApi {
|
||||
|
||||
@Inject
|
||||
KeycloakAdminClient keycloakAdminClient;
|
||||
|
||||
@Override
|
||||
@RolesAllowed({ "admin", "user_manager", "auditor" })
|
||||
public UserSessionStatsDTO getUserSessionStats(String realmName) {
|
||||
String effectiveRealm = (realmName == null || realmName.isBlank()) ? "master" : realmName;
|
||||
log.info("GET /api/metrics/users/sessions - realm={}", effectiveRealm);
|
||||
|
||||
try {
|
||||
RealmResource realm = keycloakAdminClient.getRealm(effectiveRealm);
|
||||
UsersResource usersResource = realm.users();
|
||||
|
||||
// Liste complète des utilisateurs du realm (source de vérité Keycloak)
|
||||
List<UserRepresentation> users = usersResource.list();
|
||||
long totalUsers = users.size();
|
||||
|
||||
long activeSessions = 0L;
|
||||
long onlineUsers = 0L;
|
||||
|
||||
for (UserRepresentation user : users) {
|
||||
UserResource userResource = usersResource.get(user.getId());
|
||||
int sessionsForUser = userResource.getUserSessions().size();
|
||||
|
||||
activeSessions += sessionsForUser;
|
||||
if (sessionsForUser > 0) {
|
||||
onlineUsers++;
|
||||
}
|
||||
}
|
||||
|
||||
return UserSessionStatsDTO.builder()
|
||||
.realmName(effectiveRealm)
|
||||
.totalUsers(totalUsers)
|
||||
.activeSessions(activeSessions)
|
||||
.onlineUsers(onlineUsers)
|
||||
.build();
|
||||
} catch (Exception e) {
|
||||
log.error("Erreur lors du calcul des statistiques de sessions pour le realm {}", effectiveRealm, e);
|
||||
// On laisse l'exception remonter pour signaler une vraie erreur (pas de valeur approximative)
|
||||
throw new RuntimeException("Impossible de calculer les statistiques de sessions en temps réel", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,162 +1,161 @@
|
||||
package dev.lions.user.manager.resource;
|
||||
|
||||
import dev.lions.user.manager.api.UserResourceApi;
|
||||
import dev.lions.user.manager.dto.common.ApiErrorDTO;
|
||||
import dev.lions.user.manager.dto.importexport.ImportResultDTO;
|
||||
import dev.lions.user.manager.dto.user.*;
|
||||
import dev.lions.user.manager.service.UserService;
|
||||
import jakarta.annotation.security.RolesAllowed;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.ws.rs.GET;
|
||||
import jakarta.ws.rs.POST;
|
||||
import jakarta.ws.rs.QueryParam;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* REST Resource pour la gestion des utilisateurs
|
||||
* Implémente l'interface API commune.
|
||||
*/
|
||||
@Slf4j
|
||||
@jakarta.enterprise.context.ApplicationScoped
|
||||
@jakarta.ws.rs.Path("/api/users")
|
||||
public class UserResource implements UserResourceApi {
|
||||
|
||||
@Inject
|
||||
UserService userService;
|
||||
|
||||
@Override
|
||||
@RolesAllowed({ "admin", "user_manager", "ADMIN", "SUPER_ADMIN" })
|
||||
public UserSearchResultDTO searchUsers(@Valid @NotNull UserSearchCriteriaDTO criteria) {
|
||||
log.info("POST /api/users/search - Recherche d'utilisateurs");
|
||||
return userService.searchUsers(criteria);
|
||||
}
|
||||
|
||||
@Override
|
||||
@RolesAllowed({ "admin", "user_manager", "user_viewer", "ADMIN", "SUPER_ADMIN", "USER" })
|
||||
public UserDTO getUserById(String userId, String realmName) {
|
||||
log.info("GET /api/users/{} - realm: {}", userId, realmName);
|
||||
return userService.getUserById(userId, realmName)
|
||||
.orElseThrow(() -> new RuntimeException("Utilisateur non trouvé")); // ExceptionMapper should handle/map
|
||||
// to 404
|
||||
}
|
||||
|
||||
@Override
|
||||
@RolesAllowed({ "admin", "user_manager", "user_viewer", "ADMIN", "SUPER_ADMIN", "USER" })
|
||||
public UserSearchResultDTO getAllUsers(String realmName, int page, int pageSize) {
|
||||
log.info("GET /api/users - realm: {}, page: {}, pageSize: {}", realmName, page, pageSize);
|
||||
return userService.getAllUsers(realmName, page, pageSize);
|
||||
}
|
||||
|
||||
@Override
|
||||
@RolesAllowed({ "admin", "user_manager", "ADMIN", "SUPER_ADMIN" })
|
||||
public Response createUser(@Valid @NotNull UserDTO user, String realmName) {
|
||||
log.info("POST /api/users - Création d'un utilisateur: {}", user.getUsername());
|
||||
|
||||
try {
|
||||
UserDTO createdUser = userService.createUser(user, realmName);
|
||||
return Response.status(Response.Status.CREATED).entity(createdUser).build();
|
||||
} catch (IllegalArgumentException e) {
|
||||
log.warn("Données invalides lors de la création: {}", e.getMessage());
|
||||
return Response.status(Response.Status.CONFLICT)
|
||||
.entity(new ApiErrorDTO(e.getMessage()))
|
||||
.build();
|
||||
} catch (Exception e) {
|
||||
log.error("Erreur lors de la création de l'utilisateur", e);
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@RolesAllowed({ "admin", "user_manager", "ADMIN", "SUPER_ADMIN" })
|
||||
public UserDTO updateUser(String userId, @Valid @NotNull UserDTO user, String realmName) {
|
||||
log.info("PUT /api/users/{} - Mise à jour", userId);
|
||||
return userService.updateUser(userId, user, realmName);
|
||||
}
|
||||
|
||||
@Override
|
||||
@RolesAllowed({ "admin", "ADMIN", "SUPER_ADMIN" })
|
||||
public void deleteUser(String userId, String realmName, boolean hardDelete) {
|
||||
log.info("DELETE /api/users/{} - realm: {}, hardDelete: {}", userId, realmName, hardDelete);
|
||||
userService.deleteUser(userId, realmName, hardDelete);
|
||||
}
|
||||
|
||||
@Override
|
||||
@RolesAllowed({ "admin", "user_manager", "ADMIN", "SUPER_ADMIN" })
|
||||
public void activateUser(String userId, String realmName) {
|
||||
log.info("POST /api/users/{}/activate", userId);
|
||||
userService.activateUser(userId, realmName);
|
||||
}
|
||||
|
||||
@Override
|
||||
@RolesAllowed({ "admin", "user_manager", "ADMIN", "SUPER_ADMIN" })
|
||||
public void deactivateUser(String userId, String realmName, String raison) {
|
||||
log.info("POST /api/users/{}/deactivate - raison: {}", userId, raison);
|
||||
userService.deactivateUser(userId, realmName, raison);
|
||||
}
|
||||
|
||||
@Override
|
||||
@RolesAllowed({ "admin", "user_manager" })
|
||||
public void resetPassword(String userId, String realmName, @NotNull PasswordResetRequestDTO request) {
|
||||
log.info("POST /api/users/{}/reset-password - temporary: {}", userId, request.isTemporary());
|
||||
userService.resetPassword(userId, realmName, request.getPassword(), request.isTemporary());
|
||||
}
|
||||
|
||||
@Override
|
||||
@RolesAllowed({ "admin", "user_manager" })
|
||||
public Response sendVerificationEmail(String userId, String realmName) {
|
||||
log.info("POST /api/users/{}/send-verification-email", userId);
|
||||
userService.sendVerificationEmail(userId, realmName);
|
||||
return Response.accepted().build();
|
||||
}
|
||||
|
||||
@Override
|
||||
@RolesAllowed({ "admin", "user_manager" })
|
||||
public SessionsRevokedDTO logoutAllSessions(String userId, String realmName) {
|
||||
log.info("POST /api/users/{}/logout-sessions", userId);
|
||||
int count = userService.logoutAllSessions(userId, realmName);
|
||||
return new SessionsRevokedDTO(count);
|
||||
}
|
||||
|
||||
@Override
|
||||
@RolesAllowed({ "admin", "user_manager", "user_viewer" })
|
||||
public List<String> getActiveSessions(String userId, String realmName) {
|
||||
log.info("GET /api/users/{}/sessions", userId);
|
||||
return userService.getActiveSessions(userId, realmName);
|
||||
}
|
||||
|
||||
@Override
|
||||
@GET
|
||||
@jakarta.ws.rs.Path("/export/csv")
|
||||
@jakarta.ws.rs.Produces("text/csv")
|
||||
@RolesAllowed({ "admin", "user_manager" })
|
||||
public Response exportUsersToCSV(@QueryParam("realm") String realmName) {
|
||||
log.info("GET /api/users/export/csv - realm: {}", realmName);
|
||||
UserSearchCriteriaDTO criteria = UserSearchCriteriaDTO.builder()
|
||||
.realmName(realmName)
|
||||
.page(0)
|
||||
.pageSize(10_000)
|
||||
.build();
|
||||
String csv = userService.exportUsersToCSV(criteria);
|
||||
return Response.ok(csv)
|
||||
.type(MediaType.valueOf("text/csv"))
|
||||
.header("Content-Disposition", "attachment; filename=\"users-" + (realmName != null ? realmName : "export") + ".csv\"")
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
@POST
|
||||
@jakarta.ws.rs.Path("/import/csv")
|
||||
@jakarta.ws.rs.Consumes(MediaType.TEXT_PLAIN)
|
||||
@RolesAllowed({ "admin", "user_manager" })
|
||||
public ImportResultDTO importUsersFromCSV(@QueryParam("realm") String realmName, String csvContent) {
|
||||
log.info("POST /api/users/import/csv - realm: {}", realmName);
|
||||
return userService.importUsersFromCSV(csvContent, realmName);
|
||||
}
|
||||
}
|
||||
package dev.lions.user.manager.resource;
|
||||
|
||||
import dev.lions.user.manager.api.UserResourceApi;
|
||||
import dev.lions.user.manager.dto.common.ApiErrorDTO;
|
||||
import dev.lions.user.manager.dto.importexport.ImportResultDTO;
|
||||
import dev.lions.user.manager.dto.user.*;
|
||||
import dev.lions.user.manager.service.UserService;
|
||||
import jakarta.annotation.security.RolesAllowed;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.ws.rs.GET;
|
||||
import jakarta.ws.rs.POST;
|
||||
import jakarta.ws.rs.QueryParam;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* REST Resource pour la gestion des utilisateurs
|
||||
* Implémente l'interface API commune.
|
||||
*/
|
||||
@Slf4j
|
||||
@jakarta.enterprise.context.ApplicationScoped
|
||||
@jakarta.ws.rs.Path("/api/users")
|
||||
public class UserResource implements UserResourceApi {
|
||||
|
||||
@Inject
|
||||
UserService userService;
|
||||
|
||||
@Override
|
||||
@RolesAllowed({ "admin", "user_manager", "ADMIN", "SUPER_ADMIN" })
|
||||
public UserSearchResultDTO searchUsers(@Valid @NotNull UserSearchCriteriaDTO criteria) {
|
||||
log.info("POST /api/users/search - Recherche d'utilisateurs");
|
||||
return userService.searchUsers(criteria);
|
||||
}
|
||||
|
||||
@Override
|
||||
@RolesAllowed({ "admin", "user_manager", "user_viewer", "ADMIN", "SUPER_ADMIN", "USER" })
|
||||
public UserDTO getUserById(String userId, String realmName) {
|
||||
log.info("GET /api/users/{} - realm: {}", userId, realmName);
|
||||
return userService.getUserById(userId, realmName)
|
||||
.orElseThrow(() -> new jakarta.ws.rs.NotFoundException("Utilisateur non trouvé"));
|
||||
}
|
||||
|
||||
@Override
|
||||
@RolesAllowed({ "admin", "user_manager", "user_viewer", "ADMIN", "SUPER_ADMIN", "USER" })
|
||||
public UserSearchResultDTO getAllUsers(String realmName, int page, int pageSize) {
|
||||
log.info("GET /api/users - realm: {}, page: {}, pageSize: {}", realmName, page, pageSize);
|
||||
return userService.getAllUsers(realmName, page, pageSize);
|
||||
}
|
||||
|
||||
@Override
|
||||
@RolesAllowed({ "admin", "user_manager", "ADMIN", "SUPER_ADMIN" })
|
||||
public Response createUser(@Valid @NotNull UserDTO user, String realmName) {
|
||||
log.info("POST /api/users - Création d'un utilisateur: {}", user.getUsername());
|
||||
|
||||
try {
|
||||
UserDTO createdUser = userService.createUser(user, realmName);
|
||||
return Response.status(Response.Status.CREATED).entity(createdUser).build();
|
||||
} catch (IllegalArgumentException e) {
|
||||
log.warn("Données invalides lors de la création: {}", e.getMessage());
|
||||
return Response.status(Response.Status.CONFLICT)
|
||||
.entity(new ApiErrorDTO(e.getMessage()))
|
||||
.build();
|
||||
} catch (Exception e) {
|
||||
log.error("Erreur lors de la création de l'utilisateur", e);
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@RolesAllowed({ "admin", "user_manager", "ADMIN", "SUPER_ADMIN" })
|
||||
public UserDTO updateUser(String userId, @Valid @NotNull UserDTO user, String realmName) {
|
||||
log.info("PUT /api/users/{} - Mise à jour", userId);
|
||||
return userService.updateUser(userId, user, realmName);
|
||||
}
|
||||
|
||||
@Override
|
||||
@RolesAllowed({ "admin", "ADMIN", "SUPER_ADMIN" })
|
||||
public void deleteUser(String userId, String realmName, boolean hardDelete) {
|
||||
log.info("DELETE /api/users/{} - realm: {}, hardDelete: {}", userId, realmName, hardDelete);
|
||||
userService.deleteUser(userId, realmName, hardDelete);
|
||||
}
|
||||
|
||||
@Override
|
||||
@RolesAllowed({ "admin", "user_manager", "ADMIN", "SUPER_ADMIN" })
|
||||
public void activateUser(String userId, String realmName) {
|
||||
log.info("POST /api/users/{}/activate", userId);
|
||||
userService.activateUser(userId, realmName);
|
||||
}
|
||||
|
||||
@Override
|
||||
@RolesAllowed({ "admin", "user_manager", "ADMIN", "SUPER_ADMIN" })
|
||||
public void deactivateUser(String userId, String realmName, String raison) {
|
||||
log.info("POST /api/users/{}/deactivate - raison: {}", userId, raison);
|
||||
userService.deactivateUser(userId, realmName, raison);
|
||||
}
|
||||
|
||||
@Override
|
||||
@RolesAllowed({ "admin", "user_manager" })
|
||||
public void resetPassword(String userId, String realmName, @NotNull PasswordResetRequestDTO request) {
|
||||
log.info("POST /api/users/{}/reset-password - temporary: {}", userId, request.isTemporary());
|
||||
userService.resetPassword(userId, realmName, request.getPassword(), request.isTemporary());
|
||||
}
|
||||
|
||||
@Override
|
||||
@RolesAllowed({ "admin", "user_manager" })
|
||||
public Response sendVerificationEmail(String userId, String realmName) {
|
||||
log.info("POST /api/users/{}/send-verification-email", userId);
|
||||
userService.sendVerificationEmail(userId, realmName);
|
||||
return Response.accepted().build();
|
||||
}
|
||||
|
||||
@Override
|
||||
@RolesAllowed({ "admin", "user_manager" })
|
||||
public SessionsRevokedDTO logoutAllSessions(String userId, String realmName) {
|
||||
log.info("POST /api/users/{}/logout-sessions", userId);
|
||||
int count = userService.logoutAllSessions(userId, realmName);
|
||||
return new SessionsRevokedDTO(count);
|
||||
}
|
||||
|
||||
@Override
|
||||
@RolesAllowed({ "admin", "user_manager", "user_viewer" })
|
||||
public List<String> getActiveSessions(String userId, String realmName) {
|
||||
log.info("GET /api/users/{}/sessions", userId);
|
||||
return userService.getActiveSessions(userId, realmName);
|
||||
}
|
||||
|
||||
@Override
|
||||
@GET
|
||||
@jakarta.ws.rs.Path("/export/csv")
|
||||
@jakarta.ws.rs.Produces("text/csv")
|
||||
@RolesAllowed({ "admin", "user_manager" })
|
||||
public Response exportUsersToCSV(@QueryParam("realm") String realmName) {
|
||||
log.info("GET /api/users/export/csv - realm: {}", realmName);
|
||||
UserSearchCriteriaDTO criteria = UserSearchCriteriaDTO.builder()
|
||||
.realmName(realmName)
|
||||
.page(0)
|
||||
.pageSize(10_000)
|
||||
.build();
|
||||
String csv = userService.exportUsersToCSV(criteria);
|
||||
return Response.ok(csv)
|
||||
.type(MediaType.valueOf("text/csv"))
|
||||
.header("Content-Disposition", "attachment; filename=\"users-" + (realmName != null ? realmName : "export") + ".csv\"")
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
@POST
|
||||
@jakarta.ws.rs.Path("/import/csv")
|
||||
@jakarta.ws.rs.Consumes(MediaType.TEXT_PLAIN)
|
||||
@RolesAllowed({ "admin", "user_manager" })
|
||||
public ImportResultDTO importUsersFromCSV(@QueryParam("realm") String realmName, String csvContent) {
|
||||
log.info("POST /api/users/import/csv - realm: {}", realmName);
|
||||
return userService.importUsersFromCSV(csvContent, realmName);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,38 +1,38 @@
|
||||
package dev.lions.user.manager.security;
|
||||
|
||||
import io.quarkus.security.identity.AuthenticationRequestContext;
|
||||
import io.quarkus.security.identity.SecurityIdentity;
|
||||
import io.quarkus.security.identity.SecurityIdentityAugmentor;
|
||||
import io.quarkus.security.runtime.QuarkusSecurityIdentity;
|
||||
import io.quarkus.arc.profile.IfBuildProfile;
|
||||
import io.smallrye.mutiny.Uni;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import org.eclipse.microprofile.config.inject.ConfigProperty;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Augmenteur de sécurité pour le mode DEV
|
||||
* Ajoute automatiquement les rôles admin et user_manager à toutes les requêtes
|
||||
* Permet de tester l'API sans authentification Keycloak
|
||||
*/
|
||||
@ApplicationScoped
|
||||
@IfBuildProfile("dev")
|
||||
public class DevModeSecurityAugmentor implements SecurityIdentityAugmentor {
|
||||
|
||||
@ConfigProperty(name = "quarkus.oidc.enabled", defaultValue = "true")
|
||||
boolean oidcEnabled;
|
||||
|
||||
@Override
|
||||
public Uni<SecurityIdentity> augment(SecurityIdentity identity, AuthenticationRequestContext context) {
|
||||
// Seulement actif si OIDC est désactivé (mode DEV)
|
||||
if (!oidcEnabled && identity.isAnonymous()) {
|
||||
// Créer une identité avec les rôles nécessaires pour DEV
|
||||
return Uni.createFrom().item(QuarkusSecurityIdentity.builder(identity)
|
||||
.setPrincipal(() -> "dev-user")
|
||||
.addRoles(Set.of("admin", "user_manager", "user_viewer"))
|
||||
.build());
|
||||
}
|
||||
return Uni.createFrom().item(identity);
|
||||
}
|
||||
}
|
||||
package dev.lions.user.manager.security;
|
||||
|
||||
import io.quarkus.security.identity.AuthenticationRequestContext;
|
||||
import io.quarkus.security.identity.SecurityIdentity;
|
||||
import io.quarkus.security.identity.SecurityIdentityAugmentor;
|
||||
import io.quarkus.security.runtime.QuarkusSecurityIdentity;
|
||||
import io.quarkus.arc.profile.IfBuildProfile;
|
||||
import io.smallrye.mutiny.Uni;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import org.eclipse.microprofile.config.inject.ConfigProperty;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Augmenteur de sécurité pour le mode DEV
|
||||
* Ajoute automatiquement les rôles admin et user_manager à toutes les requêtes
|
||||
* Permet de tester l'API sans authentification Keycloak
|
||||
*/
|
||||
@ApplicationScoped
|
||||
@IfBuildProfile("dev")
|
||||
public class DevModeSecurityAugmentor implements SecurityIdentityAugmentor {
|
||||
|
||||
@ConfigProperty(name = "quarkus.oidc.enabled", defaultValue = "true")
|
||||
boolean oidcEnabled;
|
||||
|
||||
@Override
|
||||
public Uni<SecurityIdentity> augment(SecurityIdentity identity, AuthenticationRequestContext context) {
|
||||
// Seulement actif si OIDC est désactivé (mode DEV)
|
||||
if (!oidcEnabled && identity.isAnonymous()) {
|
||||
// Créer une identité avec les rôles nécessaires pour DEV
|
||||
return Uni.createFrom().item(QuarkusSecurityIdentity.builder(identity)
|
||||
.setPrincipal(() -> "dev-user")
|
||||
.addRoles(Set.of("admin", "user_manager", "user_viewer"))
|
||||
.build());
|
||||
}
|
||||
return Uni.createFrom().item(identity);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,94 +1,94 @@
|
||||
package dev.lions.user.manager.security;
|
||||
|
||||
import jakarta.annotation.Priority;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.ws.rs.Priorities;
|
||||
import jakarta.ws.rs.container.ContainerRequestContext;
|
||||
import jakarta.ws.rs.container.ContainerRequestFilter;
|
||||
import jakarta.ws.rs.core.SecurityContext;
|
||||
import jakarta.ws.rs.ext.Provider;
|
||||
import org.eclipse.microprofile.config.inject.ConfigProperty;
|
||||
import org.jboss.logging.Logger;
|
||||
|
||||
import java.security.Principal;
|
||||
|
||||
/**
|
||||
* Filtre JAX-RS pour remplacer le SecurityContext en mode développement
|
||||
* En dev, remplace le SecurityContext par un mock qui autorise tous les rôles
|
||||
* En prod, laisse le SecurityContext réel de Quarkus
|
||||
*/
|
||||
@Provider
|
||||
@Priority(Priorities.AUTHENTICATION - 10) // S'exécute très tôt, avant l'authentification
|
||||
public class DevSecurityContextProducer implements ContainerRequestFilter {
|
||||
|
||||
private static final Logger LOG = Logger.getLogger(DevSecurityContextProducer.class);
|
||||
|
||||
@Inject
|
||||
@ConfigProperty(name = "quarkus.profile", defaultValue = "prod")
|
||||
String profile;
|
||||
|
||||
@Inject
|
||||
@ConfigProperty(name = "quarkus.oidc.enabled", defaultValue = "true")
|
||||
boolean oidcEnabled;
|
||||
|
||||
@Override
|
||||
public void filter(ContainerRequestContext requestContext) {
|
||||
// Détecter le mode dev : si OIDC est désactivé, on est probablement en dev
|
||||
// ou si le profil est explicitement "dev" ou "development"
|
||||
boolean isDevMode = !oidcEnabled || "dev".equals(profile) || "development".equals(profile);
|
||||
|
||||
if (isDevMode) {
|
||||
String path = requestContext.getUriInfo().getPath();
|
||||
LOG.infof("Mode dev détecté (profile=%s, oidc.enabled=%s): remplacement du SecurityContext pour le chemin %s",
|
||||
profile, oidcEnabled, path);
|
||||
SecurityContext original = requestContext.getSecurityContext();
|
||||
requestContext.setSecurityContext(new DevSecurityContext(original));
|
||||
LOG.debugf("SecurityContext remplacé - isUserInRole('admin')=%s, isUserInRole('user_manager')=%s",
|
||||
new DevSecurityContext(original).isUserInRole("admin"),
|
||||
new DevSecurityContext(original).isUserInRole("user_manager"));
|
||||
} else {
|
||||
LOG.debugf("Mode prod - SecurityContext original conservé (profile=%s, oidc.enabled=%s)", profile, oidcEnabled);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* SecurityContext mock pour le mode développement
|
||||
* Simule un utilisateur avec tous les rôles nécessaires
|
||||
*/
|
||||
private static class DevSecurityContext implements SecurityContext {
|
||||
|
||||
private final SecurityContext original;
|
||||
private final Principal principal = new Principal() {
|
||||
@Override
|
||||
public String getName() {
|
||||
return "dev-user";
|
||||
}
|
||||
};
|
||||
|
||||
public DevSecurityContext(SecurityContext original) {
|
||||
this.original = original;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Principal getUserPrincipal() {
|
||||
return principal;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isUserInRole(String role) {
|
||||
// En dev, autoriser tous les rôles
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSecure() {
|
||||
return original != null ? original.isSecure() : false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAuthenticationScheme() {
|
||||
return "DEV";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
package dev.lions.user.manager.security;
|
||||
|
||||
import jakarta.annotation.Priority;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.ws.rs.Priorities;
|
||||
import jakarta.ws.rs.container.ContainerRequestContext;
|
||||
import jakarta.ws.rs.container.ContainerRequestFilter;
|
||||
import jakarta.ws.rs.core.SecurityContext;
|
||||
import jakarta.ws.rs.ext.Provider;
|
||||
import org.eclipse.microprofile.config.inject.ConfigProperty;
|
||||
import org.jboss.logging.Logger;
|
||||
|
||||
import java.security.Principal;
|
||||
|
||||
/**
|
||||
* Filtre JAX-RS pour remplacer le SecurityContext en mode développement
|
||||
* En dev, remplace le SecurityContext par un mock qui autorise tous les rôles
|
||||
* En prod, laisse le SecurityContext réel de Quarkus
|
||||
*/
|
||||
@Provider
|
||||
@Priority(Priorities.AUTHENTICATION - 10) // S'exécute très tôt, avant l'authentification
|
||||
public class DevSecurityContextProducer implements ContainerRequestFilter {
|
||||
|
||||
private static final Logger LOG = Logger.getLogger(DevSecurityContextProducer.class);
|
||||
|
||||
@Inject
|
||||
@ConfigProperty(name = "quarkus.profile", defaultValue = "prod")
|
||||
String profile;
|
||||
|
||||
@Inject
|
||||
@ConfigProperty(name = "quarkus.oidc.enabled", defaultValue = "true")
|
||||
boolean oidcEnabled;
|
||||
|
||||
@Override
|
||||
public void filter(ContainerRequestContext requestContext) {
|
||||
// Détecter le mode dev : si OIDC est désactivé, on est probablement en dev
|
||||
// ou si le profil est explicitement "dev" ou "development"
|
||||
boolean isDevMode = !oidcEnabled || "dev".equals(profile) || "development".equals(profile);
|
||||
|
||||
if (isDevMode) {
|
||||
String path = requestContext.getUriInfo().getPath();
|
||||
LOG.infof("Mode dev détecté (profile=%s, oidc.enabled=%s): remplacement du SecurityContext pour le chemin %s",
|
||||
profile, oidcEnabled, path);
|
||||
SecurityContext original = requestContext.getSecurityContext();
|
||||
requestContext.setSecurityContext(new DevSecurityContext(original));
|
||||
LOG.debugf("SecurityContext remplacé - isUserInRole('admin')=%s, isUserInRole('user_manager')=%s",
|
||||
new DevSecurityContext(original).isUserInRole("admin"),
|
||||
new DevSecurityContext(original).isUserInRole("user_manager"));
|
||||
} else {
|
||||
LOG.debugf("Mode prod - SecurityContext original conservé (profile=%s, oidc.enabled=%s)", profile, oidcEnabled);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* SecurityContext mock pour le mode développement
|
||||
* Simule un utilisateur avec tous les rôles nécessaires
|
||||
*/
|
||||
private static class DevSecurityContext implements SecurityContext {
|
||||
|
||||
private final SecurityContext original;
|
||||
private final Principal principal = new Principal() {
|
||||
@Override
|
||||
public String getName() {
|
||||
return "dev-user";
|
||||
}
|
||||
};
|
||||
|
||||
public DevSecurityContext(SecurityContext original) {
|
||||
this.original = original;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Principal getUserPrincipal() {
|
||||
return principal;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isUserInRole(String role) {
|
||||
// En dev, autoriser tous les rôles
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSecure() {
|
||||
return original != null ? original.isSecure() : false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAuthenticationScheme() {
|
||||
return "DEV";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,209 +1,209 @@
|
||||
package dev.lions.user.manager.server.impl.entity;
|
||||
|
||||
import dev.lions.user.manager.enums.audit.TypeActionAudit;
|
||||
import io.quarkus.hibernate.orm.panache.PanacheEntity;
|
||||
import jakarta.persistence.*;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* Entité JPA pour la persistance des logs d'audit en base de données PostgreSQL.
|
||||
*
|
||||
* <p>Cette entité représente un enregistrement d'audit qui track toutes les actions
|
||||
* effectuées sur les utilisateurs du système (création, modification, suppression, etc.).</p>
|
||||
*
|
||||
* <p><b>Utilisation:</b></p>
|
||||
* <pre>
|
||||
* AuditLogEntity auditLog = new AuditLogEntity();
|
||||
* auditLog.setUserId("user-123");
|
||||
* auditLog.setAction(TypeActionAudit.CREATION_UTILISATEUR);
|
||||
* auditLog.setDetails("Utilisateur créé avec succès");
|
||||
* auditLog.setAuteurAction("admin");
|
||||
* auditLog.setTimestamp(LocalDateTime.now());
|
||||
* auditLog.persist();
|
||||
* </pre>
|
||||
*
|
||||
* @see dev.lions.user.manager.server.api.dto.AuditLogDTO
|
||||
* @see TypeActionAudit
|
||||
* @author Lions Development Team
|
||||
* @version 1.0.0
|
||||
* @since 2026-01-02
|
||||
*/
|
||||
@Entity
|
||||
@Table(
|
||||
name = "audit_logs",
|
||||
indexes = {
|
||||
@Index(name = "idx_audit_user_id", columnList = "user_id"),
|
||||
@Index(name = "idx_audit_action", columnList = "action"),
|
||||
@Index(name = "idx_audit_timestamp", columnList = "timestamp"),
|
||||
@Index(name = "idx_audit_auteur", columnList = "auteur_action")
|
||||
}
|
||||
)
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class AuditLogEntity extends PanacheEntity {
|
||||
|
||||
/**
|
||||
* ID de l'utilisateur concerné par l'action.
|
||||
* <p>Peut être null pour les actions système qui ne concernent pas un utilisateur spécifique.</p>
|
||||
*/
|
||||
@Column(name = "user_id", length = 255)
|
||||
private String userId;
|
||||
|
||||
/**
|
||||
* Type d'action effectuée (CREATION_UTILISATEUR, MODIFICATION_UTILISATEUR, etc.).
|
||||
* <p>Stocké en tant que STRING pour faciliter la lecture en base de données.</p>
|
||||
*/
|
||||
@Column(name = "action", nullable = false, length = 100)
|
||||
@Enumerated(EnumType.STRING)
|
||||
private TypeActionAudit action;
|
||||
|
||||
/**
|
||||
* Détails complémentaires sur l'action effectuée.
|
||||
* <p>Peut contenir des informations contextuelles comme les champs modifiés,
|
||||
* les raisons d'une action, ou des messages d'erreur.</p>
|
||||
*/
|
||||
@Column(name = "details", columnDefinition = "TEXT")
|
||||
private String details;
|
||||
|
||||
/**
|
||||
* Identifiant de l'utilisateur qui a effectué l'action.
|
||||
* <p>Généralement l'username ou l'ID de l'administrateur/utilisateur connecté.</p>
|
||||
*/
|
||||
@Column(name = "auteur_action", nullable = false, length = 255)
|
||||
private String auteurAction;
|
||||
|
||||
/**
|
||||
* Timestamp précis de l'action.
|
||||
* <p>Utilisé pour l'ordre chronologique des logs et le filtrage temporel.</p>
|
||||
*/
|
||||
@Column(name = "timestamp", nullable = false)
|
||||
private LocalDateTime timestamp;
|
||||
|
||||
/**
|
||||
* Adresse IP de l'auteur de l'action.
|
||||
* <p>Utile pour la traçabilité et la détection d'anomalies.</p>
|
||||
*/
|
||||
@Column(name = "ip_address", length = 45)
|
||||
private String ipAddress;
|
||||
|
||||
/**
|
||||
* User-Agent du client (navigateur, application, etc.).
|
||||
* <p>Permet d'identifier le type de client utilisé pour l'action.</p>
|
||||
*/
|
||||
@Column(name = "user_agent", length = 500)
|
||||
private String userAgent;
|
||||
|
||||
/**
|
||||
* Nom du realm Keycloak concerné.
|
||||
* <p>Important dans un environnement multi-tenant pour isoler les logs par realm.</p>
|
||||
*/
|
||||
@Column(name = "realm_name", length = 255)
|
||||
private String realmName;
|
||||
|
||||
/**
|
||||
* Indique si l'action a réussi ou échoué.
|
||||
* <p>Permet de filtrer facilement les actions en erreur pour analyse.</p>
|
||||
*/
|
||||
@Column(name = "success", nullable = false)
|
||||
private Boolean success = true;
|
||||
|
||||
/**
|
||||
* Message d'erreur en cas d'échec de l'action.
|
||||
* <p>Null si success = true.</p>
|
||||
*/
|
||||
@Column(name = "error_message", columnDefinition = "TEXT")
|
||||
private String errorMessage;
|
||||
|
||||
/**
|
||||
* Constructeur par défaut requis par JPA.
|
||||
*/
|
||||
public AuditLogEntity() {
|
||||
this.timestamp = LocalDateTime.now();
|
||||
}
|
||||
|
||||
/**
|
||||
* Recherche tous les logs d'audit pour un utilisateur donné.
|
||||
*
|
||||
* @param userId ID de l'utilisateur
|
||||
* @return Liste des logs triés par timestamp décroissant
|
||||
*/
|
||||
public static java.util.List<AuditLogEntity> findByUserId(String userId) {
|
||||
return list("userId = ?1 order by timestamp desc", userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Recherche tous les logs d'audit d'un type d'action donné.
|
||||
*
|
||||
* @param action Type d'action
|
||||
* @return Liste des logs triés par timestamp décroissant
|
||||
*/
|
||||
public static java.util.List<AuditLogEntity> findByAction(TypeActionAudit action) {
|
||||
return list("action = ?1 order by timestamp desc", action);
|
||||
}
|
||||
|
||||
/**
|
||||
* Recherche tous les logs d'audit pour un auteur donné.
|
||||
*
|
||||
* @param auteurAction Identifiant de l'auteur
|
||||
* @return Liste des logs triés par timestamp décroissant
|
||||
*/
|
||||
public static java.util.List<AuditLogEntity> findByAuteur(String auteurAction) {
|
||||
return list("auteurAction = ?1 order by timestamp desc", auteurAction);
|
||||
}
|
||||
|
||||
/**
|
||||
* Recherche tous les logs d'audit dans une période donnée.
|
||||
*
|
||||
* @param startDate Date de début (inclusive)
|
||||
* @param endDate Date de fin (inclusive)
|
||||
* @return Liste des logs dans la période, triés par timestamp décroissant
|
||||
*/
|
||||
public static java.util.List<AuditLogEntity> findByPeriod(LocalDateTime startDate, LocalDateTime endDate) {
|
||||
return list("timestamp >= ?1 and timestamp <= ?2 order by timestamp desc", startDate, endDate);
|
||||
}
|
||||
|
||||
/**
|
||||
* Recherche tous les logs d'audit pour un realm donné.
|
||||
*
|
||||
* @param realmName Nom du realm
|
||||
* @return Liste des logs triés par timestamp décroissant
|
||||
*/
|
||||
public static java.util.List<AuditLogEntity> findByRealm(String realmName) {
|
||||
return list("realmName = ?1 order by timestamp desc", realmName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Supprime tous les logs d'audit plus anciens qu'une date donnée.
|
||||
* <p>Utile pour la maintenance et le respect des politiques de rétention.</p>
|
||||
*
|
||||
* @param beforeDate Date limite (les logs avant cette date seront supprimés)
|
||||
* @return Nombre de logs supprimés
|
||||
*/
|
||||
public static long deleteOlderThan(LocalDateTime beforeDate) {
|
||||
return delete("timestamp < ?1", beforeDate);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compte le nombre d'actions effectuées par un auteur donné.
|
||||
*
|
||||
* @param auteurAction Identifiant de l'auteur
|
||||
* @return Nombre d'actions
|
||||
*/
|
||||
public static long countByAuteur(String auteurAction) {
|
||||
return count("auteurAction = ?1", auteurAction);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compte le nombre d'échecs pour un utilisateur donné.
|
||||
* <p>Utile pour détecter des problèmes récurrents.</p>
|
||||
*
|
||||
* @param userId ID de l'utilisateur
|
||||
* @return Nombre d'échecs
|
||||
*/
|
||||
public static long countFailuresByUserId(String userId) {
|
||||
return count("userId = ?1 and success = false", userId);
|
||||
}
|
||||
}
|
||||
package dev.lions.user.manager.server.impl.entity;
|
||||
|
||||
import dev.lions.user.manager.enums.audit.TypeActionAudit;
|
||||
import io.quarkus.hibernate.orm.panache.PanacheEntity;
|
||||
import jakarta.persistence.*;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* Entité JPA pour la persistance des logs d'audit en base de données PostgreSQL.
|
||||
*
|
||||
* <p>Cette entité représente un enregistrement d'audit qui track toutes les actions
|
||||
* effectuées sur les utilisateurs du système (création, modification, suppression, etc.).</p>
|
||||
*
|
||||
* <p><b>Utilisation:</b></p>
|
||||
* <pre>
|
||||
* AuditLogEntity auditLog = new AuditLogEntity();
|
||||
* auditLog.setUserId("user-123");
|
||||
* auditLog.setAction(TypeActionAudit.CREATION_UTILISATEUR);
|
||||
* auditLog.setDetails("Utilisateur créé avec succès");
|
||||
* auditLog.setAuteurAction("admin");
|
||||
* auditLog.setTimestamp(LocalDateTime.now());
|
||||
* auditLog.persist();
|
||||
* </pre>
|
||||
*
|
||||
* @see dev.lions.user.manager.server.api.dto.AuditLogDTO
|
||||
* @see TypeActionAudit
|
||||
* @author Lions Development Team
|
||||
* @version 1.0.0
|
||||
* @since 2026-01-02
|
||||
*/
|
||||
@Entity
|
||||
@Table(
|
||||
name = "audit_logs",
|
||||
indexes = {
|
||||
@Index(name = "idx_audit_user_id", columnList = "user_id"),
|
||||
@Index(name = "idx_audit_action", columnList = "action"),
|
||||
@Index(name = "idx_audit_timestamp", columnList = "timestamp"),
|
||||
@Index(name = "idx_audit_auteur", columnList = "auteur_action")
|
||||
}
|
||||
)
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class AuditLogEntity extends PanacheEntity {
|
||||
|
||||
/**
|
||||
* ID de l'utilisateur concerné par l'action.
|
||||
* <p>Peut être null pour les actions système qui ne concernent pas un utilisateur spécifique.</p>
|
||||
*/
|
||||
@Column(name = "user_id", length = 255)
|
||||
private String userId;
|
||||
|
||||
/**
|
||||
* Type d'action effectuée (CREATION_UTILISATEUR, MODIFICATION_UTILISATEUR, etc.).
|
||||
* <p>Stocké en tant que STRING pour faciliter la lecture en base de données.</p>
|
||||
*/
|
||||
@Column(name = "action", nullable = false, length = 100)
|
||||
@Enumerated(EnumType.STRING)
|
||||
private TypeActionAudit action;
|
||||
|
||||
/**
|
||||
* Détails complémentaires sur l'action effectuée.
|
||||
* <p>Peut contenir des informations contextuelles comme les champs modifiés,
|
||||
* les raisons d'une action, ou des messages d'erreur.</p>
|
||||
*/
|
||||
@Column(name = "details", columnDefinition = "TEXT")
|
||||
private String details;
|
||||
|
||||
/**
|
||||
* Identifiant de l'utilisateur qui a effectué l'action.
|
||||
* <p>Généralement l'username ou l'ID de l'administrateur/utilisateur connecté.</p>
|
||||
*/
|
||||
@Column(name = "auteur_action", nullable = false, length = 255)
|
||||
private String auteurAction;
|
||||
|
||||
/**
|
||||
* Timestamp précis de l'action.
|
||||
* <p>Utilisé pour l'ordre chronologique des logs et le filtrage temporel.</p>
|
||||
*/
|
||||
@Column(name = "timestamp", nullable = false)
|
||||
private LocalDateTime timestamp;
|
||||
|
||||
/**
|
||||
* Adresse IP de l'auteur de l'action.
|
||||
* <p>Utile pour la traçabilité et la détection d'anomalies.</p>
|
||||
*/
|
||||
@Column(name = "ip_address", length = 45)
|
||||
private String ipAddress;
|
||||
|
||||
/**
|
||||
* User-Agent du client (navigateur, application, etc.).
|
||||
* <p>Permet d'identifier le type de client utilisé pour l'action.</p>
|
||||
*/
|
||||
@Column(name = "user_agent", length = 500)
|
||||
private String userAgent;
|
||||
|
||||
/**
|
||||
* Nom du realm Keycloak concerné.
|
||||
* <p>Important dans un environnement multi-tenant pour isoler les logs par realm.</p>
|
||||
*/
|
||||
@Column(name = "realm_name", length = 255)
|
||||
private String realmName;
|
||||
|
||||
/**
|
||||
* Indique si l'action a réussi ou échoué.
|
||||
* <p>Permet de filtrer facilement les actions en erreur pour analyse.</p>
|
||||
*/
|
||||
@Column(name = "success", nullable = false)
|
||||
private Boolean success = true;
|
||||
|
||||
/**
|
||||
* Message d'erreur en cas d'échec de l'action.
|
||||
* <p>Null si success = true.</p>
|
||||
*/
|
||||
@Column(name = "error_message", columnDefinition = "TEXT")
|
||||
private String errorMessage;
|
||||
|
||||
/**
|
||||
* Constructeur par défaut requis par JPA.
|
||||
*/
|
||||
public AuditLogEntity() {
|
||||
this.timestamp = LocalDateTime.now();
|
||||
}
|
||||
|
||||
/**
|
||||
* Recherche tous les logs d'audit pour un utilisateur donné.
|
||||
*
|
||||
* @param userId ID de l'utilisateur
|
||||
* @return Liste des logs triés par timestamp décroissant
|
||||
*/
|
||||
public static java.util.List<AuditLogEntity> findByUserId(String userId) {
|
||||
return list("userId = ?1 order by timestamp desc", userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Recherche tous les logs d'audit d'un type d'action donné.
|
||||
*
|
||||
* @param action Type d'action
|
||||
* @return Liste des logs triés par timestamp décroissant
|
||||
*/
|
||||
public static java.util.List<AuditLogEntity> findByAction(TypeActionAudit action) {
|
||||
return list("action = ?1 order by timestamp desc", action);
|
||||
}
|
||||
|
||||
/**
|
||||
* Recherche tous les logs d'audit pour un auteur donné.
|
||||
*
|
||||
* @param auteurAction Identifiant de l'auteur
|
||||
* @return Liste des logs triés par timestamp décroissant
|
||||
*/
|
||||
public static java.util.List<AuditLogEntity> findByAuteur(String auteurAction) {
|
||||
return list("auteurAction = ?1 order by timestamp desc", auteurAction);
|
||||
}
|
||||
|
||||
/**
|
||||
* Recherche tous les logs d'audit dans une période donnée.
|
||||
*
|
||||
* @param startDate Date de début (inclusive)
|
||||
* @param endDate Date de fin (inclusive)
|
||||
* @return Liste des logs dans la période, triés par timestamp décroissant
|
||||
*/
|
||||
public static java.util.List<AuditLogEntity> findByPeriod(LocalDateTime startDate, LocalDateTime endDate) {
|
||||
return list("timestamp >= ?1 and timestamp <= ?2 order by timestamp desc", startDate, endDate);
|
||||
}
|
||||
|
||||
/**
|
||||
* Recherche tous les logs d'audit pour un realm donné.
|
||||
*
|
||||
* @param realmName Nom du realm
|
||||
* @return Liste des logs triés par timestamp décroissant
|
||||
*/
|
||||
public static java.util.List<AuditLogEntity> findByRealm(String realmName) {
|
||||
return list("realmName = ?1 order by timestamp desc", realmName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Supprime tous les logs d'audit plus anciens qu'une date donnée.
|
||||
* <p>Utile pour la maintenance et le respect des politiques de rétention.</p>
|
||||
*
|
||||
* @param beforeDate Date limite (les logs avant cette date seront supprimés)
|
||||
* @return Nombre de logs supprimés
|
||||
*/
|
||||
public static long deleteOlderThan(LocalDateTime beforeDate) {
|
||||
return delete("timestamp < ?1", beforeDate);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compte le nombre d'actions effectuées par un auteur donné.
|
||||
*
|
||||
* @param auteurAction Identifiant de l'auteur
|
||||
* @return Nombre d'actions
|
||||
*/
|
||||
public static long countByAuteur(String auteurAction) {
|
||||
return count("auteurAction = ?1", auteurAction);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compte le nombre d'échecs pour un utilisateur donné.
|
||||
* <p>Utile pour détecter des problèmes récurrents.</p>
|
||||
*
|
||||
* @param userId ID de l'utilisateur
|
||||
* @return Nombre d'échecs
|
||||
*/
|
||||
public static long countFailuresByUserId(String userId) {
|
||||
return count("userId = ?1 and success = false", userId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,50 +1,50 @@
|
||||
package dev.lions.user.manager.server.impl.entity;
|
||||
|
||||
import io.quarkus.hibernate.orm.panache.PanacheEntity;
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.Table;
|
||||
import jakarta.persistence.Index;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* Entité représentant l'historique des synchronisations avec Keycloak.
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "sync_history", indexes = {
|
||||
@Index(name = "idx_sync_realm", columnList = "realm_name"),
|
||||
@Index(name = "idx_sync_date", columnList = "sync_date")
|
||||
})
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class SyncHistoryEntity extends PanacheEntity {
|
||||
|
||||
@Column(name = "realm_name", nullable = false)
|
||||
private String realmName;
|
||||
|
||||
@Column(name = "sync_date", nullable = false)
|
||||
private LocalDateTime syncDate;
|
||||
|
||||
// USER ou ROLE
|
||||
@Column(name = "sync_type", nullable = false)
|
||||
private String syncType;
|
||||
|
||||
@Column(name = "status", nullable = false) // SUCCESS, FAILURE
|
||||
private String status;
|
||||
|
||||
@Column(name = "items_processed")
|
||||
private Integer itemsProcessed;
|
||||
|
||||
@Column(name = "duration_ms")
|
||||
private Long durationMs;
|
||||
|
||||
@Column(name = "error_message", columnDefinition = "TEXT")
|
||||
private String errorMessage;
|
||||
|
||||
public SyncHistoryEntity() {
|
||||
this.syncDate = LocalDateTime.now();
|
||||
}
|
||||
}
|
||||
package dev.lions.user.manager.server.impl.entity;
|
||||
|
||||
import io.quarkus.hibernate.orm.panache.PanacheEntity;
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.Table;
|
||||
import jakarta.persistence.Index;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* Entité représentant l'historique des synchronisations avec Keycloak.
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "sync_history", indexes = {
|
||||
@Index(name = "idx_sync_realm", columnList = "realm_name"),
|
||||
@Index(name = "idx_sync_date", columnList = "sync_date")
|
||||
})
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class SyncHistoryEntity extends PanacheEntity {
|
||||
|
||||
@Column(name = "realm_name", nullable = false)
|
||||
private String realmName;
|
||||
|
||||
@Column(name = "sync_date", nullable = false)
|
||||
private LocalDateTime syncDate;
|
||||
|
||||
// USER ou ROLE
|
||||
@Column(name = "sync_type", nullable = false)
|
||||
private String syncType;
|
||||
|
||||
@Column(name = "status", nullable = false) // SUCCESS, FAILURE
|
||||
private String status;
|
||||
|
||||
@Column(name = "items_processed")
|
||||
private Integer itemsProcessed;
|
||||
|
||||
@Column(name = "duration_ms")
|
||||
private Long durationMs;
|
||||
|
||||
@Column(name = "error_message", columnDefinition = "TEXT")
|
||||
private String errorMessage;
|
||||
|
||||
public SyncHistoryEntity() {
|
||||
this.syncDate = LocalDateTime.now();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,32 +1,32 @@
|
||||
package dev.lions.user.manager.server.impl.entity;
|
||||
|
||||
import io.quarkus.hibernate.orm.panache.PanacheEntity;
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.Index;
|
||||
import jakarta.persistence.Table;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
/**
|
||||
* Snapshot local d'un rôle Keycloak synchronisé.
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "synced_role", indexes = {
|
||||
@Index(name = "idx_synced_role_realm", columnList = "realm_name"),
|
||||
@Index(name = "idx_synced_role_realm_name", columnList = "realm_name,role_name", unique = true)
|
||||
})
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class SyncedRoleEntity extends PanacheEntity {
|
||||
|
||||
@Column(name = "realm_name", nullable = false)
|
||||
private String realmName;
|
||||
|
||||
@Column(name = "role_name", nullable = false)
|
||||
private String roleName;
|
||||
|
||||
@Column(name = "description")
|
||||
private String description;
|
||||
}
|
||||
|
||||
package dev.lions.user.manager.server.impl.entity;
|
||||
|
||||
import io.quarkus.hibernate.orm.panache.PanacheEntity;
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.Index;
|
||||
import jakarta.persistence.Table;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
/**
|
||||
* Snapshot local d'un rôle Keycloak synchronisé.
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "synced_role", indexes = {
|
||||
@Index(name = "idx_synced_role_realm", columnList = "realm_name"),
|
||||
@Index(name = "idx_synced_role_realm_name", columnList = "realm_name,role_name", unique = true)
|
||||
})
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class SyncedRoleEntity extends PanacheEntity {
|
||||
|
||||
@Column(name = "realm_name", nullable = false)
|
||||
private String realmName;
|
||||
|
||||
@Column(name = "role_name", nullable = false)
|
||||
private String roleName;
|
||||
|
||||
@Column(name = "description")
|
||||
private String description;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,47 +1,47 @@
|
||||
package dev.lions.user.manager.server.impl.entity;
|
||||
|
||||
import io.quarkus.hibernate.orm.panache.PanacheEntity;
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.Index;
|
||||
import jakarta.persistence.Table;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* Snapshot local d'un utilisateur Keycloak synchronisé.
|
||||
* Permet de conserver un état minimal pour des rapports ou vérifications de cohérence.
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "synced_user", indexes = {
|
||||
@Index(name = "idx_synced_user_realm", columnList = "realm_name"),
|
||||
@Index(name = "idx_synced_user_realm_kc_id", columnList = "realm_name,keycloak_id", unique = true)
|
||||
})
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class SyncedUserEntity extends PanacheEntity {
|
||||
|
||||
@Column(name = "realm_name", nullable = false)
|
||||
private String realmName;
|
||||
|
||||
@Column(name = "keycloak_id", nullable = false)
|
||||
private String keycloakId;
|
||||
|
||||
@Column(name = "username", nullable = false)
|
||||
private String username;
|
||||
|
||||
@Column(name = "email")
|
||||
private String email;
|
||||
|
||||
@Column(name = "enabled")
|
||||
private Boolean enabled;
|
||||
|
||||
@Column(name = "email_verified")
|
||||
private Boolean emailVerified;
|
||||
|
||||
@Column(name = "created_at")
|
||||
private LocalDateTime createdAt;
|
||||
}
|
||||
|
||||
package dev.lions.user.manager.server.impl.entity;
|
||||
|
||||
import io.quarkus.hibernate.orm.panache.PanacheEntity;
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.Index;
|
||||
import jakarta.persistence.Table;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* Snapshot local d'un utilisateur Keycloak synchronisé.
|
||||
* Permet de conserver un état minimal pour des rapports ou vérifications de cohérence.
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "synced_user", indexes = {
|
||||
@Index(name = "idx_synced_user_realm", columnList = "realm_name"),
|
||||
@Index(name = "idx_synced_user_realm_kc_id", columnList = "realm_name,keycloak_id", unique = true)
|
||||
})
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class SyncedUserEntity extends PanacheEntity {
|
||||
|
||||
@Column(name = "realm_name", nullable = false)
|
||||
private String realmName;
|
||||
|
||||
@Column(name = "keycloak_id", nullable = false)
|
||||
private String keycloakId;
|
||||
|
||||
@Column(name = "username", nullable = false)
|
||||
private String username;
|
||||
|
||||
@Column(name = "email")
|
||||
private String email;
|
||||
|
||||
@Column(name = "enabled")
|
||||
private Boolean enabled;
|
||||
|
||||
@Column(name = "email_verified")
|
||||
private Boolean emailVerified;
|
||||
|
||||
@Column(name = "created_at")
|
||||
private LocalDateTime createdAt;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,93 +1,93 @@
|
||||
package dev.lions.user.manager.server.impl.interceptor;
|
||||
|
||||
import dev.lions.user.manager.dto.audit.AuditLogDTO;
|
||||
import dev.lions.user.manager.enums.audit.TypeActionAudit;
|
||||
import dev.lions.user.manager.service.AuditService;
|
||||
import io.quarkus.security.identity.SecurityIdentity;
|
||||
import jakarta.annotation.Priority;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.interceptor.AroundInvoke;
|
||||
import jakarta.interceptor.Interceptor;
|
||||
import jakarta.interceptor.InvocationContext;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Logged
|
||||
@Interceptor
|
||||
@Priority(Interceptor.Priority.APPLICATION)
|
||||
@Slf4j
|
||||
public class AuditInterceptor {
|
||||
|
||||
@Inject
|
||||
AuditService auditService;
|
||||
|
||||
@Inject
|
||||
SecurityIdentity securityIdentity;
|
||||
|
||||
@AroundInvoke
|
||||
public Object auditMethod(InvocationContext context) throws Exception {
|
||||
Logged annotation = context.getMethod().getAnnotation(Logged.class);
|
||||
if (annotation == null) {
|
||||
annotation = context.getTarget().getClass().getAnnotation(Logged.class);
|
||||
}
|
||||
|
||||
String actionStr = annotation != null ? annotation.action() : "UNKNOWN";
|
||||
String resourceType = annotation != null ? annotation.resource() : "UNKNOWN";
|
||||
String username = securityIdentity.isAnonymous() ? "anonymous" : securityIdentity.getPrincipal().getName();
|
||||
|
||||
// Extraction du realm depuis l'issuer JWT (ex: http://keycloak/realms/lions-user-manager)
|
||||
String realmName = "unknown";
|
||||
if (!securityIdentity.isAnonymous()
|
||||
&& securityIdentity.getPrincipal() instanceof org.eclipse.microprofile.jwt.JsonWebToken jwt) {
|
||||
String issuer = jwt.getIssuer();
|
||||
if (issuer != null && issuer.contains("/realms/")) {
|
||||
realmName = issuer.substring(issuer.lastIndexOf("/realms/") + 8);
|
||||
}
|
||||
}
|
||||
|
||||
// Tentative d'extraction de l'ID de la ressource (1er argument String)
|
||||
String resourceId = "";
|
||||
if (context.getParameters().length > 0 && context.getParameters()[0] instanceof String) {
|
||||
resourceId = (String) context.getParameters()[0];
|
||||
}
|
||||
|
||||
try {
|
||||
Object result = context.proceed();
|
||||
|
||||
// Log Success
|
||||
try {
|
||||
TypeActionAudit action = TypeActionAudit.valueOf(actionStr);
|
||||
auditService.logSuccess(
|
||||
action,
|
||||
resourceType,
|
||||
resourceId,
|
||||
null,
|
||||
realmName,
|
||||
username,
|
||||
"Action réussie via AOP");
|
||||
} catch (IllegalArgumentException e) {
|
||||
log.warn("Type d'action audit inconnu: {}", actionStr);
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (Exception e) {
|
||||
// Log Failure
|
||||
try {
|
||||
TypeActionAudit action = TypeActionAudit.valueOf(actionStr);
|
||||
auditService.logFailure(
|
||||
action,
|
||||
resourceType,
|
||||
resourceId,
|
||||
null,
|
||||
realmName,
|
||||
username,
|
||||
"ERROR",
|
||||
e.getMessage());
|
||||
} catch (IllegalArgumentException ex) {
|
||||
log.warn("Type d'action audit inconnu: {}", actionStr);
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
package dev.lions.user.manager.server.impl.interceptor;
|
||||
|
||||
import dev.lions.user.manager.dto.audit.AuditLogDTO;
|
||||
import dev.lions.user.manager.enums.audit.TypeActionAudit;
|
||||
import dev.lions.user.manager.service.AuditService;
|
||||
import io.quarkus.security.identity.SecurityIdentity;
|
||||
import jakarta.annotation.Priority;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.interceptor.AroundInvoke;
|
||||
import jakarta.interceptor.Interceptor;
|
||||
import jakarta.interceptor.InvocationContext;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Logged
|
||||
@Interceptor
|
||||
@Priority(Interceptor.Priority.APPLICATION)
|
||||
@Slf4j
|
||||
public class AuditInterceptor {
|
||||
|
||||
@Inject
|
||||
AuditService auditService;
|
||||
|
||||
@Inject
|
||||
SecurityIdentity securityIdentity;
|
||||
|
||||
@AroundInvoke
|
||||
public Object auditMethod(InvocationContext context) throws Exception {
|
||||
Logged annotation = context.getMethod().getAnnotation(Logged.class);
|
||||
if (annotation == null) {
|
||||
annotation = context.getTarget().getClass().getAnnotation(Logged.class);
|
||||
}
|
||||
|
||||
String actionStr = annotation != null ? annotation.action() : "UNKNOWN";
|
||||
String resourceType = annotation != null ? annotation.resource() : "UNKNOWN";
|
||||
String username = securityIdentity.isAnonymous() ? "anonymous" : securityIdentity.getPrincipal().getName();
|
||||
|
||||
// Extraction du realm depuis l'issuer JWT (ex: http://keycloak/realms/lions-user-manager)
|
||||
String realmName = "unknown";
|
||||
if (!securityIdentity.isAnonymous()
|
||||
&& securityIdentity.getPrincipal() instanceof org.eclipse.microprofile.jwt.JsonWebToken jwt) {
|
||||
String issuer = jwt.getIssuer();
|
||||
if (issuer != null && issuer.contains("/realms/")) {
|
||||
realmName = issuer.substring(issuer.lastIndexOf("/realms/") + 8);
|
||||
}
|
||||
}
|
||||
|
||||
// Tentative d'extraction de l'ID de la ressource (1er argument String)
|
||||
String resourceId = "";
|
||||
if (context.getParameters().length > 0 && context.getParameters()[0] instanceof String) {
|
||||
resourceId = (String) context.getParameters()[0];
|
||||
}
|
||||
|
||||
try {
|
||||
Object result = context.proceed();
|
||||
|
||||
// Log Success
|
||||
try {
|
||||
TypeActionAudit action = TypeActionAudit.valueOf(actionStr);
|
||||
auditService.logSuccess(
|
||||
action,
|
||||
resourceType,
|
||||
resourceId,
|
||||
null,
|
||||
realmName,
|
||||
username,
|
||||
"Action réussie via AOP");
|
||||
} catch (IllegalArgumentException e) {
|
||||
log.warn("Type d'action audit inconnu: {}", actionStr);
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (Exception e) {
|
||||
// Log Failure
|
||||
try {
|
||||
TypeActionAudit action = TypeActionAudit.valueOf(actionStr);
|
||||
auditService.logFailure(
|
||||
action,
|
||||
resourceType,
|
||||
resourceId,
|
||||
null,
|
||||
realmName,
|
||||
username,
|
||||
"ERROR",
|
||||
e.getMessage());
|
||||
} catch (IllegalArgumentException ex) {
|
||||
log.warn("Type d'action audit inconnu: {}", actionStr);
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,26 +1,26 @@
|
||||
package dev.lions.user.manager.server.impl.interceptor;
|
||||
|
||||
import jakarta.interceptor.InterceptorBinding;
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
/**
|
||||
* Annotation pour auditer automatiquement l'exécution d'une méthode.
|
||||
*/
|
||||
@InterceptorBinding
|
||||
@Target({ ElementType.METHOD, ElementType.TYPE })
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface Logged {
|
||||
|
||||
/**
|
||||
* Type d'action d'audit (ex: UPDATE_USER).
|
||||
*/
|
||||
String action() default "";
|
||||
|
||||
/**
|
||||
* Type de ressource concernée (ex: USER).
|
||||
*/
|
||||
String resource() default "";
|
||||
}
|
||||
package dev.lions.user.manager.server.impl.interceptor;
|
||||
|
||||
import jakarta.interceptor.InterceptorBinding;
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
/**
|
||||
* Annotation pour auditer automatiquement l'exécution d'une méthode.
|
||||
*/
|
||||
@InterceptorBinding
|
||||
@Target({ ElementType.METHOD, ElementType.TYPE })
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface Logged {
|
||||
|
||||
/**
|
||||
* Type d'action d'audit (ex: UPDATE_USER).
|
||||
*/
|
||||
String action() default "";
|
||||
|
||||
/**
|
||||
* Type de ressource concernée (ex: USER).
|
||||
*/
|
||||
String resource() default "";
|
||||
}
|
||||
|
||||
@@ -1,179 +1,179 @@
|
||||
package dev.lions.user.manager.server.impl.mapper;
|
||||
|
||||
import dev.lions.user.manager.dto.audit.AuditLogDTO;
|
||||
import dev.lions.user.manager.server.impl.entity.AuditLogEntity;
|
||||
import org.mapstruct.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Mapper MapStruct pour la conversion entre AuditLogEntity (JPA) et AuditLogDTO (API).
|
||||
*
|
||||
* <p>Ce mapper gère la transformation bidirectionnelle entre l'entité de persistance
|
||||
* et le DTO exposé via l'API REST, avec mapping automatique des champs compatibles.</p>
|
||||
*
|
||||
* <p><b>Fonctionnalités:</b></p>
|
||||
* <ul>
|
||||
* <li>Conversion Entity → DTO pour lecture/API</li>
|
||||
* <li>Conversion DTO → Entity pour persistance</li>
|
||||
* <li>Mapping de listes pour opérations bulk</li>
|
||||
* <li>Gestion automatique des types LocalDateTime</li>
|
||||
* <li>Mapping des enums (TypeActionAudit)</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p><b>Utilisation:</b></p>
|
||||
* <pre>
|
||||
* {@literal @}Inject
|
||||
* AuditLogMapper mapper;
|
||||
*
|
||||
* // Entity → DTO
|
||||
* AuditLogDTO dto = mapper.toDTO(entity);
|
||||
*
|
||||
* // DTO → Entity
|
||||
* AuditLogEntity entity = mapper.toEntity(dto);
|
||||
*
|
||||
* // Liste Entity → Liste DTO
|
||||
* List<AuditLogDTO> dtos = mapper.toDTOList(entities);
|
||||
* </pre>
|
||||
*
|
||||
* @see AuditLogEntity
|
||||
* @see AuditLogDTO
|
||||
* @author Lions Development Team
|
||||
* @version 1.0.0
|
||||
* @since 2026-01-02
|
||||
*/
|
||||
@Mapper(
|
||||
componentModel = MappingConstants.ComponentModel.JAKARTA_CDI,
|
||||
injectionStrategy = InjectionStrategy.CONSTRUCTOR,
|
||||
unmappedTargetPolicy = ReportingPolicy.IGNORE
|
||||
)
|
||||
public interface AuditLogMapper {
|
||||
|
||||
/**
|
||||
* Convertit une entité AuditLogEntity en DTO AuditLogDTO.
|
||||
*
|
||||
* <p>Mapping des champs Entity → DTO:</p>
|
||||
* <ul>
|
||||
* <li>id (Long) → id (String)</li>
|
||||
* <li>userId → ressourceId</li>
|
||||
* <li>action → typeAction</li>
|
||||
* <li>details → description</li>
|
||||
* <li>auteurAction → acteurUsername</li>
|
||||
* <li>timestamp → dateAction</li>
|
||||
* <li>ipAddress → ipAddress</li>
|
||||
* <li>userAgent → userAgent</li>
|
||||
* <li>realmName → realmName</li>
|
||||
* <li>success → success</li>
|
||||
* <li>errorMessage → errorMessage</li>
|
||||
* </ul>
|
||||
*
|
||||
* @param entity L'entité JPA à convertir (peut être null)
|
||||
* @return Le DTO correspondant, ou null si l'entité est null
|
||||
*/
|
||||
@Mapping(target = "id", source = "id", qualifiedByName = "longToString")
|
||||
@Mapping(target = "ressourceId", source = "userId")
|
||||
@Mapping(target = "typeAction", source = "action")
|
||||
@Mapping(target = "description", source = "details")
|
||||
@Mapping(target = "acteurUsername", source = "auteurAction")
|
||||
@Mapping(target = "dateAction", source = "timestamp")
|
||||
AuditLogDTO toDTO(AuditLogEntity entity);
|
||||
|
||||
/**
|
||||
* Convertit un DTO AuditLogDTO en entité AuditLogEntity.
|
||||
*
|
||||
* <p>Utilisé pour créer une nouvelle entité à persister depuis les données API.</p>
|
||||
*
|
||||
* <p><b>Note:</b> L'ID de l'entité sera null (auto-généré par la DB),
|
||||
* même si l'ID du DTO est renseigné.</p>
|
||||
*
|
||||
* @param dto Le DTO à convertir (peut être null)
|
||||
* @return L'entité JPA correspondante, ou null si le DTO est null
|
||||
*/
|
||||
@Mapping(target = "id", ignore = true) // L'ID sera généré par la DB
|
||||
@Mapping(target = "userId", source = "ressourceId")
|
||||
@Mapping(target = "action", source = "typeAction")
|
||||
@Mapping(target = "details", source = "description")
|
||||
@Mapping(target = "auteurAction", source = "acteurUsername")
|
||||
@Mapping(target = "timestamp", source = "dateAction")
|
||||
AuditLogEntity toEntity(AuditLogDTO dto);
|
||||
|
||||
/**
|
||||
* Convertit une liste d'entités en liste de DTOs.
|
||||
*
|
||||
* <p>Utile pour les recherches qui retournent plusieurs résultats.</p>
|
||||
*
|
||||
* @param entities Liste des entités à convertir (peut être null ou vide)
|
||||
* @return Liste des DTOs correspondants, ou liste vide si entities est null/vide
|
||||
*/
|
||||
List<AuditLogDTO> toDTOList(List<AuditLogEntity> entities);
|
||||
|
||||
/**
|
||||
* Convertit une liste de DTOs en liste d'entités.
|
||||
*
|
||||
* <p>Utile pour les opérations d'import ou de création en masse.</p>
|
||||
*
|
||||
* @param dtos Liste des DTOs à convertir (peut être null ou vide)
|
||||
* @return Liste des entités correspondantes, ou liste vide si dtos est null/vide
|
||||
*/
|
||||
List<AuditLogEntity> toEntityList(List<AuditLogDTO> dtos);
|
||||
|
||||
/**
|
||||
* Met à jour une entité existante avec les données d'un DTO.
|
||||
*
|
||||
* <p>Préserve l'ID de l'entité et ne met à jour que les champs
|
||||
* présents dans le DTO.</p>
|
||||
*
|
||||
* <p><b>Utilisation:</b></p>
|
||||
* <pre>
|
||||
* AuditLogEntity existingEntity = AuditLogEntity.findById(id);
|
||||
* mapper.updateEntityFromDTO(dto, existingEntity);
|
||||
* existingEntity.persist();
|
||||
* </pre>
|
||||
*
|
||||
* @param dto Le DTO source contenant les nouvelles valeurs
|
||||
* @param entity L'entité cible à mettre à jour
|
||||
*/
|
||||
@Mapping(target = "id", ignore = true) // Préserve l'ID existant
|
||||
@Mapping(target = "userId", source = "ressourceId")
|
||||
@Mapping(target = "action", source = "typeAction")
|
||||
@Mapping(target = "details", source = "description")
|
||||
@Mapping(target = "auteurAction", source = "acteurUsername")
|
||||
@Mapping(target = "timestamp", source = "dateAction")
|
||||
@BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE)
|
||||
void updateEntityFromDTO(AuditLogDTO dto, @MappingTarget AuditLogEntity entity);
|
||||
|
||||
/**
|
||||
* Convertit un Long (ID de l'entité) en String (ID du DTO).
|
||||
*
|
||||
* <p>MapStruct appelle automatiquement cette méthode pour le mapping de l'ID.</p>
|
||||
*
|
||||
* @param id L'ID de type Long (peut être null)
|
||||
* @return L'ID converti en String, ou null si l'input est null
|
||||
*/
|
||||
@Named("longToString")
|
||||
default String longToString(Long id) {
|
||||
return id != null ? id.toString() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convertit un String (ID du DTO) en Long (ID de l'entité).
|
||||
*
|
||||
* <p>Utilisé lors de la conversion DTO → Entity si nécessaire.</p>
|
||||
*
|
||||
* @param id L'ID de type String (peut être null)
|
||||
* @return L'ID converti en Long, ou null si l'input est null ou invalide
|
||||
*/
|
||||
@Named("stringToLong")
|
||||
default Long stringToLong(String id) {
|
||||
if (id == null || id.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return Long.parseLong(id);
|
||||
} catch (NumberFormatException e) {
|
||||
// Log warning et retourne null en cas de format invalide
|
||||
System.err.println("WARN: Invalid ID format for conversion to Long: " + id);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
package dev.lions.user.manager.server.impl.mapper;
|
||||
|
||||
import dev.lions.user.manager.dto.audit.AuditLogDTO;
|
||||
import dev.lions.user.manager.server.impl.entity.AuditLogEntity;
|
||||
import org.mapstruct.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Mapper MapStruct pour la conversion entre AuditLogEntity (JPA) et AuditLogDTO (API).
|
||||
*
|
||||
* <p>Ce mapper gère la transformation bidirectionnelle entre l'entité de persistance
|
||||
* et le DTO exposé via l'API REST, avec mapping automatique des champs compatibles.</p>
|
||||
*
|
||||
* <p><b>Fonctionnalités:</b></p>
|
||||
* <ul>
|
||||
* <li>Conversion Entity → DTO pour lecture/API</li>
|
||||
* <li>Conversion DTO → Entity pour persistance</li>
|
||||
* <li>Mapping de listes pour opérations bulk</li>
|
||||
* <li>Gestion automatique des types LocalDateTime</li>
|
||||
* <li>Mapping des enums (TypeActionAudit)</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p><b>Utilisation:</b></p>
|
||||
* <pre>
|
||||
* {@literal @}Inject
|
||||
* AuditLogMapper mapper;
|
||||
*
|
||||
* // Entity → DTO
|
||||
* AuditLogDTO dto = mapper.toDTO(entity);
|
||||
*
|
||||
* // DTO → Entity
|
||||
* AuditLogEntity entity = mapper.toEntity(dto);
|
||||
*
|
||||
* // Liste Entity → Liste DTO
|
||||
* List<AuditLogDTO> dtos = mapper.toDTOList(entities);
|
||||
* </pre>
|
||||
*
|
||||
* @see AuditLogEntity
|
||||
* @see AuditLogDTO
|
||||
* @author Lions Development Team
|
||||
* @version 1.0.0
|
||||
* @since 2026-01-02
|
||||
*/
|
||||
@Mapper(
|
||||
componentModel = MappingConstants.ComponentModel.JAKARTA_CDI,
|
||||
injectionStrategy = InjectionStrategy.CONSTRUCTOR,
|
||||
unmappedTargetPolicy = ReportingPolicy.IGNORE
|
||||
)
|
||||
public interface AuditLogMapper {
|
||||
|
||||
/**
|
||||
* Convertit une entité AuditLogEntity en DTO AuditLogDTO.
|
||||
*
|
||||
* <p>Mapping des champs Entity → DTO:</p>
|
||||
* <ul>
|
||||
* <li>id (Long) → id (String)</li>
|
||||
* <li>userId → ressourceId</li>
|
||||
* <li>action → typeAction</li>
|
||||
* <li>details → description</li>
|
||||
* <li>auteurAction → acteurUsername</li>
|
||||
* <li>timestamp → dateAction</li>
|
||||
* <li>ipAddress → ipAddress</li>
|
||||
* <li>userAgent → userAgent</li>
|
||||
* <li>realmName → realmName</li>
|
||||
* <li>success → success</li>
|
||||
* <li>errorMessage → errorMessage</li>
|
||||
* </ul>
|
||||
*
|
||||
* @param entity L'entité JPA à convertir (peut être null)
|
||||
* @return Le DTO correspondant, ou null si l'entité est null
|
||||
*/
|
||||
@Mapping(target = "id", source = "id", qualifiedByName = "longToString")
|
||||
@Mapping(target = "ressourceId", source = "userId")
|
||||
@Mapping(target = "typeAction", source = "action")
|
||||
@Mapping(target = "description", source = "details")
|
||||
@Mapping(target = "acteurUsername", source = "auteurAction")
|
||||
@Mapping(target = "dateAction", source = "timestamp")
|
||||
AuditLogDTO toDTO(AuditLogEntity entity);
|
||||
|
||||
/**
|
||||
* Convertit un DTO AuditLogDTO en entité AuditLogEntity.
|
||||
*
|
||||
* <p>Utilisé pour créer une nouvelle entité à persister depuis les données API.</p>
|
||||
*
|
||||
* <p><b>Note:</b> L'ID de l'entité sera null (auto-généré par la DB),
|
||||
* même si l'ID du DTO est renseigné.</p>
|
||||
*
|
||||
* @param dto Le DTO à convertir (peut être null)
|
||||
* @return L'entité JPA correspondante, ou null si le DTO est null
|
||||
*/
|
||||
@Mapping(target = "id", ignore = true) // L'ID sera généré par la DB
|
||||
@Mapping(target = "userId", source = "ressourceId")
|
||||
@Mapping(target = "action", source = "typeAction")
|
||||
@Mapping(target = "details", source = "description")
|
||||
@Mapping(target = "auteurAction", source = "acteurUsername")
|
||||
@Mapping(target = "timestamp", source = "dateAction")
|
||||
AuditLogEntity toEntity(AuditLogDTO dto);
|
||||
|
||||
/**
|
||||
* Convertit une liste d'entités en liste de DTOs.
|
||||
*
|
||||
* <p>Utile pour les recherches qui retournent plusieurs résultats.</p>
|
||||
*
|
||||
* @param entities Liste des entités à convertir (peut être null ou vide)
|
||||
* @return Liste des DTOs correspondants, ou liste vide si entities est null/vide
|
||||
*/
|
||||
List<AuditLogDTO> toDTOList(List<AuditLogEntity> entities);
|
||||
|
||||
/**
|
||||
* Convertit une liste de DTOs en liste d'entités.
|
||||
*
|
||||
* <p>Utile pour les opérations d'import ou de création en masse.</p>
|
||||
*
|
||||
* @param dtos Liste des DTOs à convertir (peut être null ou vide)
|
||||
* @return Liste des entités correspondantes, ou liste vide si dtos est null/vide
|
||||
*/
|
||||
List<AuditLogEntity> toEntityList(List<AuditLogDTO> dtos);
|
||||
|
||||
/**
|
||||
* Met à jour une entité existante avec les données d'un DTO.
|
||||
*
|
||||
* <p>Préserve l'ID de l'entité et ne met à jour que les champs
|
||||
* présents dans le DTO.</p>
|
||||
*
|
||||
* <p><b>Utilisation:</b></p>
|
||||
* <pre>
|
||||
* AuditLogEntity existingEntity = AuditLogEntity.findById(id);
|
||||
* mapper.updateEntityFromDTO(dto, existingEntity);
|
||||
* existingEntity.persist();
|
||||
* </pre>
|
||||
*
|
||||
* @param dto Le DTO source contenant les nouvelles valeurs
|
||||
* @param entity L'entité cible à mettre à jour
|
||||
*/
|
||||
@Mapping(target = "id", ignore = true) // Préserve l'ID existant
|
||||
@Mapping(target = "userId", source = "ressourceId")
|
||||
@Mapping(target = "action", source = "typeAction")
|
||||
@Mapping(target = "details", source = "description")
|
||||
@Mapping(target = "auteurAction", source = "acteurUsername")
|
||||
@Mapping(target = "timestamp", source = "dateAction")
|
||||
@BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE)
|
||||
void updateEntityFromDTO(AuditLogDTO dto, @MappingTarget AuditLogEntity entity);
|
||||
|
||||
/**
|
||||
* Convertit un Long (ID de l'entité) en String (ID du DTO).
|
||||
*
|
||||
* <p>MapStruct appelle automatiquement cette méthode pour le mapping de l'ID.</p>
|
||||
*
|
||||
* @param id L'ID de type Long (peut être null)
|
||||
* @return L'ID converti en String, ou null si l'input est null
|
||||
*/
|
||||
@Named("longToString")
|
||||
default String longToString(Long id) {
|
||||
return id != null ? id.toString() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convertit un String (ID du DTO) en Long (ID de l'entité).
|
||||
*
|
||||
* <p>Utilisé lors de la conversion DTO → Entity si nécessaire.</p>
|
||||
*
|
||||
* @param id L'ID de type String (peut être null)
|
||||
* @return L'ID converti en Long, ou null si l'input est null ou invalide
|
||||
*/
|
||||
@Named("stringToLong")
|
||||
default Long stringToLong(String id) {
|
||||
if (id == null || id.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return Long.parseLong(id);
|
||||
} catch (NumberFormatException e) {
|
||||
// Log warning et retourne null en cas de format invalide
|
||||
System.err.println("WARN: Invalid ID format for conversion to Long: " + id);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
package dev.lions.user.manager.server.impl.mapper;
|
||||
|
||||
import dev.lions.user.manager.dto.sync.SyncHistoryDTO;
|
||||
import dev.lions.user.manager.server.impl.entity.SyncHistoryEntity;
|
||||
import org.mapstruct.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Mapper(componentModel = MappingConstants.ComponentModel.JAKARTA_CDI, injectionStrategy = InjectionStrategy.CONSTRUCTOR, unmappedTargetPolicy = ReportingPolicy.IGNORE)
|
||||
public interface SyncHistoryMapper {
|
||||
|
||||
@Mapping(target = "id", source = "id", qualifiedByName = "longToString")
|
||||
SyncHistoryDTO toDTO(SyncHistoryEntity entity);
|
||||
|
||||
List<SyncHistoryDTO> toDTOList(List<SyncHistoryEntity> entities);
|
||||
|
||||
@Named("longToString")
|
||||
default String longToString(Long id) {
|
||||
return id != null ? id.toString() : null;
|
||||
}
|
||||
}
|
||||
package dev.lions.user.manager.server.impl.mapper;
|
||||
|
||||
import dev.lions.user.manager.dto.sync.SyncHistoryDTO;
|
||||
import dev.lions.user.manager.server.impl.entity.SyncHistoryEntity;
|
||||
import org.mapstruct.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Mapper(componentModel = MappingConstants.ComponentModel.JAKARTA_CDI, injectionStrategy = InjectionStrategy.CONSTRUCTOR, unmappedTargetPolicy = ReportingPolicy.IGNORE)
|
||||
public interface SyncHistoryMapper {
|
||||
|
||||
@Mapping(target = "id", source = "id", qualifiedByName = "longToString")
|
||||
SyncHistoryDTO toDTO(SyncHistoryEntity entity);
|
||||
|
||||
List<SyncHistoryDTO> toDTOList(List<SyncHistoryEntity> entities);
|
||||
|
||||
@Named("longToString")
|
||||
default String longToString(Long id) {
|
||||
return id != null ? id.toString() : null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,62 +1,62 @@
|
||||
package dev.lions.user.manager.server.impl.repository;
|
||||
|
||||
import dev.lions.user.manager.enums.audit.TypeActionAudit;
|
||||
import dev.lions.user.manager.server.impl.entity.AuditLogEntity;
|
||||
import io.quarkus.hibernate.orm.panache.PanacheRepository;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@ApplicationScoped
|
||||
public class AuditLogRepository implements PanacheRepository<AuditLogEntity> {
|
||||
|
||||
public List<AuditLogEntity> search(String realmName,
|
||||
String auteurAction,
|
||||
LocalDateTime dateDebut,
|
||||
LocalDateTime dateFin,
|
||||
String typeAction,
|
||||
Boolean success,
|
||||
int page,
|
||||
int pageSize) {
|
||||
|
||||
StringBuilder query = new StringBuilder("1=1");
|
||||
Map<String, Object> params = new HashMap<>();
|
||||
|
||||
// Construction dynamique de la requête
|
||||
if (realmName != null && !realmName.isEmpty()) {
|
||||
query.append(" AND realmName = :realmName");
|
||||
params.put("realmName", realmName);
|
||||
}
|
||||
if (auteurAction != null && !auteurAction.isEmpty()) {
|
||||
query.append(" AND auteurAction = :auteurAction");
|
||||
params.put("auteurAction", auteurAction);
|
||||
}
|
||||
if (dateDebut != null) {
|
||||
query.append(" AND timestamp >= :dateDebut");
|
||||
params.put("dateDebut", dateDebut);
|
||||
}
|
||||
if (dateFin != null) {
|
||||
query.append(" AND timestamp <= :dateFin");
|
||||
params.put("dateFin", dateFin);
|
||||
}
|
||||
if (typeAction != null && !typeAction.isEmpty()) {
|
||||
try {
|
||||
TypeActionAudit actionEnum = TypeActionAudit.valueOf(typeAction);
|
||||
query.append(" AND action = :actionEnum");
|
||||
params.put("actionEnum", actionEnum);
|
||||
} catch (IllegalArgumentException e) {
|
||||
// Ignore invalid enum value filter
|
||||
}
|
||||
}
|
||||
if (success != null) {
|
||||
query.append(" AND success = :success");
|
||||
params.put("success", success);
|
||||
}
|
||||
|
||||
query.append(" ORDER BY timestamp DESC");
|
||||
return find(query.toString(), params).page(page, pageSize).list();
|
||||
}
|
||||
}
|
||||
package dev.lions.user.manager.server.impl.repository;
|
||||
|
||||
import dev.lions.user.manager.enums.audit.TypeActionAudit;
|
||||
import dev.lions.user.manager.server.impl.entity.AuditLogEntity;
|
||||
import io.quarkus.hibernate.orm.panache.PanacheRepository;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@ApplicationScoped
|
||||
public class AuditLogRepository implements PanacheRepository<AuditLogEntity> {
|
||||
|
||||
public List<AuditLogEntity> search(String realmName,
|
||||
String auteurAction,
|
||||
LocalDateTime dateDebut,
|
||||
LocalDateTime dateFin,
|
||||
String typeAction,
|
||||
Boolean success,
|
||||
int page,
|
||||
int pageSize) {
|
||||
|
||||
StringBuilder query = new StringBuilder("1=1");
|
||||
Map<String, Object> params = new HashMap<>();
|
||||
|
||||
// Construction dynamique de la requête
|
||||
if (realmName != null && !realmName.isEmpty()) {
|
||||
query.append(" AND realmName = :realmName");
|
||||
params.put("realmName", realmName);
|
||||
}
|
||||
if (auteurAction != null && !auteurAction.isEmpty()) {
|
||||
query.append(" AND auteurAction = :auteurAction");
|
||||
params.put("auteurAction", auteurAction);
|
||||
}
|
||||
if (dateDebut != null) {
|
||||
query.append(" AND timestamp >= :dateDebut");
|
||||
params.put("dateDebut", dateDebut);
|
||||
}
|
||||
if (dateFin != null) {
|
||||
query.append(" AND timestamp <= :dateFin");
|
||||
params.put("dateFin", dateFin);
|
||||
}
|
||||
if (typeAction != null && !typeAction.isEmpty()) {
|
||||
try {
|
||||
TypeActionAudit actionEnum = TypeActionAudit.valueOf(typeAction);
|
||||
query.append(" AND action = :actionEnum");
|
||||
params.put("actionEnum", actionEnum);
|
||||
} catch (IllegalArgumentException e) {
|
||||
// Ignore invalid enum value filter
|
||||
}
|
||||
}
|
||||
if (success != null) {
|
||||
query.append(" AND success = :success");
|
||||
params.put("success", success);
|
||||
}
|
||||
|
||||
query.append(" ORDER BY timestamp DESC");
|
||||
return find(query.toString(), params).page(page, pageSize).list();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
package dev.lions.user.manager.server.impl.repository;
|
||||
|
||||
import dev.lions.user.manager.server.impl.entity.SyncHistoryEntity;
|
||||
import io.quarkus.hibernate.orm.panache.PanacheRepository;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@ApplicationScoped
|
||||
public class SyncHistoryRepository implements PanacheRepository<SyncHistoryEntity> {
|
||||
|
||||
public List<SyncHistoryEntity> findLatestByRealm(String realmName, int limit) {
|
||||
return find("realmName = ?1 ORDER BY syncDate DESC", realmName)
|
||||
.page(0, limit)
|
||||
.list();
|
||||
}
|
||||
}
|
||||
package dev.lions.user.manager.server.impl.repository;
|
||||
|
||||
import dev.lions.user.manager.server.impl.entity.SyncHistoryEntity;
|
||||
import io.quarkus.hibernate.orm.panache.PanacheRepository;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@ApplicationScoped
|
||||
public class SyncHistoryRepository implements PanacheRepository<SyncHistoryEntity> {
|
||||
|
||||
public List<SyncHistoryEntity> findLatestByRealm(String realmName, int limit) {
|
||||
return find("realmName = ?1 ORDER BY syncDate DESC", realmName)
|
||||
.page(0, limit)
|
||||
.list();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
package dev.lions.user.manager.server.impl.repository;
|
||||
|
||||
import dev.lions.user.manager.server.impl.entity.SyncedRoleEntity;
|
||||
import io.quarkus.hibernate.orm.panache.PanacheRepository;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@ApplicationScoped
|
||||
public class SyncedRoleRepository implements PanacheRepository<SyncedRoleEntity> {
|
||||
|
||||
/**
|
||||
* Remplace l'ensemble des snapshots de rôles pour un realm donné.
|
||||
*/
|
||||
public void replaceForRealm(String realmName, List<SyncedRoleEntity> roles) {
|
||||
delete("realmName", realmName);
|
||||
persist(roles);
|
||||
}
|
||||
}
|
||||
|
||||
package dev.lions.user.manager.server.impl.repository;
|
||||
|
||||
import dev.lions.user.manager.server.impl.entity.SyncedRoleEntity;
|
||||
import io.quarkus.hibernate.orm.panache.PanacheRepository;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@ApplicationScoped
|
||||
public class SyncedRoleRepository implements PanacheRepository<SyncedRoleEntity> {
|
||||
|
||||
/**
|
||||
* Remplace l'ensemble des snapshots de rôles pour un realm donné.
|
||||
*/
|
||||
public void replaceForRealm(String realmName, List<SyncedRoleEntity> roles) {
|
||||
delete("realmName", realmName);
|
||||
persist(roles);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
package dev.lions.user.manager.server.impl.repository;
|
||||
|
||||
import dev.lions.user.manager.server.impl.entity.SyncedUserEntity;
|
||||
import io.quarkus.hibernate.orm.panache.PanacheRepository;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@ApplicationScoped
|
||||
public class SyncedUserRepository implements PanacheRepository<SyncedUserEntity> {
|
||||
|
||||
/**
|
||||
* Remplace l'ensemble des snapshots d'utilisateurs pour un realm donné.
|
||||
*/
|
||||
public void replaceForRealm(String realmName, List<SyncedUserEntity> users) {
|
||||
delete("realmName", realmName);
|
||||
persist(users);
|
||||
}
|
||||
}
|
||||
|
||||
package dev.lions.user.manager.server.impl.repository;
|
||||
|
||||
import dev.lions.user.manager.server.impl.entity.SyncedUserEntity;
|
||||
import io.quarkus.hibernate.orm.panache.PanacheRepository;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@ApplicationScoped
|
||||
public class SyncedUserRepository implements PanacheRepository<SyncedUserEntity> {
|
||||
|
||||
/**
|
||||
* Remplace l'ensemble des snapshots d'utilisateurs pour un realm donné.
|
||||
*/
|
||||
public void replaceForRealm(String realmName, List<SyncedUserEntity> users) {
|
||||
delete("realmName", realmName);
|
||||
persist(users);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,72 +1,72 @@
|
||||
package dev.lions.user.manager.service.exception;
|
||||
|
||||
/**
|
||||
* Exception levée lorsqu'une erreur survient lors de l'appel au service Keycloak.
|
||||
*
|
||||
* @author Lions User Manager Team
|
||||
* @version 1.0
|
||||
*/
|
||||
public class KeycloakServiceException extends RuntimeException {
|
||||
|
||||
private final int httpStatus;
|
||||
private final String serviceName;
|
||||
|
||||
public KeycloakServiceException(String message) {
|
||||
super(message);
|
||||
this.httpStatus = 0;
|
||||
this.serviceName = "Keycloak";
|
||||
}
|
||||
|
||||
public KeycloakServiceException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
this.httpStatus = 0;
|
||||
this.serviceName = "Keycloak";
|
||||
}
|
||||
|
||||
public KeycloakServiceException(String message, int httpStatus) {
|
||||
super(message);
|
||||
this.httpStatus = httpStatus;
|
||||
this.serviceName = "Keycloak";
|
||||
}
|
||||
|
||||
public KeycloakServiceException(String message, int httpStatus, Throwable cause) {
|
||||
super(message, cause);
|
||||
this.httpStatus = httpStatus;
|
||||
this.serviceName = "Keycloak";
|
||||
}
|
||||
|
||||
public int getHttpStatus() {
|
||||
return httpStatus;
|
||||
}
|
||||
|
||||
public String getServiceName() {
|
||||
return serviceName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Exception spécifique pour les erreurs de connexion (service indisponible)
|
||||
*/
|
||||
public static class ServiceUnavailableException extends KeycloakServiceException {
|
||||
public ServiceUnavailableException(String message) {
|
||||
super("Service Keycloak indisponible: " + message);
|
||||
}
|
||||
|
||||
public ServiceUnavailableException(String message, Throwable cause) {
|
||||
super("Service Keycloak indisponible: " + message, cause);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Exception spécifique pour les erreurs de timeout
|
||||
*/
|
||||
public static class TimeoutException extends KeycloakServiceException {
|
||||
public TimeoutException(String message) {
|
||||
super("Timeout lors de l'appel au service Keycloak: " + message);
|
||||
}
|
||||
|
||||
public TimeoutException(String message, Throwable cause) {
|
||||
super("Timeout lors de l'appel au service Keycloak: " + message, cause);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
package dev.lions.user.manager.service.exception;
|
||||
|
||||
/**
|
||||
* Exception levée lorsqu'une erreur survient lors de l'appel au service Keycloak.
|
||||
*
|
||||
* @author Lions User Manager Team
|
||||
* @version 1.0
|
||||
*/
|
||||
public class KeycloakServiceException extends RuntimeException {
|
||||
|
||||
private final int httpStatus;
|
||||
private final String serviceName;
|
||||
|
||||
public KeycloakServiceException(String message) {
|
||||
super(message);
|
||||
this.httpStatus = 0;
|
||||
this.serviceName = "Keycloak";
|
||||
}
|
||||
|
||||
public KeycloakServiceException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
this.httpStatus = 0;
|
||||
this.serviceName = "Keycloak";
|
||||
}
|
||||
|
||||
public KeycloakServiceException(String message, int httpStatus) {
|
||||
super(message);
|
||||
this.httpStatus = httpStatus;
|
||||
this.serviceName = "Keycloak";
|
||||
}
|
||||
|
||||
public KeycloakServiceException(String message, int httpStatus, Throwable cause) {
|
||||
super(message, cause);
|
||||
this.httpStatus = httpStatus;
|
||||
this.serviceName = "Keycloak";
|
||||
}
|
||||
|
||||
public int getHttpStatus() {
|
||||
return httpStatus;
|
||||
}
|
||||
|
||||
public String getServiceName() {
|
||||
return serviceName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Exception spécifique pour les erreurs de connexion (service indisponible)
|
||||
*/
|
||||
public static class ServiceUnavailableException extends KeycloakServiceException {
|
||||
public ServiceUnavailableException(String message) {
|
||||
super("Service Keycloak indisponible: " + message);
|
||||
}
|
||||
|
||||
public ServiceUnavailableException(String message, Throwable cause) {
|
||||
super("Service Keycloak indisponible: " + message, cause);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Exception spécifique pour les erreurs de timeout
|
||||
*/
|
||||
public static class TimeoutException extends KeycloakServiceException {
|
||||
public TimeoutException(String message) {
|
||||
super("Timeout lors de l'appel au service Keycloak: " + message);
|
||||
}
|
||||
|
||||
public TimeoutException(String message, Throwable cause) {
|
||||
super("Timeout lors de l'appel au service Keycloak: " + message, cause);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,362 +1,362 @@
|
||||
package dev.lions.user.manager.service.impl;
|
||||
|
||||
import dev.lions.user.manager.dto.audit.AuditLogDTO;
|
||||
import dev.lions.user.manager.enums.audit.TypeActionAudit;
|
||||
// import dev.lions.user.manager.mapper.AuditLogMapper; // DELETE - Wrong package
|
||||
import dev.lions.user.manager.server.impl.mapper.AuditLogMapper; // ADD - Correct package
|
||||
import dev.lions.user.manager.server.impl.entity.AuditLogEntity;
|
||||
import dev.lions.user.manager.server.impl.repository.AuditLogRepository;
|
||||
import dev.lions.user.manager.service.AuditService;
|
||||
import io.quarkus.hibernate.orm.panache.PanacheQuery;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.persistence.EntityManager;
|
||||
import jakarta.transaction.Transactional;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.eclipse.microprofile.config.inject.ConfigProperty;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@ApplicationScoped
|
||||
@Slf4j
|
||||
public class AuditServiceImpl implements AuditService {
|
||||
|
||||
@Inject
|
||||
AuditLogRepository auditLogRepository;
|
||||
|
||||
@Inject
|
||||
AuditLogMapper auditLogMapper;
|
||||
|
||||
@Inject
|
||||
EntityManager entityManager;
|
||||
|
||||
@ConfigProperty(name = "lions.audit.enabled", defaultValue = "true")
|
||||
boolean auditEnabled;
|
||||
|
||||
@ConfigProperty(name = "lions.audit.log-to-database", defaultValue = "true")
|
||||
boolean logToDatabase;
|
||||
|
||||
@Override
|
||||
@Transactional(Transactional.TxType.REQUIRES_NEW)
|
||||
public AuditLogDTO logAction(@Valid @NotNull AuditLogDTO auditLog) {
|
||||
if (!auditEnabled) {
|
||||
log.debug("Audit désactivé, action ignorée: {}", auditLog.getTypeAction());
|
||||
return auditLog;
|
||||
}
|
||||
|
||||
log.info("AUDIT: [{}] {} - user:{} - ressource:{}/{} - status:{}",
|
||||
auditLog.getRealmName(),
|
||||
auditLog.getTypeAction(),
|
||||
auditLog.getActeurUsername(), // ou getActeurUserId()
|
||||
auditLog.getRessourceType(),
|
||||
auditLog.getRessourceId(),
|
||||
auditLog.getSuccess() != null && auditLog.getSuccess() ? "SUCCESS" : "FAILURE");
|
||||
|
||||
if (logToDatabase) {
|
||||
try {
|
||||
// Ensure dateAction is set
|
||||
if (auditLog.getDateAction() == null) {
|
||||
auditLog.setDateAction(LocalDateTime.now());
|
||||
}
|
||||
|
||||
AuditLogEntity entity = auditLogMapper.toEntity(auditLog);
|
||||
auditLogRepository.persist(entity);
|
||||
|
||||
// Mettre à jour l'ID du DTO avec l'ID généré par la base
|
||||
if (entity.id != null) {
|
||||
auditLog.setId(entity.id.toString());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("Erreur lors de la persistance du log d'audit", e);
|
||||
// On ne bloque pas l'action métier si l'audit échoue (sauf exigence contraire)
|
||||
}
|
||||
}
|
||||
|
||||
return auditLog;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(Transactional.TxType.REQUIRES_NEW)
|
||||
public void logSuccess(@NotNull TypeActionAudit typeAction,
|
||||
@NotBlank String ressourceType,
|
||||
String ressourceId,
|
||||
String ressourceName,
|
||||
@NotBlank String realmName,
|
||||
@NotBlank String acteurUserId,
|
||||
String description) {
|
||||
|
||||
AuditLogDTO log = AuditLogDTO.builder()
|
||||
.typeAction(typeAction)
|
||||
.ressourceType(ressourceType)
|
||||
.ressourceId(ressourceId)
|
||||
.ressourceName(ressourceName)
|
||||
.realmName(realmName)
|
||||
.acteurUserId(acteurUserId)
|
||||
.acteurUsername(acteurUserId) // On map aussi le username pour la persistence Entity
|
||||
.description(description)
|
||||
.dateAction(LocalDateTime.now())
|
||||
.success(true)
|
||||
.build();
|
||||
|
||||
logAction(log);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(Transactional.TxType.REQUIRES_NEW)
|
||||
public void logFailure(@NotNull TypeActionAudit typeAction,
|
||||
@NotBlank String ressourceType,
|
||||
String ressourceId,
|
||||
String ressourceName,
|
||||
@NotBlank String realmName,
|
||||
@NotBlank String acteurUserId,
|
||||
String errorCode,
|
||||
String errorMessage) {
|
||||
|
||||
AuditLogDTO log = AuditLogDTO.builder()
|
||||
.typeAction(typeAction)
|
||||
.ressourceType(ressourceType)
|
||||
.ressourceId(ressourceId)
|
||||
.ressourceName(ressourceName)
|
||||
.realmName(realmName)
|
||||
.acteurUserId(acteurUserId)
|
||||
.acteurUsername(acteurUserId)
|
||||
.description("Echec: " + errorCode)
|
||||
.errorMessage(errorMessage)
|
||||
.dateAction(LocalDateTime.now())
|
||||
.success(false)
|
||||
.build();
|
||||
|
||||
logAction(log);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<AuditLogDTO> findByActeur(@NotBlank String acteurUserId,
|
||||
LocalDateTime dateDebut,
|
||||
LocalDateTime dateFin,
|
||||
int page,
|
||||
int pageSize) {
|
||||
// Le repository cherche par auteurAction, qui est mappé sur acteurUsername dans
|
||||
// le DTO
|
||||
List<AuditLogEntity> entities = auditLogRepository.search(null, acteurUserId, dateDebut, dateFin, null, null,
|
||||
page,
|
||||
pageSize);
|
||||
return auditLogMapper.toDTOList(entities);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<AuditLogDTO> findByRessource(@NotBlank String ressourceType,
|
||||
@NotBlank String ressourceId,
|
||||
LocalDateTime dateDebut,
|
||||
LocalDateTime dateFin,
|
||||
int page,
|
||||
int pageSize) {
|
||||
|
||||
// Utilisation de Panache query directe car le repo search générique est limité
|
||||
// On cherche dans 'details' (description) ou 'userId' (ressourceId)
|
||||
String filter = "%" + ressourceId + "%";
|
||||
// Correction: userId est le nom du champ dans l'entité qui mappe ressourceId
|
||||
PanacheQuery<AuditLogEntity> q = auditLogRepository.find("userId = ?1 or details like ?2", ressourceId, filter);
|
||||
|
||||
return auditLogMapper.toDTOList(q.page(page, pageSize).list());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<AuditLogDTO> findByTypeAction(@NotNull TypeActionAudit typeAction,
|
||||
@NotBlank String realmName,
|
||||
LocalDateTime dateDebut,
|
||||
LocalDateTime dateFin,
|
||||
int page,
|
||||
int pageSize) {
|
||||
List<AuditLogEntity> entities = auditLogRepository.search(realmName, null, dateDebut, dateFin,
|
||||
typeAction.name(), null, page,
|
||||
pageSize);
|
||||
return auditLogMapper.toDTOList(entities);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<AuditLogDTO> findByRealm(@NotBlank String realmName,
|
||||
LocalDateTime dateDebut,
|
||||
LocalDateTime dateFin,
|
||||
int page,
|
||||
int pageSize) {
|
||||
List<AuditLogEntity> entities = auditLogRepository.search(realmName, null, dateDebut, dateFin, null, null, page,
|
||||
pageSize);
|
||||
return auditLogMapper.toDTOList(entities);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<AuditLogDTO> findFailures(@NotBlank String realmName,
|
||||
LocalDateTime dateDebut,
|
||||
LocalDateTime dateFin,
|
||||
int page,
|
||||
int pageSize) {
|
||||
List<AuditLogEntity> entities = auditLogRepository.search(realmName, null, dateDebut, dateFin, null, false,
|
||||
page,
|
||||
pageSize);
|
||||
return auditLogMapper.toDTOList(entities);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<AuditLogDTO> findCriticalActions(@NotBlank String realmName,
|
||||
LocalDateTime dateDebut,
|
||||
LocalDateTime dateFin,
|
||||
int page,
|
||||
int pageSize) {
|
||||
List<AuditLogEntity> entities = auditLogRepository.search(realmName, null, dateDebut, dateFin, null, false,
|
||||
page, pageSize);
|
||||
return auditLogMapper.toDTOList(entities);
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public Map<TypeActionAudit, Long> countByActionType(@NotBlank String realmName,
|
||||
LocalDateTime dateDebut,
|
||||
LocalDateTime dateFin) {
|
||||
StringBuilder sql = new StringBuilder("SELECT action, COUNT(*) AS cnt FROM audit_logs WHERE realm_name = :realmName");
|
||||
if (dateDebut != null) sql.append(" AND timestamp >= :dateDebut");
|
||||
if (dateFin != null) sql.append(" AND timestamp <= :dateFin");
|
||||
sql.append(" GROUP BY action");
|
||||
var query = entityManager.createNativeQuery(sql.toString())
|
||||
.setParameter("realmName", realmName);
|
||||
if (dateDebut != null) query.setParameter("dateDebut", dateDebut);
|
||||
if (dateFin != null) query.setParameter("dateFin", dateFin);
|
||||
List<Object[]> rows = query.getResultList();
|
||||
Map<TypeActionAudit, Long> result = new HashMap<>();
|
||||
for (Object[] row : rows) {
|
||||
String actionStr = (String) row[0];
|
||||
Long count = ((Number) row[1]).longValue();
|
||||
try {
|
||||
result.put(TypeActionAudit.valueOf(actionStr), count);
|
||||
} catch (IllegalArgumentException e) {
|
||||
log.debug("TypeActionAudit inconnu ignoré: {}", actionStr);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public Map<String, Long> countByActeur(@NotBlank String realmName,
|
||||
LocalDateTime dateDebut,
|
||||
LocalDateTime dateFin) {
|
||||
StringBuilder sql = new StringBuilder("SELECT auteur_action, COUNT(*) AS cnt FROM audit_logs WHERE realm_name = :realmName");
|
||||
if (dateDebut != null) sql.append(" AND timestamp >= :dateDebut");
|
||||
if (dateFin != null) sql.append(" AND timestamp <= :dateFin");
|
||||
sql.append(" GROUP BY auteur_action ORDER BY COUNT(*) DESC LIMIT 10");
|
||||
var query = entityManager.createNativeQuery(sql.toString())
|
||||
.setParameter("realmName", realmName);
|
||||
if (dateDebut != null) query.setParameter("dateDebut", dateDebut);
|
||||
if (dateFin != null) query.setParameter("dateFin", dateFin);
|
||||
List<Object[]> rows = query.getResultList();
|
||||
Map<String, Long> result = new HashMap<>();
|
||||
for (Object[] row : rows) {
|
||||
result.put((String) row[0], ((Number) row[1]).longValue());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public Map<String, Long> countSuccessVsFailure(@NotBlank String realmName,
|
||||
LocalDateTime dateDebut,
|
||||
LocalDateTime dateFin) {
|
||||
StringBuilder sql = new StringBuilder("SELECT success, COUNT(*) AS cnt FROM audit_logs WHERE realm_name = :realmName");
|
||||
if (dateDebut != null) sql.append(" AND timestamp >= :dateDebut");
|
||||
if (dateFin != null) sql.append(" AND timestamp <= :dateFin");
|
||||
sql.append(" GROUP BY success");
|
||||
var query = entityManager.createNativeQuery(sql.toString())
|
||||
.setParameter("realmName", realmName);
|
||||
if (dateDebut != null) query.setParameter("dateDebut", dateDebut);
|
||||
if (dateFin != null) query.setParameter("dateFin", dateFin);
|
||||
List<Object[]> rows = query.getResultList();
|
||||
Map<String, Long> result = new HashMap<>();
|
||||
result.put("success", 0L);
|
||||
result.put("failure", 0L);
|
||||
for (Object[] row : rows) {
|
||||
Boolean success = (Boolean) row[0];
|
||||
Long count = ((Number) row[1]).longValue();
|
||||
result.put(Boolean.TRUE.equals(success) ? "success" : "failure", count);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String exportToCSV(@NotBlank String realmName,
|
||||
LocalDateTime dateDebut,
|
||||
LocalDateTime dateFin) {
|
||||
List<AuditLogEntity> entities = auditLogRepository.search(realmName, null, dateDebut, dateFin, null, null, 0, Integer.MAX_VALUE);
|
||||
List<AuditLogDTO> logs = auditLogMapper.toDTOList(entities);
|
||||
StringBuilder csv = new StringBuilder();
|
||||
csv.append("id;typeAction;acteur;realmName;ressourceType;ressourceId;succes;dateAction;message\n");
|
||||
for (AuditLogDTO dto : logs) {
|
||||
csv.append(escapeCsv(dto.getId()));
|
||||
csv.append(";");
|
||||
csv.append(escapeCsv(dto.getTypeAction() != null ? dto.getTypeAction().name() : ""));
|
||||
csv.append(";");
|
||||
csv.append(escapeCsv(dto.getActeurUsername()));
|
||||
csv.append(";");
|
||||
csv.append(escapeCsv(dto.getRealmName()));
|
||||
csv.append(";");
|
||||
csv.append(escapeCsv(dto.getRessourceType()));
|
||||
csv.append(";");
|
||||
csv.append(escapeCsv(dto.getRessourceId()));
|
||||
csv.append(";");
|
||||
csv.append(dto.getSuccess() != null && dto.getSuccess() ? "true" : "false");
|
||||
csv.append(";");
|
||||
csv.append(dto.getDateAction() != null ? dto.getDateAction().toString() : "");
|
||||
csv.append(";");
|
||||
csv.append(escapeCsv(dto.getErrorMessage() != null ? dto.getErrorMessage() : (dto.getDescription() != null ? dto.getDescription() : "")));
|
||||
csv.append("\n");
|
||||
}
|
||||
return csv.toString();
|
||||
}
|
||||
|
||||
private static String escapeCsv(String value) {
|
||||
if (value == null) return "";
|
||||
if (value.contains(";") || value.contains("\"") || value.contains("\n")) {
|
||||
return "\"" + value.replace("\"", "\"\"") + "\"";
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public long purgeOldLogs(@NotNull LocalDateTime dateLimite) {
|
||||
return auditLogRepository.delete("timestamp < ?1", dateLimite);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> getAuditStatistics(@NotBlank String realmName,
|
||||
LocalDateTime dateDebut,
|
||||
LocalDateTime dateFin) {
|
||||
Map<String, Object> stats = new java.util.HashMap<>();
|
||||
stats.put("total", auditLogRepository.count("realmName", realmName));
|
||||
return stats;
|
||||
}
|
||||
|
||||
// ==================== Méthodes utilitaires ====================
|
||||
|
||||
/**
|
||||
* Retourne le nombre total de logs (Utilisé par les tests)
|
||||
*/
|
||||
public long getTotalCount() {
|
||||
return auditLogRepository.count();
|
||||
}
|
||||
|
||||
/**
|
||||
* Vide tous les logs (Utilisé par les tests)
|
||||
*/
|
||||
@Transactional
|
||||
public void clearAll() {
|
||||
log.warn("ATTENTION: Suppression de tous les logs d'audit en base");
|
||||
auditLogRepository.deleteAll();
|
||||
}
|
||||
}
|
||||
package dev.lions.user.manager.service.impl;
|
||||
|
||||
import dev.lions.user.manager.dto.audit.AuditLogDTO;
|
||||
import dev.lions.user.manager.enums.audit.TypeActionAudit;
|
||||
// import dev.lions.user.manager.mapper.AuditLogMapper; // DELETE - Wrong package
|
||||
import dev.lions.user.manager.server.impl.mapper.AuditLogMapper; // ADD - Correct package
|
||||
import dev.lions.user.manager.server.impl.entity.AuditLogEntity;
|
||||
import dev.lions.user.manager.server.impl.repository.AuditLogRepository;
|
||||
import dev.lions.user.manager.service.AuditService;
|
||||
import io.quarkus.hibernate.orm.panache.PanacheQuery;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.persistence.EntityManager;
|
||||
import jakarta.transaction.Transactional;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.eclipse.microprofile.config.inject.ConfigProperty;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@ApplicationScoped
|
||||
@Slf4j
|
||||
public class AuditServiceImpl implements AuditService {
|
||||
|
||||
@Inject
|
||||
AuditLogRepository auditLogRepository;
|
||||
|
||||
@Inject
|
||||
AuditLogMapper auditLogMapper;
|
||||
|
||||
@Inject
|
||||
EntityManager entityManager;
|
||||
|
||||
@ConfigProperty(name = "lions.audit.enabled", defaultValue = "true")
|
||||
boolean auditEnabled;
|
||||
|
||||
@ConfigProperty(name = "lions.audit.log-to-database", defaultValue = "true")
|
||||
boolean logToDatabase;
|
||||
|
||||
@Override
|
||||
@Transactional(Transactional.TxType.REQUIRES_NEW)
|
||||
public AuditLogDTO logAction(@Valid @NotNull AuditLogDTO auditLog) {
|
||||
if (!auditEnabled) {
|
||||
log.debug("Audit désactivé, action ignorée: {}", auditLog.getTypeAction());
|
||||
return auditLog;
|
||||
}
|
||||
|
||||
log.info("AUDIT: [{}] {} - user:{} - ressource:{}/{} - status:{}",
|
||||
auditLog.getRealmName(),
|
||||
auditLog.getTypeAction(),
|
||||
auditLog.getActeurUsername(), // ou getActeurUserId()
|
||||
auditLog.getRessourceType(),
|
||||
auditLog.getRessourceId(),
|
||||
auditLog.getSuccess() != null && auditLog.getSuccess() ? "SUCCESS" : "FAILURE");
|
||||
|
||||
if (logToDatabase) {
|
||||
try {
|
||||
// Ensure dateAction is set
|
||||
if (auditLog.getDateAction() == null) {
|
||||
auditLog.setDateAction(LocalDateTime.now());
|
||||
}
|
||||
|
||||
AuditLogEntity entity = auditLogMapper.toEntity(auditLog);
|
||||
auditLogRepository.persist(entity);
|
||||
|
||||
// Mettre à jour l'ID du DTO avec l'ID généré par la base
|
||||
if (entity.id != null) {
|
||||
auditLog.setId(entity.id.toString());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("Erreur lors de la persistance du log d'audit", e);
|
||||
// On ne bloque pas l'action métier si l'audit échoue (sauf exigence contraire)
|
||||
}
|
||||
}
|
||||
|
||||
return auditLog;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(Transactional.TxType.REQUIRES_NEW)
|
||||
public void logSuccess(@NotNull TypeActionAudit typeAction,
|
||||
@NotBlank String ressourceType,
|
||||
String ressourceId,
|
||||
String ressourceName,
|
||||
@NotBlank String realmName,
|
||||
@NotBlank String acteurUserId,
|
||||
String description) {
|
||||
|
||||
AuditLogDTO log = AuditLogDTO.builder()
|
||||
.typeAction(typeAction)
|
||||
.ressourceType(ressourceType)
|
||||
.ressourceId(ressourceId)
|
||||
.ressourceName(ressourceName)
|
||||
.realmName(realmName)
|
||||
.acteurUserId(acteurUserId)
|
||||
.acteurUsername(acteurUserId) // On map aussi le username pour la persistence Entity
|
||||
.description(description)
|
||||
.dateAction(LocalDateTime.now())
|
||||
.success(true)
|
||||
.build();
|
||||
|
||||
logAction(log);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(Transactional.TxType.REQUIRES_NEW)
|
||||
public void logFailure(@NotNull TypeActionAudit typeAction,
|
||||
@NotBlank String ressourceType,
|
||||
String ressourceId,
|
||||
String ressourceName,
|
||||
@NotBlank String realmName,
|
||||
@NotBlank String acteurUserId,
|
||||
String errorCode,
|
||||
String errorMessage) {
|
||||
|
||||
AuditLogDTO log = AuditLogDTO.builder()
|
||||
.typeAction(typeAction)
|
||||
.ressourceType(ressourceType)
|
||||
.ressourceId(ressourceId)
|
||||
.ressourceName(ressourceName)
|
||||
.realmName(realmName)
|
||||
.acteurUserId(acteurUserId)
|
||||
.acteurUsername(acteurUserId)
|
||||
.description("Echec: " + errorCode)
|
||||
.errorMessage(errorMessage)
|
||||
.dateAction(LocalDateTime.now())
|
||||
.success(false)
|
||||
.build();
|
||||
|
||||
logAction(log);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<AuditLogDTO> findByActeur(@NotBlank String acteurUserId,
|
||||
LocalDateTime dateDebut,
|
||||
LocalDateTime dateFin,
|
||||
int page,
|
||||
int pageSize) {
|
||||
// Le repository cherche par auteurAction, qui est mappé sur acteurUsername dans
|
||||
// le DTO
|
||||
List<AuditLogEntity> entities = auditLogRepository.search(null, acteurUserId, dateDebut, dateFin, null, null,
|
||||
page,
|
||||
pageSize);
|
||||
return auditLogMapper.toDTOList(entities);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<AuditLogDTO> findByRessource(@NotBlank String ressourceType,
|
||||
@NotBlank String ressourceId,
|
||||
LocalDateTime dateDebut,
|
||||
LocalDateTime dateFin,
|
||||
int page,
|
||||
int pageSize) {
|
||||
|
||||
// Utilisation de Panache query directe car le repo search générique est limité
|
||||
// On cherche dans 'details' (description) ou 'userId' (ressourceId)
|
||||
String filter = "%" + ressourceId + "%";
|
||||
// Correction: userId est le nom du champ dans l'entité qui mappe ressourceId
|
||||
PanacheQuery<AuditLogEntity> q = auditLogRepository.find("userId = ?1 or details like ?2", ressourceId, filter);
|
||||
|
||||
return auditLogMapper.toDTOList(q.page(page, pageSize).list());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<AuditLogDTO> findByTypeAction(@NotNull TypeActionAudit typeAction,
|
||||
@NotBlank String realmName,
|
||||
LocalDateTime dateDebut,
|
||||
LocalDateTime dateFin,
|
||||
int page,
|
||||
int pageSize) {
|
||||
List<AuditLogEntity> entities = auditLogRepository.search(realmName, null, dateDebut, dateFin,
|
||||
typeAction.name(), null, page,
|
||||
pageSize);
|
||||
return auditLogMapper.toDTOList(entities);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<AuditLogDTO> findByRealm(@NotBlank String realmName,
|
||||
LocalDateTime dateDebut,
|
||||
LocalDateTime dateFin,
|
||||
int page,
|
||||
int pageSize) {
|
||||
List<AuditLogEntity> entities = auditLogRepository.search(realmName, null, dateDebut, dateFin, null, null, page,
|
||||
pageSize);
|
||||
return auditLogMapper.toDTOList(entities);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<AuditLogDTO> findFailures(@NotBlank String realmName,
|
||||
LocalDateTime dateDebut,
|
||||
LocalDateTime dateFin,
|
||||
int page,
|
||||
int pageSize) {
|
||||
List<AuditLogEntity> entities = auditLogRepository.search(realmName, null, dateDebut, dateFin, null, false,
|
||||
page,
|
||||
pageSize);
|
||||
return auditLogMapper.toDTOList(entities);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<AuditLogDTO> findCriticalActions(@NotBlank String realmName,
|
||||
LocalDateTime dateDebut,
|
||||
LocalDateTime dateFin,
|
||||
int page,
|
||||
int pageSize) {
|
||||
List<AuditLogEntity> entities = auditLogRepository.search(realmName, null, dateDebut, dateFin, null, false,
|
||||
page, pageSize);
|
||||
return auditLogMapper.toDTOList(entities);
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public Map<TypeActionAudit, Long> countByActionType(@NotBlank String realmName,
|
||||
LocalDateTime dateDebut,
|
||||
LocalDateTime dateFin) {
|
||||
StringBuilder sql = new StringBuilder("SELECT action, COUNT(*) AS cnt FROM audit_logs WHERE realm_name = :realmName");
|
||||
if (dateDebut != null) sql.append(" AND timestamp >= :dateDebut");
|
||||
if (dateFin != null) sql.append(" AND timestamp <= :dateFin");
|
||||
sql.append(" GROUP BY action");
|
||||
var query = entityManager.createNativeQuery(sql.toString())
|
||||
.setParameter("realmName", realmName);
|
||||
if (dateDebut != null) query.setParameter("dateDebut", dateDebut);
|
||||
if (dateFin != null) query.setParameter("dateFin", dateFin);
|
||||
List<Object[]> rows = query.getResultList();
|
||||
Map<TypeActionAudit, Long> result = new HashMap<>();
|
||||
for (Object[] row : rows) {
|
||||
String actionStr = (String) row[0];
|
||||
Long count = ((Number) row[1]).longValue();
|
||||
try {
|
||||
result.put(TypeActionAudit.valueOf(actionStr), count);
|
||||
} catch (IllegalArgumentException e) {
|
||||
log.debug("TypeActionAudit inconnu ignoré: {}", actionStr);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public Map<String, Long> countByActeur(@NotBlank String realmName,
|
||||
LocalDateTime dateDebut,
|
||||
LocalDateTime dateFin) {
|
||||
StringBuilder sql = new StringBuilder("SELECT auteur_action, COUNT(*) AS cnt FROM audit_logs WHERE realm_name = :realmName");
|
||||
if (dateDebut != null) sql.append(" AND timestamp >= :dateDebut");
|
||||
if (dateFin != null) sql.append(" AND timestamp <= :dateFin");
|
||||
sql.append(" GROUP BY auteur_action ORDER BY COUNT(*) DESC LIMIT 10");
|
||||
var query = entityManager.createNativeQuery(sql.toString())
|
||||
.setParameter("realmName", realmName);
|
||||
if (dateDebut != null) query.setParameter("dateDebut", dateDebut);
|
||||
if (dateFin != null) query.setParameter("dateFin", dateFin);
|
||||
List<Object[]> rows = query.getResultList();
|
||||
Map<String, Long> result = new HashMap<>();
|
||||
for (Object[] row : rows) {
|
||||
result.put((String) row[0], ((Number) row[1]).longValue());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public Map<String, Long> countSuccessVsFailure(@NotBlank String realmName,
|
||||
LocalDateTime dateDebut,
|
||||
LocalDateTime dateFin) {
|
||||
StringBuilder sql = new StringBuilder("SELECT success, COUNT(*) AS cnt FROM audit_logs WHERE realm_name = :realmName");
|
||||
if (dateDebut != null) sql.append(" AND timestamp >= :dateDebut");
|
||||
if (dateFin != null) sql.append(" AND timestamp <= :dateFin");
|
||||
sql.append(" GROUP BY success");
|
||||
var query = entityManager.createNativeQuery(sql.toString())
|
||||
.setParameter("realmName", realmName);
|
||||
if (dateDebut != null) query.setParameter("dateDebut", dateDebut);
|
||||
if (dateFin != null) query.setParameter("dateFin", dateFin);
|
||||
List<Object[]> rows = query.getResultList();
|
||||
Map<String, Long> result = new HashMap<>();
|
||||
result.put("success", 0L);
|
||||
result.put("failure", 0L);
|
||||
for (Object[] row : rows) {
|
||||
Boolean success = (Boolean) row[0];
|
||||
Long count = ((Number) row[1]).longValue();
|
||||
result.put(Boolean.TRUE.equals(success) ? "success" : "failure", count);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String exportToCSV(@NotBlank String realmName,
|
||||
LocalDateTime dateDebut,
|
||||
LocalDateTime dateFin) {
|
||||
List<AuditLogEntity> entities = auditLogRepository.search(realmName, null, dateDebut, dateFin, null, null, 0, Integer.MAX_VALUE);
|
||||
List<AuditLogDTO> logs = auditLogMapper.toDTOList(entities);
|
||||
StringBuilder csv = new StringBuilder();
|
||||
csv.append("id;typeAction;acteur;realmName;ressourceType;ressourceId;succes;dateAction;message\n");
|
||||
for (AuditLogDTO dto : logs) {
|
||||
csv.append(escapeCsv(dto.getId()));
|
||||
csv.append(";");
|
||||
csv.append(escapeCsv(dto.getTypeAction() != null ? dto.getTypeAction().name() : ""));
|
||||
csv.append(";");
|
||||
csv.append(escapeCsv(dto.getActeurUsername()));
|
||||
csv.append(";");
|
||||
csv.append(escapeCsv(dto.getRealmName()));
|
||||
csv.append(";");
|
||||
csv.append(escapeCsv(dto.getRessourceType()));
|
||||
csv.append(";");
|
||||
csv.append(escapeCsv(dto.getRessourceId()));
|
||||
csv.append(";");
|
||||
csv.append(dto.getSuccess() != null && dto.getSuccess() ? "true" : "false");
|
||||
csv.append(";");
|
||||
csv.append(dto.getDateAction() != null ? dto.getDateAction().toString() : "");
|
||||
csv.append(";");
|
||||
csv.append(escapeCsv(dto.getErrorMessage() != null ? dto.getErrorMessage() : (dto.getDescription() != null ? dto.getDescription() : "")));
|
||||
csv.append("\n");
|
||||
}
|
||||
return csv.toString();
|
||||
}
|
||||
|
||||
private static String escapeCsv(String value) {
|
||||
if (value == null) return "";
|
||||
if (value.contains(";") || value.contains("\"") || value.contains("\n")) {
|
||||
return "\"" + value.replace("\"", "\"\"") + "\"";
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public long purgeOldLogs(@NotNull LocalDateTime dateLimite) {
|
||||
return auditLogRepository.delete("timestamp < ?1", dateLimite);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> getAuditStatistics(@NotBlank String realmName,
|
||||
LocalDateTime dateDebut,
|
||||
LocalDateTime dateFin) {
|
||||
Map<String, Object> stats = new java.util.HashMap<>();
|
||||
stats.put("total", auditLogRepository.count("realmName", realmName));
|
||||
return stats;
|
||||
}
|
||||
|
||||
// ==================== Méthodes utilitaires ====================
|
||||
|
||||
/**
|
||||
* Retourne le nombre total de logs (Utilisé par les tests)
|
||||
*/
|
||||
public long getTotalCount() {
|
||||
return auditLogRepository.count();
|
||||
}
|
||||
|
||||
/**
|
||||
* Vide tous les logs (Utilisé par les tests)
|
||||
*/
|
||||
@Transactional
|
||||
public void clearAll() {
|
||||
log.warn("ATTENTION: Suppression de tous les logs d'audit en base");
|
||||
auditLogRepository.deleteAll();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,176 +1,176 @@
|
||||
package dev.lions.user.manager.service.impl;
|
||||
|
||||
import lombok.experimental.UtilityClass;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* Classe utilitaire pour la validation des données CSV lors de l'import d'utilisateurs
|
||||
*
|
||||
* @author Lions Development Team
|
||||
* @version 1.0.0
|
||||
* @since 2026-01-02
|
||||
*/
|
||||
@Slf4j
|
||||
@UtilityClass
|
||||
public class CsvValidationHelper {
|
||||
|
||||
/**
|
||||
* Pattern pour valider le format d'email selon RFC 5322 (simplifié)
|
||||
*/
|
||||
private static final Pattern EMAIL_PATTERN = Pattern.compile(
|
||||
"^[a-zA-Z0-9_+&*-]+(?:\\.[a-zA-Z0-9_+&*-]+)*@(?:[a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,7}$"
|
||||
);
|
||||
|
||||
/**
|
||||
* Pattern pour valider le username (alphanumérique, tirets, underscores, points)
|
||||
*/
|
||||
private static final Pattern USERNAME_PATTERN = Pattern.compile(
|
||||
"^[a-zA-Z0-9._-]{2,255}$"
|
||||
);
|
||||
|
||||
/**
|
||||
* Longueur minimale pour un username
|
||||
*/
|
||||
private static final int USERNAME_MIN_LENGTH = 2;
|
||||
|
||||
/**
|
||||
* Longueur maximale pour un username
|
||||
*/
|
||||
private static final int USERNAME_MAX_LENGTH = 255;
|
||||
|
||||
/**
|
||||
* Longueur maximale pour un nom ou prénom
|
||||
*/
|
||||
private static final int NAME_MAX_LENGTH = 255;
|
||||
|
||||
/**
|
||||
* Valide le format d'un email
|
||||
*
|
||||
* @param email Email à valider
|
||||
* @return true si l'email est valide, false sinon
|
||||
*/
|
||||
public static boolean isValidEmail(String email) {
|
||||
if (email == null || email.isBlank()) {
|
||||
return false;
|
||||
}
|
||||
return EMAIL_PATTERN.matcher(email.trim()).matches();
|
||||
}
|
||||
|
||||
/**
|
||||
* Valide un username
|
||||
*
|
||||
* @param username Username à valider
|
||||
* @return Message d'erreur si invalide, null si valide
|
||||
*/
|
||||
public static String validateUsername(String username) {
|
||||
if (username == null || username.isBlank()) {
|
||||
return "Username obligatoire";
|
||||
}
|
||||
|
||||
String trimmed = username.trim();
|
||||
|
||||
if (trimmed.length() < USERNAME_MIN_LENGTH) {
|
||||
return String.format("Username trop court (minimum %d caractères)", USERNAME_MIN_LENGTH);
|
||||
}
|
||||
|
||||
if (trimmed.length() > USERNAME_MAX_LENGTH) {
|
||||
return String.format("Username trop long (maximum %d caractères)", USERNAME_MAX_LENGTH);
|
||||
}
|
||||
|
||||
if (!USERNAME_PATTERN.matcher(trimmed).matches()) {
|
||||
return "Username invalide (autorisé: lettres, chiffres, .-_)";
|
||||
}
|
||||
|
||||
return null; // Valide
|
||||
}
|
||||
|
||||
/**
|
||||
* Valide un email (peut être vide)
|
||||
*
|
||||
* @param email Email à valider
|
||||
* @return Message d'erreur si invalide, null si valide ou vide
|
||||
*/
|
||||
public static String validateEmail(String email) {
|
||||
if (email == null || email.isBlank()) {
|
||||
return null; // Email optionnel
|
||||
}
|
||||
|
||||
if (!isValidEmail(email)) {
|
||||
return "Format d'email invalide";
|
||||
}
|
||||
|
||||
return null; // Valide
|
||||
}
|
||||
|
||||
/**
|
||||
* Valide un nom ou prénom
|
||||
*
|
||||
* @param name Nom à valider
|
||||
* @param fieldName Nom du champ pour les messages d'erreur
|
||||
* @return Message d'erreur si invalide, null si valide
|
||||
*/
|
||||
public static String validateName(String name, String fieldName) {
|
||||
if (name == null || name.isBlank()) {
|
||||
return null; // Nom optionnel
|
||||
}
|
||||
|
||||
String trimmed = name.trim();
|
||||
|
||||
if (trimmed.length() > NAME_MAX_LENGTH) {
|
||||
return String.format("%s trop long (maximum %d caractères)", fieldName, NAME_MAX_LENGTH);
|
||||
}
|
||||
|
||||
return null; // Valide
|
||||
}
|
||||
|
||||
/**
|
||||
* Valide une valeur boolean
|
||||
*
|
||||
* @param value Valeur à valider
|
||||
* @return Message d'erreur si invalide, null si valide
|
||||
*/
|
||||
public static String validateBoolean(String value) {
|
||||
if (value == null || value.isBlank()) {
|
||||
return null; // Optionnel, défaut à false
|
||||
}
|
||||
|
||||
String trimmed = value.trim().toLowerCase();
|
||||
if (!trimmed.equals("true") && !trimmed.equals("false") &&
|
||||
!trimmed.equals("1") && !trimmed.equals("0") &&
|
||||
!trimmed.equals("yes") && !trimmed.equals("no")) {
|
||||
return "Valeur boolean invalide (attendu: true/false, 1/0, yes/no)";
|
||||
}
|
||||
|
||||
return null; // Valide
|
||||
}
|
||||
|
||||
/**
|
||||
* Convertit une chaîne en boolean
|
||||
*
|
||||
* @param value Valeur à convertir
|
||||
* @return boolean correspondant
|
||||
*/
|
||||
public static boolean parseBoolean(String value) {
|
||||
if (value == null || value.isBlank()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
String trimmed = value.trim().toLowerCase();
|
||||
return trimmed.equals("true") || trimmed.equals("1") || trimmed.equals("yes");
|
||||
}
|
||||
|
||||
/**
|
||||
* Nettoie une chaîne (trim et null si vide)
|
||||
*
|
||||
* @param value Valeur à nettoyer
|
||||
* @return Valeur nettoyée ou null
|
||||
*/
|
||||
public static String clean(String value) {
|
||||
if (value == null || value.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
return value.trim();
|
||||
}
|
||||
}
|
||||
package dev.lions.user.manager.service.impl;
|
||||
|
||||
import lombok.experimental.UtilityClass;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* Classe utilitaire pour la validation des données CSV lors de l'import d'utilisateurs
|
||||
*
|
||||
* @author Lions Development Team
|
||||
* @version 1.0.0
|
||||
* @since 2026-01-02
|
||||
*/
|
||||
@Slf4j
|
||||
@UtilityClass
|
||||
public class CsvValidationHelper {
|
||||
|
||||
/**
|
||||
* Pattern pour valider le format d'email selon RFC 5322 (simplifié)
|
||||
*/
|
||||
private static final Pattern EMAIL_PATTERN = Pattern.compile(
|
||||
"^[a-zA-Z0-9_+&*-]+(?:\\.[a-zA-Z0-9_+&*-]+)*@(?:[a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,7}$"
|
||||
);
|
||||
|
||||
/**
|
||||
* Pattern pour valider le username (alphanumérique, tirets, underscores, points)
|
||||
*/
|
||||
private static final Pattern USERNAME_PATTERN = Pattern.compile(
|
||||
"^[a-zA-Z0-9._-]{2,255}$"
|
||||
);
|
||||
|
||||
/**
|
||||
* Longueur minimale pour un username
|
||||
*/
|
||||
private static final int USERNAME_MIN_LENGTH = 2;
|
||||
|
||||
/**
|
||||
* Longueur maximale pour un username
|
||||
*/
|
||||
private static final int USERNAME_MAX_LENGTH = 255;
|
||||
|
||||
/**
|
||||
* Longueur maximale pour un nom ou prénom
|
||||
*/
|
||||
private static final int NAME_MAX_LENGTH = 255;
|
||||
|
||||
/**
|
||||
* Valide le format d'un email
|
||||
*
|
||||
* @param email Email à valider
|
||||
* @return true si l'email est valide, false sinon
|
||||
*/
|
||||
public static boolean isValidEmail(String email) {
|
||||
if (email == null || email.isBlank()) {
|
||||
return false;
|
||||
}
|
||||
return EMAIL_PATTERN.matcher(email.trim()).matches();
|
||||
}
|
||||
|
||||
/**
|
||||
* Valide un username
|
||||
*
|
||||
* @param username Username à valider
|
||||
* @return Message d'erreur si invalide, null si valide
|
||||
*/
|
||||
public static String validateUsername(String username) {
|
||||
if (username == null || username.isBlank()) {
|
||||
return "Username obligatoire";
|
||||
}
|
||||
|
||||
String trimmed = username.trim();
|
||||
|
||||
if (trimmed.length() < USERNAME_MIN_LENGTH) {
|
||||
return String.format("Username trop court (minimum %d caractères)", USERNAME_MIN_LENGTH);
|
||||
}
|
||||
|
||||
if (trimmed.length() > USERNAME_MAX_LENGTH) {
|
||||
return String.format("Username trop long (maximum %d caractères)", USERNAME_MAX_LENGTH);
|
||||
}
|
||||
|
||||
if (!USERNAME_PATTERN.matcher(trimmed).matches()) {
|
||||
return "Username invalide (autorisé: lettres, chiffres, .-_)";
|
||||
}
|
||||
|
||||
return null; // Valide
|
||||
}
|
||||
|
||||
/**
|
||||
* Valide un email (peut être vide)
|
||||
*
|
||||
* @param email Email à valider
|
||||
* @return Message d'erreur si invalide, null si valide ou vide
|
||||
*/
|
||||
public static String validateEmail(String email) {
|
||||
if (email == null || email.isBlank()) {
|
||||
return null; // Email optionnel
|
||||
}
|
||||
|
||||
if (!isValidEmail(email)) {
|
||||
return "Format d'email invalide";
|
||||
}
|
||||
|
||||
return null; // Valide
|
||||
}
|
||||
|
||||
/**
|
||||
* Valide un nom ou prénom
|
||||
*
|
||||
* @param name Nom à valider
|
||||
* @param fieldName Nom du champ pour les messages d'erreur
|
||||
* @return Message d'erreur si invalide, null si valide
|
||||
*/
|
||||
public static String validateName(String name, String fieldName) {
|
||||
if (name == null || name.isBlank()) {
|
||||
return null; // Nom optionnel
|
||||
}
|
||||
|
||||
String trimmed = name.trim();
|
||||
|
||||
if (trimmed.length() > NAME_MAX_LENGTH) {
|
||||
return String.format("%s trop long (maximum %d caractères)", fieldName, NAME_MAX_LENGTH);
|
||||
}
|
||||
|
||||
return null; // Valide
|
||||
}
|
||||
|
||||
/**
|
||||
* Valide une valeur boolean
|
||||
*
|
||||
* @param value Valeur à valider
|
||||
* @return Message d'erreur si invalide, null si valide
|
||||
*/
|
||||
public static String validateBoolean(String value) {
|
||||
if (value == null || value.isBlank()) {
|
||||
return null; // Optionnel, défaut à false
|
||||
}
|
||||
|
||||
String trimmed = value.trim().toLowerCase();
|
||||
if (!trimmed.equals("true") && !trimmed.equals("false") &&
|
||||
!trimmed.equals("1") && !trimmed.equals("0") &&
|
||||
!trimmed.equals("yes") && !trimmed.equals("no")) {
|
||||
return "Valeur boolean invalide (attendu: true/false, 1/0, yes/no)";
|
||||
}
|
||||
|
||||
return null; // Valide
|
||||
}
|
||||
|
||||
/**
|
||||
* Convertit une chaîne en boolean
|
||||
*
|
||||
* @param value Valeur à convertir
|
||||
* @return boolean correspondant
|
||||
*/
|
||||
public static boolean parseBoolean(String value) {
|
||||
if (value == null || value.isBlank()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
String trimmed = value.trim().toLowerCase();
|
||||
return trimmed.equals("true") || trimmed.equals("1") || trimmed.equals("yes");
|
||||
}
|
||||
|
||||
/**
|
||||
* Nettoie une chaîne (trim et null si vide)
|
||||
*
|
||||
* @param value Valeur à nettoyer
|
||||
* @return Valeur nettoyée ou null
|
||||
*/
|
||||
public static String clean(String value) {
|
||||
if (value == null || value.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
return value.trim();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,346 +1,346 @@
|
||||
package dev.lions.user.manager.service.impl;
|
||||
|
||||
import dev.lions.user.manager.dto.realm.RealmAssignmentDTO;
|
||||
import dev.lions.user.manager.enums.audit.TypeActionAudit;
|
||||
import dev.lions.user.manager.service.AuditService;
|
||||
import dev.lions.user.manager.service.RealmAuthorizationService;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Implémentation du service d'autorisation multi-tenant par realm
|
||||
*
|
||||
* NOTE: Cette implémentation utilise un stockage en mémoire (ConcurrentHashMap)
|
||||
* Pour la production, migrer vers une base de données PostgreSQL
|
||||
*/
|
||||
@ApplicationScoped
|
||||
@Slf4j
|
||||
public class RealmAuthorizationServiceImpl implements RealmAuthorizationService {
|
||||
|
||||
@Inject
|
||||
AuditService auditService;
|
||||
|
||||
// Stockage temporaire en mémoire (à remplacer par BD en production)
|
||||
private final Map<String, RealmAssignmentDTO> assignmentsById = new ConcurrentHashMap<>();
|
||||
private final Map<String, Set<String>> userToRealms = new ConcurrentHashMap<>();
|
||||
private final Map<String, Set<String>> realmToUsers = new ConcurrentHashMap<>();
|
||||
private final Set<String> superAdmins = ConcurrentHashMap.newKeySet();
|
||||
|
||||
@Override
|
||||
public List<RealmAssignmentDTO> getAllAssignments() {
|
||||
log.debug("Récupération de toutes les assignations de realms");
|
||||
return new ArrayList<>(assignmentsById.values());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<RealmAssignmentDTO> getAssignmentsByUser(@NotBlank String userId) {
|
||||
log.debug("Récupération des assignations pour l'utilisateur: {}", userId);
|
||||
|
||||
return assignmentsById.values().stream()
|
||||
.filter(assignment -> assignment.getUserId().equals(userId))
|
||||
.filter(RealmAssignmentDTO::isActive)
|
||||
.filter(assignment -> !assignment.isExpired())
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<RealmAssignmentDTO> getAssignmentsByRealm(@NotBlank String realmName) {
|
||||
log.debug("Récupération des assignations pour le realm: {}", realmName);
|
||||
|
||||
return assignmentsById.values().stream()
|
||||
.filter(assignment -> assignment.getRealmName().equals(realmName))
|
||||
.filter(RealmAssignmentDTO::isActive)
|
||||
.filter(assignment -> !assignment.isExpired())
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<RealmAssignmentDTO> getAssignmentById(@NotBlank String assignmentId) {
|
||||
log.debug("Récupération de l'assignation: {}", assignmentId);
|
||||
return Optional.ofNullable(assignmentsById.get(assignmentId));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canManageRealm(@NotBlank String userId, @NotBlank String realmName) {
|
||||
log.debug("Vérification si {} peut gérer le realm {}", userId, realmName);
|
||||
|
||||
// Super admin peut tout gérer
|
||||
if (isSuperAdmin(userId)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Vérifier les assignations actives et non expirées
|
||||
return assignmentsById.values().stream()
|
||||
.anyMatch(assignment ->
|
||||
assignment.getUserId().equals(userId) &&
|
||||
assignment.getRealmName().equals(realmName) &&
|
||||
assignment.isActive() &&
|
||||
!assignment.isExpired()
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSuperAdmin(@NotBlank String userId) {
|
||||
return superAdmins.contains(userId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getAuthorizedRealms(@NotBlank String userId) {
|
||||
log.debug("Récupération des realms autorisés pour: {}", userId);
|
||||
|
||||
// Super admin retourne liste vide (convention: peut tout gérer)
|
||||
if (isSuperAdmin(userId)) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
// Retourner les realms assignés actifs et non expirés
|
||||
return assignmentsById.values().stream()
|
||||
.filter(assignment -> assignment.getUserId().equals(userId))
|
||||
.filter(RealmAssignmentDTO::isActive)
|
||||
.filter(assignment -> !assignment.isExpired())
|
||||
.map(RealmAssignmentDTO::getRealmName)
|
||||
.distinct()
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public RealmAssignmentDTO assignRealmToUser(@Valid @NotNull RealmAssignmentDTO assignment) {
|
||||
log.info("Assignation du realm {} à l'utilisateur {}",
|
||||
assignment.getRealmName(), assignment.getUserId());
|
||||
|
||||
// Validation
|
||||
if (assignment.getUserId() == null || assignment.getUserId().isBlank()) {
|
||||
throw new IllegalArgumentException("L'ID utilisateur est obligatoire");
|
||||
}
|
||||
if (assignment.getRealmName() == null || assignment.getRealmName().isBlank()) {
|
||||
throw new IllegalArgumentException("Le nom du realm est obligatoire");
|
||||
}
|
||||
|
||||
// Vérifier si l'assignation existe déjà
|
||||
if (assignmentExists(assignment.getUserId(), assignment.getRealmName())) {
|
||||
throw new IllegalArgumentException(
|
||||
String.format("L'utilisateur %s a déjà accès au realm %s",
|
||||
assignment.getUserId(), assignment.getRealmName())
|
||||
);
|
||||
}
|
||||
|
||||
// Générer ID si absent
|
||||
if (assignment.getId() == null) {
|
||||
assignment.setId(UUID.randomUUID().toString());
|
||||
}
|
||||
|
||||
// Compléter les métadonnées
|
||||
assignment.setAssignedAt(LocalDateTime.now());
|
||||
assignment.setActive(true);
|
||||
assignment.setDateCreation(LocalDateTime.now());
|
||||
|
||||
// Stocker l'assignation
|
||||
assignmentsById.put(assignment.getId(), assignment);
|
||||
|
||||
// Mettre à jour les index
|
||||
userToRealms.computeIfAbsent(assignment.getUserId(), k -> ConcurrentHashMap.newKeySet())
|
||||
.add(assignment.getRealmName());
|
||||
realmToUsers.computeIfAbsent(assignment.getRealmName(), k -> ConcurrentHashMap.newKeySet())
|
||||
.add(assignment.getUserId());
|
||||
|
||||
// Audit
|
||||
auditService.logSuccess(
|
||||
TypeActionAudit.REALM_ASSIGN,
|
||||
"REALM_ASSIGNMENT",
|
||||
assignment.getId(),
|
||||
assignment.getUsername(),
|
||||
assignment.getRealmName(),
|
||||
assignment.getAssignedBy() != null ? assignment.getAssignedBy() : "system",
|
||||
String.format("Assignation du realm %s à %s", assignment.getRealmName(), assignment.getUsername())
|
||||
);
|
||||
|
||||
log.info("Realm {} assigné avec succès à {}", assignment.getRealmName(), assignment.getUserId());
|
||||
return assignment;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void revokeRealmFromUser(@NotBlank String userId, @NotBlank String realmName) {
|
||||
log.info("Révocation du realm {} pour l'utilisateur {}", realmName, userId);
|
||||
|
||||
// Trouver et supprimer l'assignation
|
||||
Optional<RealmAssignmentDTO> assignment = assignmentsById.values().stream()
|
||||
.filter(a -> a.getUserId().equals(userId) && a.getRealmName().equals(realmName))
|
||||
.findFirst();
|
||||
|
||||
if (assignment.isEmpty()) {
|
||||
log.warn("Aucune assignation trouvée pour {} / {}", userId, realmName);
|
||||
return;
|
||||
}
|
||||
|
||||
RealmAssignmentDTO assignmentToRemove = assignment.get();
|
||||
assignmentsById.remove(assignmentToRemove.getId());
|
||||
|
||||
// Mettre à jour les index
|
||||
Set<String> realms = userToRealms.get(userId);
|
||||
if (realms != null) {
|
||||
realms.remove(realmName);
|
||||
if (realms.isEmpty()) {
|
||||
userToRealms.remove(userId);
|
||||
}
|
||||
}
|
||||
|
||||
Set<String> users = realmToUsers.get(realmName);
|
||||
if (users != null) {
|
||||
users.remove(userId);
|
||||
if (users.isEmpty()) {
|
||||
realmToUsers.remove(realmName);
|
||||
}
|
||||
}
|
||||
|
||||
// Audit
|
||||
auditService.logSuccess(
|
||||
TypeActionAudit.REALM_REVOKE,
|
||||
"REALM_ASSIGNMENT",
|
||||
assignmentToRemove.getId(),
|
||||
assignmentToRemove.getUsername(),
|
||||
realmName,
|
||||
"system",
|
||||
String.format("Révocation du realm %s pour %s", realmName, assignmentToRemove.getUsername())
|
||||
);
|
||||
|
||||
log.info("Realm {} révoqué avec succès pour {}", realmName, userId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void revokeAllRealmsFromUser(@NotBlank String userId) {
|
||||
log.info("Révocation de tous les realms pour l'utilisateur {}", userId);
|
||||
|
||||
List<RealmAssignmentDTO> userAssignments = getAssignmentsByUser(userId);
|
||||
userAssignments.forEach(assignment ->
|
||||
revokeRealmFromUser(userId, assignment.getRealmName())
|
||||
);
|
||||
|
||||
log.info("{} realm(s) révoqué(s) pour {}", userAssignments.size(), userId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void revokeAllUsersFromRealm(@NotBlank String realmName) {
|
||||
log.info("Révocation de tous les utilisateurs du realm {}", realmName);
|
||||
|
||||
List<RealmAssignmentDTO> realmAssignments = getAssignmentsByRealm(realmName);
|
||||
realmAssignments.forEach(assignment ->
|
||||
revokeRealmFromUser(assignment.getUserId(), realmName)
|
||||
);
|
||||
|
||||
log.info("{} utilisateur(s) révoqué(s) du realm {}", realmAssignments.size(), realmName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setSuperAdmin(@NotBlank String userId, boolean superAdmin) {
|
||||
log.info("Définition de {} comme super admin: {}", userId, superAdmin);
|
||||
|
||||
if (superAdmin) {
|
||||
superAdmins.add(userId);
|
||||
auditService.logSuccess(
|
||||
TypeActionAudit.REALM_SET_SUPER_ADMIN,
|
||||
"USER",
|
||||
userId,
|
||||
userId,
|
||||
"lions-user-manager",
|
||||
"system",
|
||||
String.format("Utilisateur %s défini comme super admin", userId)
|
||||
);
|
||||
} else {
|
||||
superAdmins.remove(userId);
|
||||
auditService.logSuccess(
|
||||
TypeActionAudit.REALM_SET_SUPER_ADMIN,
|
||||
"USER",
|
||||
userId,
|
||||
userId,
|
||||
"lions-user-manager",
|
||||
"system",
|
||||
String.format("Privilèges super admin retirés pour %s", userId)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deactivateAssignment(@NotBlank String assignmentId) {
|
||||
log.info("Désactivation de l'assignation {}", assignmentId);
|
||||
|
||||
RealmAssignmentDTO assignment = assignmentsById.get(assignmentId);
|
||||
if (assignment == null) {
|
||||
throw new IllegalArgumentException("Assignation non trouvée: " + assignmentId);
|
||||
}
|
||||
|
||||
assignment.setActive(false);
|
||||
assignment.setDateModification(LocalDateTime.now());
|
||||
|
||||
auditService.logSuccess(
|
||||
TypeActionAudit.REALM_DEACTIVATE,
|
||||
"REALM_ASSIGNMENT",
|
||||
assignment.getId(),
|
||||
assignment.getUsername(),
|
||||
assignment.getRealmName(),
|
||||
"system",
|
||||
String.format("Désactivation de l'assignation %s", assignmentId)
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void activateAssignment(@NotBlank String assignmentId) {
|
||||
log.info("Activation de l'assignation {}", assignmentId);
|
||||
|
||||
RealmAssignmentDTO assignment = assignmentsById.get(assignmentId);
|
||||
if (assignment == null) {
|
||||
throw new IllegalArgumentException("Assignation non trouvée: " + assignmentId);
|
||||
}
|
||||
|
||||
assignment.setActive(true);
|
||||
assignment.setDateModification(LocalDateTime.now());
|
||||
|
||||
auditService.logSuccess(
|
||||
TypeActionAudit.REALM_ACTIVATE,
|
||||
"REALM_ASSIGNMENT",
|
||||
assignment.getId(),
|
||||
assignment.getUsername(),
|
||||
assignment.getRealmName(),
|
||||
"system",
|
||||
String.format("Activation de l'assignation %s", assignmentId)
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long countAssignmentsByUser(@NotBlank String userId) {
|
||||
return assignmentsById.values().stream()
|
||||
.filter(assignment -> assignment.getUserId().equals(userId))
|
||||
.filter(RealmAssignmentDTO::isActive)
|
||||
.filter(assignment -> !assignment.isExpired())
|
||||
.count();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long countUsersByRealm(@NotBlank String realmName) {
|
||||
return assignmentsById.values().stream()
|
||||
.filter(assignment -> assignment.getRealmName().equals(realmName))
|
||||
.filter(RealmAssignmentDTO::isActive)
|
||||
.filter(assignment -> !assignment.isExpired())
|
||||
.map(RealmAssignmentDTO::getUserId)
|
||||
.distinct()
|
||||
.count();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean assignmentExists(@NotBlank String userId, @NotBlank String realmName) {
|
||||
return assignmentsById.values().stream()
|
||||
.anyMatch(assignment ->
|
||||
assignment.getUserId().equals(userId) &&
|
||||
assignment.getRealmName().equals(realmName) &&
|
||||
assignment.isActive()
|
||||
);
|
||||
}
|
||||
}
|
||||
package dev.lions.user.manager.service.impl;
|
||||
|
||||
import dev.lions.user.manager.dto.realm.RealmAssignmentDTO;
|
||||
import dev.lions.user.manager.enums.audit.TypeActionAudit;
|
||||
import dev.lions.user.manager.service.AuditService;
|
||||
import dev.lions.user.manager.service.RealmAuthorizationService;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Implémentation du service d'autorisation multi-tenant par realm
|
||||
*
|
||||
* NOTE: Cette implémentation utilise un stockage en mémoire (ConcurrentHashMap)
|
||||
* Pour la production, migrer vers une base de données PostgreSQL
|
||||
*/
|
||||
@ApplicationScoped
|
||||
@Slf4j
|
||||
public class RealmAuthorizationServiceImpl implements RealmAuthorizationService {
|
||||
|
||||
@Inject
|
||||
AuditService auditService;
|
||||
|
||||
// Stockage temporaire en mémoire (à remplacer par BD en production)
|
||||
private final Map<String, RealmAssignmentDTO> assignmentsById = new ConcurrentHashMap<>();
|
||||
private final Map<String, Set<String>> userToRealms = new ConcurrentHashMap<>();
|
||||
private final Map<String, Set<String>> realmToUsers = new ConcurrentHashMap<>();
|
||||
private final Set<String> superAdmins = ConcurrentHashMap.newKeySet();
|
||||
|
||||
@Override
|
||||
public List<RealmAssignmentDTO> getAllAssignments() {
|
||||
log.debug("Récupération de toutes les assignations de realms");
|
||||
return new ArrayList<>(assignmentsById.values());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<RealmAssignmentDTO> getAssignmentsByUser(@NotBlank String userId) {
|
||||
log.debug("Récupération des assignations pour l'utilisateur: {}", userId);
|
||||
|
||||
return assignmentsById.values().stream()
|
||||
.filter(assignment -> assignment.getUserId().equals(userId))
|
||||
.filter(RealmAssignmentDTO::isActive)
|
||||
.filter(assignment -> !assignment.isExpired())
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<RealmAssignmentDTO> getAssignmentsByRealm(@NotBlank String realmName) {
|
||||
log.debug("Récupération des assignations pour le realm: {}", realmName);
|
||||
|
||||
return assignmentsById.values().stream()
|
||||
.filter(assignment -> assignment.getRealmName().equals(realmName))
|
||||
.filter(RealmAssignmentDTO::isActive)
|
||||
.filter(assignment -> !assignment.isExpired())
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<RealmAssignmentDTO> getAssignmentById(@NotBlank String assignmentId) {
|
||||
log.debug("Récupération de l'assignation: {}", assignmentId);
|
||||
return Optional.ofNullable(assignmentsById.get(assignmentId));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canManageRealm(@NotBlank String userId, @NotBlank String realmName) {
|
||||
log.debug("Vérification si {} peut gérer le realm {}", userId, realmName);
|
||||
|
||||
// Super admin peut tout gérer
|
||||
if (isSuperAdmin(userId)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Vérifier les assignations actives et non expirées
|
||||
return assignmentsById.values().stream()
|
||||
.anyMatch(assignment ->
|
||||
assignment.getUserId().equals(userId) &&
|
||||
assignment.getRealmName().equals(realmName) &&
|
||||
assignment.isActive() &&
|
||||
!assignment.isExpired()
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSuperAdmin(@NotBlank String userId) {
|
||||
return superAdmins.contains(userId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getAuthorizedRealms(@NotBlank String userId) {
|
||||
log.debug("Récupération des realms autorisés pour: {}", userId);
|
||||
|
||||
// Super admin retourne liste vide (convention: peut tout gérer)
|
||||
if (isSuperAdmin(userId)) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
// Retourner les realms assignés actifs et non expirés
|
||||
return assignmentsById.values().stream()
|
||||
.filter(assignment -> assignment.getUserId().equals(userId))
|
||||
.filter(RealmAssignmentDTO::isActive)
|
||||
.filter(assignment -> !assignment.isExpired())
|
||||
.map(RealmAssignmentDTO::getRealmName)
|
||||
.distinct()
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public RealmAssignmentDTO assignRealmToUser(@Valid @NotNull RealmAssignmentDTO assignment) {
|
||||
log.info("Assignation du realm {} à l'utilisateur {}",
|
||||
assignment.getRealmName(), assignment.getUserId());
|
||||
|
||||
// Validation
|
||||
if (assignment.getUserId() == null || assignment.getUserId().isBlank()) {
|
||||
throw new IllegalArgumentException("L'ID utilisateur est obligatoire");
|
||||
}
|
||||
if (assignment.getRealmName() == null || assignment.getRealmName().isBlank()) {
|
||||
throw new IllegalArgumentException("Le nom du realm est obligatoire");
|
||||
}
|
||||
|
||||
// Vérifier si l'assignation existe déjà
|
||||
if (assignmentExists(assignment.getUserId(), assignment.getRealmName())) {
|
||||
throw new IllegalArgumentException(
|
||||
String.format("L'utilisateur %s a déjà accès au realm %s",
|
||||
assignment.getUserId(), assignment.getRealmName())
|
||||
);
|
||||
}
|
||||
|
||||
// Générer ID si absent
|
||||
if (assignment.getId() == null) {
|
||||
assignment.setId(UUID.randomUUID().toString());
|
||||
}
|
||||
|
||||
// Compléter les métadonnées
|
||||
assignment.setAssignedAt(LocalDateTime.now());
|
||||
assignment.setActive(true);
|
||||
assignment.setDateCreation(LocalDateTime.now());
|
||||
|
||||
// Stocker l'assignation
|
||||
assignmentsById.put(assignment.getId(), assignment);
|
||||
|
||||
// Mettre à jour les index
|
||||
userToRealms.computeIfAbsent(assignment.getUserId(), k -> ConcurrentHashMap.newKeySet())
|
||||
.add(assignment.getRealmName());
|
||||
realmToUsers.computeIfAbsent(assignment.getRealmName(), k -> ConcurrentHashMap.newKeySet())
|
||||
.add(assignment.getUserId());
|
||||
|
||||
// Audit
|
||||
auditService.logSuccess(
|
||||
TypeActionAudit.REALM_ASSIGN,
|
||||
"REALM_ASSIGNMENT",
|
||||
assignment.getId(),
|
||||
assignment.getUsername(),
|
||||
assignment.getRealmName(),
|
||||
assignment.getAssignedBy() != null ? assignment.getAssignedBy() : "system",
|
||||
String.format("Assignation du realm %s à %s", assignment.getRealmName(), assignment.getUsername())
|
||||
);
|
||||
|
||||
log.info("Realm {} assigné avec succès à {}", assignment.getRealmName(), assignment.getUserId());
|
||||
return assignment;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void revokeRealmFromUser(@NotBlank String userId, @NotBlank String realmName) {
|
||||
log.info("Révocation du realm {} pour l'utilisateur {}", realmName, userId);
|
||||
|
||||
// Trouver et supprimer l'assignation
|
||||
Optional<RealmAssignmentDTO> assignment = assignmentsById.values().stream()
|
||||
.filter(a -> a.getUserId().equals(userId) && a.getRealmName().equals(realmName))
|
||||
.findFirst();
|
||||
|
||||
if (assignment.isEmpty()) {
|
||||
log.warn("Aucune assignation trouvée pour {} / {}", userId, realmName);
|
||||
return;
|
||||
}
|
||||
|
||||
RealmAssignmentDTO assignmentToRemove = assignment.get();
|
||||
assignmentsById.remove(assignmentToRemove.getId());
|
||||
|
||||
// Mettre à jour les index
|
||||
Set<String> realms = userToRealms.get(userId);
|
||||
if (realms != null) {
|
||||
realms.remove(realmName);
|
||||
if (realms.isEmpty()) {
|
||||
userToRealms.remove(userId);
|
||||
}
|
||||
}
|
||||
|
||||
Set<String> users = realmToUsers.get(realmName);
|
||||
if (users != null) {
|
||||
users.remove(userId);
|
||||
if (users.isEmpty()) {
|
||||
realmToUsers.remove(realmName);
|
||||
}
|
||||
}
|
||||
|
||||
// Audit
|
||||
auditService.logSuccess(
|
||||
TypeActionAudit.REALM_REVOKE,
|
||||
"REALM_ASSIGNMENT",
|
||||
assignmentToRemove.getId(),
|
||||
assignmentToRemove.getUsername(),
|
||||
realmName,
|
||||
"system",
|
||||
String.format("Révocation du realm %s pour %s", realmName, assignmentToRemove.getUsername())
|
||||
);
|
||||
|
||||
log.info("Realm {} révoqué avec succès pour {}", realmName, userId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void revokeAllRealmsFromUser(@NotBlank String userId) {
|
||||
log.info("Révocation de tous les realms pour l'utilisateur {}", userId);
|
||||
|
||||
List<RealmAssignmentDTO> userAssignments = getAssignmentsByUser(userId);
|
||||
userAssignments.forEach(assignment ->
|
||||
revokeRealmFromUser(userId, assignment.getRealmName())
|
||||
);
|
||||
|
||||
log.info("{} realm(s) révoqué(s) pour {}", userAssignments.size(), userId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void revokeAllUsersFromRealm(@NotBlank String realmName) {
|
||||
log.info("Révocation de tous les utilisateurs du realm {}", realmName);
|
||||
|
||||
List<RealmAssignmentDTO> realmAssignments = getAssignmentsByRealm(realmName);
|
||||
realmAssignments.forEach(assignment ->
|
||||
revokeRealmFromUser(assignment.getUserId(), realmName)
|
||||
);
|
||||
|
||||
log.info("{} utilisateur(s) révoqué(s) du realm {}", realmAssignments.size(), realmName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setSuperAdmin(@NotBlank String userId, boolean superAdmin) {
|
||||
log.info("Définition de {} comme super admin: {}", userId, superAdmin);
|
||||
|
||||
if (superAdmin) {
|
||||
superAdmins.add(userId);
|
||||
auditService.logSuccess(
|
||||
TypeActionAudit.REALM_SET_SUPER_ADMIN,
|
||||
"USER",
|
||||
userId,
|
||||
userId,
|
||||
"lions-user-manager",
|
||||
"system",
|
||||
String.format("Utilisateur %s défini comme super admin", userId)
|
||||
);
|
||||
} else {
|
||||
superAdmins.remove(userId);
|
||||
auditService.logSuccess(
|
||||
TypeActionAudit.REALM_SET_SUPER_ADMIN,
|
||||
"USER",
|
||||
userId,
|
||||
userId,
|
||||
"lions-user-manager",
|
||||
"system",
|
||||
String.format("Privilèges super admin retirés pour %s", userId)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deactivateAssignment(@NotBlank String assignmentId) {
|
||||
log.info("Désactivation de l'assignation {}", assignmentId);
|
||||
|
||||
RealmAssignmentDTO assignment = assignmentsById.get(assignmentId);
|
||||
if (assignment == null) {
|
||||
throw new IllegalArgumentException("Assignation non trouvée: " + assignmentId);
|
||||
}
|
||||
|
||||
assignment.setActive(false);
|
||||
assignment.setDateModification(LocalDateTime.now());
|
||||
|
||||
auditService.logSuccess(
|
||||
TypeActionAudit.REALM_DEACTIVATE,
|
||||
"REALM_ASSIGNMENT",
|
||||
assignment.getId(),
|
||||
assignment.getUsername(),
|
||||
assignment.getRealmName(),
|
||||
"system",
|
||||
String.format("Désactivation de l'assignation %s", assignmentId)
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void activateAssignment(@NotBlank String assignmentId) {
|
||||
log.info("Activation de l'assignation {}", assignmentId);
|
||||
|
||||
RealmAssignmentDTO assignment = assignmentsById.get(assignmentId);
|
||||
if (assignment == null) {
|
||||
throw new IllegalArgumentException("Assignation non trouvée: " + assignmentId);
|
||||
}
|
||||
|
||||
assignment.setActive(true);
|
||||
assignment.setDateModification(LocalDateTime.now());
|
||||
|
||||
auditService.logSuccess(
|
||||
TypeActionAudit.REALM_ACTIVATE,
|
||||
"REALM_ASSIGNMENT",
|
||||
assignment.getId(),
|
||||
assignment.getUsername(),
|
||||
assignment.getRealmName(),
|
||||
"system",
|
||||
String.format("Activation de l'assignation %s", assignmentId)
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long countAssignmentsByUser(@NotBlank String userId) {
|
||||
return assignmentsById.values().stream()
|
||||
.filter(assignment -> assignment.getUserId().equals(userId))
|
||||
.filter(RealmAssignmentDTO::isActive)
|
||||
.filter(assignment -> !assignment.isExpired())
|
||||
.count();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long countUsersByRealm(@NotBlank String realmName) {
|
||||
return assignmentsById.values().stream()
|
||||
.filter(assignment -> assignment.getRealmName().equals(realmName))
|
||||
.filter(RealmAssignmentDTO::isActive)
|
||||
.filter(assignment -> !assignment.isExpired())
|
||||
.map(RealmAssignmentDTO::getUserId)
|
||||
.distinct()
|
||||
.count();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean assignmentExists(@NotBlank String userId, @NotBlank String realmName) {
|
||||
return assignmentsById.values().stream()
|
||||
.anyMatch(assignment ->
|
||||
assignment.getUserId().equals(userId) &&
|
||||
assignment.getRealmName().equals(realmName) &&
|
||||
assignment.isActive()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,389 +1,389 @@
|
||||
package dev.lions.user.manager.service.impl;
|
||||
|
||||
import dev.lions.user.manager.server.impl.entity.SyncHistoryEntity;
|
||||
import dev.lions.user.manager.server.impl.entity.SyncedRoleEntity;
|
||||
import dev.lions.user.manager.server.impl.entity.SyncedUserEntity;
|
||||
import dev.lions.user.manager.server.impl.interceptor.Logged;
|
||||
import dev.lions.user.manager.server.impl.repository.SyncHistoryRepository;
|
||||
import dev.lions.user.manager.server.impl.repository.SyncedRoleRepository;
|
||||
import dev.lions.user.manager.server.impl.repository.SyncedUserRepository;
|
||||
import dev.lions.user.manager.service.SyncService;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.transaction.Transactional;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import dev.lions.user.manager.client.KeycloakAdminClient;
|
||||
import org.eclipse.microprofile.config.inject.ConfigProperty;
|
||||
import org.keycloak.admin.client.Keycloak;
|
||||
import org.keycloak.representations.idm.RoleRepresentation;
|
||||
import org.keycloak.representations.idm.UserRepresentation;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneOffset;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
@ApplicationScoped
|
||||
@Slf4j
|
||||
public class SyncServiceImpl implements SyncService {
|
||||
|
||||
@Inject
|
||||
Keycloak keycloak;
|
||||
|
||||
@Inject
|
||||
KeycloakAdminClient keycloakAdminClient;
|
||||
|
||||
@Inject
|
||||
SyncHistoryRepository syncHistoryRepository;
|
||||
|
||||
// Repositories optionnels pour la persistance locale des snapshots.
|
||||
// Ils sont marqués @Inject mais l'utilisation dans le code est protégée
|
||||
// par des checks null pour ne pas casser les tests existants.
|
||||
@Inject
|
||||
SyncedUserRepository syncedUserRepository;
|
||||
|
||||
@Inject
|
||||
SyncedRoleRepository syncedRoleRepository;
|
||||
|
||||
@ConfigProperty(name = "lions.keycloak.server-url")
|
||||
String keycloakServerUrl;
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
@Logged(action = "SYNC_USERS", resource = "REALM")
|
||||
public int syncUsersFromRealm(@NotBlank String realmName) {
|
||||
log.info("Synchronisation des utilisateurs depuis le realm: {}", realmName);
|
||||
LocalDateTime start = LocalDateTime.now();
|
||||
int count = 0;
|
||||
String status = "SUCCESS";
|
||||
String errorMessage = null;
|
||||
|
||||
try {
|
||||
List<UserRepresentation> users = keycloak.realm(realmName).users().list();
|
||||
count = users.size();
|
||||
|
||||
// Persister un snapshot minimal des utilisateurs dans la base locale si le
|
||||
// repository est disponible.
|
||||
if (syncedUserRepository != null && !users.isEmpty()) {
|
||||
List<SyncedUserEntity> snapshots = users.stream()
|
||||
.map(user -> {
|
||||
SyncedUserEntity entity = new SyncedUserEntity();
|
||||
entity.setRealmName(realmName);
|
||||
entity.setKeycloakId(user.getId());
|
||||
entity.setUsername(user.getUsername());
|
||||
entity.setEmail(user.getEmail());
|
||||
entity.setEnabled(user.isEnabled());
|
||||
entity.setEmailVerified(user.isEmailVerified());
|
||||
|
||||
if (user.getCreatedTimestamp() != null) {
|
||||
LocalDateTime createdAt = LocalDateTime.ofInstant(
|
||||
Instant.ofEpochMilli(user.getCreatedTimestamp()),
|
||||
ZoneOffset.UTC);
|
||||
entity.setCreatedAt(createdAt);
|
||||
}
|
||||
return entity;
|
||||
})
|
||||
.toList();
|
||||
|
||||
syncedUserRepository.replaceForRealm(realmName, snapshots);
|
||||
log.info("Persisted {} synced user snapshots for realm {}", snapshots.size(), realmName);
|
||||
}
|
||||
|
||||
log.info("✅ {} utilisateurs synchronisés depuis le realm {}", count, realmName);
|
||||
} catch (Exception e) {
|
||||
log.error("❌ Erreur lors de la synchronisation des utilisateurs depuis le realm {}", realmName, e);
|
||||
status = "FAILURE";
|
||||
errorMessage = e.getMessage();
|
||||
throw new RuntimeException("Erreur de synchronisation utilisateurs: " + e.getMessage(), e);
|
||||
} finally {
|
||||
recordSyncHistory(realmName, "USER", status, count, start, errorMessage);
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
@Logged(action = "SYNC_ROLES", resource = "REALM")
|
||||
public int syncRolesFromRealm(@NotBlank String realmName) {
|
||||
log.info("Synchronisation des rôles depuis le realm: {}", realmName);
|
||||
LocalDateTime start = LocalDateTime.now();
|
||||
int count = 0;
|
||||
String status = "SUCCESS";
|
||||
String errorMessage = null;
|
||||
|
||||
try {
|
||||
List<RoleRepresentation> roles = keycloak.realm(realmName).roles().list();
|
||||
count = roles.size();
|
||||
|
||||
// Persister un snapshot minimal des rôles dans la base locale si le repository
|
||||
// est disponible.
|
||||
if (syncedRoleRepository != null && !roles.isEmpty()) {
|
||||
List<SyncedRoleEntity> snapshots = roles.stream()
|
||||
.map(role -> {
|
||||
SyncedRoleEntity entity = new SyncedRoleEntity();
|
||||
entity.setRealmName(realmName);
|
||||
entity.setRoleName(role.getName());
|
||||
entity.setDescription(role.getDescription());
|
||||
return entity;
|
||||
})
|
||||
.toList();
|
||||
|
||||
syncedRoleRepository.replaceForRealm(realmName, snapshots);
|
||||
log.info("Persisted {} synced role snapshots for realm {}", snapshots.size(), realmName);
|
||||
}
|
||||
|
||||
log.info("✅ {} rôles synchronisés depuis le realm {}", count, realmName);
|
||||
} catch (Exception e) {
|
||||
log.error("❌ Erreur lors de la synchronisation des rôles depuis le realm {}", realmName, e);
|
||||
status = "FAILURE";
|
||||
errorMessage = e.getMessage();
|
||||
throw new RuntimeException("Erreur de synchronisation rôles: " + e.getMessage(), e);
|
||||
} finally {
|
||||
recordSyncHistory(realmName, "ROLE", status, count, start, errorMessage);
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
@Logged(action = "REALM_SYNC", resource = "SYSTEM")
|
||||
public Map<String, Integer> syncAllRealms() {
|
||||
Map<String, Integer> result = new HashMap<>();
|
||||
try {
|
||||
// getAllRealms() utilise un HttpClient raw avec ObjectMapper(FAIL_ON_UNKNOWN_PROPERTIES=false)
|
||||
// pour éviter les erreurs de désérialisation de RealmRepresentation avec Keycloak 26+
|
||||
List<String> realmNames = keycloakAdminClient.getAllRealms();
|
||||
|
||||
for (String realmName : realmNames) {
|
||||
if (realmName == null || realmName.isBlank()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
log.info("Synchronisation complète du realm {}", realmName);
|
||||
int totalForRealm = 0;
|
||||
try {
|
||||
int users = syncUsersFromRealm(realmName);
|
||||
int roles = syncRolesFromRealm(realmName);
|
||||
totalForRealm = users + roles;
|
||||
log.info("✅ Realm {} synchronisé (users={}, roles={})", realmName, users, roles);
|
||||
} catch (Exception e) {
|
||||
log.error("❌ Erreur lors de la synchronisation du realm {}", realmName, e);
|
||||
// On enregistre quand même le realm dans le résultat avec 0 éléments traités
|
||||
totalForRealm = 0;
|
||||
}
|
||||
result.put(realmName, totalForRealm);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("❌ Erreur lors de la récupération de la liste des realms pour synchronisation globale", e);
|
||||
// En cas d'erreur globale, on retourne simplement une map vide (aucune
|
||||
// approximation)
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> checkDataConsistency(@NotBlank String realmName) {
|
||||
Map<String, Object> report = new HashMap<>();
|
||||
report.put("realmName", realmName);
|
||||
|
||||
try {
|
||||
// Données actuelles dans Keycloak
|
||||
List<UserRepresentation> kcUsers = keycloak.realm(realmName).users().list();
|
||||
List<RoleRepresentation> kcRoles = keycloak.realm(realmName).roles().list();
|
||||
|
||||
// Snapshots locaux
|
||||
List<SyncedUserEntity> localUsers = syncedUserRepository.list("realmName", realmName);
|
||||
List<SyncedRoleEntity> localRoles = syncedRoleRepository.list("realmName", realmName);
|
||||
|
||||
// Comparaison exacte des identifiants utilisateurs
|
||||
Set<String> kcUserIds = kcUsers.stream()
|
||||
.map(UserRepresentation::getId)
|
||||
.filter(id -> id != null && !id.isBlank())
|
||||
.collect(java.util.stream.Collectors.toSet());
|
||||
|
||||
Set<String> localUserIds = localUsers.stream()
|
||||
.map(SyncedUserEntity::getKeycloakId)
|
||||
.filter(id -> id != null && !id.isBlank())
|
||||
.collect(java.util.stream.Collectors.toSet());
|
||||
|
||||
Set<String> missingUsersInLocal = new HashSet<>(kcUserIds);
|
||||
missingUsersInLocal.removeAll(localUserIds);
|
||||
|
||||
Set<String> missingUsersInKeycloak = new HashSet<>(localUserIds);
|
||||
missingUsersInKeycloak.removeAll(kcUserIds);
|
||||
|
||||
// Comparaison exacte des noms de rôles
|
||||
Set<String> kcRoleNames = kcRoles.stream()
|
||||
.map(RoleRepresentation::getName)
|
||||
.filter(name -> name != null && !name.isBlank())
|
||||
.collect(java.util.stream.Collectors.toSet());
|
||||
|
||||
Set<String> localRoleNames = localRoles.stream()
|
||||
.map(SyncedRoleEntity::getRoleName)
|
||||
.filter(name -> name != null && !name.isBlank())
|
||||
.collect(java.util.stream.Collectors.toSet());
|
||||
|
||||
Set<String> missingRolesInLocal = new HashSet<>(kcRoleNames);
|
||||
missingRolesInLocal.removeAll(localRoleNames);
|
||||
|
||||
Set<String> missingRolesInKeycloak = new HashSet<>(localRoleNames);
|
||||
missingRolesInKeycloak.removeAll(kcRoleNames);
|
||||
|
||||
boolean usersOk = missingUsersInLocal.isEmpty() && missingUsersInKeycloak.isEmpty();
|
||||
boolean rolesOk = missingRolesInLocal.isEmpty() && missingRolesInKeycloak.isEmpty();
|
||||
|
||||
report.put("status", (usersOk && rolesOk) ? "OK" : "MISMATCH");
|
||||
|
||||
report.put("usersKeycloakCount", kcUserIds.size());
|
||||
report.put("usersLocalCount", localUserIds.size());
|
||||
report.put("missingUsersInLocal", missingUsersInLocal);
|
||||
report.put("missingUsersInKeycloak", missingUsersInKeycloak);
|
||||
|
||||
report.put("rolesKeycloakCount", kcRoleNames.size());
|
||||
report.put("rolesLocalCount", localRoleNames.size());
|
||||
report.put("missingRolesInLocal", missingRolesInLocal);
|
||||
report.put("missingRolesInKeycloak", missingRolesInKeycloak);
|
||||
} catch (Exception e) {
|
||||
log.error("❌ Erreur lors du contrôle de cohérence des données pour le realm {}", realmName, e);
|
||||
report.put("status", "ERROR");
|
||||
report.put("error", e.getMessage());
|
||||
}
|
||||
|
||||
return report;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public Map<String, Object> forceSyncRealm(@NotBlank String realmName) {
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
try {
|
||||
int users = syncUsersFromRealm(realmName);
|
||||
int roles = syncRolesFromRealm(realmName);
|
||||
result.put("usersSynced", users);
|
||||
result.put("rolesSynced", roles);
|
||||
result.put("status", "SUCCESS");
|
||||
} catch (Exception e) {
|
||||
result.put("status", "FAILURE");
|
||||
result.put("error", e.getMessage());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> getLastSyncStatus(@NotBlank String realmName) {
|
||||
List<SyncHistoryEntity> history = syncHistoryRepository.findLatestByRealm(realmName, 1);
|
||||
if (history.isEmpty()) {
|
||||
return Collections.singletonMap("status", "NEVER_SYNCED");
|
||||
}
|
||||
SyncHistoryEntity lastSync = history.get(0);
|
||||
|
||||
Map<String, Object> statusMap = new HashMap<>(); // Utilisation de HashMap pour permettre nulls si besoin
|
||||
statusMap.put("lastSyncDate", lastSync.getSyncDate());
|
||||
statusMap.put("status", lastSync.getStatus());
|
||||
statusMap.put("type", lastSync.getSyncType());
|
||||
statusMap.put("itemsProcessed", lastSync.getItemsProcessed());
|
||||
return statusMap;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isKeycloakAvailable() {
|
||||
try {
|
||||
// getAllRealms() utilise un HttpClient raw : pas de désérialisation de RealmRepresentation
|
||||
// donc pas d'erreur UnrecognizedPropertyException avec Keycloak 26+
|
||||
keycloakAdminClient.getAllRealms();
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
log.warn("Keycloak availability check failed: {}", e.getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> getKeycloakHealthInfo() {
|
||||
Map<String, Object> health = new HashMap<>();
|
||||
try {
|
||||
var info = keycloak.serverInfo().getInfo();
|
||||
health.put("status", "UP");
|
||||
health.put("version", info.getSystemInfo().getVersion());
|
||||
health.put("serverTime", info.getSystemInfo().getServerTime());
|
||||
} catch (Exception e) {
|
||||
log.debug("serverInfo().getInfo() failed, trying raw HTTP fallback: {}", e.getMessage());
|
||||
fetchVersionViaHttp(health);
|
||||
}
|
||||
return health;
|
||||
}
|
||||
|
||||
private void fetchVersionViaHttp(Map<String, Object> health) {
|
||||
try {
|
||||
String token = keycloak.tokenManager().getAccessTokenString();
|
||||
var client = java.net.http.HttpClient.newHttpClient();
|
||||
var request = java.net.http.HttpRequest.newBuilder()
|
||||
.uri(java.net.URI.create(keycloakServerUrl + "/admin/serverinfo"))
|
||||
.header("Authorization", "Bearer " + token)
|
||||
.header("Accept", "application/json")
|
||||
.GET().build();
|
||||
var response = client.send(request, java.net.http.HttpResponse.BodyHandlers.ofString());
|
||||
if (response.statusCode() == 200) {
|
||||
String body = response.body();
|
||||
health.put("status", "UP");
|
||||
int sysInfoIdx = body.indexOf("\"systemInfo\"");
|
||||
if (sysInfoIdx >= 0) {
|
||||
extractJsonStringField(body, "version", sysInfoIdx)
|
||||
.ifPresent(v -> health.put("version", v));
|
||||
extractJsonStringField(body, "serverTime", sysInfoIdx)
|
||||
.ifPresent(v -> health.put("serverTime", v));
|
||||
}
|
||||
if (!health.containsKey("version")) {
|
||||
health.put("version", "UP (version non parsée)");
|
||||
}
|
||||
} else {
|
||||
health.put("status", "UP");
|
||||
health.put("version", "UP (serverinfo HTTP " + response.statusCode() + ")");
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
log.error("Fallback HTTP serverinfo also failed: {}", ex.getMessage());
|
||||
health.put("status", "DOWN");
|
||||
health.put("error", ex.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private java.util.Optional<String> extractJsonStringField(String json, String field, int searchFrom) {
|
||||
String pattern = "\"" + field + "\"";
|
||||
int idx = json.indexOf(pattern, searchFrom);
|
||||
if (idx < 0) return java.util.Optional.empty();
|
||||
int colonIdx = json.indexOf(':', idx + pattern.length());
|
||||
if (colonIdx < 0) return java.util.Optional.empty();
|
||||
int startQuote = json.indexOf('"', colonIdx + 1);
|
||||
if (startQuote < 0) return java.util.Optional.empty();
|
||||
int endQuote = json.indexOf('"', startQuote + 1);
|
||||
if (endQuote < 0) return java.util.Optional.empty();
|
||||
return java.util.Optional.of(json.substring(startQuote + 1, endQuote));
|
||||
}
|
||||
|
||||
// Helper method to record history
|
||||
private void recordSyncHistory(String realmName, String type, String status, int count, LocalDateTime start,
|
||||
String errorMessage) {
|
||||
try {
|
||||
SyncHistoryEntity history = new SyncHistoryEntity();
|
||||
history.setRealmName(realmName);
|
||||
history.setSyncType(type);
|
||||
history.setStatus(status);
|
||||
history.setItemsProcessed(count);
|
||||
history.setSyncDate(LocalDateTime.now());
|
||||
history.setDurationMs(ChronoUnit.MILLIS.between(start, LocalDateTime.now()));
|
||||
history.setErrorMessage(errorMessage);
|
||||
|
||||
// Persist the history entity
|
||||
syncHistoryRepository.persist(history);
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to record sync history", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
package dev.lions.user.manager.service.impl;
|
||||
|
||||
import dev.lions.user.manager.server.impl.entity.SyncHistoryEntity;
|
||||
import dev.lions.user.manager.server.impl.entity.SyncedRoleEntity;
|
||||
import dev.lions.user.manager.server.impl.entity.SyncedUserEntity;
|
||||
import dev.lions.user.manager.server.impl.interceptor.Logged;
|
||||
import dev.lions.user.manager.server.impl.repository.SyncHistoryRepository;
|
||||
import dev.lions.user.manager.server.impl.repository.SyncedRoleRepository;
|
||||
import dev.lions.user.manager.server.impl.repository.SyncedUserRepository;
|
||||
import dev.lions.user.manager.service.SyncService;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.transaction.Transactional;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import dev.lions.user.manager.client.KeycloakAdminClient;
|
||||
import org.eclipse.microprofile.config.inject.ConfigProperty;
|
||||
import org.keycloak.admin.client.Keycloak;
|
||||
import org.keycloak.representations.idm.RoleRepresentation;
|
||||
import org.keycloak.representations.idm.UserRepresentation;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneOffset;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
@ApplicationScoped
|
||||
@Slf4j
|
||||
public class SyncServiceImpl implements SyncService {
|
||||
|
||||
@Inject
|
||||
Keycloak keycloak;
|
||||
|
||||
@Inject
|
||||
KeycloakAdminClient keycloakAdminClient;
|
||||
|
||||
@Inject
|
||||
SyncHistoryRepository syncHistoryRepository;
|
||||
|
||||
// Repositories optionnels pour la persistance locale des snapshots.
|
||||
// Ils sont marqués @Inject mais l'utilisation dans le code est protégée
|
||||
// par des checks null pour ne pas casser les tests existants.
|
||||
@Inject
|
||||
SyncedUserRepository syncedUserRepository;
|
||||
|
||||
@Inject
|
||||
SyncedRoleRepository syncedRoleRepository;
|
||||
|
||||
@ConfigProperty(name = "lions.keycloak.server-url")
|
||||
String keycloakServerUrl;
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
@Logged(action = "SYNC_USERS", resource = "REALM")
|
||||
public int syncUsersFromRealm(@NotBlank String realmName) {
|
||||
log.info("Synchronisation des utilisateurs depuis le realm: {}", realmName);
|
||||
LocalDateTime start = LocalDateTime.now();
|
||||
int count = 0;
|
||||
String status = "SUCCESS";
|
||||
String errorMessage = null;
|
||||
|
||||
try {
|
||||
List<UserRepresentation> users = keycloak.realm(realmName).users().list();
|
||||
count = users.size();
|
||||
|
||||
// Persister un snapshot minimal des utilisateurs dans la base locale si le
|
||||
// repository est disponible.
|
||||
if (syncedUserRepository != null && !users.isEmpty()) {
|
||||
List<SyncedUserEntity> snapshots = users.stream()
|
||||
.map(user -> {
|
||||
SyncedUserEntity entity = new SyncedUserEntity();
|
||||
entity.setRealmName(realmName);
|
||||
entity.setKeycloakId(user.getId());
|
||||
entity.setUsername(user.getUsername());
|
||||
entity.setEmail(user.getEmail());
|
||||
entity.setEnabled(user.isEnabled());
|
||||
entity.setEmailVerified(user.isEmailVerified());
|
||||
|
||||
if (user.getCreatedTimestamp() != null) {
|
||||
LocalDateTime createdAt = LocalDateTime.ofInstant(
|
||||
Instant.ofEpochMilli(user.getCreatedTimestamp()),
|
||||
ZoneOffset.UTC);
|
||||
entity.setCreatedAt(createdAt);
|
||||
}
|
||||
return entity;
|
||||
})
|
||||
.toList();
|
||||
|
||||
syncedUserRepository.replaceForRealm(realmName, snapshots);
|
||||
log.info("Persisted {} synced user snapshots for realm {}", snapshots.size(), realmName);
|
||||
}
|
||||
|
||||
log.info("✅ {} utilisateurs synchronisés depuis le realm {}", count, realmName);
|
||||
} catch (Exception e) {
|
||||
log.error("❌ Erreur lors de la synchronisation des utilisateurs depuis le realm {}", realmName, e);
|
||||
status = "FAILURE";
|
||||
errorMessage = e.getMessage();
|
||||
throw new RuntimeException("Erreur de synchronisation utilisateurs: " + e.getMessage(), e);
|
||||
} finally {
|
||||
recordSyncHistory(realmName, "USER", status, count, start, errorMessage);
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
@Logged(action = "SYNC_ROLES", resource = "REALM")
|
||||
public int syncRolesFromRealm(@NotBlank String realmName) {
|
||||
log.info("Synchronisation des rôles depuis le realm: {}", realmName);
|
||||
LocalDateTime start = LocalDateTime.now();
|
||||
int count = 0;
|
||||
String status = "SUCCESS";
|
||||
String errorMessage = null;
|
||||
|
||||
try {
|
||||
List<RoleRepresentation> roles = keycloak.realm(realmName).roles().list();
|
||||
count = roles.size();
|
||||
|
||||
// Persister un snapshot minimal des rôles dans la base locale si le repository
|
||||
// est disponible.
|
||||
if (syncedRoleRepository != null && !roles.isEmpty()) {
|
||||
List<SyncedRoleEntity> snapshots = roles.stream()
|
||||
.map(role -> {
|
||||
SyncedRoleEntity entity = new SyncedRoleEntity();
|
||||
entity.setRealmName(realmName);
|
||||
entity.setRoleName(role.getName());
|
||||
entity.setDescription(role.getDescription());
|
||||
return entity;
|
||||
})
|
||||
.toList();
|
||||
|
||||
syncedRoleRepository.replaceForRealm(realmName, snapshots);
|
||||
log.info("Persisted {} synced role snapshots for realm {}", snapshots.size(), realmName);
|
||||
}
|
||||
|
||||
log.info("✅ {} rôles synchronisés depuis le realm {}", count, realmName);
|
||||
} catch (Exception e) {
|
||||
log.error("❌ Erreur lors de la synchronisation des rôles depuis le realm {}", realmName, e);
|
||||
status = "FAILURE";
|
||||
errorMessage = e.getMessage();
|
||||
throw new RuntimeException("Erreur de synchronisation rôles: " + e.getMessage(), e);
|
||||
} finally {
|
||||
recordSyncHistory(realmName, "ROLE", status, count, start, errorMessage);
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
@Logged(action = "REALM_SYNC", resource = "SYSTEM")
|
||||
public Map<String, Integer> syncAllRealms() {
|
||||
Map<String, Integer> result = new HashMap<>();
|
||||
try {
|
||||
// getAllRealms() utilise un HttpClient raw avec ObjectMapper(FAIL_ON_UNKNOWN_PROPERTIES=false)
|
||||
// pour éviter les erreurs de désérialisation de RealmRepresentation avec Keycloak 26+
|
||||
List<String> realmNames = keycloakAdminClient.getAllRealms();
|
||||
|
||||
for (String realmName : realmNames) {
|
||||
if (realmName == null || realmName.isBlank()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
log.info("Synchronisation complète du realm {}", realmName);
|
||||
int totalForRealm = 0;
|
||||
try {
|
||||
int users = syncUsersFromRealm(realmName);
|
||||
int roles = syncRolesFromRealm(realmName);
|
||||
totalForRealm = users + roles;
|
||||
log.info("✅ Realm {} synchronisé (users={}, roles={})", realmName, users, roles);
|
||||
} catch (Exception e) {
|
||||
log.error("❌ Erreur lors de la synchronisation du realm {}", realmName, e);
|
||||
// On enregistre quand même le realm dans le résultat avec 0 éléments traités
|
||||
totalForRealm = 0;
|
||||
}
|
||||
result.put(realmName, totalForRealm);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("❌ Erreur lors de la récupération de la liste des realms pour synchronisation globale", e);
|
||||
// En cas d'erreur globale, on retourne simplement une map vide (aucune
|
||||
// approximation)
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> checkDataConsistency(@NotBlank String realmName) {
|
||||
Map<String, Object> report = new HashMap<>();
|
||||
report.put("realmName", realmName);
|
||||
|
||||
try {
|
||||
// Données actuelles dans Keycloak
|
||||
List<UserRepresentation> kcUsers = keycloak.realm(realmName).users().list();
|
||||
List<RoleRepresentation> kcRoles = keycloak.realm(realmName).roles().list();
|
||||
|
||||
// Snapshots locaux
|
||||
List<SyncedUserEntity> localUsers = syncedUserRepository.list("realmName", realmName);
|
||||
List<SyncedRoleEntity> localRoles = syncedRoleRepository.list("realmName", realmName);
|
||||
|
||||
// Comparaison exacte des identifiants utilisateurs
|
||||
Set<String> kcUserIds = kcUsers.stream()
|
||||
.map(UserRepresentation::getId)
|
||||
.filter(id -> id != null && !id.isBlank())
|
||||
.collect(java.util.stream.Collectors.toSet());
|
||||
|
||||
Set<String> localUserIds = localUsers.stream()
|
||||
.map(SyncedUserEntity::getKeycloakId)
|
||||
.filter(id -> id != null && !id.isBlank())
|
||||
.collect(java.util.stream.Collectors.toSet());
|
||||
|
||||
Set<String> missingUsersInLocal = new HashSet<>(kcUserIds);
|
||||
missingUsersInLocal.removeAll(localUserIds);
|
||||
|
||||
Set<String> missingUsersInKeycloak = new HashSet<>(localUserIds);
|
||||
missingUsersInKeycloak.removeAll(kcUserIds);
|
||||
|
||||
// Comparaison exacte des noms de rôles
|
||||
Set<String> kcRoleNames = kcRoles.stream()
|
||||
.map(RoleRepresentation::getName)
|
||||
.filter(name -> name != null && !name.isBlank())
|
||||
.collect(java.util.stream.Collectors.toSet());
|
||||
|
||||
Set<String> localRoleNames = localRoles.stream()
|
||||
.map(SyncedRoleEntity::getRoleName)
|
||||
.filter(name -> name != null && !name.isBlank())
|
||||
.collect(java.util.stream.Collectors.toSet());
|
||||
|
||||
Set<String> missingRolesInLocal = new HashSet<>(kcRoleNames);
|
||||
missingRolesInLocal.removeAll(localRoleNames);
|
||||
|
||||
Set<String> missingRolesInKeycloak = new HashSet<>(localRoleNames);
|
||||
missingRolesInKeycloak.removeAll(kcRoleNames);
|
||||
|
||||
boolean usersOk = missingUsersInLocal.isEmpty() && missingUsersInKeycloak.isEmpty();
|
||||
boolean rolesOk = missingRolesInLocal.isEmpty() && missingRolesInKeycloak.isEmpty();
|
||||
|
||||
report.put("status", (usersOk && rolesOk) ? "OK" : "MISMATCH");
|
||||
|
||||
report.put("usersKeycloakCount", kcUserIds.size());
|
||||
report.put("usersLocalCount", localUserIds.size());
|
||||
report.put("missingUsersInLocal", missingUsersInLocal);
|
||||
report.put("missingUsersInKeycloak", missingUsersInKeycloak);
|
||||
|
||||
report.put("rolesKeycloakCount", kcRoleNames.size());
|
||||
report.put("rolesLocalCount", localRoleNames.size());
|
||||
report.put("missingRolesInLocal", missingRolesInLocal);
|
||||
report.put("missingRolesInKeycloak", missingRolesInKeycloak);
|
||||
} catch (Exception e) {
|
||||
log.error("❌ Erreur lors du contrôle de cohérence des données pour le realm {}", realmName, e);
|
||||
report.put("status", "ERROR");
|
||||
report.put("error", e.getMessage());
|
||||
}
|
||||
|
||||
return report;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public Map<String, Object> forceSyncRealm(@NotBlank String realmName) {
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
try {
|
||||
int users = syncUsersFromRealm(realmName);
|
||||
int roles = syncRolesFromRealm(realmName);
|
||||
result.put("usersSynced", users);
|
||||
result.put("rolesSynced", roles);
|
||||
result.put("status", "SUCCESS");
|
||||
} catch (Exception e) {
|
||||
result.put("status", "FAILURE");
|
||||
result.put("error", e.getMessage());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> getLastSyncStatus(@NotBlank String realmName) {
|
||||
List<SyncHistoryEntity> history = syncHistoryRepository.findLatestByRealm(realmName, 1);
|
||||
if (history.isEmpty()) {
|
||||
return Collections.singletonMap("status", "NEVER_SYNCED");
|
||||
}
|
||||
SyncHistoryEntity lastSync = history.get(0);
|
||||
|
||||
Map<String, Object> statusMap = new HashMap<>(); // Utilisation de HashMap pour permettre nulls si besoin
|
||||
statusMap.put("lastSyncDate", lastSync.getSyncDate());
|
||||
statusMap.put("status", lastSync.getStatus());
|
||||
statusMap.put("type", lastSync.getSyncType());
|
||||
statusMap.put("itemsProcessed", lastSync.getItemsProcessed());
|
||||
return statusMap;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isKeycloakAvailable() {
|
||||
try {
|
||||
// getAllRealms() utilise un HttpClient raw : pas de désérialisation de RealmRepresentation
|
||||
// donc pas d'erreur UnrecognizedPropertyException avec Keycloak 26+
|
||||
keycloakAdminClient.getAllRealms();
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
log.warn("Keycloak availability check failed: {}", e.getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> getKeycloakHealthInfo() {
|
||||
Map<String, Object> health = new HashMap<>();
|
||||
try {
|
||||
var info = keycloak.serverInfo().getInfo();
|
||||
health.put("status", "UP");
|
||||
health.put("version", info.getSystemInfo().getVersion());
|
||||
health.put("serverTime", info.getSystemInfo().getServerTime());
|
||||
} catch (Exception e) {
|
||||
log.debug("serverInfo().getInfo() failed, trying raw HTTP fallback: {}", e.getMessage());
|
||||
fetchVersionViaHttp(health);
|
||||
}
|
||||
return health;
|
||||
}
|
||||
|
||||
private void fetchVersionViaHttp(Map<String, Object> health) {
|
||||
try {
|
||||
String token = keycloak.tokenManager().getAccessTokenString();
|
||||
var client = java.net.http.HttpClient.newHttpClient();
|
||||
var request = java.net.http.HttpRequest.newBuilder()
|
||||
.uri(java.net.URI.create(keycloakServerUrl + "/admin/serverinfo"))
|
||||
.header("Authorization", "Bearer " + token)
|
||||
.header("Accept", "application/json")
|
||||
.GET().build();
|
||||
var response = client.send(request, java.net.http.HttpResponse.BodyHandlers.ofString());
|
||||
if (response.statusCode() == 200) {
|
||||
String body = response.body();
|
||||
health.put("status", "UP");
|
||||
int sysInfoIdx = body.indexOf("\"systemInfo\"");
|
||||
if (sysInfoIdx >= 0) {
|
||||
extractJsonStringField(body, "version", sysInfoIdx)
|
||||
.ifPresent(v -> health.put("version", v));
|
||||
extractJsonStringField(body, "serverTime", sysInfoIdx)
|
||||
.ifPresent(v -> health.put("serverTime", v));
|
||||
}
|
||||
if (!health.containsKey("version")) {
|
||||
health.put("version", "UP (version non parsée)");
|
||||
}
|
||||
} else {
|
||||
health.put("status", "UP");
|
||||
health.put("version", "UP (serverinfo HTTP " + response.statusCode() + ")");
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
log.error("Fallback HTTP serverinfo also failed: {}", ex.getMessage());
|
||||
health.put("status", "DOWN");
|
||||
health.put("error", ex.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private java.util.Optional<String> extractJsonStringField(String json, String field, int searchFrom) {
|
||||
String pattern = "\"" + field + "\"";
|
||||
int idx = json.indexOf(pattern, searchFrom);
|
||||
if (idx < 0) return java.util.Optional.empty();
|
||||
int colonIdx = json.indexOf(':', idx + pattern.length());
|
||||
if (colonIdx < 0) return java.util.Optional.empty();
|
||||
int startQuote = json.indexOf('"', colonIdx + 1);
|
||||
if (startQuote < 0) return java.util.Optional.empty();
|
||||
int endQuote = json.indexOf('"', startQuote + 1);
|
||||
if (endQuote < 0) return java.util.Optional.empty();
|
||||
return java.util.Optional.of(json.substring(startQuote + 1, endQuote));
|
||||
}
|
||||
|
||||
// Helper method to record history
|
||||
private void recordSyncHistory(String realmName, String type, String status, int count, LocalDateTime start,
|
||||
String errorMessage) {
|
||||
try {
|
||||
SyncHistoryEntity history = new SyncHistoryEntity();
|
||||
history.setRealmName(realmName);
|
||||
history.setSyncType(type);
|
||||
history.setStatus(status);
|
||||
history.setItemsProcessed(count);
|
||||
history.setSyncDate(LocalDateTime.now());
|
||||
history.setDurationMs(ChronoUnit.MILLIS.between(start, LocalDateTime.now()));
|
||||
history.setErrorMessage(errorMessage);
|
||||
|
||||
// Persist the history entity
|
||||
syncHistoryRepository.persist(history);
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to record sync history", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user